hecks 1.3.0 → 1.5.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 +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -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/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -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 +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- 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 +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- 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 +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- 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 +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- 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 +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -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 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- 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 +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -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 +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- 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 +118 -66
- 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 +34 -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 +33 -9
- 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 +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- 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 +48 -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 +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -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 +23 -3
- 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 +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- 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 +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- 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 +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
data/lib/hecks/facade/handle.rb
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
require_relative "../naming"
|
|
2
|
+
require_relative "../runtime/caller"
|
|
3
|
+
require_relative "../ports/authorization"
|
|
2
4
|
|
|
3
5
|
module Hecks
|
|
4
6
|
module Facade
|
|
5
|
-
#
|
|
7
|
+
# One record in hand — the object `Pizza.create_pizza!(...)` and
|
|
6
8
|
# `Pizza.find(id)` give back.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# verbs through
|
|
12
|
-
# aggregate's IR — so a boot mints no
|
|
13
|
-
# one process each hand out handles bound
|
|
10
|
+
# One shared class, not one minted per aggregate. Rather than subclassing
|
|
11
|
+
# per head and defining a reader per field, this wraps a
|
|
12
|
+
# `Runtime::Instance` state hash, answers readers through
|
|
13
|
+
# `method_missing` and verbs through per-handle singleton methods, and
|
|
14
|
+
# closes over the dispatcher and the aggregate's IR — so a boot mints no
|
|
15
|
+
# classes at all, and two boots in one process each hand out handles bound
|
|
16
|
+
# to their own dispatcher.
|
|
14
17
|
#
|
|
15
18
|
# A non-creating verb is a method returning self, so commands chain :
|
|
16
19
|
#
|
|
17
|
-
# Pizza.create_pizza(...).add_topping(...).purchase(...)
|
|
20
|
+
# Pizza.create_pizza!(...).add_topping!(...).purchase!(...)
|
|
18
21
|
class Handle
|
|
19
22
|
attr_reader :id
|
|
20
23
|
|
|
24
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
25
|
+
# dispatcher this record's verbs, `events` and `reload` go through
|
|
26
|
+
# @param domain [String] the owning chapter's name, the first half of `fqn`
|
|
27
|
+
# @param aggregate [Bluebook::Aggregate] the IR of the aggregate this record is one of
|
|
28
|
+
# @param instance [Runtime::Instance] the stored record; its `id` and `state` are
|
|
29
|
+
# read once here and the instance itself is not retained
|
|
21
30
|
def initialize(dispatcher:, domain:, aggregate:, instance:)
|
|
22
31
|
@dispatcher = dispatcher
|
|
23
32
|
@domain = domain
|
|
@@ -28,37 +37,67 @@ module Hecks
|
|
|
28
37
|
define_verb_methods
|
|
29
38
|
end
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
# Reads one field's raw stored value, without hydrating a reference the way the
|
|
41
|
+
# reference accessor of the same name does.
|
|
42
|
+
#
|
|
43
|
+
# @param key [Symbol, String] the attribute name
|
|
44
|
+
# @return [Object, nil] the value held in state (a scalar, a value object, a list,
|
|
45
|
+
# or a referenced record's id); `nil` when the field is unset or not in state
|
|
46
|
+
def [](key) = redacted(key.to_sym)
|
|
32
47
|
|
|
33
|
-
#
|
|
48
|
+
# Answers the record's state as a plain Hash with the bare identity under `:id`.
|
|
49
|
+
#
|
|
50
|
+
# `id: @id` last, not first — an aggregate is free to declare its own
|
|
34
51
|
# attribute literally named `id` (BurningManPrep's `Item`, `attribute
|
|
35
52
|
# :id, ItemId`, is real corpus now: `identified_by :id` reads
|
|
36
|
-
#
|
|
53
|
+
# that attribute for identity). When it does, `@state[:id]` holds the
|
|
37
54
|
# full wrapped value object, not the bare identity string — merging
|
|
38
|
-
# `@state` on top of `{ id: @id }` let that wrapped VO silently
|
|
55
|
+
# `@state` on top of `{ id: @id }` would let that wrapped VO silently
|
|
39
56
|
# 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
|
|
57
|
+
# door's own `/api/:coll` listing, in particular) would get an object
|
|
58
|
+
# where a plain identity string belongs. `@id` merged last always wins,
|
|
42
59
|
# so `to_h[:id]` is always the true bare identity, regardless of
|
|
43
60
|
# whether the aggregate also happens to declare a same-named field.
|
|
44
|
-
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
|
|
63
|
+
# name, plus `:id` holding the identity String
|
|
64
|
+
def to_h = @state.to_h { |key, _| [key, redacted(key)] }.merge(id: @id)
|
|
45
65
|
|
|
66
|
+
# Names the aggregate this record belongs to, in the form every dispatch verb and
|
|
67
|
+
# event is addressed by.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] the fully qualified aggregate name, such as `"Pizzas::Pizza"`
|
|
46
70
|
def fqn = "#{@domain}::#{@ir.hecks_name}"
|
|
47
71
|
|
|
72
|
+
# Lists the events this one record has emitted, filtered out of the dispatcher's
|
|
73
|
+
# whole event log on each call.
|
|
74
|
+
#
|
|
75
|
+
# @return [Array<Runtime::Event>] this record's events in the order the log holds
|
|
76
|
+
# them; `[]` when it has emitted none
|
|
48
77
|
def events
|
|
49
78
|
@dispatcher.events.select { |event| event.aggregate == fqn && event.id == @id }
|
|
50
79
|
end
|
|
51
80
|
|
|
81
|
+
# Refreshes this handle's state from the repository, picking up writes made through
|
|
82
|
+
# another handle or door. Keeps the current state when the record is not found.
|
|
83
|
+
#
|
|
84
|
+
# @return [Facade::Handle] this handle, so the call chains
|
|
85
|
+
# @raise [Runtime::WiringError] if the aggregate's persistence bind cannot be
|
|
86
|
+
# resolved into a repository
|
|
52
87
|
def reload
|
|
53
88
|
stored = repository.find(@id)
|
|
54
89
|
@state = stored.state if stored
|
|
55
90
|
self
|
|
56
91
|
end
|
|
57
92
|
|
|
58
|
-
# Equality is (
|
|
93
|
+
# Equality is (which aggregate, which ID) — two handles to the same record
|
|
59
94
|
# are the same record, and a Pizza never equals an Account that happens to
|
|
60
|
-
# share an id.
|
|
61
|
-
#
|
|
95
|
+
# share an id. With one shared class for every aggregate,
|
|
96
|
+
# `other.is_a?(self.class)` cannot tell them apart ; the fqn says it in data.
|
|
97
|
+
#
|
|
98
|
+
# @param other [Object] anything; only another `Handle` can be equal
|
|
99
|
+
# @return [Boolean] true when `other` is a `Handle` with the same `fqn` and `id`,
|
|
100
|
+
# whatever state either one holds
|
|
62
101
|
def ==(other) = other.is_a?(Handle) && other.fqn == fqn && other.id == @id
|
|
63
102
|
alias eql? ==
|
|
64
103
|
def hash = [Handle, fqn, @id].hash
|
|
@@ -69,11 +108,21 @@ module Hecks
|
|
|
69
108
|
end
|
|
70
109
|
alias to_s inspect
|
|
71
110
|
|
|
111
|
+
# Answers a field reader: `pizza.name` reads `name` out of state.
|
|
112
|
+
#
|
|
72
113
|
# A declared field not yet written arrives here too (nil, the way a
|
|
73
|
-
# defined reader
|
|
114
|
+
# defined reader answers). Verbs are not handled here — see
|
|
74
115
|
# `define_verb_methods` for why.
|
|
116
|
+
#
|
|
117
|
+
# @param name [Symbol] the method called, read as an attribute or lifecycle field name
|
|
118
|
+
# @param args [Array<Object>] ignored by a reader; passed on to `super` otherwise
|
|
119
|
+
# @param kwargs [Hash{Symbol => Object}] ignored by a reader; passed on to `super`
|
|
120
|
+
# otherwise
|
|
121
|
+
# @return [Object, nil] the field's value; `nil` for a declared field with nothing
|
|
122
|
+
# written yet
|
|
123
|
+
# @raise [NoMethodError] if `name` is neither a key in state nor a declared field
|
|
75
124
|
def method_missing(name, *args, **kwargs, &)
|
|
76
|
-
return
|
|
125
|
+
return redacted(name) if @state.key?(name) || reader?(name)
|
|
77
126
|
|
|
78
127
|
super
|
|
79
128
|
end
|
|
@@ -86,20 +135,91 @@ module Hecks
|
|
|
86
135
|
|
|
87
136
|
def repository = @dispatcher.registry.repository(@domain, @ir)
|
|
88
137
|
|
|
138
|
+
# One field's value, with any Privacy::Marking-flagged leaf masked
|
|
139
|
+
# out unless the ambient caller holds a live Governance grant of
|
|
140
|
+
# the marking's own `readable_by` — the read-side half of the
|
|
141
|
+
# Privacy framework member (lib/hecks/framework/bluebook/
|
|
142
|
+
# privacy.bluebook): a marking's presence is what makes a read
|
|
143
|
+
# redacted, not a separate flag this class carries itself.
|
|
144
|
+
#
|
|
145
|
+
# ALWAYS THE STRONG CHECK, never the weak string-only fallback
|
|
146
|
+
# `CommandRules::Authorization#refuse_role_mismatch` allows an
|
|
147
|
+
# unidentified caller — a read gate gone wrong is a leak, not a
|
|
148
|
+
# refused command, so an ambient caller with no `actor_id` (or no
|
|
149
|
+
# caller at all) is masked here, full stop, rather than waved
|
|
150
|
+
# through the way a self-asserted `role` string is for a command.
|
|
151
|
+
#
|
|
152
|
+
# ONE LEVEL OF NESTING ONLY — `attendee.medications` masks inside
|
|
153
|
+
# the returned `Runtime::Value` via its own `#with`; a marking two
|
|
154
|
+
# levels deep is not supported and is left unmasked rather than
|
|
155
|
+
# silently mishandled, since nothing in this corpus needs it yet.
|
|
156
|
+
#
|
|
157
|
+
# @param field [Symbol] the state key being read
|
|
158
|
+
# @return [Object, nil] `@state[field]`, or a copy with the marked leaf replaced by
|
|
159
|
+
# the literal String `"[redacted]"` when the caller is not authorized to see it
|
|
160
|
+
def redacted(field)
|
|
161
|
+
raw = @state[field]
|
|
162
|
+
rows = marked_paths.select { |row| row[:attribute_path][:value].to_s.split(".", 2).first == field.to_s }
|
|
163
|
+
return raw if rows.empty?
|
|
164
|
+
|
|
165
|
+
rows.each do |row|
|
|
166
|
+
path = row[:attribute_path][:value].to_s
|
|
167
|
+
next if authorized_for?(row[:readable_by][:value].to_s)
|
|
168
|
+
|
|
169
|
+
segments = path.split(".", 2)
|
|
170
|
+
if segments.size == 1
|
|
171
|
+
raw = "[redacted]"
|
|
172
|
+
elsif raw.is_a?(Runtime::Value)
|
|
173
|
+
raw = raw.with(segments[1], "[redacted]")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
raw
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Every Privacy::Marking declared for this record's own aggregate — `[]` when the
|
|
181
|
+
# Privacy framework member is not attached in this boot at all, checked once and
|
|
182
|
+
# cheaply rather than paying for a query dispatch every read on a domain that never
|
|
183
|
+
# attached Privacy.
|
|
184
|
+
#
|
|
185
|
+
# @return [Array<Hash>] `Privacy::Marking.ForDomain`'s own rows for this `fqn`
|
|
186
|
+
def marked_paths
|
|
187
|
+
return @marked_paths if defined?(@marked_paths)
|
|
188
|
+
return @marked_paths = [] unless @dispatcher.registry.bluebook("Privacy")
|
|
189
|
+
|
|
190
|
+
@marked_paths = @dispatcher.query("Privacy::Marking.ForDomain", domain: fqn)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Whether the ambient caller holds a live Governance grant of `role`, over this
|
|
194
|
+
# record's own domain — `false` outright for an unidentified caller or a domain
|
|
195
|
+
# with no authorization provider attached, never the weak fallback a command's own
|
|
196
|
+
# role check allows (see `redacted`'s own header for why).
|
|
197
|
+
#
|
|
198
|
+
# @param role [String] the marking's own `readable_by`
|
|
199
|
+
# @return [Boolean] true only for an identified caller holding a live grant
|
|
200
|
+
def authorized_for?(role)
|
|
201
|
+
caller = Runtime::Caller.current
|
|
202
|
+
return false unless caller&.actor_id
|
|
203
|
+
return false unless @dispatcher.registry.authorization_provider_for(@domain)
|
|
204
|
+
|
|
205
|
+
Ports::Authorization.holds_role?(@dispatcher.registry, actor_id: caller.actor_id, role: role,
|
|
206
|
+
as_of: caller.as_of, scope: caller.scope)
|
|
207
|
+
end
|
|
208
|
+
|
|
89
209
|
def reader?(name)
|
|
90
210
|
!@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
|
|
91
211
|
end
|
|
92
212
|
|
|
93
|
-
#
|
|
213
|
+
# Non-creating verbs are defined, not dispatched through method_missing.
|
|
94
214
|
#
|
|
95
|
-
# method_missing only runs once Ruby finds no
|
|
215
|
+
# method_missing only runs once Ruby finds no real method already
|
|
96
216
|
# answering the name — and every object already answers `freeze` and
|
|
97
217
|
# `send` (Kernel/Object), among others. A verb whose snake-cased name
|
|
98
218
|
# collided with one of those — `Account::Freeze` -> `freeze`,
|
|
99
219
|
# `ExternalTransfer::Send` -> `send` in the banking corpus, both real —
|
|
100
|
-
#
|
|
101
|
-
# error, no refusal, the call just
|
|
102
|
-
# real singleton method per verb
|
|
220
|
+
# would silently run the Kernel method instead of dispatching: no
|
|
221
|
+
# error, no refusal, the call just does the wrong thing. Defining a
|
|
222
|
+
# real singleton method per verb closes that; the `!` suffix (every
|
|
103
223
|
# command, door and Handle alike) closes it a second, permanent way —
|
|
104
224
|
# `freeze!`/`send!` name nothing Kernel/Object already answers to,
|
|
105
225
|
# so this exact class of collision cannot recur no matter what a
|
|
@@ -112,7 +232,7 @@ module Hecks
|
|
|
112
232
|
end
|
|
113
233
|
end
|
|
114
234
|
|
|
115
|
-
#
|
|
235
|
+
# One head addresses the same way as several. `@ir.identified_by` is only
|
|
116
236
|
# the single-head shorthand — nil the moment an identity is composite
|
|
117
237
|
# (`SafeDepositBox`'s `branch_code`/`box_number`) — so building the
|
|
118
238
|
# identity payload from `identity_heads` instead reads every head, one
|
|
@@ -122,7 +242,7 @@ module Hecks
|
|
|
122
242
|
self
|
|
123
243
|
end
|
|
124
244
|
|
|
125
|
-
#
|
|
245
|
+
# **The other half of a cross-reference**. `transfer.source` already reads
|
|
126
246
|
# the raw value — a plain reader, same as any other attribute, still
|
|
127
247
|
# needed by a `given`. This is the hydrated hop docs/rails-integration.md
|
|
128
248
|
# designed and marked "nothing built": `transfer.source_account`
|
|
@@ -134,25 +254,25 @@ module Hecks
|
|
|
134
254
|
# option — that shape was considered and rejected in the same design
|
|
135
255
|
# note for hiding how many lookups actually happened behind one call.
|
|
136
256
|
#
|
|
137
|
-
# Defined
|
|
257
|
+
# Defined before verb methods, not after — on the vanishing chance a
|
|
138
258
|
# reference's own accessor name collided with a command's, the verb
|
|
139
259
|
# should win; `initialize` calls this first so `define_verb_methods`
|
|
140
260
|
# defines second and last.
|
|
141
|
-
#
|
|
261
|
+
# No derivation left (ADR 0025, "References"): `reference_to`
|
|
142
262
|
# itself mints the bare attribute name now — `:account`, never
|
|
143
263
|
# `:account_id` — so the accessor is spelled exactly like the
|
|
144
264
|
# attribute it reads, with no `_id`-strip or `as:`-suffix rule to
|
|
145
|
-
# apply first. `piece.account` (a
|
|
265
|
+
# apply first. `piece.account` (a method, defined here) and
|
|
146
266
|
# `piece[:account]` (`Handle#[]`, bracket access reading the raw
|
|
147
267
|
# 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.
|
|
268
|
+
# name — Ruby dispatches the two completely differently — so no
|
|
269
|
+
# renamed accessor (a "studio_studio"-style double suffix) is needed
|
|
270
|
+
# to keep them apart.
|
|
152
271
|
def define_reference_accessors
|
|
153
272
|
@ir.attributes.select(&:reference?).each do |attribute|
|
|
154
273
|
target = attribute.type.resolve
|
|
155
|
-
|
|
274
|
+
# Cross-domain, or otherwise unresolvable — no accessor rather than a guess.
|
|
275
|
+
next unless target
|
|
156
276
|
|
|
157
277
|
domain = @domain
|
|
158
278
|
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
|