hecks 1.2.0 → 1.4.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 +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +51 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +301 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -20
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +124 -0
- data/lib/hecks/fuzzing/form_census.rb +199 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
- data/lib/hecks/fuzzing/properties/guards.rb +129 -26
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
- data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +84 -39
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +224 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +676 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +39 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
- data/lib/hecks/fuzzing/value_generator.rb +66 -14
- data/lib/hecks/fuzzing.rb +11 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +89 -21
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +250 -0
- data/lib/hecks/projections/glossary/markdown.rb +105 -0
- data/lib/hecks/projections/glossary/mermaid.rb +110 -0
- data/lib/hecks/projections/glossary/page.css +271 -0
- data/lib/hecks/projections/glossary/page.js +72 -0
- data/lib/hecks/projections/glossary/sections.rb +17 -0
- data/lib/hecks/projections/glossary/sentences.rb +205 -0
- data/lib/hecks/projections/glossary.rb +214 -286
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +15 -22
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +132 -24
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +94 -19
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +282 -46
- data/lib/hecks/runtime/entity_interpreter.rb +160 -69
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +111 -56
- data/lib/hecks/runtime/reaction_invocation.rb +76 -9
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -112
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +56 -27
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -88
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +93 -56
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +379 -226
- data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
- data/lib/hecks/runtime/value.rb +28 -23
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +207 -5
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +43 -2
|
@@ -16,6 +16,29 @@ module Hecks
|
|
|
16
16
|
module CommandRequest
|
|
17
17
|
module_function
|
|
18
18
|
|
|
19
|
+
# Splits one external request into routing (`to:`) and facts (`with:`), accepting
|
|
20
|
+
# either the explicit envelope or a flat Hash, and refuses a receiver that does
|
|
21
|
+
# not fit the kind of command being called.
|
|
22
|
+
#
|
|
23
|
+
# Keys are symbolized at every depth first, so a parsed JSON body and a Ruby Hash
|
|
24
|
+
# are treated alike. In a flat Hash everything but `to` (or the legacy receiver
|
|
25
|
+
# keys) is a fact.
|
|
26
|
+
#
|
|
27
|
+
# @param input [Hash, Object] the request, with String or Symbol keys; anything
|
|
28
|
+
# that is not a Hash is refused
|
|
29
|
+
# @param receiver [Symbol, nil] the kind of receiver the command takes:
|
|
30
|
+
# `:aggregate` (an identity), `:entity` (a Hash of `aggregate:` and `entity:`
|
|
31
|
+
# identities), or `nil` for a command that takes none, such as a creating command
|
|
32
|
+
# @param legacy_receiver [Symbol, String, Hash{Symbol => Symbol, String}, nil] where
|
|
33
|
+
# a flat request without `to` may carry its receiver instead: one key name (such
|
|
34
|
+
# as `:id`) for an `:aggregate` receiver, or `{ aggregate: key, entity: key }` for
|
|
35
|
+
# an `:entity` receiver; `nil` accepts no legacy spelling
|
|
36
|
+
# @return [Hash{Symbol => Object}] `{ with: facts }`, plus `to:` holding the route
|
|
37
|
+
# whenever `receiver` is not `nil`; ready to pass to `Dispatcher#dispatch_flat`
|
|
38
|
+
# @raise [Runtime::TypeMismatch] if `input` or its `with:` is not a Hash, if an
|
|
39
|
+
# explicit envelope carries keys other than `to:` and `with:`, or if the route
|
|
40
|
+
# is missing, blank, malformed, or given to a command that takes no receiver
|
|
41
|
+
# @raise [ArgumentError] if `receiver` is not `nil`, `:aggregate` or `:entity`
|
|
19
42
|
def normalize(input, receiver:, legacy_receiver: nil)
|
|
20
43
|
request = symbolize(input)
|
|
21
44
|
raise Runtime::TypeMismatch, "a command request must be a hash" unless request.is_a?(Hash)
|
data/lib/hecks/facade/handle.rb
CHANGED
|
@@ -2,22 +2,29 @@ require_relative "../naming"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Facade
|
|
5
|
-
#
|
|
5
|
+
# One record in hand — the object `Pizza.create_pizza!(...)` and
|
|
6
6
|
# `Pizza.find(id)` give back.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# verbs through
|
|
12
|
-
# aggregate's IR — so a boot mints no
|
|
13
|
-
# one process each hand out handles bound
|
|
8
|
+
# One shared class, not one minted per aggregate. Rather than subclassing
|
|
9
|
+
# per head and defining a reader per field, this wraps a
|
|
10
|
+
# `Runtime::Instance` state hash, answers readers through
|
|
11
|
+
# `method_missing` and verbs through per-handle singleton methods, and
|
|
12
|
+
# closes over the dispatcher and the aggregate's IR — so a boot mints no
|
|
13
|
+
# classes at all, and two boots in one process each hand out handles bound
|
|
14
|
+
# to their own dispatcher.
|
|
14
15
|
#
|
|
15
16
|
# A non-creating verb is a method returning self, so commands chain :
|
|
16
17
|
#
|
|
17
|
-
# Pizza.create_pizza(...).add_topping(...).purchase(...)
|
|
18
|
+
# Pizza.create_pizza!(...).add_topping!(...).purchase!(...)
|
|
18
19
|
class Handle
|
|
19
20
|
attr_reader :id
|
|
20
21
|
|
|
22
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
23
|
+
# dispatcher this record's verbs, `events` and `reload` go through
|
|
24
|
+
# @param domain [String] the owning chapter's name, the first half of `fqn`
|
|
25
|
+
# @param aggregate [Bluebook::Aggregate] the IR of the aggregate this record is one of
|
|
26
|
+
# @param instance [Runtime::Instance] the stored record; its `id` and `state` are
|
|
27
|
+
# read once here and the instance itself is not retained
|
|
21
28
|
def initialize(dispatcher:, domain:, aggregate:, instance:)
|
|
22
29
|
@dispatcher = dispatcher
|
|
23
30
|
@domain = domain
|
|
@@ -28,37 +35,67 @@ module Hecks
|
|
|
28
35
|
define_verb_methods
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
# Reads one field's raw stored value, without hydrating a reference the way the
|
|
39
|
+
# reference accessor of the same name does.
|
|
40
|
+
#
|
|
41
|
+
# @param key [Symbol, String] the attribute name
|
|
42
|
+
# @return [Object, nil] the value held in state (a scalar, a value object, a list,
|
|
43
|
+
# or a referenced record's id); `nil` when the field is unset or not in state
|
|
31
44
|
def [](key) = @state[key.to_sym]
|
|
32
45
|
|
|
33
|
-
#
|
|
46
|
+
# Answers the record's state as a plain Hash with the bare identity under `:id`.
|
|
47
|
+
#
|
|
48
|
+
# `id: @id` last, not first — an aggregate is free to declare its own
|
|
34
49
|
# attribute literally named `id` (BurningManPrep's `Item`, `attribute
|
|
35
50
|
# :id, ItemId`, is real corpus now: `identified_by :id` reads
|
|
36
|
-
#
|
|
51
|
+
# that attribute for identity). When it does, `@state[:id]` holds the
|
|
37
52
|
# full wrapped value object, not the bare identity string — merging
|
|
38
|
-
# `@state` on top of `{ id: @id }` let that wrapped VO silently
|
|
53
|
+
# `@state` on top of `{ id: @id }` would let that wrapped VO silently
|
|
39
54
|
# clobber the correct bare `@id`, so every caller of `to_h` (the JSON
|
|
40
|
-
# door's own `/api/:coll` listing, in particular)
|
|
41
|
-
# a plain identity string
|
|
55
|
+
# door's own `/api/:coll` listing, in particular) would get an object
|
|
56
|
+
# where a plain identity string belongs. `@id` merged last always wins,
|
|
42
57
|
# so `to_h[:id]` is always the true bare identity, regardless of
|
|
43
58
|
# whether the aggregate also happens to declare a same-named field.
|
|
59
|
+
#
|
|
60
|
+
# @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
|
|
61
|
+
# name, plus `:id` holding the identity String
|
|
44
62
|
def to_h = @state.merge(id: @id)
|
|
45
63
|
|
|
64
|
+
# Names the aggregate this record belongs to, in the form every dispatch verb and
|
|
65
|
+
# event is addressed by.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] the fully qualified aggregate name, such as `"Pizzas::Pizza"`
|
|
46
68
|
def fqn = "#{@domain}::#{@ir.hecks_name}"
|
|
47
69
|
|
|
70
|
+
# Lists the events this one record has emitted, filtered out of the dispatcher's
|
|
71
|
+
# whole event log on each call.
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<Runtime::Event>] this record's events in the order the log holds
|
|
74
|
+
# them; `[]` when it has emitted none
|
|
48
75
|
def events
|
|
49
76
|
@dispatcher.events.select { |event| event.aggregate == fqn && event.id == @id }
|
|
50
77
|
end
|
|
51
78
|
|
|
79
|
+
# Refreshes this handle's state from the repository, picking up writes made through
|
|
80
|
+
# another handle or door. Keeps the current state when the record is not found.
|
|
81
|
+
#
|
|
82
|
+
# @return [Facade::Handle] this handle, so the call chains
|
|
83
|
+
# @raise [Runtime::WiringError] if the aggregate's persistence bind cannot be
|
|
84
|
+
# resolved into a repository
|
|
52
85
|
def reload
|
|
53
86
|
stored = repository.find(@id)
|
|
54
87
|
@state = stored.state if stored
|
|
55
88
|
self
|
|
56
89
|
end
|
|
57
90
|
|
|
58
|
-
# Equality is (
|
|
91
|
+
# Equality is (which aggregate, which ID) — two handles to the same record
|
|
59
92
|
# are the same record, and a Pizza never equals an Account that happens to
|
|
60
|
-
# share an id.
|
|
61
|
-
#
|
|
93
|
+
# share an id. With one shared class for every aggregate,
|
|
94
|
+
# `other.is_a?(self.class)` cannot tell them apart ; the fqn says it in data.
|
|
95
|
+
#
|
|
96
|
+
# @param other [Object] anything; only another `Handle` can be equal
|
|
97
|
+
# @return [Boolean] true when `other` is a `Handle` with the same `fqn` and `id`,
|
|
98
|
+
# whatever state either one holds
|
|
62
99
|
def ==(other) = other.is_a?(Handle) && other.fqn == fqn && other.id == @id
|
|
63
100
|
alias eql? ==
|
|
64
101
|
def hash = [Handle, fqn, @id].hash
|
|
@@ -69,9 +106,19 @@ module Hecks
|
|
|
69
106
|
end
|
|
70
107
|
alias to_s inspect
|
|
71
108
|
|
|
109
|
+
# Answers a field reader: `pizza.name` reads `name` out of state.
|
|
110
|
+
#
|
|
72
111
|
# A declared field not yet written arrives here too (nil, the way a
|
|
73
|
-
# defined reader
|
|
112
|
+
# defined reader answers). Verbs are not handled here — see
|
|
74
113
|
# `define_verb_methods` for why.
|
|
114
|
+
#
|
|
115
|
+
# @param name [Symbol] the method called, read as an attribute or lifecycle field name
|
|
116
|
+
# @param args [Array<Object>] ignored by a reader; passed on to `super` otherwise
|
|
117
|
+
# @param kwargs [Hash{Symbol => Object}] ignored by a reader; passed on to `super`
|
|
118
|
+
# otherwise
|
|
119
|
+
# @return [Object, nil] the field's value; `nil` for a declared field with nothing
|
|
120
|
+
# written yet
|
|
121
|
+
# @raise [NoMethodError] if `name` is neither a key in state nor a declared field
|
|
75
122
|
def method_missing(name, *args, **kwargs, &)
|
|
76
123
|
return @state[name] if @state.key?(name) || reader?(name)
|
|
77
124
|
|
|
@@ -90,16 +137,16 @@ module Hecks
|
|
|
90
137
|
!@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
|
|
91
138
|
end
|
|
92
139
|
|
|
93
|
-
#
|
|
140
|
+
# Non-creating verbs are defined, not dispatched through method_missing.
|
|
94
141
|
#
|
|
95
|
-
# method_missing only runs once Ruby finds no
|
|
142
|
+
# method_missing only runs once Ruby finds no real method already
|
|
96
143
|
# answering the name — and every object already answers `freeze` and
|
|
97
144
|
# `send` (Kernel/Object), among others. A verb whose snake-cased name
|
|
98
145
|
# collided with one of those — `Account::Freeze` -> `freeze`,
|
|
99
146
|
# `ExternalTransfer::Send` -> `send` in the banking corpus, both real —
|
|
100
|
-
#
|
|
101
|
-
# error, no refusal, the call just
|
|
102
|
-
# real singleton method per verb
|
|
147
|
+
# would silently run the Kernel method instead of dispatching: no
|
|
148
|
+
# error, no refusal, the call just does the wrong thing. Defining a
|
|
149
|
+
# real singleton method per verb closes that; the `!` suffix (every
|
|
103
150
|
# command, door and Handle alike) closes it a second, permanent way —
|
|
104
151
|
# `freeze!`/`send!` name nothing Kernel/Object already answers to,
|
|
105
152
|
# so this exact class of collision cannot recur no matter what a
|
|
@@ -112,7 +159,7 @@ module Hecks
|
|
|
112
159
|
end
|
|
113
160
|
end
|
|
114
161
|
|
|
115
|
-
#
|
|
162
|
+
# One head addresses the same way as several. `@ir.identified_by` is only
|
|
116
163
|
# the single-head shorthand — nil the moment an identity is composite
|
|
117
164
|
# (`SafeDepositBox`'s `branch_code`/`box_number`) — so building the
|
|
118
165
|
# identity payload from `identity_heads` instead reads every head, one
|
|
@@ -122,7 +169,7 @@ module Hecks
|
|
|
122
169
|
self
|
|
123
170
|
end
|
|
124
171
|
|
|
125
|
-
#
|
|
172
|
+
# **The other half of a cross-reference**. `transfer.source` already reads
|
|
126
173
|
# the raw value — a plain reader, same as any other attribute, still
|
|
127
174
|
# needed by a `given`. This is the hydrated hop docs/rails-integration.md
|
|
128
175
|
# designed and marked "nothing built": `transfer.source_account`
|
|
@@ -134,25 +181,25 @@ module Hecks
|
|
|
134
181
|
# option — that shape was considered and rejected in the same design
|
|
135
182
|
# note for hiding how many lookups actually happened behind one call.
|
|
136
183
|
#
|
|
137
|
-
# Defined
|
|
184
|
+
# Defined before verb methods, not after — on the vanishing chance a
|
|
138
185
|
# reference's own accessor name collided with a command's, the verb
|
|
139
186
|
# should win; `initialize` calls this first so `define_verb_methods`
|
|
140
187
|
# defines second and last.
|
|
141
|
-
#
|
|
188
|
+
# No derivation left (ADR 0025, "References"): `reference_to`
|
|
142
189
|
# itself mints the bare attribute name now — `:account`, never
|
|
143
190
|
# `:account_id` — so the accessor is spelled exactly like the
|
|
144
191
|
# attribute it reads, with no `_id`-strip or `as:`-suffix rule to
|
|
145
|
-
# apply first. `piece.account` (a
|
|
192
|
+
# apply first. `piece.account` (a method, defined here) and
|
|
146
193
|
# `piece[:account]` (`Handle#[]`, bracket access reading the raw
|
|
147
194
|
# id straight off `@instance`) never collide despite sharing a
|
|
148
|
-
# name — Ruby dispatches the two completely differently —
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
# that non-collision) unnecessary rather than merely simplified.
|
|
195
|
+
# name — Ruby dispatches the two completely differently — so no
|
|
196
|
+
# renamed accessor (a "studio_studio"-style double suffix) is needed
|
|
197
|
+
# to keep them apart.
|
|
152
198
|
def define_reference_accessors
|
|
153
199
|
@ir.attributes.select(&:reference?).each do |attribute|
|
|
154
200
|
target = attribute.type.resolve
|
|
155
|
-
|
|
201
|
+
# Cross-domain, or otherwise unresolvable — no accessor rather than a guess.
|
|
202
|
+
next unless target
|
|
156
203
|
|
|
157
204
|
domain = @domain
|
|
158
205
|
field = attribute.name
|
|
@@ -7,22 +7,23 @@ require_relative "../runtime/value"
|
|
|
7
7
|
|
|
8
8
|
module Hecks
|
|
9
9
|
module Facade
|
|
10
|
-
#
|
|
10
|
+
# **The JSON door** — where Facade meets body-in/body-out callers.
|
|
11
11
|
#
|
|
12
12
|
# `Handle`/`Surface` are Ruby sugar over the dispatcher for a Ruby caller
|
|
13
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
|
|
14
|
+
# `Handle` back in hand. A REST-ish JSON API is a caller holding strings
|
|
15
15
|
# instead: a URL segment naming a collection, a URL segment naming a
|
|
16
16
|
# record or a verb, a parsed request body whose every key arrived as a
|
|
17
17
|
# String because that is all JSON ever gives. Every app that wants to put
|
|
18
18
|
# a JSON API in front of a booted domain has to do that translation —
|
|
19
|
-
# name to
|
|
20
|
-
# data
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# reads rather than re-deriving "how do I find an aggregate by name".
|
|
19
|
+
# name to door module, string to symbol, nested `Runtime::Value` back to
|
|
20
|
+
# plain data. This is that translation written once, generic, reading the
|
|
21
|
+
# same IR the rest of the facade already reads rather than each app
|
|
22
|
+
# re-deriving "how do I find an aggregate by name" against its own routes.
|
|
24
23
|
#
|
|
25
|
-
#
|
|
24
|
+
# ## No HTTP lives here
|
|
25
|
+
#
|
|
26
|
+
# Same discipline `Router` and `Surface` already
|
|
26
27
|
# hold: this module never sees a request object, never picks a status
|
|
27
28
|
# code, never calls `halt`. Every method here takes plain Ruby values in
|
|
28
29
|
# — a raw JSON String is the one exception, see `.parse` below, every
|
|
@@ -31,10 +32,12 @@ module Hecks
|
|
|
31
32
|
# turning a raised exception into an HTTP status, both stay the calling
|
|
32
33
|
# app's job, exactly the way they already are for `Router#dispatch`.
|
|
33
34
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
35
|
+
# ## One refusal class for every miss
|
|
36
|
+
#
|
|
37
|
+
# Every "that doesn't exist" case raises `Runtime::NotFound` — the same
|
|
38
|
+
# class every time, not a new one per caller. `Runtime::NotFound` already
|
|
36
39
|
# sits in `Runtime::DOMAIN_REFUSALS` (runtime/errors.rb) — the family a
|
|
37
|
-
# booted app already has, or trivially can have,
|
|
40
|
+
# booted app already has, or trivially can have, one generic `error`
|
|
38
41
|
# handler for, mapping the whole family to a status code without a
|
|
39
42
|
# special case per refusal. A bespoke `JsonDoor::CollectionNotFound` (or
|
|
40
43
|
# three of those, one per flavor of "not found") would just force every
|
|
@@ -45,12 +48,15 @@ module Hecks
|
|
|
45
48
|
module JsonDoor
|
|
46
49
|
module_function
|
|
47
50
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
51
|
+
# Resolves a domain name and an aggregate name, as two URL segments carry them, to
|
|
52
|
+
# that aggregate's door.
|
|
53
|
+
#
|
|
54
|
+
# "Banking", "Customer" -> the `Banking::Customer` door module `.find` /
|
|
55
|
+
# `.create_...!` / etc already answer for — the same module
|
|
50
56
|
# `Facade::Handle`'s own reference accessors reach with
|
|
51
57
|
# `Object.const_get` (see handle.rb's `define_reference_accessors`).
|
|
52
58
|
#
|
|
53
|
-
# Checked against the
|
|
59
|
+
# Checked against the current boot's IR first, not against Ruby's
|
|
54
60
|
# constant table directly — a name that names nothing in this
|
|
55
61
|
# registry should refuse before ever asking Ruby whether some
|
|
56
62
|
# same-named constant happens to exist (possibly a stale one, left
|
|
@@ -58,6 +64,16 @@ module Hecks
|
|
|
58
64
|
# `AggregateDoor#port`'s own comment describes at length). Only once
|
|
59
65
|
# the IR confirms the aggregate is real does this read the constant
|
|
60
66
|
# the current boot's `Surface.install` actually minted for it.
|
|
67
|
+
#
|
|
68
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
69
|
+
# dispatcher whose registry decides whether the aggregate exists
|
|
70
|
+
# @param domain [String, Symbol] the chapter name, such as `"Banking"`
|
|
71
|
+
# @param name [String, Symbol] the aggregate's declared name, such as `"Customer"`
|
|
72
|
+
# @return [Module] the aggregate door installed at `domain::name`
|
|
73
|
+
# @raise [Runtime::NotFound] if the registry holds no such chapter, or the chapter
|
|
74
|
+
# declares no aggregate of that name
|
|
75
|
+
# @raise [NameError] if the IR has the aggregate but no facade constant is
|
|
76
|
+
# installed for it, as after a boot with `install_facade: false`
|
|
61
77
|
def aggregate(dispatcher, domain, name)
|
|
62
78
|
ir = dispatcher.registry.bluebook(domain)&.aggregate(name)
|
|
63
79
|
raise Runtime::NotFound, "#{domain} declares no aggregate named #{name.inspect}" unless ir
|
|
@@ -65,14 +81,22 @@ module Hecks
|
|
|
65
81
|
Object.const_get("#{domain}::#{ir.hecks_name}")
|
|
66
82
|
end
|
|
67
83
|
|
|
84
|
+
# Names the door method that creates a record of this aggregate.
|
|
85
|
+
#
|
|
68
86
|
# The one command a POST to a bare collection URL means — "make one of
|
|
69
|
-
# these".
|
|
70
|
-
#
|
|
71
|
-
#
|
|
87
|
+
# these". A creating command is one that declares no `references`
|
|
88
|
+
# (`Command#creates?`), and the first one the aggregate declares is the
|
|
89
|
+
# one named here; this just names it the same
|
|
72
90
|
# snake_case-plus-bang a Ruby caller would already ask the door for
|
|
73
91
|
# (`Naming.snake`, the identical call `AggregateDoor` itself makes
|
|
74
92
|
# when it defines that singleton method in the first place — `!`
|
|
75
|
-
# because every command
|
|
93
|
+
# because every command carries it, door and Handle alike).
|
|
94
|
+
#
|
|
95
|
+
# @param klass [Module] an aggregate door, as `aggregate` returns; anything
|
|
96
|
+
# answering `ir` with a `Bluebook::Aggregate` works
|
|
97
|
+
# @return [String] the method name to `public_send` to the door, such as
|
|
98
|
+
# `"create_pizza!"`
|
|
99
|
+
# @raise [Runtime::NotFound] if the aggregate declares no creating command
|
|
76
100
|
def creating_command(klass)
|
|
77
101
|
creating = klass.ir.commands.find(&:creates?)
|
|
78
102
|
raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no creating command" unless creating
|
|
@@ -80,21 +104,31 @@ module Hecks
|
|
|
80
104
|
"#{Naming.snake(creating.hecks_name)}!"
|
|
81
105
|
end
|
|
82
106
|
|
|
107
|
+
# Confirms that a command name arriving as text is one a `Handle` of this aggregate
|
|
108
|
+
# answers, before a caller `public_send`s it.
|
|
109
|
+
#
|
|
83
110
|
# A URL segment or a JSON body's "command" field, checked against what
|
|
84
|
-
# a `Handle` can actually dispatch —
|
|
111
|
+
# a `Handle` can actually dispatch — not `klass.commands`, which is
|
|
85
112
|
# `AggregateDoor`'s own door-level list and includes the one creating
|
|
86
113
|
# command too (`aggregate_door.rb`'s `commands` singleton method maps
|
|
87
114
|
# every `ir.commands`, full stop). A `Handle` only ever defines
|
|
88
|
-
# singleton methods for the
|
|
115
|
+
# singleton methods for the non-creating ones
|
|
89
116
|
# (`Handle#define_verb_methods`, `@ir.commands.reject(&:creates?)`) —
|
|
90
117
|
# the creating command lives on the aggregate class itself, dispatched
|
|
91
118
|
# through `.creating_command` above, not through a `Handle` in hand.
|
|
92
|
-
# Accepting a creating-command name here let it past this gate
|
|
93
|
-
# only to blow up as a raw `NoMethodError` the moment a caller
|
|
94
|
-
# `handle.public_send(name, **args)`, instead of the 404 this
|
|
95
|
-
# promises. Filtering `reject(&:creates?)` here, the same filter
|
|
119
|
+
# Accepting a creating-command name here would let it past this gate
|
|
120
|
+
# clean, only to blow up as a raw `NoMethodError` the moment a caller
|
|
121
|
+
# tries `handle.public_send(name, **args)`, instead of the 404 this
|
|
122
|
+
# method promises. Filtering `reject(&:creates?)` here, the same filter
|
|
96
123
|
# `Handle` itself applies, is what keeps "accepted here" and
|
|
97
124
|
# "dispatchable there" the same set.
|
|
125
|
+
#
|
|
126
|
+
# @param klass [Module] an aggregate door, as `aggregate` returns
|
|
127
|
+
# @param name [String, Symbol] the wanted method name with its bang, such as
|
|
128
|
+
# `"add_topping!"`
|
|
129
|
+
# @return [String] `name` as a String, unchanged, when a `Handle` answers it
|
|
130
|
+
# @raise [Runtime::NotFound] if no non-creating command has that method name,
|
|
131
|
+
# including when `name` is the creating command's or lacks the `!`
|
|
98
132
|
def validate_command!(klass, name)
|
|
99
133
|
wanted = name.to_s
|
|
100
134
|
dispatchable = klass.ir.commands.reject(&:creates?).map { |command| "#{Naming.snake(command.hecks_name)}!" }
|
|
@@ -103,20 +137,34 @@ module Hecks
|
|
|
103
137
|
raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no command named #{wanted.inspect}"
|
|
104
138
|
end
|
|
105
139
|
|
|
140
|
+
# Fetches one record by id, refusing a miss instead of answering nil.
|
|
141
|
+
#
|
|
106
142
|
# `klass.find` already answers nil-on-miss — the right shape for a
|
|
107
143
|
# Ruby caller that means to check for itself. A JSON caller asking for
|
|
108
|
-
# one record by id off a URL means to
|
|
144
|
+
# one record by id off a URL means to have it, or answer 404 — this is
|
|
109
145
|
# that stricter wrapper, raising the same `Runtime::NotFound` the rest
|
|
110
146
|
# of this door raises rather than handing back nil for the caller to
|
|
111
147
|
# remember to check.
|
|
148
|
+
#
|
|
149
|
+
# @param klass [Module] an aggregate door, as `aggregate` returns
|
|
150
|
+
# @param id [String] the record's identity, as the URL carried it
|
|
151
|
+
# @return [Facade::Handle] the record in hand
|
|
152
|
+
# @raise [Runtime::NotFound] if the repository holds no record with that id
|
|
112
153
|
def find!(klass, id)
|
|
113
154
|
klass.find(id) or raise Runtime::NotFound, "no #{klass.ir.hecks_name} found for id #{id.inspect}"
|
|
114
155
|
end
|
|
115
156
|
|
|
157
|
+
# Converts every Hash key to a Symbol, at every depth of a parsed JSON body.
|
|
158
|
+
#
|
|
116
159
|
# JSON only ever hands back String keys. A command's args, and every
|
|
117
160
|
# nested value-object literal inside them, need symbol keys before
|
|
118
161
|
# `Handle`/`Dispatcher` will accept them at all — this is that
|
|
119
162
|
# recursive conversion, blind to how deep a body nests.
|
|
163
|
+
#
|
|
164
|
+
# @param value [Hash, Array, Object] parsed JSON: a Hash or Array is walked, any
|
|
165
|
+
# other value is a leaf
|
|
166
|
+
# @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape
|
|
167
|
+
# with Symbol keys; a leaf is returned as it came
|
|
120
168
|
def deep_symbolize(value)
|
|
121
169
|
case value
|
|
122
170
|
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
@@ -125,6 +173,9 @@ module Hecks
|
|
|
125
173
|
end
|
|
126
174
|
end
|
|
127
175
|
|
|
176
|
+
# Unwraps a record, a query row, or any value holding `Runtime::Value`s into plain
|
|
177
|
+
# Hashes, Arrays and scalars.
|
|
178
|
+
#
|
|
128
179
|
# The other direction: a `Handle`, or a query row's plain state hash,
|
|
129
180
|
# carrying a `Runtime::Value` at every level a value object sits at —
|
|
130
181
|
# down to plain Ruby a JSON encoder can walk without knowing what a
|
|
@@ -139,19 +190,44 @@ module Hecks
|
|
|
139
190
|
# already, and a `Handle` is none of those three, so the one thing
|
|
140
191
|
# this adds is `#to_h`'ing a `Handle` first so `materialize`'s own
|
|
141
192
|
# `Hash` case can take it from there.
|
|
193
|
+
#
|
|
194
|
+
# @param value [Facade::Handle, Runtime::Value, Hash, Array, Object] what to unwrap;
|
|
195
|
+
# a `Handle` is read through its `to_h`, so its `:id` comes along
|
|
196
|
+
# @return [Hash, Array, Object] the same data with every `Runtime::Value` replaced
|
|
197
|
+
# by a Hash of its fields; a value that is none of the listed types is returned
|
|
198
|
+
# as it came
|
|
142
199
|
def materialize(value)
|
|
143
200
|
value = value.to_h if value.is_a?(Handle)
|
|
144
201
|
Runtime::Value.materialize(value)
|
|
145
202
|
end
|
|
146
203
|
|
|
204
|
+
# Turns a command's JSON body, raw or already parsed, into the `to:`/`with:`
|
|
205
|
+
# envelope a dispatcher takes.
|
|
206
|
+
#
|
|
147
207
|
# Parsed JSON and raw JSON text cross the same receiver/payload boundary
|
|
148
208
|
# as CLI and forms. The result is ready to splat into Dispatcher#dispatch
|
|
149
209
|
# and contains no loose routing fields.
|
|
210
|
+
#
|
|
211
|
+
# @param body [String, Hash] raw JSON text, or the Hash it parses to, with String
|
|
212
|
+
# or Symbol keys
|
|
213
|
+
# @param receiver [Symbol, nil] the kind of receiver the command takes: `:aggregate`,
|
|
214
|
+
# `:entity`, or `nil` for none (see `CommandRequest.normalize`)
|
|
215
|
+
# @param legacy_receiver [Symbol, String, Hash{Symbol => Symbol, String}, nil] the
|
|
216
|
+
# flat key, or pair of keys, a body without `to` may name its receiver under;
|
|
217
|
+
# `nil` accepts none
|
|
218
|
+
# @return [Hash{Symbol => Object}] `{ with: facts }`, plus `to:` whenever `receiver`
|
|
219
|
+
# is not `nil`
|
|
220
|
+
# @raise [JSON::ParserError] if `body` is a String that is not valid JSON
|
|
221
|
+
# @raise [Runtime::TypeMismatch] if the body is not a JSON object, or its routing is
|
|
222
|
+
# missing, malformed, or mixed with loose keys beside an explicit `with:`
|
|
223
|
+
# @raise [ArgumentError] if `receiver` is not `nil`, `:aggregate` or `:entity`
|
|
150
224
|
def command_request(body, receiver:, legacy_receiver: nil)
|
|
151
225
|
input = body.is_a?(String) ? parse(body) : body
|
|
152
226
|
CommandRequest.normalize(input, receiver: receiver, legacy_receiver: legacy_receiver)
|
|
153
227
|
end
|
|
154
228
|
|
|
229
|
+
# Parses a raw request body, leaving keys as Strings.
|
|
230
|
+
#
|
|
155
231
|
# The one place a raw JSON string is legitimate input for this door —
|
|
156
232
|
# a POST body, still text at the point a generic, HTTP-blind layer can
|
|
157
233
|
# see it. `JSON::ParserError` already names "this wasn't JSON" exactly
|
|
@@ -160,6 +236,11 @@ module Hecks
|
|
|
160
236
|
# `JSON.parse` raises it — a calling app catches it the same standard
|
|
161
237
|
# way it would catch any other malformed-input error, no new class to
|
|
162
238
|
# learn.
|
|
239
|
+
#
|
|
240
|
+
# @param raw_json [String] JSON text, such as a POST body
|
|
241
|
+
# @return [Hash{String => Object}, Array, String, Numeric, Boolean, nil] whatever
|
|
242
|
+
# the text encodes; a JSON object becomes a Hash with String keys
|
|
243
|
+
# @raise [JSON::ParserError] if the text is not valid JSON
|
|
163
244
|
def parse(raw_json) = JSON.parse(raw_json)
|
|
164
245
|
end
|
|
165
246
|
end
|
|
@@ -13,16 +13,31 @@ module Hecks
|
|
|
13
13
|
# `persisted_by`-style binding collector a `.hecksagon` lands on, and
|
|
14
14
|
# the aggregate-scoped `port` a `.hecksagon` lands on beside it.
|
|
15
15
|
module AggregateDoor
|
|
16
|
+
# Builds the anonymous module that is one aggregate's door: a `name!` method per
|
|
17
|
+
# creating command, a bare method per query, `find`/`all`/`count`/`events`,
|
|
18
|
+
# `project`/`docs`/`narrate`, and the `.hecksagon` binding hooks.
|
|
19
|
+
#
|
|
20
|
+
# Warns once for each attribute whose name is in `RESERVED`.
|
|
21
|
+
#
|
|
16
22
|
# One method building one `door` module's ~20 singleton methods
|
|
17
23
|
# looks like it splits along each `define_singleton_method` call,
|
|
18
24
|
# but three of those blocks (`:port`, `:method_missing`,
|
|
19
25
|
# `:const_missing`) are a single cross-referencing essay on the
|
|
20
26
|
# stale-facade-across-boots hazard — each one's comment explicitly
|
|
21
|
-
# points at "below"/"above" as part of the
|
|
27
|
+
# points at "below"/"above" as part of the same method. Splitting
|
|
22
28
|
# into helper methods wouldn't break anything at runtime (no
|
|
23
29
|
# shared mutable state beyond the closed-over args, which just
|
|
24
30
|
# become parameters), but it would sever that narrative across
|
|
25
31
|
# method boundaries for no functional gain.
|
|
32
|
+
#
|
|
33
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
34
|
+
# dispatcher the door's methods dispatch, query and read repositories through
|
|
35
|
+
# @param domain [String] the owning chapter's name, the first half of the
|
|
36
|
+
# aggregate's FQN (`"Pizzas"` in `"Pizzas::Pizza"`)
|
|
37
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to build a door for
|
|
38
|
+
# @return [Module] a fresh, unnamed module carrying the door's singleton methods
|
|
39
|
+
# @raise [Bluebook::DSL::Malformed] never while building; the returned door's
|
|
40
|
+
# `port` raises it when called with no boot in progress
|
|
26
41
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
27
42
|
def aggregate_module(dispatcher, domain, aggregate)
|
|
28
43
|
fqn = "#{domain}::#{aggregate.hecks_name}"
|
|
@@ -34,13 +49,13 @@ module Hecks
|
|
|
34
49
|
warn "[hecks] #{aggregate.hecks_name}##{attribute.name} shadows a built-in — no reader defined"
|
|
35
50
|
end
|
|
36
51
|
|
|
37
|
-
# A creating verb is a
|
|
52
|
+
# A creating verb is a module method returning the new record in hand ;
|
|
38
53
|
# a verb that reaches an existing record lives on the Handle. `!` —
|
|
39
|
-
# a command
|
|
40
|
-
# convention for that ; `Naming.snake` alone
|
|
54
|
+
# a command does something (mutates, may refuse), Ruby's own
|
|
55
|
+
# convention for that ; `Naming.snake` alone would leave a
|
|
41
56
|
# creating command's bare name claiming the exact spelling a
|
|
42
|
-
#
|
|
43
|
-
# "Open" — the creating command
|
|
57
|
+
# query of the same business name also wants (`Account`'s own
|
|
58
|
+
# "Open" — the creating command and a query listing open
|
|
44
59
|
# accounts, a real same-aggregate collision this corpus already
|
|
45
60
|
# has) — the suffix is what makes both nameable at all, not
|
|
46
61
|
# merely a style choice.
|
|
@@ -51,7 +66,7 @@ module Hecks
|
|
|
51
66
|
end
|
|
52
67
|
end
|
|
53
68
|
|
|
54
|
-
# A query is a
|
|
69
|
+
# A query is a module method too — same level as a creating
|
|
55
70
|
# command, since neither needs an existing record in hand — but
|
|
56
71
|
# bare: a query reads and returns, nothing to warn a caller
|
|
57
72
|
# about the way `!` does for a command. Answers the raw row
|
|
@@ -66,7 +81,7 @@ module Hecks
|
|
|
66
81
|
door.define_singleton_method(:fqn) { fqn }
|
|
67
82
|
door.define_singleton_method(:ir) { aggregate }
|
|
68
83
|
|
|
69
|
-
#
|
|
84
|
+
# The same verb the chapter answers, one level down. Every
|
|
70
85
|
# construct emits its own IR (Hecks::IR), so an aggregate
|
|
71
86
|
# is a legitimate thing to project — `Pizzas::Order.project(
|
|
72
87
|
# Projections::IR)` is this aggregate's IR, not the chapter's.
|
|
@@ -74,8 +89,8 @@ module Hecks
|
|
|
74
89
|
# A chapter-scoped target refuses here rather than inventing an
|
|
75
90
|
# answer: `Projector.admits!` is what tells `Projections::IR`
|
|
76
91
|
# (`from: :any`) apart from `Projections::Shape`
|
|
77
|
-
# (`from: :chapter`), which
|
|
78
|
-
# `{"aggregates" => []}` for an aggregate.
|
|
92
|
+
# (`from: :chapter`), which would otherwise answer a confidently
|
|
93
|
+
# empty `{"aggregates" => []}` for an aggregate.
|
|
79
94
|
door.define_singleton_method(:project) do |target, out: nil, **options|
|
|
80
95
|
key = Projector.key_for(target)
|
|
81
96
|
artifact = Projector.call(key, bluebook: aggregate, options: options)
|
|
@@ -88,7 +103,7 @@ module Hecks
|
|
|
88
103
|
aggregate.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort
|
|
89
104
|
end
|
|
90
105
|
door.define_singleton_method(:queries) { aggregate.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
|
|
91
|
-
#
|
|
106
|
+
# **One aggregate's usage document** — the same projection the chapter
|
|
92
107
|
# answers with, narrowed to this head. `commands` above already
|
|
93
108
|
# answers "what can I call"; this answers "and what does each one
|
|
94
109
|
# want, refuse, and guarantee", which is the rest of the question.
|
|
@@ -96,7 +111,7 @@ module Hecks
|
|
|
96
111
|
Projector.call(:docs, bluebook: dispatcher.registry.bluebook(domain),
|
|
97
112
|
options: options.merge(aggregate: aggregate.hecks_name))
|
|
98
113
|
end
|
|
99
|
-
#
|
|
114
|
+
# **One aggregate, read back in english** — the same narrowing `:docs`
|
|
100
115
|
# takes, aimed at `:narrate` instead.
|
|
101
116
|
door.define_singleton_method(:narrate) do |**options|
|
|
102
117
|
Projector.call(:narrate, bluebook: dispatcher.registry.bluebook(domain),
|
|
@@ -116,18 +131,18 @@ module Hecks
|
|
|
116
131
|
end
|
|
117
132
|
end
|
|
118
133
|
|
|
119
|
-
#
|
|
120
|
-
# left over from a
|
|
134
|
+
# The same reason `method_missing` below exists at all — a facade
|
|
135
|
+
# left over from a previous boot in this process shadows the fresh
|
|
121
136
|
# `BindingProxy` a `.hecksagon` would otherwise reach through
|
|
122
137
|
# `ConstShim`/`const_missing`, so `Pizzas::Pizza.port(...)` lands
|
|
123
|
-
#
|
|
138
|
+
# here instead once any boot has run before.
|
|
124
139
|
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
140
|
+
# Re-resolved, not the closed-over `aggregate` — this door can be a
|
|
141
|
+
# stale one, built by a boot from earlier in this same process,
|
|
127
142
|
# sitting on the `Pizzas`/`Pizza` constants only because nothing
|
|
128
|
-
# has re-installed them since. Attaching to this door's
|
|
143
|
+
# has re-installed them since. Attaching to this door's own
|
|
129
144
|
# `aggregate` would attach the port to a discarded aggregate from
|
|
130
|
-
# that old boot, invisible to the
|
|
145
|
+
# that old boot, invisible to the current one actually being
|
|
131
146
|
# loaded — silently, the exact way `method_missing` below already
|
|
132
147
|
# has to avoid it for a plain bind, via `HecksagonBuilder.collector`
|
|
133
148
|
# rather than anything this door closes over. `Hecks.current_registry`
|
|
@@ -166,18 +181,18 @@ module Hecks
|
|
|
166
181
|
!Bluebook::DSL::HecksagonBuilder.collector.nil? || super(name, include_private)
|
|
167
182
|
end
|
|
168
183
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
184
|
+
# The same stale-facade hazard `method_missing`/`port` above already
|
|
185
|
+
# document, one door lower — `Surface.install` installs an aggregate's
|
|
186
|
+
# own name as a bare top-level constant too (`Namespace.install(Object,
|
|
172
187
|
# aggregate.hecks_name, ...)`, surface.rb's own `install`), not only
|
|
173
|
-
# nested under its chapter. So once
|
|
174
|
-
# process, `Account::Debit` written while declaring some
|
|
175
|
-
# bluebook — same domain or a different one — reaches
|
|
188
|
+
# nested under its chapter. So once any domain has booted once in this
|
|
189
|
+
# process, `Account::Debit` written while declaring some other
|
|
190
|
+
# bluebook — same domain or a different one — reaches this door's
|
|
176
191
|
# const_missing directly, never `Object.const_missing`/`ConstShim::
|
|
177
192
|
# Hook` at all: real modules resolve without ever calling that.
|
|
178
193
|
#
|
|
179
|
-
# `aggregate.hecks_name`,
|
|
180
|
-
# reference has to read the
|
|
194
|
+
# `aggregate.hecks_name`, not the qualified `fqn` — a scoped
|
|
195
|
+
# reference has to read the same either way, whether or not a stale
|
|
181
196
|
# door happens to be sitting on this process from an earlier boot;
|
|
182
197
|
# qualifying it here would make `Account::Debit`'s own meaning
|
|
183
198
|
# depend on incidental process history, which is the exact
|