hecks 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +427 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +145 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +78 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +66 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +166 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +341 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +333 -0
- data/lib/hecks/language/port.bluebook +120 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +174 -0
- data/lib/hecks/ports/access_control.port +9 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +96 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +642 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +44 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +411 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +92 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "cli_door"
|
|
3
|
+
require_relative "command_request"
|
|
4
|
+
require_relative "json_door"
|
|
5
|
+
require_relative "../projector"
|
|
6
|
+
require_relative "../ports/clock"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Facade
|
|
10
|
+
# THE RUNNER BEHIND A PROJECTED CLI.
|
|
11
|
+
#
|
|
12
|
+
# `Projector::CliProjector` answers what a domain's command line LOOKS
|
|
13
|
+
# like; this is the twenty lines that parse against it and dispatch. It
|
|
14
|
+
# lives in `lib/` rather than in a `bin/` because more than one front door
|
|
15
|
+
# wants it — `bin/run` for whichever domain you are standing in, `bin/qc`
|
|
16
|
+
# pinned to the QA ledger — and a second copy of the parse-and-dispatch
|
|
17
|
+
# would be the exact duplication the projection exists to avoid.
|
|
18
|
+
#
|
|
19
|
+
# NO IO. It answers `[text, status]` and never prints or exits, so a spec
|
|
20
|
+
# can call it without capturing streams or trapping SystemExit. The `bin/`
|
|
21
|
+
# scripts do the printing, the same division `Router` and `JsonDoor`
|
|
22
|
+
# already keep against HTTP.
|
|
23
|
+
module CliRunner
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# `[text, status]` — status 0 answered, 1 refused or misused.
|
|
27
|
+
def call(runtime:, argv:, program: "bin/run")
|
|
28
|
+
bluebook = runtime.registry.bluebooks.values.first
|
|
29
|
+
cli = Projector.call(:cli, bluebook: bluebook, options: { program: program })
|
|
30
|
+
|
|
31
|
+
name = argv.first
|
|
32
|
+
return [cli[:usage], 0] if name.nil? || %w[--help -h help].include?(name)
|
|
33
|
+
|
|
34
|
+
# `ask` PUTS A QUESTION IN ITS OWN NAMESPACE — a chapter may declare a
|
|
35
|
+
# command and a query of one name, and banking does.
|
|
36
|
+
asking = name == "ask"
|
|
37
|
+
argv = argv[1..] if asking
|
|
38
|
+
name = argv.first
|
|
39
|
+
return [cli[:usage], 1] if name.nil?
|
|
40
|
+
|
|
41
|
+
# RESOLVED THROUGH THE ALIAS MAP, so `pizzas create_pizza` and
|
|
42
|
+
# `pizzas order.create_pizza` reach the same verb — the aggregate is
|
|
43
|
+
# worth typing only when two of them declare the same word.
|
|
44
|
+
pool = asking ? cli[:questions] : cli[:verbs]
|
|
45
|
+
key = cli[:names][asking ? :question : :command][name]
|
|
46
|
+
spec = pool[key]
|
|
47
|
+
return [unknown(cli, name, asking, program), 1] unless spec
|
|
48
|
+
|
|
49
|
+
rest = argv[1..]
|
|
50
|
+
if rest.include?("--help")
|
|
51
|
+
help = Projector.call(:cli, bluebook: bluebook,
|
|
52
|
+
options: { program: program, verb: name, ask: asking })[:usage]
|
|
53
|
+
return [help, 0]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
dispatch(runtime, spec, name, rest, program, asking)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dispatch(runtime, spec, name, rest, program, asking)
|
|
60
|
+
args = stamp_time(runtime, spec, CliDoor.arguments(spec, rest))
|
|
61
|
+
|
|
62
|
+
if spec[:kind] == :query
|
|
63
|
+
rows = runtime.query(spec[:verb], **args)
|
|
64
|
+
return [JSON.pretty_generate(rows.map { |row| JsonDoor.materialize(row) }), 0]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# THE ANSWER IS SCOPED TO WHAT WAS ASKED. `bin/run`'s step-list form
|
|
68
|
+
# reports the whole store because a corpus run is judged on all of it;
|
|
69
|
+
# somebody who issued one verb wants that verb's outcome, and against a
|
|
70
|
+
# Postgres-backed domain the full dump is every record there has been.
|
|
71
|
+
request = CommandRequest.normalize(args, receiver: spec[:receiver],
|
|
72
|
+
legacy_receiver: spec[:legacy_receiver])
|
|
73
|
+
handle = runtime.dispatch(spec[:verb], **request)
|
|
74
|
+
return [JSON.pretty_generate(answered(handle)), 0] if handle.state.nil?
|
|
75
|
+
|
|
76
|
+
[JSON.pretty_generate(id: handle.id,
|
|
77
|
+
state: JsonDoor.materialize(handle.state),
|
|
78
|
+
events: handle.events.map(&:name)), 0]
|
|
79
|
+
rescue Runtime::NotFound, Runtime::TypeMismatch => e
|
|
80
|
+
# A BAD ARGUMENT AND A MISSING RECORD BOTH LAND HERE, and both want the
|
|
81
|
+
# same next step: read what the verb actually takes.
|
|
82
|
+
["#{e.message}\n\n #{program} #{asking ? 'ask ' : ''}#{name} --help", 1]
|
|
83
|
+
rescue *Runtime::DOMAIN_REFUSALS => e
|
|
84
|
+
# THE REFUSAL IS THE PRODUCT — the chapter's own sentence, verbatim.
|
|
85
|
+
[e.message, 1]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# THE CLOCK, FILLED IN AT THE DOOR.
|
|
89
|
+
#
|
|
90
|
+
# A staleness rule needs the time, and the sublanguage cannot ask for it
|
|
91
|
+
# — a `given` that read the clock would judge the same record differently
|
|
92
|
+
# on two runs, and every replay, audit and fuzz oracle here assumes it
|
|
93
|
+
# does not. So `now` stays an ARGUMENT the predicate merely reads, and
|
|
94
|
+
# the question becomes who types it. Before this, the caller did:
|
|
95
|
+
#
|
|
96
|
+
# qa/quality_control target.claim id=QC held_by.value=me \
|
|
97
|
+
# now.value=$(date +%s) window.value=900
|
|
98
|
+
#
|
|
99
|
+
# which is a shell incantation in front of every claim, and one an agent
|
|
100
|
+
# gets wrong by pasting a stale number.
|
|
101
|
+
#
|
|
102
|
+
# AT THE DOOR, NOT IN THE RUNTIME, and the distinction is load-bearing.
|
|
103
|
+
# `Ports::IdentityGeneration` reasons the same question through for a
|
|
104
|
+
# minted uuid and lands on "the value is baked into the caller's args at
|
|
105
|
+
# the first live dispatch". A clock consulted INSIDE the interpreter
|
|
106
|
+
# would not have that property — a recorded corpus step replayed
|
|
107
|
+
# tomorrow would quietly get tomorrow's time, and the fuzzer's oracle and
|
|
108
|
+
# the adapter-agreement gate both compare runs of exactly that shape. Here
|
|
109
|
+
# it fills only what a person or an agent is typing, and `runtime.dispatch`
|
|
110
|
+
# is left alone.
|
|
111
|
+
#
|
|
112
|
+
# AN EXPLICIT VALUE ALWAYS WINS, so a spec or a caller reproducing a
|
|
113
|
+
# moment says so and is believed. This only supplies what was omitted.
|
|
114
|
+
#
|
|
115
|
+
# BY NAME, WHICH IS THE ONE UNCOMFORTABLE PART. `now` is a plausible
|
|
116
|
+
# domain word and nothing declares that it means the clock. It is
|
|
117
|
+
# tolerable because this is a convenience layer rather than semantics —
|
|
118
|
+
# the verb's own help says the argument exists, dispatch is unchanged,
|
|
119
|
+
# and passing it explicitly is always available. The honest version is a
|
|
120
|
+
# declaration in the chapter (`attribute :now, Instant, from: :clock`),
|
|
121
|
+
# which is a language change: DSL, IR, the self-hosted grammar and its
|
|
122
|
+
# goldens. Worth doing; not worth smuggling in here.
|
|
123
|
+
def stamp_time(runtime, spec, args)
|
|
124
|
+
return args unless spec[:arguments].any? { |argument| argument[:path].start_with?("now.") }
|
|
125
|
+
return args if args.key?(:now)
|
|
126
|
+
|
|
127
|
+
args.merge(now: { value: Ports::Clock.now(runtime.registry) })
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A PORT OPERATION HAS NO STATE, AND ITS PAYLOAD IS THE ENTIRE POINT.
|
|
131
|
+
#
|
|
132
|
+
# A command answers with the record it changed, so naming the events is
|
|
133
|
+
# enough — the interesting part is in `state`. A port operation changes
|
|
134
|
+
# no record: it asked something outside and came back with what was
|
|
135
|
+
# said, and that lives ONLY in the event payload. Reporting names alone
|
|
136
|
+
# would print `SpecsCompleted` and drop the spec output on the floor.
|
|
137
|
+
#
|
|
138
|
+
# This is what makes a projected CLI usable as somebody's only door. An
|
|
139
|
+
# agent that may not shell out cannot run `rspec` and read the terminal;
|
|
140
|
+
# it asks the port and reads the answer, and if the answer is a bare
|
|
141
|
+
# event name then the door leads nowhere and it needs a shell after all.
|
|
142
|
+
#
|
|
143
|
+
# BOTH ENDINGS COME BACK THE SAME WAY, and the status stays 0 for both.
|
|
144
|
+
# A refusal here is not a misuse — `IssueStillOpen` and `SuiteFailed`
|
|
145
|
+
# are answers the caller asked for, correctly delivered. Exit 1 is for
|
|
146
|
+
# "you typed something wrong", and conflating the two would have a
|
|
147
|
+
# scripted agent treat a healthy no as a broken call.
|
|
148
|
+
def answered(handle)
|
|
149
|
+
# THE ID COMES OFF THE EVENT, because a port operation hydrates no
|
|
150
|
+
# instance and the handle's own `id` is nil by design. The event knows
|
|
151
|
+
# which record was asked about — it was stamped with it — and printing
|
|
152
|
+
# `null` beside a payload that plainly says `SW-TOOL` would read as a
|
|
153
|
+
# bug in something.
|
|
154
|
+
{ id: handle.id || handle.events.first&.id,
|
|
155
|
+
events: handle.events.map do |event|
|
|
156
|
+
{ name: event.name, payload: JsonDoor.materialize(event.payload) }
|
|
157
|
+
end }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A NEAR MISS IS WORTH MORE THAN A LIST. Somebody who typed
|
|
161
|
+
# `bug.discovr` wants one line, not eighty-seven of them.
|
|
162
|
+
#
|
|
163
|
+
# RANKED BY SHARED PREFIX, not by substring. Substring was the first
|
|
164
|
+
# attempt and it finds nothing for the commonest typo of all — a dropped
|
|
165
|
+
# letter, `order.create_piza`, which is a substring of nothing. Prefix
|
|
166
|
+
# length survives an error anywhere after it, which is where errors are.
|
|
167
|
+
def unknown(cli, name, asking, program)
|
|
168
|
+
# BOTH SPELLINGS ARE CANDIDATES. A caller who typed the qualified
|
|
169
|
+
# form with a typo — `order.create_piza` — shares no prefix with the
|
|
170
|
+
# short name `create_pizza`, so pooling only one of them suggests
|
|
171
|
+
# nothing for half the mistakes anybody makes.
|
|
172
|
+
pool = cli[:names][asking ? :question : :command].keys
|
|
173
|
+
near = pool.map { |candidate| [shared_prefix(candidate, name), candidate] }
|
|
174
|
+
.select { |shared, _| shared >= [name.length / 2, 3].max }
|
|
175
|
+
.sort_by { |shared, candidate| [-shared, candidate] }
|
|
176
|
+
.map(&:last)
|
|
177
|
+
|
|
178
|
+
lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
|
|
179
|
+
lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
|
|
180
|
+
lines += ["", " #{program}#{asking ? ' ask' : ''} for the full list"]
|
|
181
|
+
lines.join("\n")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def shared_prefix(one, other)
|
|
185
|
+
length = [one.length, other.length].min
|
|
186
|
+
(0...length).find { |index| one[index] != other[index] } || length
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require_relative "../runtime/errors"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Facade
|
|
5
|
+
# Turns an external command request into the dispatcher's receiver/payload
|
|
6
|
+
# envelope. Human-facing doors may accept their old flat id spelling at
|
|
7
|
+
# the edge, but every call leaving this boundary has one shape:
|
|
8
|
+
#
|
|
9
|
+
# aggregate command: { to: "record-id", with: { declared: "facts" } }
|
|
10
|
+
# entity command: { to: { aggregate: "...", entity: "..." },
|
|
11
|
+
# with: { declared: "facts" } }
|
|
12
|
+
#
|
|
13
|
+
# The command interpreter remains the authority on which facts are
|
|
14
|
+
# declared. This helper only prevents routing fields from leaking into the
|
|
15
|
+
# fact payload and gives every external door the same wire contract.
|
|
16
|
+
module CommandRequest
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def normalize(input, receiver:, legacy_receiver: nil)
|
|
20
|
+
request = symbolize(input)
|
|
21
|
+
unless request.is_a?(Hash)
|
|
22
|
+
raise Runtime::TypeMismatch, "a command request must be a hash"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
route, facts = split(request, receiver: receiver, legacy_receiver: legacy_receiver)
|
|
26
|
+
validate_route!(route, receiver)
|
|
27
|
+
|
|
28
|
+
envelope = { with: facts }
|
|
29
|
+
envelope[:to] = route if receiver
|
|
30
|
+
envelope
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def split(request, receiver:, legacy_receiver:)
|
|
34
|
+
if request.key?(:with)
|
|
35
|
+
loose = request.keys - [:to, :with]
|
|
36
|
+
unless loose.empty?
|
|
37
|
+
raise Runtime::TypeMismatch,
|
|
38
|
+
"an explicit command envelope takes routing in to: and facts in with:, not loose #{loose.sort.join(', ')}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
facts = request[:with]
|
|
42
|
+
raise Runtime::TypeMismatch, "with: must be a hash of command facts" unless facts.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
return [request[:to], facts]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
flat = request.dup
|
|
48
|
+
route = flat.delete(:to)
|
|
49
|
+
route = take_legacy_route(flat, receiver, legacy_receiver) if route.nil?
|
|
50
|
+
[route, flat]
|
|
51
|
+
end
|
|
52
|
+
private_class_method :split
|
|
53
|
+
|
|
54
|
+
def take_legacy_route(flat, receiver, legacy_receiver)
|
|
55
|
+
return unless legacy_receiver
|
|
56
|
+
|
|
57
|
+
if receiver == :aggregate
|
|
58
|
+
return flat.delete(legacy_receiver.to_sym)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return unless receiver == :entity && legacy_receiver.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
aggregate_key = legacy_receiver.fetch(:aggregate).to_sym
|
|
64
|
+
entity_key = legacy_receiver.fetch(:entity).to_sym
|
|
65
|
+
return unless flat.key?(aggregate_key) || flat.key?(entity_key)
|
|
66
|
+
|
|
67
|
+
{ aggregate: flat.delete(aggregate_key), entity: flat.delete(entity_key) }
|
|
68
|
+
end
|
|
69
|
+
private_class_method :take_legacy_route
|
|
70
|
+
|
|
71
|
+
def validate_route!(route, receiver)
|
|
72
|
+
case receiver
|
|
73
|
+
when nil
|
|
74
|
+
raise Runtime::TypeMismatch, "this command does not take a receiver in to:" unless route.nil?
|
|
75
|
+
when :aggregate
|
|
76
|
+
if route.nil? || route.to_s.empty? || route.is_a?(Hash)
|
|
77
|
+
raise Runtime::TypeMismatch, "to: must name the receiving aggregate identity"
|
|
78
|
+
end
|
|
79
|
+
when :entity
|
|
80
|
+
unless route.is_a?(Hash)
|
|
81
|
+
raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
extra = route.keys - [:aggregate, :entity]
|
|
85
|
+
missing = [:aggregate, :entity].select { |key| route[key].nil? || route[key].to_s.empty? }
|
|
86
|
+
unless extra.empty? && missing.empty?
|
|
87
|
+
raise Runtime::TypeMismatch, "to: for an entity command must contain only aggregate: and entity:"
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
raise ArgumentError, "unknown receiver kind #{receiver.inspect}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_class_method :validate_route!
|
|
94
|
+
|
|
95
|
+
def symbolize(value)
|
|
96
|
+
case value
|
|
97
|
+
when Hash then value.to_h { |key, nested| [key.to_sym, symbolize(nested)] }
|
|
98
|
+
when Array then value.map { |nested| symbolize(nested) }
|
|
99
|
+
else value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_class_method :symbolize
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Facade
|
|
5
|
+
# ONE record in hand — the object `Pizza.create_pizza(...)` and
|
|
6
|
+
# `Pizza.find(id)` give back.
|
|
7
|
+
#
|
|
8
|
+
# ONE SHARED CLASS, not one minted per aggregate. The old door subclassed
|
|
9
|
+
# `Hecks::Aggregate` per head and defined a reader per field ; this
|
|
10
|
+
# wraps the same `Runtime::Instance` state hash and answers readers and
|
|
11
|
+
# verbs through `method_missing`, closing over the dispatcher and the
|
|
12
|
+
# aggregate's IR — so a boot mints no classes at all, and two boots in
|
|
13
|
+
# one process each hand out handles bound to their own dispatcher.
|
|
14
|
+
#
|
|
15
|
+
# A non-creating verb is a method returning self, so commands chain :
|
|
16
|
+
#
|
|
17
|
+
# Pizza.create_pizza(...).add_topping(...).purchase(...)
|
|
18
|
+
class Handle
|
|
19
|
+
attr_reader :id
|
|
20
|
+
|
|
21
|
+
def initialize(dispatcher:, domain:, ir:, instance:)
|
|
22
|
+
@dispatcher = dispatcher
|
|
23
|
+
@domain = domain
|
|
24
|
+
@ir = ir
|
|
25
|
+
@id = instance.id
|
|
26
|
+
@state = instance.state
|
|
27
|
+
define_reference_accessors
|
|
28
|
+
define_verb_methods
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def [](key) = @state[key.to_sym]
|
|
32
|
+
|
|
33
|
+
# `id: @id` LAST, not first — an aggregate is free to declare its own
|
|
34
|
+
# attribute literally named `id` (BurningManPrep's `Item`, `attribute
|
|
35
|
+
# :id, ItemId`, is real corpus now: `identified_by :id` reads
|
|
36
|
+
# THAT attribute for identity). When it does, `@state[:id]` holds the
|
|
37
|
+
# full wrapped value object, not the bare identity string — merging
|
|
38
|
+
# `@state` on top of `{ id: @id }` let that wrapped VO silently
|
|
39
|
+
# clobber the correct bare `@id`, so every caller of `to_h` (the JSON
|
|
40
|
+
# door's own `/api/:coll` listing, in particular) got an object where
|
|
41
|
+
# a plain identity string belonged. `@id` merged LAST always wins,
|
|
42
|
+
# so `to_h[:id]` is always the true bare identity, regardless of
|
|
43
|
+
# whether the aggregate also happens to declare a same-named field.
|
|
44
|
+
def to_h = @state.merge(id: @id)
|
|
45
|
+
|
|
46
|
+
def fqn = "#{@domain}::#{@ir.hecks_name}"
|
|
47
|
+
|
|
48
|
+
def events
|
|
49
|
+
@dispatcher.events.select { |event| event.aggregate == fqn && event.id == @id }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reload
|
|
53
|
+
stored = repository.find(@id)
|
|
54
|
+
@state = stored.state if stored
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Equality is (WHICH AGGREGATE, WHICH ID) — two handles to the same record
|
|
59
|
+
# are the same record, and a Pizza never equals an Account that happens to
|
|
60
|
+
# share an id. The old door said this with `other.is_a?(self.class)`,
|
|
61
|
+
# leaning on one class per aggregate ; the fqn says it in data.
|
|
62
|
+
def ==(other) = other.is_a?(Handle) && other.fqn == fqn && other.id == @id
|
|
63
|
+
alias eql? ==
|
|
64
|
+
def hash = [Handle, fqn, @id].hash
|
|
65
|
+
|
|
66
|
+
def inspect
|
|
67
|
+
fields = @state.map { |key, value| "#{key}=#{value.inspect}" }.join(" ")
|
|
68
|
+
"#<#{@ir.hecks_name} #{@id} #{fields}>"
|
|
69
|
+
end
|
|
70
|
+
alias to_s inspect
|
|
71
|
+
|
|
72
|
+
# A declared field not yet written arrives here too (nil, the way a
|
|
73
|
+
# defined reader answered). Verbs are NOT handled here — see
|
|
74
|
+
# `define_verb_methods` for why.
|
|
75
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
76
|
+
return @state[name] if @state.key?(name) || reader?(name)
|
|
77
|
+
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def respond_to_missing?(name, include_private = false)
|
|
82
|
+
@state.key?(name) || reader?(name) || super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def repository = @dispatcher.registry.repository(@domain, @ir)
|
|
88
|
+
|
|
89
|
+
def reader?(name)
|
|
90
|
+
!@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# NON-CREATING VERBS ARE DEFINED, NOT DISPATCHED THROUGH method_missing.
|
|
94
|
+
#
|
|
95
|
+
# method_missing only runs once Ruby finds no REAL method already
|
|
96
|
+
# answering the name — and every object already answers `freeze` and
|
|
97
|
+
# `send` (Kernel/Object), among others. A verb whose snake-cased name
|
|
98
|
+
# collided with one of those — `Account::Freeze` -> `freeze`,
|
|
99
|
+
# `ExternalTransfer::Send` -> `send` in the banking corpus, both real —
|
|
100
|
+
# used to silently run the Kernel method instead of dispatching: no
|
|
101
|
+
# error, no refusal, the call just did the wrong thing. Defining a
|
|
102
|
+
# real singleton method per verb closed that; the `!` suffix (every
|
|
103
|
+
# command, door and Handle alike) closes it a second, permanent way —
|
|
104
|
+
# `freeze!`/`send!` name nothing Kernel/Object already answers to,
|
|
105
|
+
# so this exact class of collision cannot recur no matter what a
|
|
106
|
+
# future domain names a command.
|
|
107
|
+
def define_verb_methods
|
|
108
|
+
@ir.commands.reject(&:creates?).each do |command|
|
|
109
|
+
define_singleton_method("#{Naming.snake(command.hecks_name)}!") do |**args|
|
|
110
|
+
run(command, **args)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ONE HEAD ADDRESSES THE SAME WAY AS SEVERAL. `@ir.identified_by` is only
|
|
116
|
+
# the single-head shorthand — nil the moment an identity is composite
|
|
117
|
+
# (`SafeDepositBox`'s `branch_code`/`box_number`) — so building the
|
|
118
|
+
# identity payload from `identity_heads` instead reads every head, one
|
|
119
|
+
# or many alike, straight out of state that already carries them.
|
|
120
|
+
def run(command, **args)
|
|
121
|
+
@state = @dispatcher.dispatch("#{fqn}.#{command.hecks_name}", to: @id, with: args).instance.state
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# THE OTHER HALF OF A CROSS-REFERENCE. `transfer.source` already reads
|
|
126
|
+
# the raw value — a plain reader, same as any other attribute, still
|
|
127
|
+
# needed by a `given`. This is the hydrated hop docs/rails-integration.md
|
|
128
|
+
# designed and marked "nothing built": `transfer.source_account`
|
|
129
|
+
# resolves it to the actual Account record, on demand — nothing loads
|
|
130
|
+
# until called, and this hop never triggers the next one. Plain
|
|
131
|
+
# chaining composes for free from here : `payment.disputed_by_customer.name`
|
|
132
|
+
# is two ordinary calls, each individually lazy, which is exactly why
|
|
133
|
+
# this is a named accessor per reference rather than a `through:`
|
|
134
|
+
# option — that shape was considered and rejected in the same design
|
|
135
|
+
# note for hiding how many lookups actually happened behind one call.
|
|
136
|
+
#
|
|
137
|
+
# Defined BEFORE verb methods, not after — on the vanishing chance a
|
|
138
|
+
# reference's own accessor name collided with a command's, the verb
|
|
139
|
+
# should win; `initialize` calls this first so `define_verb_methods`
|
|
140
|
+
# defines second and last.
|
|
141
|
+
# NO DERIVATION LEFT (ADR 0025, "References"): `reference_to`
|
|
142
|
+
# itself mints the bare attribute name now — `:account`, never
|
|
143
|
+
# `:account_id` — so the accessor is spelled exactly like the
|
|
144
|
+
# attribute it reads, with no `_id`-strip or `as:`-suffix rule to
|
|
145
|
+
# apply first. `piece.account` (a METHOD, defined here) and
|
|
146
|
+
# `piece[:account]` (`Handle#[]`, bracket access reading the raw
|
|
147
|
+
# id straight off `@instance`) never collide despite sharing a
|
|
148
|
+
# name — Ruby dispatches the two completely differently — which is
|
|
149
|
+
# what makes the OLD "studio_studio" double-suffix workaround
|
|
150
|
+
# (this method used to force a DIFFERENT name specifically to dodge
|
|
151
|
+
# that non-collision) unnecessary rather than merely simplified.
|
|
152
|
+
def define_reference_accessors
|
|
153
|
+
@ir.attributes.select(&:reference?).each do |attribute|
|
|
154
|
+
target = attribute.type.resolve
|
|
155
|
+
next unless target # cross-domain, or otherwise unresolvable — no accessor rather than a guess
|
|
156
|
+
|
|
157
|
+
domain = @domain
|
|
158
|
+
field = attribute.name
|
|
159
|
+
list = attribute.list?
|
|
160
|
+
target_fqn = "#{domain}::#{target.hecks_name}"
|
|
161
|
+
|
|
162
|
+
define_singleton_method(field) do
|
|
163
|
+
value = self[field]
|
|
164
|
+
door = Object.const_get(target_fqn)
|
|
165
|
+
next Array(value).map { |identity| door.find(identity) } if list
|
|
166
|
+
|
|
167
|
+
value && door.find(value)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "handle"
|
|
3
|
+
require_relative "command_request"
|
|
4
|
+
require_relative "../naming"
|
|
5
|
+
require_relative "../runtime/errors"
|
|
6
|
+
require_relative "../runtime/value"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Facade
|
|
10
|
+
# THE JSON DOOR — WHERE Facade MEETS BODY-IN/BODY-OUT CALLERS.
|
|
11
|
+
#
|
|
12
|
+
# `Handle`/`Surface` are Ruby sugar over the dispatcher for a Ruby caller
|
|
13
|
+
# holding real objects — a symbol verb name, a `**kwargs` payload, a
|
|
14
|
+
# `Handle` back in hand. A REST-ish JSON API is a caller holding STRINGS
|
|
15
|
+
# instead: a URL segment naming a collection, a URL segment naming a
|
|
16
|
+
# record or a verb, a parsed request body whose every key arrived as a
|
|
17
|
+
# String because that is all JSON ever gives. Every app that wants to put
|
|
18
|
+
# a JSON API in front of a booted domain has to do that translation —
|
|
19
|
+
# name to class, string to symbol, nested `Runtime::Value` back to plain
|
|
20
|
+
# data — and one sibling app had already hand-written it once, bespoke,
|
|
21
|
+
# against its own routes, before this existed. This is that translation
|
|
22
|
+
# pulled out, generic, reading the SAME IR the rest of the facade already
|
|
23
|
+
# reads rather than re-deriving "how do I find an aggregate by name".
|
|
24
|
+
#
|
|
25
|
+
# NO HTTP LIVES HERE. Same discipline `Router` and `Surface` already
|
|
26
|
+
# hold: this module never sees a request object, never picks a status
|
|
27
|
+
# code, never calls `halt`. Every method here takes plain Ruby values in
|
|
28
|
+
# — a raw JSON String is the one exception, see `.parse` below, every
|
|
29
|
+
# other input is already-parsed data — and returns plain Ruby values
|
|
30
|
+
# out, or raises. Turning a raw request body into that input, and
|
|
31
|
+
# turning a raised exception into an HTTP status, both stay the calling
|
|
32
|
+
# app's job, exactly the way they already are for `Router#dispatch`.
|
|
33
|
+
#
|
|
34
|
+
# EVERY "THAT DOESN'T EXIST" CASE RAISES `Runtime::NotFound` — THE SAME
|
|
35
|
+
# CLASS EVERY TIME, NOT A NEW ONE PER CALLER. `Runtime::NotFound` already
|
|
36
|
+
# sits in `Runtime::DOMAIN_REFUSALS` (runtime/errors.rb) — the family a
|
|
37
|
+
# booted app already has, or trivially can have, ONE generic `error`
|
|
38
|
+
# handler for, mapping the whole family to a status code without a
|
|
39
|
+
# special case per refusal. A bespoke `JsonDoor::CollectionNotFound` (or
|
|
40
|
+
# three of those, one per flavor of "not found") would just force every
|
|
41
|
+
# app that adopts this door to widen its rescue clause to match it — the
|
|
42
|
+
# opposite of what a shared refusal family is for. So "no such
|
|
43
|
+
# aggregate", "no creating command", "no such command", and "no record
|
|
44
|
+
# with that id" all raise the one class, distinguished only by message.
|
|
45
|
+
module JsonDoor
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# "Banking", "Customer" -> the `Banking::Customer` class `.find` /
|
|
49
|
+
# `.create_...` / etc already answer for — the same class
|
|
50
|
+
# `Facade::Handle`'s own reference accessors reach with
|
|
51
|
+
# `Object.const_get` (see handle.rb's `define_reference_accessors`).
|
|
52
|
+
#
|
|
53
|
+
# Checked against the CURRENT boot's IR first, not against Ruby's
|
|
54
|
+
# constant table directly — a name that names nothing in this
|
|
55
|
+
# registry should refuse before ever asking Ruby whether some
|
|
56
|
+
# same-named constant happens to exist (possibly a stale one, left
|
|
57
|
+
# over from an earlier boot in this same process — the exact hazard
|
|
58
|
+
# `AggregateDoor#port`'s own comment describes at length). Only once
|
|
59
|
+
# the IR confirms the aggregate is real does this read the constant
|
|
60
|
+
# the current boot's `Surface.install` actually minted for it.
|
|
61
|
+
def aggregate(dispatcher, domain, name)
|
|
62
|
+
ir = dispatcher.registry.bluebook(domain)&.aggregate(name)
|
|
63
|
+
raise Runtime::NotFound, "#{domain} declares no aggregate named #{name.inspect}" unless ir
|
|
64
|
+
|
|
65
|
+
Object.const_get("#{domain}::#{ir.hecks_name}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The one command a POST to a bare collection URL means — "make one of
|
|
69
|
+
# these". `AggregateDoor` already enforces exactly one creating
|
|
70
|
+
# command per aggregate (`Command#creates?`, true exactly when the
|
|
71
|
+
# command declares no `references`); this just names it the same
|
|
72
|
+
# snake_case-plus-bang a Ruby caller would already ask the door for
|
|
73
|
+
# (`Naming.snake`, the identical call `AggregateDoor` itself makes
|
|
74
|
+
# when it defines that singleton method in the first place — `!`
|
|
75
|
+
# because every command does now, door and Handle alike).
|
|
76
|
+
def creating_command(klass)
|
|
77
|
+
creating = klass.ir.commands.find(&:creates?)
|
|
78
|
+
raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no creating command" unless creating
|
|
79
|
+
|
|
80
|
+
"#{Naming.snake(creating.hecks_name)}!"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A URL segment or a JSON body's "command" field, checked against what
|
|
84
|
+
# a `Handle` can actually dispatch — NOT `klass.commands`, which is
|
|
85
|
+
# `AggregateDoor`'s own door-level list and includes the one creating
|
|
86
|
+
# command too (`aggregate_door.rb`'s `commands` singleton method maps
|
|
87
|
+
# every `ir.commands`, full stop). A `Handle` only ever defines
|
|
88
|
+
# singleton methods for the NON-creating ones
|
|
89
|
+
# (`Handle#define_verb_methods`, `@ir.commands.reject(&:creates?)`) —
|
|
90
|
+
# the creating command lives on the aggregate class itself, dispatched
|
|
91
|
+
# through `.creating_command` above, not through a `Handle` in hand.
|
|
92
|
+
# Accepting a creating-command name here let it past this gate clean,
|
|
93
|
+
# only to blow up as a raw `NoMethodError` the moment a caller tried
|
|
94
|
+
# `handle.public_send(name, **args)`, instead of the 404 this method
|
|
95
|
+
# promises. Filtering `reject(&:creates?)` here, the same filter
|
|
96
|
+
# `Handle` itself applies, is what keeps "accepted here" and
|
|
97
|
+
# "dispatchable there" the same set.
|
|
98
|
+
def validate_command!(klass, name)
|
|
99
|
+
wanted = name.to_s
|
|
100
|
+
dispatchable = klass.ir.commands.reject(&:creates?).map { |command| "#{Naming.snake(command.hecks_name)}!" }
|
|
101
|
+
return wanted if dispatchable.include?(wanted)
|
|
102
|
+
|
|
103
|
+
raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no command named #{wanted.inspect}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# `klass.find` already answers nil-on-miss — the right shape for a
|
|
107
|
+
# Ruby caller that means to check for itself. A JSON caller asking for
|
|
108
|
+
# one record by id off a URL means to HAVE it, or answer 404 — this is
|
|
109
|
+
# that stricter wrapper, raising the same `Runtime::NotFound` the rest
|
|
110
|
+
# of this door raises rather than handing back nil for the caller to
|
|
111
|
+
# remember to check.
|
|
112
|
+
def find!(klass, id)
|
|
113
|
+
klass.find(id) or raise Runtime::NotFound, "no #{klass.ir.hecks_name} found for id #{id.inspect}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# JSON only ever hands back String keys. A command's args, and every
|
|
117
|
+
# nested value-object literal inside them, need symbol keys before
|
|
118
|
+
# `Handle`/`Dispatcher` will accept them at all — this is that
|
|
119
|
+
# recursive conversion, blind to how deep a body nests.
|
|
120
|
+
def deep_symbolize(value)
|
|
121
|
+
case value
|
|
122
|
+
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
123
|
+
when Array then value.map { |item| deep_symbolize(item) }
|
|
124
|
+
else value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The other direction: a `Handle`, or a query row's plain state hash,
|
|
129
|
+
# carrying a `Runtime::Value` at every level a value object sits at —
|
|
130
|
+
# down to plain Ruby a JSON encoder can walk without knowing what a
|
|
131
|
+
# `Runtime::Value` is.
|
|
132
|
+
#
|
|
133
|
+
# `Runtime::Value.materialize` (runtime/value.rb) already does the
|
|
134
|
+
# actual recursion — its `Hash` branch calls itself on every value,
|
|
135
|
+
# its `Value` branch unwraps through `#to_h`, which itself calls
|
|
136
|
+
# `materialize` on every field, so a value object three levels deep
|
|
137
|
+
# unwraps three levels deep for free. This is not a second unwrapper
|
|
138
|
+
# sitting next to it: `materialize` covers `Hash`/`Array`/`Value`
|
|
139
|
+
# already, and a `Handle` is none of those three, so the one thing
|
|
140
|
+
# this adds is `#to_h`'ing a `Handle` first so `materialize`'s own
|
|
141
|
+
# `Hash` case can take it from there.
|
|
142
|
+
def materialize(value)
|
|
143
|
+
value = value.to_h if value.is_a?(Handle)
|
|
144
|
+
Runtime::Value.materialize(value)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Parsed JSON and raw JSON text cross the same receiver/payload boundary
|
|
148
|
+
# as CLI and forms. The result is ready to splat into Dispatcher#dispatch
|
|
149
|
+
# and contains no loose routing fields.
|
|
150
|
+
def command_request(body, receiver:, legacy_receiver: nil)
|
|
151
|
+
input = body.is_a?(String) ? parse(body) : body
|
|
152
|
+
CommandRequest.normalize(input, receiver: receiver, legacy_receiver: legacy_receiver)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The one place a raw JSON string is legitimate input for this door —
|
|
156
|
+
# a POST body, still text at the point a generic, HTTP-blind layer can
|
|
157
|
+
# see it. `JSON::ParserError` already names "this wasn't JSON" exactly
|
|
158
|
+
# right ; wrapping it in a bespoke `JsonDoor`-specific class would
|
|
159
|
+
# only be a second name for the same fact, so it propagates exactly as
|
|
160
|
+
# `JSON.parse` raises it — a calling app catches it the same standard
|
|
161
|
+
# way it would catch any other malformed-input error, no new class to
|
|
162
|
+
# learn.
|
|
163
|
+
def parse(raw_json) = JSON.parse(raw_json)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|