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
|
@@ -12,7 +12,7 @@ module Hecks
|
|
|
12
12
|
# typed Value. Extended into Value, so every method here reads as
|
|
13
13
|
# `Value.for`, `Value.build`, … — `self` is the Value class.
|
|
14
14
|
module Coercion
|
|
15
|
-
#
|
|
15
|
+
# The four `SHAPES` an attribute's value can take — named here because
|
|
16
16
|
# `for_attribute` immediately below is the one place that actually
|
|
17
17
|
# branches on all four, and nowhere else in the language collects
|
|
18
18
|
# them into a single closed list. `Attribute#list?`/`#optional?`
|
|
@@ -23,7 +23,7 @@ module Hecks
|
|
|
23
23
|
# below — but the branch is exactly as real, so it gets a name here
|
|
24
24
|
# too rather than staying anonymous.
|
|
25
25
|
#
|
|
26
|
-
# A
|
|
26
|
+
# A second runtime's kernel ports this method by hand (rust/src/
|
|
27
27
|
# kernel/attribute_shapes/*.rs — one file per name in this array,
|
|
28
28
|
# generated into a Rust enum by bin/project_kernel_capabilities so
|
|
29
29
|
# every match over it is compiler-checked exhaustive). If a fifth
|
|
@@ -34,6 +34,20 @@ module Hecks
|
|
|
34
34
|
# about no matter how correct the Ruby below is.
|
|
35
35
|
SHAPES = %i[scalar list optional composite].freeze
|
|
36
36
|
|
|
37
|
+
# Coerces `value` for one of `aggregate`'s declared attributes, by name.
|
|
38
|
+
#
|
|
39
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
40
|
+
# `name` is looked up on
|
|
41
|
+
# @param name [String, Symbol] the declared attribute name
|
|
42
|
+
# @param value [Object] the raw value to coerce
|
|
43
|
+
# @return [Runtime::Value, Object, nil] `value` unchanged when `aggregate`
|
|
44
|
+
# declares no such attribute; otherwise `for_attribute`'s own result
|
|
45
|
+
# @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
|
|
46
|
+
# attribute's declared type
|
|
47
|
+
# @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
|
|
48
|
+
# attribute's own value-object type does not declare
|
|
49
|
+
# @raise [Runtime::InvariantViolation] if a coerced value object breaks one
|
|
50
|
+
# of its own invariants
|
|
37
51
|
def for(aggregate, name, value)
|
|
38
52
|
attribute = aggregate.attribute(name)
|
|
39
53
|
return value unless attribute
|
|
@@ -49,13 +63,13 @@ module Hecks
|
|
|
49
63
|
# recursively via `build`) with `scalar` as what's left once
|
|
50
64
|
# neither of those applies (the raw value, passed through
|
|
51
65
|
# unchanged).
|
|
52
|
-
# `boundary: false` is the
|
|
66
|
+
# `boundary: false` is the query door (`QueryInterpreter#normalize_args`):
|
|
53
67
|
# a query attribute's declared type names the argument for callers and
|
|
54
68
|
# generators, never a runtime shape — comparison unwraps both sides
|
|
55
69
|
# itself, so a `reference: {value: ...}` offered against a `String`
|
|
56
70
|
# query field is the documented allowance (see banking's own
|
|
57
71
|
# `Account.OpenForCustomer`), not a C3.8 mismatch.
|
|
58
|
-
# `argument: true` is the
|
|
72
|
+
# `argument: true` is the command/entity/port argument door only
|
|
59
73
|
# (`Interpreting#normalize_args`, every command/entity/port
|
|
60
74
|
# dispatch): the one place a nil for a non-optional attribute is
|
|
61
75
|
# the caller leaving a required argument empty (C3.7), absorbed
|
|
@@ -66,11 +80,35 @@ module Hecks
|
|
|
66
80
|
# legitimate "absent is not empty" value the aggregate's own
|
|
67
81
|
# attribute may hold. `QueryInterpreter#normalize_args` never
|
|
68
82
|
# passes `argument: true` — a null required value-object-typed
|
|
69
|
-
#
|
|
83
|
+
# query argument is checked, and refused, entirely on its own
|
|
70
84
|
# side (`null_vo_argument!`, query_interpreter.rb) precisely so
|
|
71
|
-
# it does
|
|
85
|
+
# it does not reach this default-absorbing fallback (QualityControl
|
|
72
86
|
# BUG#36 — a query's own null VO argument must refuse regardless
|
|
73
87
|
# of any default, unlike a command's).
|
|
88
|
+
#
|
|
89
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
90
|
+
# `attribute` is declared on
|
|
91
|
+
# @param attribute [Bluebook::Attribute, nil] the attribute to coerce `value`
|
|
92
|
+
# for; nil is treated as an unknown attribute (no shape to coerce against)
|
|
93
|
+
# @param value [Object] the raw value to coerce
|
|
94
|
+
# @param boundary [Boolean] whether a bare-primitive attribute's own scalar
|
|
95
|
+
# shape is checked; false at the query door, where a declared type names
|
|
96
|
+
# the argument for callers/generators only
|
|
97
|
+
# @param argument [Boolean] whether this is the command/entity/port argument
|
|
98
|
+
# door, where a nil for a non-optional attribute is a left-empty argument
|
|
99
|
+
# (C3.7) rather than ordinary state-assembly nil
|
|
100
|
+
# @return [Runtime::Value, Object, nil] the coerced value: a built `Value`
|
|
101
|
+
# for a composite-typed attribute, a frozen Array for a `has_many`
|
|
102
|
+
# reference or hydrated entity list, a joined identity String for a
|
|
103
|
+
# scalar reference, or `value` passed through unchanged for a bare
|
|
104
|
+
# primitive or an unknown attribute; nil for an absent optional attribute
|
|
105
|
+
# @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
|
|
106
|
+
# attribute's declared type, or a required reference/attribute is offered
|
|
107
|
+
# as a wrong-shaped value
|
|
108
|
+
# @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
|
|
109
|
+
# attribute's own value-object type does not declare
|
|
110
|
+
# @raise [Runtime::InvariantViolation] if a coerced value object breaks one
|
|
111
|
+
# of its own invariants
|
|
74
112
|
def for_attribute(aggregate, attribute, value, boundary: true, argument: false)
|
|
75
113
|
return nil_or_missing(aggregate, attribute, value, argument) if attribute.nil? || value.nil?
|
|
76
114
|
return reference_list(attribute, value) if attribute.list? && attribute.reference?
|
|
@@ -78,12 +116,12 @@ module Hecks
|
|
|
78
116
|
return hydrate_entity_list(aggregate, attribute, value) if attribute.list? # :list
|
|
79
117
|
return value unless aggregate.respond_to?(:value_object)
|
|
80
118
|
|
|
81
|
-
#
|
|
119
|
+
# The set the attribute names is checked where the attribute is known.
|
|
82
120
|
# `build` below sees only the value object, never which attribute asked
|
|
83
121
|
# for it, so a command argument's `admits:` has to be read here — this
|
|
84
122
|
# is the door every argument and every head field comes through.
|
|
85
123
|
#
|
|
86
|
-
#
|
|
124
|
+
# After coercion, not before: a scalar arrives wrapped in whatever holder
|
|
87
125
|
# its type names (`{value: "append"}` for an OpName), and checking the
|
|
88
126
|
# raw payload would be checking the envelope.
|
|
89
127
|
value_object = value_object_for(aggregate, attribute.type)
|
|
@@ -99,12 +137,12 @@ module Hecks
|
|
|
99
137
|
coerced
|
|
100
138
|
end
|
|
101
139
|
|
|
102
|
-
# NIL
|
|
140
|
+
# NIL is not a value for a non-optional argument (C3.7/C3.8). An
|
|
103
141
|
# `optional:` attribute and a load from the store pass nil through
|
|
104
142
|
# as they always did; a command argument offered as null for a
|
|
105
143
|
# required attribute is
|
|
106
144
|
# refused as the field the caller left empty — a value object is
|
|
107
|
-
#
|
|
145
|
+
# built from no fields, so its first required field refuses with
|
|
108
146
|
# exactly the wording the Rust side's `from_json` gives it
|
|
109
147
|
# ("Money.cents expects Integer, got nil"), and a bare scalar
|
|
110
148
|
# refuses through `check_bare_primitive`'s own wording. Lists and
|
|
@@ -112,7 +150,7 @@ module Hecks
|
|
|
112
150
|
# relationship, `validate_relationship_cardinality`'s business).
|
|
113
151
|
# The `attribute.nil?`/`value.nil?` branch of `for_attribute`,
|
|
114
152
|
# pulled out on its own — an unknown attribute has no shape left
|
|
115
|
-
# to branch on, and a nil
|
|
153
|
+
# to branch on, and a nil value is either an ordinary absence
|
|
116
154
|
# (state assembly, hydration, a query ask) or, at the argument
|
|
117
155
|
# door only, `nil_argument`'s own C3.7 refusal.
|
|
118
156
|
private def nil_or_missing(aggregate, attribute, value, argument)
|
|
@@ -129,12 +167,12 @@ module Hecks
|
|
|
129
167
|
return build(value_object, {}, aggregate) if value_object
|
|
130
168
|
|
|
131
169
|
raise TypeMismatch,
|
|
132
|
-
RefusalWording.
|
|
133
|
-
|
|
134
|
-
|
|
170
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
171
|
+
type: aggregate.hecks_name, field: attribute.name,
|
|
172
|
+
expected: attribute.type, offered: "nil")
|
|
135
173
|
end
|
|
136
174
|
|
|
137
|
-
# A
|
|
175
|
+
# A bare primitive is type-checked at the boundary too (C3.8,
|
|
138
176
|
# docs/semantics/bluebook-semantics.md) — the same two predicates a
|
|
139
177
|
# value object's own fields get, so wrong-typed caller input is a
|
|
140
178
|
# TypeMismatch refusal here, never an evaluation fault later. Its
|
|
@@ -145,10 +183,20 @@ module Hecks
|
|
|
145
183
|
value
|
|
146
184
|
end
|
|
147
185
|
|
|
186
|
+
# Resolves the value-object class `type` names, searching `aggregate`'s own
|
|
187
|
+
# declarations first, then its chapter's other aggregates.
|
|
188
|
+
#
|
|
148
189
|
# Aggregate-local value objects remain authoritative, which permits
|
|
149
190
|
# intentional duplication. An ordinary fact may also name an identity
|
|
150
191
|
# value object declared on another aggregate; that shape is borrowed
|
|
151
192
|
# only when every chapter declaration with the name agrees.
|
|
193
|
+
#
|
|
194
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct to
|
|
195
|
+
# search first
|
|
196
|
+
# @param type [String, Symbol, #to_s] the declared type name to resolve
|
|
197
|
+
# @return [Class, nil] the `Bluebook::ValueObject` subclass `type` names; nil
|
|
198
|
+
# if `aggregate` declares none, and its chapter's other aggregates disagree
|
|
199
|
+
# on the shape of every same-named one (or declare none either)
|
|
152
200
|
def value_object_for(aggregate, type)
|
|
153
201
|
local = aggregate.value_object(type)
|
|
154
202
|
return local if local
|
|
@@ -163,11 +211,21 @@ module Hecks
|
|
|
163
211
|
shapes.size == 1 ? matches.first : nil
|
|
164
212
|
end
|
|
165
213
|
|
|
214
|
+
# Coerces a reference-typed attribute's offered value into the target's own
|
|
215
|
+
# canonical identity string.
|
|
216
|
+
#
|
|
166
217
|
# Retained relationships store canonical target identities, not Ruby
|
|
167
218
|
# Value wrappers. Raw scalar IDs remain a compatibility input. A named
|
|
168
219
|
# identity VO omits its minted aggregate field at the command boundary;
|
|
169
220
|
# a bespoke compound VO may instead name the target heads directly.
|
|
170
221
|
# Neither form requires reverse-splitting a canonical ID.
|
|
222
|
+
#
|
|
223
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute
|
|
224
|
+
# @param value [Object] the offered value: a bare scalar identity, a
|
|
225
|
+
# `Runtime::Value`, or a Hash naming the target's own identity fields
|
|
226
|
+
# @return [String, Object] the joined canonical identity String once every
|
|
227
|
+
# identity part resolves; `value` unchanged otherwise (a bare scalar, an
|
|
228
|
+
# unresolvable target, or a shape `sole_scalar_identity` cannot unwrap)
|
|
171
229
|
def reference_identity(attribute, value)
|
|
172
230
|
return value unless value.is_a?(self) || value.is_a?(Hash)
|
|
173
231
|
|
|
@@ -180,14 +238,64 @@ module Hecks
|
|
|
180
238
|
|
|
181
239
|
direct_head = direct_identity_head(value, target)
|
|
182
240
|
parts = paths.map { |path| identity_part(materialized, path, direct_head) }
|
|
183
|
-
|
|
241
|
+
if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
242
|
+
return sole_scalar_identity(value, paths) || value
|
|
243
|
+
end
|
|
184
244
|
|
|
185
245
|
Naming.identity(parts)
|
|
186
246
|
end
|
|
187
247
|
|
|
248
|
+
# A command may redeclare a `reference_to` field under its own,
|
|
249
|
+
# differently-named single-attribute value object (`attribute
|
|
250
|
+
# :venue, VenueHandle; sets :venue`, not `reference_to Venue` —
|
|
251
|
+
# QualityControl BUG#121, `qa/stress_domains/generated_revalued_
|
|
252
|
+
# shape`'s own `Hangar.Repoint`) instead of naming the target's
|
|
253
|
+
# own identity field(s) directly. `identity_part`'s path walk
|
|
254
|
+
# above only ever matches an incoming shape that already uses the
|
|
255
|
+
# target's own field names (or is the target's own identity value
|
|
256
|
+
# object, `direct_identity_head`) — an ad hoc wrapper around a
|
|
257
|
+
# bare scalar under some other field name (`VenueHandle`'s own
|
|
258
|
+
# `value`, not `Venue`'s own `code`) fails every path lookup, and
|
|
259
|
+
# without this would fall through to the unresolved `return value`
|
|
260
|
+
# above, storing the wrapped Value. That leaves one reference field on
|
|
261
|
+
# one aggregate holding two different shapes depending on which
|
|
262
|
+
# command last wrote it — `Open`'s own bare `reference_to Venue`
|
|
263
|
+
# argument was never wrapped in the first place (the top guard
|
|
264
|
+
# clause passes a bare scalar straight through), so it always
|
|
265
|
+
# stored the canonical bare identity this class's own header
|
|
266
|
+
# comment promises ("canonical target identities, not Ruby Value
|
|
267
|
+
# wrappers") — while `Repoint` silently kept the wrapper instead.
|
|
268
|
+
#
|
|
269
|
+
# Unambiguous only when both sides admit exactly one scalar: the
|
|
270
|
+
# target names exactly one identity path (`paths.one?` — a
|
|
271
|
+
# compound identity has no single field either side could stand
|
|
272
|
+
# in for) and the offered value is itself a single-attribute
|
|
273
|
+
# value object (`sole_attribute` — a multi-field VO has no one
|
|
274
|
+
# scalar to unwrap either). `materialize_unwrapped` recurses
|
|
275
|
+
# through any further single-field wrapping the same way it
|
|
276
|
+
# already does for `Value.materialize_unwrapped`'s other callers,
|
|
277
|
+
# landing on the bare scalar `Open`'s own path already produces
|
|
278
|
+
# for the identical target field.
|
|
279
|
+
#
|
|
280
|
+
# @param value [Object] the offered reference value to unwrap
|
|
281
|
+
# @param paths [Array<String>] the target's own declared identity paths
|
|
282
|
+
# @return [Object, nil] the unwrapped scalar when `value` is a single-field
|
|
283
|
+
# value object and `paths` names exactly one path; nil otherwise
|
|
284
|
+
def sole_scalar_identity(value, paths)
|
|
285
|
+
return nil unless value.is_a?(self) && paths.one?
|
|
286
|
+
return nil unless value.value_object.sole_attribute
|
|
287
|
+
|
|
288
|
+
materialize_unwrapped(value)
|
|
289
|
+
end
|
|
290
|
+
|
|
188
291
|
# Whether `value` is itself the target's own (single) identity
|
|
189
292
|
# value object — pure, self-contained: reads only `value` and
|
|
190
293
|
# `target`, decides nothing about any particular path.
|
|
294
|
+
#
|
|
295
|
+
# @param value [Object] the offered reference value to check
|
|
296
|
+
# @param target [Bluebook::Aggregate] the reference's own resolved target
|
|
297
|
+
# @return [String, nil] the target's own single identity head, as a String,
|
|
298
|
+
# when `value` is that head's own declared value-object type; nil otherwise
|
|
191
299
|
def direct_identity_head(value, target)
|
|
192
300
|
return nil unless value.is_a?(self) && target.identity_heads.one?
|
|
193
301
|
|
|
@@ -201,6 +309,14 @@ module Hecks
|
|
|
201
309
|
# three inputs; extracted from `reference_identity` alongside
|
|
202
310
|
# `direct_identity_head` above purely to keep that method to its
|
|
203
311
|
# own guard-clause shape.
|
|
312
|
+
#
|
|
313
|
+
# @param materialized [Hash, Object] the offered reference value, already
|
|
314
|
+
# materialized to a plain Hash (or scalar, for a path that finds nothing)
|
|
315
|
+
# @param path [String, Symbol] one dotted identity path to dig
|
|
316
|
+
# @param direct_head [String, nil] the leading segment to strip, when `path`
|
|
317
|
+
# restates the head `direct_identity_head` already matched
|
|
318
|
+
# @return [Object, nil] the value found by walking `path`'s segments; nil if
|
|
319
|
+
# any segment is missing or a non-Hash is dug into before the path ends
|
|
204
320
|
def identity_part(materialized, path, direct_head)
|
|
205
321
|
segments = path.to_s.split(".")
|
|
206
322
|
segments.shift if direct_head && segments.first == direct_head
|
|
@@ -215,6 +331,12 @@ module Hecks
|
|
|
215
331
|
end
|
|
216
332
|
end
|
|
217
333
|
|
|
334
|
+
# Coerces a `has_many` reference-typed attribute's offered value.
|
|
335
|
+
#
|
|
336
|
+
# @param attribute [Bluebook::Attribute] the `has_many` reference attribute
|
|
337
|
+
# @param value [Object] the offered value; must be an Array
|
|
338
|
+
# @return [Array] `value`, deep-frozen and duped
|
|
339
|
+
# @raise [Runtime::TypeMismatch] if `value` is not an Array
|
|
218
340
|
def reference_list(attribute, value)
|
|
219
341
|
unless value.is_a?(Array)
|
|
220
342
|
raise TypeMismatch,
|
|
@@ -224,6 +346,18 @@ module Hecks
|
|
|
224
346
|
Freezer.deep(value.dup)
|
|
225
347
|
end
|
|
226
348
|
|
|
349
|
+
# Normalizes an offered value into `value_object`'s own field Hash, before
|
|
350
|
+
# defaults, nested normalization and validation run.
|
|
351
|
+
#
|
|
352
|
+
# @param value_object [Class] the target `Bluebook::ValueObject` subclass
|
|
353
|
+
# @param name [String, Symbol] the attribute or argument name, quoted in a
|
|
354
|
+
# refusal
|
|
355
|
+
# @param value [Hash, Runtime::Value, Object] the offered value: a Hash of
|
|
356
|
+
# fields, an already-built `Value` of a differently-named but same-shaped
|
|
357
|
+
# type, or a bare scalar for a single-field value object
|
|
358
|
+
# @return [Hash{Symbol => Object}] the offered fields, keyed by attribute name
|
|
359
|
+
# @raise [Runtime::TypeMismatch] if `value` is a bare scalar and `value_object`
|
|
360
|
+
# declares more than one field
|
|
227
361
|
def fields_for(value_object, name, value)
|
|
228
362
|
return value.transform_keys(&:to_sym) if value.is_a?(Hash)
|
|
229
363
|
# Mutations may legitimately carry a value object into a differently
|
|
@@ -235,14 +369,14 @@ module Hecks
|
|
|
235
369
|
|
|
236
370
|
# Vendored addition, not (yet) upstream hecks (migration
|
|
237
371
|
# plan task 5): a bare scalar auto-wraps into a single-field
|
|
238
|
-
# value object's sole attribute -- the
|
|
372
|
+
# value object's sole attribute -- the same shape
|
|
239
373
|
# #from_identifier already establishes for identity coercion
|
|
240
374
|
# (`build(value_object, { fields.first.name => identifier }) if
|
|
241
|
-
# fields.size == 1`), made consistent here for
|
|
375
|
+
# fields.size == 1`), made consistent here for mutation
|
|
242
376
|
# coercion too. Real, corpus-wide gap: a synthesised single-
|
|
243
377
|
# field wrapper (Part 3a's bare-primitive auto-synthesis, the
|
|
244
378
|
# norm for a VO-typed aggregate field) is exactly the shape
|
|
245
|
-
# #rewrap_arithmetic_result hands back a raw scalar
|
|
379
|
+
# #rewrap_arithmetic_result hands back a raw scalar result to
|
|
246
380
|
# -- without this, every phantom-field increment/multiply on a
|
|
247
381
|
# single-field-wrapped attribute refused with "pass its fields
|
|
248
382
|
# as an object, not <scalar>" the instant it tried to re-wrap
|
|
@@ -253,40 +387,40 @@ module Hecks
|
|
|
253
387
|
return { value_object.attributes.first.name => value } if value_object.attributes.size == 1
|
|
254
388
|
|
|
255
389
|
raise TypeMismatch,
|
|
256
|
-
RefusalWording.
|
|
257
|
-
|
|
258
|
-
|
|
390
|
+
RefusalWording.render_site("TypeMismatch", "value_object_shape",
|
|
391
|
+
name: name, type: value_object.hecks_name,
|
|
392
|
+
offered: Rendering.describe(value))
|
|
259
393
|
end
|
|
260
394
|
|
|
261
395
|
# `build`'s own recursive twin of `for_attribute`'s single-level
|
|
262
|
-
# normalization — a value object's
|
|
396
|
+
# normalization — a value object's own composite-typed fields
|
|
263
397
|
# (`Pizza.price_cents`, a `Price`) never otherwise pass back
|
|
264
398
|
# through `fields_for`, so a bare scalar or partial Hash for one
|
|
265
|
-
# of
|
|
399
|
+
# of those sails past the outer VO's own shape check (`Pizza`
|
|
266
400
|
# itself has two fields, so nothing unwraps there) and lands
|
|
267
401
|
# stored one field down exactly as handed in — found live: once
|
|
268
402
|
# the fuzzer actually generated the bare-scalar shape
|
|
269
|
-
# `fields_for` has accepted at the
|
|
403
|
+
# `fields_for` has accepted at the top level since 86727afd, a
|
|
270
404
|
# nested `Price` stored as a raw Integer broke every later
|
|
271
405
|
# dotted-path read (`pizza.price_cents.cents`) expecting one
|
|
272
406
|
# more level of Hash.
|
|
273
407
|
#
|
|
274
408
|
# Stays a plain Hash, never a nested `Value` — `Value#with`'s own
|
|
275
409
|
# header and `materialize_unwrapped`'s comment already depend on
|
|
276
|
-
# a value-object-typed field of
|
|
410
|
+
# a value-object-typed field of another value object staying a
|
|
277
411
|
# plain Hash once stored, and this does not change that; it only
|
|
278
412
|
# makes sure that Hash has the shape its own type declares.
|
|
279
|
-
# `aggregate` is the one thing `
|
|
280
|
-
# nested type can only be resolved through `aggregate.
|
|
413
|
+
# `aggregate` is the one thing this needs beyond `fields_for`'s own
|
|
414
|
+
# scope — a nested type can only be resolved through `aggregate.
|
|
281
415
|
# value_object(name)`, so callers with no aggregate in reach
|
|
282
416
|
# (`Value#with`, always re-setting an already-scalar arithmetic
|
|
283
|
-
# field) simply skip this and keep their
|
|
284
|
-
#
|
|
417
|
+
# field) simply skip this and keep their own behavior unchanged.
|
|
418
|
+
# Recurses into each nested field's own validation too, not only its
|
|
285
419
|
# shape — found live alongside the shape bug this method's header
|
|
286
420
|
# already describes: a nested `Price`/`Size` (a value-object-typed
|
|
287
|
-
# field of
|
|
421
|
+
# field of another value object, e.g. `Pizza.price_cents`,
|
|
288
422
|
# `Pizza.size`) had its Hash shape normalized here but never ran
|
|
289
|
-
# `validate!` — `build`, below, only ever validated the
|
|
423
|
+
# `validate!` — `build`, below, only ever validated the outer value
|
|
290
424
|
# object's own direct fields, so a negative `price_cents.cents` or an
|
|
291
425
|
# out-of-`one_of` `size.value` sailed through a `Pizza`-typed command
|
|
292
426
|
# argument untouched, while the exact same nested type declared as a
|
|
@@ -295,11 +429,43 @@ module Hecks
|
|
|
295
429
|
# defaults` runs first, same as the outer value object gets in
|
|
296
430
|
# `build`, so a nested field's own default is filled in before its
|
|
297
431
|
# own invariants read it.
|
|
432
|
+
#
|
|
433
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
|
|
434
|
+
# nested types are resolved against; a no-op if it does not respond to
|
|
435
|
+
# `value_object`
|
|
436
|
+
# @param value_object [Class] the `Bluebook::ValueObject` subclass `fields`
|
|
437
|
+
# belongs to
|
|
438
|
+
# @param fields [Hash{Symbol => Object}] the outer value object's own fields,
|
|
439
|
+
# already defaulted; written in place
|
|
440
|
+
# @return [Hash{Symbol => Object}] `fields`, with every composite-typed field
|
|
441
|
+
# normalized (and, for a non-list one, validated) into its own declared shape
|
|
442
|
+
# @raise [Runtime::TypeMismatch] if a nested field cannot be coerced to its
|
|
443
|
+
# declared type
|
|
444
|
+
# @raise [Runtime::UnknownArgument] if a nested field's own Hash names a
|
|
445
|
+
# field its declared type does not declare
|
|
446
|
+
# @raise [Runtime::InvariantViolation] if a nested field breaks one of its
|
|
447
|
+
# own invariants
|
|
298
448
|
def normalize_composite_fields(aggregate, value_object, fields)
|
|
299
449
|
return fields unless aggregate.respond_to?(:value_object)
|
|
300
450
|
|
|
301
451
|
value_object.attributes.each do |attribute|
|
|
302
|
-
next
|
|
452
|
+
next unless fields.key?(attribute.name)
|
|
453
|
+
|
|
454
|
+
# A list member read back from the store hydrates like a top-level
|
|
455
|
+
# list — `list_of(Entity)` elements get their fields coerced,
|
|
456
|
+
# `list_of(ValueObject)` elements become Values — so a value object
|
|
457
|
+
# holding a list loads into the same shape the live dispatch that
|
|
458
|
+
# wrote it held. Found by PR A3 (every adapter through the state
|
|
459
|
+
# codec): chess-style `sets :positions, append: { pieces:
|
|
460
|
+
# state(:pieces) }` snapshots read back from Heki/Sqlite/Postgres
|
|
461
|
+
# (and now Memory's codec copy) with raw element hashes, so a
|
|
462
|
+
# `given` comparing a snapshot piece's `id` Value to a live one
|
|
463
|
+
# never matched after a restart. Load door only: an input list
|
|
464
|
+
# member is left exactly as before, refusals unchanged.
|
|
465
|
+
if attribute.list?
|
|
466
|
+
fields[attribute.name] = for_attribute(aggregate, attribute, fields[attribute.name]) if trusting_stored_state?
|
|
467
|
+
next
|
|
468
|
+
end
|
|
303
469
|
|
|
304
470
|
raw = fields[attribute.name]
|
|
305
471
|
next if raw.nil? || raw.is_a?(self)
|
|
@@ -316,26 +482,47 @@ module Hecks
|
|
|
316
482
|
fields
|
|
317
483
|
end
|
|
318
484
|
|
|
485
|
+
# Fills every declared attribute `fields` does not already hold with its
|
|
486
|
+
# own declared `default:`, when it has one.
|
|
487
|
+
#
|
|
488
|
+
# @param value_object [Class] the `Bluebook::ValueObject` subclass whose
|
|
489
|
+
# declared defaults are read
|
|
490
|
+
# @param fields [Hash{Symbol => Object}] the offered fields; written in place
|
|
491
|
+
# @return [Hash{Symbol => Object}] `fields`, with each declared default filled in
|
|
319
492
|
def apply_defaults(value_object, fields)
|
|
320
493
|
value_object.attributes.each_with_object(fields) do |attribute, completed|
|
|
321
494
|
completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
|
|
322
495
|
end
|
|
323
496
|
end
|
|
324
497
|
|
|
325
|
-
#
|
|
498
|
+
# The full door a value object's own fields pass through — shared by
|
|
326
499
|
# `build` (the outer value object) and `normalize_composite_fields`
|
|
327
500
|
# (every nested one), so a nested `Price`/`Size` is refused exactly
|
|
328
501
|
# the same way, with exactly the same wording, as the identical type
|
|
329
502
|
# declared directly on a command.
|
|
503
|
+
#
|
|
504
|
+
# @param value_object [Class] the `Bluebook::ValueObject` subclass to
|
|
505
|
+
# validate `fields` against
|
|
506
|
+
# @param fields [Hash{Symbol => Object}] the already-defaulted, already
|
|
507
|
+
# nested-normalized fields to check
|
|
508
|
+
# @return [void]
|
|
509
|
+
# @raise [Runtime::UnknownArgument] if `fields` names a key `value_object`
|
|
510
|
+
# does not declare
|
|
511
|
+
# @raise [Runtime::TypeMismatch] if a required field is missing, a numeric
|
|
512
|
+
# or pattern-constrained field is the wrong shape, or a scalar field
|
|
513
|
+
# arrives as a composite
|
|
514
|
+
# @raise [Runtime::InvariantViolation] if `fields` breaks one of
|
|
515
|
+
# `value_object`'s own declared invariants
|
|
330
516
|
def validate!(value_object, fields)
|
|
331
517
|
# C6.3 (docs/semantics/bluebook-semantics.md) — a value object is
|
|
332
|
-
# validated on
|
|
518
|
+
# validated on construction from input only; state read back from
|
|
333
519
|
# the store is trusted as it was written, so tightening an
|
|
334
520
|
# invariant never makes an old record unreadable (migration is
|
|
335
521
|
# the era system's job). `hydrate` — the one load door — sets
|
|
336
522
|
# the flag; every input door leaves it unset.
|
|
337
523
|
return if trusting_stored_state?
|
|
338
524
|
|
|
525
|
+
check_unknown_fields(value_object, fields)
|
|
339
526
|
check_required_fields(value_object, fields)
|
|
340
527
|
admit_member(value_object, fields)
|
|
341
528
|
check_admitted(value_object, fields)
|
|
@@ -346,12 +533,28 @@ module Hecks
|
|
|
346
533
|
next if Bluebook::Expression::Evaluator.call_rule(invariant, fields)
|
|
347
534
|
|
|
348
535
|
raise InvariantViolation,
|
|
349
|
-
RefusalWording.
|
|
350
|
-
|
|
351
|
-
|
|
536
|
+
RefusalWording.render_site("InvariantViolation", "value_object_invariant",
|
|
537
|
+
name: value_object.hecks_name, description: invariant.description,
|
|
538
|
+
offered: canonical_fields(fields))
|
|
352
539
|
end
|
|
353
540
|
end
|
|
354
541
|
|
|
542
|
+
# Builds one validated `Value` of `value_object`'s own type: defaults filled,
|
|
543
|
+
# nested fields normalized and validated, then the whole thing checked.
|
|
544
|
+
#
|
|
545
|
+
# @param value_object [Class] the `Bluebook::ValueObject` subclass to build
|
|
546
|
+
# @param fields [Hash{Symbol, String => Object}] the offered field values,
|
|
547
|
+
# either key spelling
|
|
548
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
|
|
549
|
+
# a nested composite field is resolved against; nil skips nested
|
|
550
|
+
# normalization entirely
|
|
551
|
+
# @return [Runtime::Value] the built, validated value object
|
|
552
|
+
# @raise [Runtime::UnknownArgument] if `fields` (or a nested field) names a
|
|
553
|
+
# key its own type does not declare
|
|
554
|
+
# @raise [Runtime::TypeMismatch] if a field (or a nested one) cannot be
|
|
555
|
+
# coerced to its declared type
|
|
556
|
+
# @raise [Runtime::InvariantViolation] if the built value object (or a
|
|
557
|
+
# nested one) breaks one of its own invariants
|
|
355
558
|
def build(value_object, fields, aggregate = nil)
|
|
356
559
|
fields = apply_defaults(value_object, fields.transform_keys(&:to_sym))
|
|
357
560
|
fields = normalize_composite_fields(aggregate, value_object, fields)
|
|
@@ -359,10 +562,33 @@ module Hecks
|
|
|
359
562
|
new(value_object, fields)
|
|
360
563
|
end
|
|
361
564
|
|
|
565
|
+
# State arrives decoded or not at all (Phase 2, Track A, PR A4).
|
|
566
|
+
# Every persistence adapter reads through `Ports::Persistence::
|
|
567
|
+
# StateCodec.decode` (A3), which symbolizes every top-level key, and
|
|
568
|
+
# the runtime's own callers (entity elements, the remote dispatcher's
|
|
569
|
+
# `symbolize_names:` parse, Era's audit) build symbol-keyed state
|
|
570
|
+
# themselves. A String key here is an adapter or caller that skipped
|
|
571
|
+
# the codec, so it is refused by name rather than respelled: a silent
|
|
572
|
+
# `to_sym` here would hide exactly this kind of bypass instead. Always
|
|
573
|
+
# on, because it costs one `is_a?` per key, no more than a `to_sym` would.
|
|
574
|
+
#
|
|
575
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
576
|
+
# whose declared attributes coerce `state`'s own values
|
|
577
|
+
# @param state [Hash{Symbol => Object}] the stored state to hydrate; every
|
|
578
|
+
# key must already be a Symbol
|
|
579
|
+
# @return [Hash{Symbol => Object}] `state`, coerced through every declared
|
|
580
|
+
# attribute it names
|
|
581
|
+
# @raise [Runtime::WiringError] if `state` holds any non-Symbol key
|
|
362
582
|
def hydrate(aggregate, state)
|
|
583
|
+
undecoded = state.keys.grep_v(Symbol)
|
|
584
|
+
unless undecoded.empty?
|
|
585
|
+
raise WiringError,
|
|
586
|
+
"#{aggregate.name} state reached hydration with non-Symbol keys #{undecoded.inspect} — " \
|
|
587
|
+
"decode stored state through Hecks::Ports::Persistence::StateCodec.decode first"
|
|
588
|
+
end
|
|
589
|
+
|
|
363
590
|
trusting_stored_state do
|
|
364
|
-
state.each_with_object({}) do |(
|
|
365
|
-
key = name.to_sym
|
|
591
|
+
state.each_with_object({}) do |(key, value), hydrated|
|
|
366
592
|
attribute = aggregate.attribute(key)
|
|
367
593
|
hydrated[key] = attribute ? for_attribute(aggregate, attribute, value) : value
|
|
368
594
|
end
|
|
@@ -371,6 +597,15 @@ module Hecks
|
|
|
371
597
|
|
|
372
598
|
TRUSTED_LOAD_KEY = :hecks_trusting_stored_state
|
|
373
599
|
|
|
600
|
+
# Marks the block as loading trusted, already-validated stored state, so
|
|
601
|
+
# `validate!` skips its own checks for the block's duration.
|
|
602
|
+
#
|
|
603
|
+
# `Thread.current`-backed, not a plain ivar, so two threads hydrating
|
|
604
|
+
# concurrently on the same `Value` singleton class never see or clear
|
|
605
|
+
# each other's flag.
|
|
606
|
+
#
|
|
607
|
+
# @yield the code that should see `trusting_stored_state?` true
|
|
608
|
+
# @return [Object] the block's result
|
|
374
609
|
def trusting_stored_state
|
|
375
610
|
previous = Thread.current[TRUSTED_LOAD_KEY]
|
|
376
611
|
Thread.current[TRUSTED_LOAD_KEY] = true
|
|
@@ -379,25 +614,89 @@ module Hecks
|
|
|
379
614
|
Thread.current[TRUSTED_LOAD_KEY] = previous
|
|
380
615
|
end
|
|
381
616
|
|
|
617
|
+
# Reports whether the current thread is inside a `trusting_stored_state` block.
|
|
618
|
+
#
|
|
619
|
+
# @return [Boolean] true if a `trusting_stored_state` block is on this
|
|
620
|
+
# thread's own call stack
|
|
382
621
|
def trusting_stored_state? = Thread.current[TRUSTED_LOAD_KEY] == true
|
|
383
622
|
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
-
#
|
|
623
|
+
# QualityControl BUG#125 — the one narrow door `check_scalar_shapes`
|
|
624
|
+
# keeps open, now that a non-string scalar is otherwise refused for a
|
|
625
|
+
# String-typed field. `MetaValidator::Judge#send_to` — the single
|
|
626
|
+
# choke point every one of the language's own self-hosted dispatches
|
|
627
|
+
# goes through while walking a bluebook's declarations into the
|
|
628
|
+
# "Bluebook" meta-domain — wraps itself in this, and nothing else
|
|
629
|
+
# does. `Judge#appends`' generic `POSITION` handling
|
|
630
|
+
# (judge.rb#appends) keys purely off a field being named "position",
|
|
631
|
+
# the convention every other append list actually uses it for
|
|
632
|
+
# (ValueObject::Member, ProcessManager::Handler, ... — all really
|
|
633
|
+
# `Position`/Integer-typed); `Normalise`'s own `NormalisationRule`
|
|
634
|
+
# happens to also name its own domain field "position"
|
|
635
|
+
# (bluebook.bluebook), but declares it `RuleText` (String) — so the
|
|
636
|
+
# same walk-index substitution (`Judge#v(index)`) hands it a raw
|
|
637
|
+
# Integer too, on every domain's very first boot (the language
|
|
638
|
+
# self-judges its own grammar via `MetaValidator.fresh_runtime`'s
|
|
639
|
+
# fixpoint). Confirmed (QualityControl BUG#125 investigation): the
|
|
640
|
+
# resulting value is never read back — `normalisations` is an
|
|
641
|
+
# `ELSEWHERE`/`derived` field spliced straight from
|
|
642
|
+
# `Expression::CanonicalForm.table` (assembly/contracts.rb), so the
|
|
643
|
+
# judged record holding the Integer is discarded whole — this is a
|
|
644
|
+
# walk-index/domain-field name collision inside `Judge#appends`, not
|
|
645
|
+
# a genuine semantic need for `position` to arrive numeric. Fixing
|
|
646
|
+
# that collision at its own root is a separate, larger change to
|
|
647
|
+
# self-hosted bootstrap mechanics that every domain's boot depends
|
|
648
|
+
# on; this flag only ever loosens scalar-shape checking for the
|
|
649
|
+
# meta-grammar's own value objects (RuleText, BluebookName, Position,
|
|
650
|
+
# …) that Judge itself constructs while walking a bluebook's
|
|
651
|
+
# declarations — never for a real domain's own declared value
|
|
652
|
+
# objects (PieceId, Money, …), which Judge never dispatches commands
|
|
653
|
+
# against. `offer` (judge.rb) already converts a `TypeMismatch` here
|
|
654
|
+
# into a recorded refusal rather than letting it propagate, but
|
|
655
|
+
# `MetaValidator.call` raises the instant `refusals` is non-empty
|
|
656
|
+
# (meta_validator.rb) — so, unexempted, this would fail every
|
|
657
|
+
# domain's boot, not just the language's own bootstrap. Composite
|
|
658
|
+
# shapes (Array/Hash) stay refused unconditionally, bootstrap or not
|
|
659
|
+
# — nothing Judge does ever legitimately needs those for a scalar
|
|
660
|
+
# field.
|
|
661
|
+
BOOTSTRAP_KEY = :hecks_judge_bootstrapping
|
|
662
|
+
|
|
663
|
+
# Marks the block as `MetaValidator::Judge#send_to`'s own self-hosted
|
|
664
|
+
# bootstrap dispatch, so `check_scalar_shapes` loosens its `String` check
|
|
665
|
+
# for the block's duration.
|
|
666
|
+
#
|
|
667
|
+
# `Thread.current`-backed, not a plain ivar, so two threads bootstrapping
|
|
668
|
+
# concurrently on the same `Value` singleton class never see or clear
|
|
669
|
+
# each other's flag.
|
|
670
|
+
#
|
|
671
|
+
# @yield the code that should see `judge_bootstrapping?` true
|
|
672
|
+
# @return [Object] the block's result
|
|
673
|
+
def judge_bootstrapping
|
|
674
|
+
previous = Thread.current[BOOTSTRAP_KEY]
|
|
675
|
+
Thread.current[BOOTSTRAP_KEY] = true
|
|
676
|
+
yield
|
|
677
|
+
ensure
|
|
678
|
+
Thread.current[BOOTSTRAP_KEY] = previous
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Reports whether the current thread is inside a `judge_bootstrapping` block.
|
|
682
|
+
#
|
|
683
|
+
# @return [Boolean] true if a `judge_bootstrapping` block is on this
|
|
684
|
+
# thread's own call stack
|
|
685
|
+
def judge_bootstrapping? = Thread.current[BOOTSTRAP_KEY] == true
|
|
686
|
+
|
|
387
687
|
# An identity names its field — `identified_by :number` — and the
|
|
388
|
-
# path is what reaches the scalar
|
|
389
|
-
#
|
|
688
|
+
# path is what reaches the scalar, never a guess at which field a
|
|
689
|
+
# one-field value object might mean. A declaration that names no
|
|
690
|
+
# field is refused when the bluebook loads, so nothing has to be
|
|
691
|
+
# unwrapped later.
|
|
390
692
|
#
|
|
391
693
|
# `scalar` below is a different job and stays: rendering a value object
|
|
392
694
|
# into a column or a message, where there is no path to consult.
|
|
393
695
|
|
|
394
|
-
#
|
|
395
|
-
#
|
|
396
|
-
# arrives as anything else — so there is nothing left to open. The comment
|
|
397
|
-
# it carried said retiring it meant changing how references are STORED ;
|
|
398
|
-
# that is what happened.
|
|
696
|
+
# A reference is the id itself — refused at the payload gate if it
|
|
697
|
+
# arrives as anything else — so there is nothing to open inside it.
|
|
399
698
|
|
|
400
|
-
# A
|
|
699
|
+
# A reference is an ID, so anything else is not one.
|
|
401
700
|
#
|
|
402
701
|
# Nothing coerces a reference — `for_attribute` misses on
|
|
403
702
|
# "Reference<Account>", which is no value object's name, and hands the
|
|
@@ -406,14 +705,14 @@ module Hecks
|
|
|
406
705
|
# refused, so whatever the first caller wrote became the shape.
|
|
407
706
|
#
|
|
408
707
|
# This is that place. It sits at the payload gate rather than inside
|
|
409
|
-
# coercion because the sentence names the
|
|
708
|
+
# coercion because the sentence names the command, and `for_attribute`
|
|
410
709
|
# never learns which command it is serving.
|
|
411
710
|
#
|
|
412
|
-
#
|
|
711
|
+
# Widened past the object shape by BUG#27 (QualityControl ledger,
|
|
413
712
|
# found live on `qa/stress_domains/referral_chain`'s `Member.Join`/
|
|
414
|
-
# `Referral.Issue`).
|
|
415
|
-
# through here untouched — nothing but Hash/Value
|
|
416
|
-
# then get `.to_s`'d into a lookup key by `CommandRules::
|
|
713
|
+
# `Referral.Issue`). Without this widening, a bare Boolean, Array, or
|
|
714
|
+
# `null` would sail through here untouched — nothing but Hash/Value
|
|
715
|
+
# ever refused — then get `.to_s`'d into a lookup key by `CommandRules::
|
|
417
716
|
# References#reference_key` ("true", "false", "[8, 8]") and answer
|
|
418
717
|
# NotFound, or, for `null`, skip the lookup outright
|
|
419
718
|
# (`next if held.nil?`, command_rules/references.rb) and let the
|
|
@@ -426,26 +725,36 @@ module Hecks
|
|
|
426
725
|
# (`normalize_args`, `Vocabulary::AggregateDispatchOrder`/
|
|
427
726
|
# `EntityDispatchOrder`), strictly before `resolve_references` ever
|
|
428
727
|
# receives a value to look up — so the two engines now agree on
|
|
429
|
-
#
|
|
728
|
+
# both kind and order, not just kind.
|
|
430
729
|
#
|
|
431
|
-
# `nil`
|
|
730
|
+
# `nil` stays legitimate for a `reference_to ..., optional: true`
|
|
432
731
|
# argument (`Improvement.Open`'s own `reference_to Angle, optional:
|
|
433
732
|
# true` — `qa/bluebook/quality_control.bluebook`): the caller
|
|
434
733
|
# genuinely may have nothing to name yet, and `nil_argument`
|
|
435
734
|
# (interpreting.rb) already passes an optional reference's `nil`
|
|
436
|
-
# through untouched. A
|
|
437
|
-
# caller leaving a required argument empty in every
|
|
438
|
-
# this runtime already refuses (C3.7) — refusing it
|
|
735
|
+
# through untouched. A required reference offered as `null` is a
|
|
736
|
+
# caller leaving a required argument empty in every other sense
|
|
737
|
+
# this runtime already refuses (C3.7) — refusing it here, rather
|
|
439
738
|
# than falling through to `resolve_references`' own nil-skip and
|
|
440
739
|
# then whatever the command's `given` happens to say, is what
|
|
441
740
|
# actually names the empty argument instead of something else.
|
|
442
741
|
#
|
|
443
742
|
# A `has_many` reference's own Array shape is still never refused
|
|
444
|
-
# by
|
|
445
|
-
# list's
|
|
743
|
+
# by its wrapper (`Array(value).find { ... }` only inspects the
|
|
744
|
+
# list's elements) — a reference is never a scalar list-of-lists
|
|
446
745
|
# today, and inventing a rule for a shape the language cannot
|
|
447
746
|
# declare is how decoration gets written. `reference_list` (below)
|
|
448
747
|
# already owns "not an Array at all" for that case.
|
|
748
|
+
#
|
|
749
|
+
# @param command [Class] the command class (`Bluebook::Command` subclass)
|
|
750
|
+
# `attribute` is declared on, named in a refusal
|
|
751
|
+
# @param attribute [Bluebook::Attribute] the attribute to check; a no-op
|
|
752
|
+
# unless it is reference-typed
|
|
753
|
+
# @param value [Object] the offered value
|
|
754
|
+
# @return [void]
|
|
755
|
+
# @raise [Runtime::TypeMismatch] if `value` (or, for a `has_many` reference,
|
|
756
|
+
# any of its elements) is a Hash or a `Runtime::Value` rather than a plain
|
|
757
|
+
# identity, or a required scalar reference is anything but a String
|
|
449
758
|
def refuse_object_reference(command, attribute, value)
|
|
450
759
|
return unless attribute.reference?
|
|
451
760
|
|
|
@@ -460,19 +769,23 @@ module Hecks
|
|
|
460
769
|
end
|
|
461
770
|
|
|
462
771
|
raise TypeMismatch,
|
|
463
|
-
RefusalWording.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
772
|
+
RefusalWording.render_site("TypeMismatch", "reference_wrong_shape",
|
|
773
|
+
command: command.hecks_name, attribute: attribute.name,
|
|
774
|
+
offered: reference_shape_description(offered),
|
|
775
|
+
known_by: known_by(attribute))
|
|
467
776
|
end
|
|
468
777
|
|
|
469
|
-
# "an object" for the Hash/Value shape — the
|
|
778
|
+
# "an object" for the Hash/Value shape — the original wording this
|
|
470
779
|
# method always gave, pinned byte for byte by
|
|
471
780
|
# `spec/runtime/reference_shape_spec.rb`, kept unchanged by BUG#27's
|
|
472
781
|
# widening. `Rendering.describe` for everything else: `true`,
|
|
473
782
|
# `false`, `nil`, `[8, 8]` — the same rendering every other
|
|
474
783
|
# TypeMismatch in this file already uses for "here is what you
|
|
475
784
|
# actually sent."
|
|
785
|
+
#
|
|
786
|
+
# @param value [Object] the wrongly-shaped offered value to describe
|
|
787
|
+
# @return [String] `"an object"` for a Hash or `Runtime::Value`; otherwise
|
|
788
|
+
# `Rendering.describe(value)`
|
|
476
789
|
def reference_shape_description(value)
|
|
477
790
|
return "an object" if value.is_a?(Hash) || value.is_a?(self)
|
|
478
791
|
|
|
@@ -485,11 +798,16 @@ module Hecks
|
|
|
485
798
|
# article-choosing rule. Silent when the target is another chapter's,
|
|
486
799
|
# where this runtime cannot see what it is known by.
|
|
487
800
|
#
|
|
488
|
-
#
|
|
489
|
-
# `identified_by`, which is the
|
|
801
|
+
# Every head, because a caller has to pass every one. This read
|
|
802
|
+
# `identified_by`, which is the single head and is nil the moment an
|
|
490
803
|
# identity has two parts — so a composite target fell through the guard
|
|
491
804
|
# and the refusal went silent exactly where it had the most to say. A
|
|
492
805
|
# single-path target reads as it always did.
|
|
806
|
+
#
|
|
807
|
+
# @param attribute [Bluebook::Attribute] the reference-typed attribute to
|
|
808
|
+
# describe the target's own identity heads for
|
|
809
|
+
# @return [String] `" (Target is known by head1, head2)"`, or `""` when the
|
|
810
|
+
# target cannot be resolved or declares no identity heads
|
|
493
811
|
def known_by(attribute)
|
|
494
812
|
heads = Array(attribute.type.resolve&.identity_heads)
|
|
495
813
|
return "" if heads.empty?
|
|
@@ -497,15 +815,37 @@ module Hecks
|
|
|
497
815
|
" (#{attribute.type.target_name} is known by #{heads.join(', ')})"
|
|
498
816
|
end
|
|
499
817
|
|
|
818
|
+
# Renders a value object into the bare scalar its one field holds — for a
|
|
819
|
+
# column or a message, where there is no path to consult.
|
|
820
|
+
#
|
|
821
|
+
# @param value [Object] the value to render; passed through unless a
|
|
822
|
+
# `Runtime::Value`
|
|
823
|
+
# @return [Object] `value` unchanged when it is not a `Runtime::Value`;
|
|
824
|
+
# otherwise its one field's own value
|
|
825
|
+
# @raise [Runtime::TypeMismatch] if `value` is a `Runtime::Value` with more
|
|
826
|
+
# than one field
|
|
500
827
|
def scalar(value)
|
|
501
828
|
return value unless value.is_a?(self)
|
|
502
829
|
|
|
503
830
|
fields = value.to_h
|
|
504
831
|
return fields.values.first if fields.size == 1
|
|
505
832
|
|
|
506
|
-
raise TypeMismatch, RefusalWording.
|
|
833
|
+
raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "multi_field_scalar", type: value.type_name)
|
|
507
834
|
end
|
|
508
835
|
|
|
836
|
+
# Coerces a derived identity string back into `attribute`'s own declared type.
|
|
837
|
+
#
|
|
838
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
839
|
+
# `attribute` is declared on
|
|
840
|
+
# @param attribute [Bluebook::Attribute] the identity attribute to coerce
|
|
841
|
+
# `identifier` for
|
|
842
|
+
# @param identifier [String, Object] the derived identity, typically a
|
|
843
|
+
# String (`Identity.of`/`Identity.from`'s own return)
|
|
844
|
+
# @return [Runtime::Value, String, Object] a built value object when
|
|
845
|
+
# `attribute`'s type names a single-field value object; `identifier`
|
|
846
|
+
# unchanged otherwise
|
|
847
|
+
# @raise [Runtime::TypeMismatch] if `attribute`'s type names a value object
|
|
848
|
+
# with more than one field
|
|
509
849
|
def from_identifier(aggregate, attribute, identifier)
|
|
510
850
|
value_object = value_object_for(aggregate, attribute.type)
|
|
511
851
|
return identifier unless value_object
|
|
@@ -516,29 +856,29 @@ module Hecks
|
|
|
516
856
|
return build(value_object, { field.name => coerce_identifier(field, identifier) })
|
|
517
857
|
end
|
|
518
858
|
|
|
519
|
-
raise TypeMismatch, RefusalWording.
|
|
859
|
+
raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "composite_identity", type: value_object.hecks_name)
|
|
520
860
|
end
|
|
521
861
|
|
|
522
862
|
# Vendored fix, not (yet) upstream hecks (migration plan
|
|
523
|
-
# task 9): `identifier` here is always the
|
|
524
|
-
#
|
|
863
|
+
# task 9): `identifier` here is always the derived identity
|
|
864
|
+
# string -- `Identity.of`/`Identity.from` intentionally return
|
|
525
865
|
# one (correct for naming a repository key), and
|
|
526
866
|
# `Runtime::Instance#materialize_identity!` calls `from_identifier`
|
|
527
867
|
# with exactly that string on every fresh hydration -- but when
|
|
528
|
-
# the identity field's
|
|
868
|
+
# the identity field's own declared type is Integer/Float (not
|
|
529
869
|
# the overwhelmingly common String), seeding it straight from
|
|
530
870
|
# that string round-trips a correctly-derived identity back in
|
|
531
|
-
# as the
|
|
871
|
+
# as the wrong Ruby type -- and #build's own
|
|
532
872
|
# `check_numeric_fields` (added specifically to catch a genuine
|
|
533
|
-
#
|
|
873
|
+
# caller mismatch) then refused the runtime's own internal
|
|
534
874
|
# identity seed instead, on every dispatch, valid input or not.
|
|
535
875
|
#
|
|
536
|
-
# Reuses
|
|
876
|
+
# Reuses this same file's own `NUMERIC` table (declared-type ->
|
|
537
877
|
# expected-Ruby-class, already read by `check_numeric_fields`)
|
|
538
|
-
# to decide
|
|
878
|
+
# to decide which declared types need converting, and
|
|
539
879
|
# Kernel#Integer/#Float to do the converting. A genuinely
|
|
540
880
|
# malformed identifier (should never happen, since an identity
|
|
541
|
-
# is always derived
|
|
881
|
+
# is always derived from a correctly-typed field in the first
|
|
542
882
|
# place, but this stays defensive rather than assume it) passes
|
|
543
883
|
# back unconverted, and `check_numeric_fields` refuses it
|
|
544
884
|
# exactly as it always has -- preserving its real job of
|
|
@@ -556,28 +896,36 @@ module Hecks
|
|
|
556
896
|
identifier
|
|
557
897
|
end
|
|
558
898
|
|
|
899
|
+
# Renders a value object's fields as a canonical JSON string, for an
|
|
900
|
+
# invariant refusal to quote.
|
|
901
|
+
#
|
|
902
|
+
# @param fields [Hash{Symbol, String => Object}] the field values to render
|
|
903
|
+
# @return [String] `fields`, sorted by key name and JSON-encoded
|
|
559
904
|
def canonical_fields(fields)
|
|
560
905
|
JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
|
|
561
906
|
end
|
|
562
907
|
|
|
563
|
-
# A field declared Integer or Float must
|
|
908
|
+
# A field declared Integer or Float must arrive as one.
|
|
564
909
|
#
|
|
565
910
|
# Without this a String sails into a numeric field and the failure surfaces
|
|
566
911
|
# later, inside a predicate, as `positive? expects a number, got "three"` —
|
|
567
|
-
# an EvaluationError, which is
|
|
912
|
+
# an EvaluationError, which is not a domain refusal. So the runtime broke
|
|
568
913
|
# where the domain should have said no, and the run contract recorded the
|
|
569
914
|
# crash beside genuine refusals as though the domain had judged it.
|
|
570
915
|
#
|
|
571
916
|
# C3.8 — the boundary check for an attribute whose type is a bare
|
|
572
917
|
# primitive rather than a value object: `Integer`/`Float` by exact
|
|
573
|
-
# numeric class (`NUMERIC`), `String`/booleans by rejecting a
|
|
574
|
-
# composite shape (`COMPOSITE_SHAPES`) —
|
|
575
|
-
# `
|
|
576
|
-
#
|
|
577
|
-
#
|
|
578
|
-
# other scalar
|
|
579
|
-
#
|
|
580
|
-
#
|
|
918
|
+
# numeric class (`NUMERIC`), `String`/booleans by rejecting only a
|
|
919
|
+
# composite shape (`COMPOSITE_SHAPES`) — not the same as
|
|
920
|
+
# `check_scalar_shapes` holds a value object's own `String` field to
|
|
921
|
+
# any more (QualityControl BUG#125 tightened that one to also refuse
|
|
922
|
+
# a non-string scalar; a bare `String` argument here still admits
|
|
923
|
+
# any other scalar, left exactly as it was — a bare-primitive
|
|
924
|
+
# attribute was never part of BUG#125's own investigation or fix,
|
|
925
|
+
# and whether it needs the same tightening, and against what real
|
|
926
|
+
# Judge dependency if any, is still open); worded by the same
|
|
927
|
+
# template with the owning construct as `type`. The Rust boundary is
|
|
928
|
+
# stricter there too, recorded in the clause.
|
|
581
929
|
private def check_bare_primitive(owner, attribute, value)
|
|
582
930
|
type = attribute.type.to_s
|
|
583
931
|
expected = NUMERIC[type]
|
|
@@ -590,9 +938,9 @@ module Hecks
|
|
|
590
938
|
end
|
|
591
939
|
if mistyped
|
|
592
940
|
raise TypeMismatch,
|
|
593
|
-
RefusalWording.
|
|
594
|
-
|
|
595
|
-
|
|
941
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
942
|
+
type: owner.hecks_name, field: attribute.name,
|
|
943
|
+
expected: type, offered: Rendering.describe(value))
|
|
596
944
|
end
|
|
597
945
|
|
|
598
946
|
check_numeric_bounds(owner.hecks_name, attribute.name, value)
|
|
@@ -607,41 +955,91 @@ module Hecks
|
|
|
607
955
|
private def check_numeric_bounds(type_name, field_name, given)
|
|
608
956
|
if given.is_a?(Integer) && !INT64_RANGE.cover?(given)
|
|
609
957
|
raise TypeMismatch,
|
|
610
|
-
RefusalWording.
|
|
611
|
-
|
|
958
|
+
RefusalWording.render_site("TypeMismatch", "integer_range",
|
|
959
|
+
type: type_name, field: field_name, offered: Rendering.describe(given))
|
|
612
960
|
end
|
|
613
961
|
return unless given.is_a?(Float) && !given.finite?
|
|
614
962
|
|
|
615
963
|
raise TypeMismatch,
|
|
616
|
-
RefusalWording.
|
|
617
|
-
|
|
964
|
+
RefusalWording.render_site("TypeMismatch", "non_finite_field",
|
|
965
|
+
type: type_name, field: field_name, offered: Rendering.describe(given))
|
|
618
966
|
end
|
|
619
967
|
|
|
620
|
-
#
|
|
968
|
+
# QualityControl BUG#41 — a value object refuses a key it does not
|
|
969
|
+
# declare, the same way a command's own payload does
|
|
970
|
+
# (`CommandInterpreter::ArgumentGate#refuse_unknown_arguments`,
|
|
971
|
+
# argument_gate.rb) — reusing that method's exact refusal wording
|
|
972
|
+
# (`UnknownArgument unknown_args`, refusal_wording.rb:
|
|
973
|
+
# "{command} does not declare {unknown} — it takes {declared}")
|
|
974
|
+
# rather than inventing a new template, because every generated
|
|
975
|
+
# Rust value-object `from_json` already renders this refusal
|
|
976
|
+
# through that identical site: `rust/project/json_codec.rb`'s
|
|
977
|
+
# `emit_unknown_argument_check` (mirrored byte-for-byte in
|
|
978
|
+
# `rust/codegen/src/json_codec.rs`) emits `v.unknown_keys(&[...])`
|
|
979
|
+
# and the same "{name} does not declare {unknown} — it takes
|
|
980
|
+
# {declared}" format string for every value object's own
|
|
981
|
+
# `from_json` — `GameLabel::from_json`
|
|
982
|
+
# (rust/src/generated/chess/game.rs) is simply the first case
|
|
983
|
+
# this gap was reproduced against.
|
|
984
|
+
#
|
|
985
|
+
# Ruby never had an equivalent check anywhere in this `validate!`
|
|
986
|
+
# door before now — `fields[attribute.name]` reads only the
|
|
987
|
+
# declared attributes, so any other key a caller's Hash carried
|
|
988
|
+
# was silently ignored. `fields` here only ever holds what a
|
|
989
|
+
# caller (or `for_attribute`'s own recursive coercion) offered
|
|
990
|
+
# for this value object — built by `fields_for`'s plain
|
|
991
|
+
# key-symbolizing (never a Hash the runtime pads with bookkeeping
|
|
992
|
+
# keys of its own; confirmed by reading every call site that
|
|
993
|
+
# reaches `validate!`) — so there is nothing legitimate here to
|
|
994
|
+
# exempt.
|
|
995
|
+
#
|
|
996
|
+
# Checked first, before `check_required_fields` and everything
|
|
997
|
+
# after it — matching Rust's own `from_json`, which checks
|
|
998
|
+
# `unknown_keys` before reading a single declared field. So a
|
|
999
|
+
# Hash offering both an unrecognized key and a missing required
|
|
1000
|
+
# one (BUG#41's own second demonstration case: `label: {extra:
|
|
1001
|
+
# "bogus"}` — unknown and missing `value`) refuses the same
|
|
1002
|
+
# UnknownArgument on both engines, not two different refusal
|
|
1003
|
+
# kinds for one malformed call.
|
|
1004
|
+
private def check_unknown_fields(value_object, fields)
|
|
1005
|
+
known = value_object.attributes.map { |attribute| attribute.name.to_sym }
|
|
1006
|
+
unknown = (fields.keys.map(&:to_sym) - known).sort
|
|
1007
|
+
return if unknown.empty?
|
|
1008
|
+
|
|
1009
|
+
declared = value_object.attributes.map(&:name)
|
|
1010
|
+
raise UnknownArgument,
|
|
1011
|
+
RefusalWording.render_site("UnknownArgument", "unknown_args",
|
|
1012
|
+
command: value_object.hecks_name, unknown: unknown,
|
|
1013
|
+
declared: declared)
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# C3.7 — a value object is a typed field product: every non-optional
|
|
621
1017
|
# field arrives, or construction refuses. A missing field and a null
|
|
622
1018
|
# one are the same absence (`fields[name]` reads nil for both), worded
|
|
623
1019
|
# as the type mismatch it is — "{type}.{field} expects {expected}, got
|
|
624
1020
|
# nil" — the identical string the Rust side's generated `from_json`
|
|
625
1021
|
# gives the same input, so the corpus can pin it on both. Checked
|
|
626
|
-
#
|
|
627
|
-
#
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
#
|
|
1022
|
+
# first among the field-content checks (after `check_unknown_fields`'s
|
|
1023
|
+
# own structural gate above, BUG#41): without this, an invariant
|
|
1024
|
+
# reading a field that never arrived would be exactly the thing that
|
|
1025
|
+
# answers "invariant violated" (or nothing at all — `ToppingName`'s
|
|
1026
|
+
# `{value: null}` would be accepted and stored). A `default:` has
|
|
1027
|
+
# already been filled in by `apply_defaults`; a list field's absence
|
|
1028
|
+
# is an empty list, never a refusal.
|
|
631
1029
|
private def check_required_fields(value_object, fields)
|
|
632
1030
|
value_object.attributes.each do |attribute|
|
|
633
1031
|
next if attribute.optional? || attribute.list?
|
|
634
1032
|
next unless fields[attribute.name].nil?
|
|
635
1033
|
|
|
636
1034
|
raise TypeMismatch,
|
|
637
|
-
RefusalWording.
|
|
638
|
-
|
|
639
|
-
|
|
1035
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
1036
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
1037
|
+
expected: attribute.type, offered: "nil")
|
|
640
1038
|
end
|
|
641
1039
|
end
|
|
642
1040
|
|
|
643
|
-
# Checked
|
|
644
|
-
# is exactly the thing that
|
|
1041
|
+
# Checked before invariants, because an invariant reading a mistyped field
|
|
1042
|
+
# is exactly the thing that would otherwise explode.
|
|
645
1043
|
NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
|
|
646
1044
|
private def check_numeric_fields(value_object, fields)
|
|
647
1045
|
value_object.attributes.each do |attribute|
|
|
@@ -653,9 +1051,9 @@ module Hecks
|
|
|
653
1051
|
|
|
654
1052
|
unless given.is_a?(expected)
|
|
655
1053
|
raise TypeMismatch,
|
|
656
|
-
RefusalWording.
|
|
657
|
-
|
|
658
|
-
|
|
1054
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
1055
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
1056
|
+
expected: attribute.type, offered: Rendering.describe(given))
|
|
659
1057
|
end
|
|
660
1058
|
|
|
661
1059
|
# PRD 05 (numeric-boundary-coverage) — `given.is_a?(expected)`
|
|
@@ -673,8 +1071,8 @@ module Hecks
|
|
|
673
1071
|
# storage, where `JSON.generate`/`#to_json` raises
|
|
674
1072
|
# `JSON::GeneratorError: NaN/Infinity not allowed in JSON` the
|
|
675
1073
|
# moment anything tries to persist or replay it — again a raw
|
|
676
|
-
# crash, not a refusal. `-0.0` is deliberately
|
|
677
|
-
# here: it
|
|
1074
|
+
# crash, not a refusal. `-0.0` is deliberately not refused
|
|
1075
|
+
# here: it is finite, round-trips through JSON as `-0.0`
|
|
678
1076
|
# cleanly (confirmed empirically), and is a legitimate,
|
|
679
1077
|
# meaningful float value (a signed zero), not a corruption
|
|
680
1078
|
# risk — only NaN and +/-Infinity are.
|
|
@@ -683,45 +1081,60 @@ module Hecks
|
|
|
683
1081
|
end
|
|
684
1082
|
|
|
685
1083
|
# A field declared `String` (or a boolean) must not arrive as a
|
|
686
|
-
#
|
|
687
|
-
# what has to be a leaf scalar.
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
#
|
|
694
|
-
#
|
|
695
|
-
#
|
|
696
|
-
#
|
|
697
|
-
# that
|
|
698
|
-
#
|
|
699
|
-
#
|
|
700
|
-
#
|
|
701
|
-
#
|
|
702
|
-
#
|
|
703
|
-
#
|
|
704
|
-
#
|
|
1084
|
+
# composite — an Array or a Hash (or a nested Value) standing in for
|
|
1085
|
+
# what has to be a leaf scalar. A `String` field, further, must not
|
|
1086
|
+
# arrive as any other non-composite scalar either (Integer, Float,
|
|
1087
|
+
# true/false) — QualityControl BUG#125, matching Rust's generated
|
|
1088
|
+
# `from_json`, which requires a JSON string node for a String-typed
|
|
1089
|
+
# field unconditionally and refuses anything else, including a JSON
|
|
1090
|
+
# number or boolean. Without this, Ruby would tolerate exactly that
|
|
1091
|
+
# (found live: `Chess::Piece.Capture`'s `PieceId`, String-typed,
|
|
1092
|
+
# offered a bignum `id` — Ruby let it pass and failed later on an
|
|
1093
|
+
# unrelated field, Rust refused on `id` itself, immediately) — except
|
|
1094
|
+
# inside `judge_bootstrapping?` (above), the one caller genuinely
|
|
1095
|
+
# relying on that leniency; see that flag's own comment for why.
|
|
1096
|
+
#
|
|
1097
|
+
# `TrueClass`/`FalseClass` stay laxer than `check_numeric_fields`
|
|
1098
|
+
# above: for those two, this still only enforces that the shape
|
|
1099
|
+
# isn't a collection, not the exact Ruby class — narrower than the
|
|
1100
|
+
# `String` case above because BUG#125 investigated and fixed String
|
|
1101
|
+
# specifically; a boolean field's own scalar-shape tolerance is a
|
|
1102
|
+
# separate, uninvestigated question left exactly as it was. No
|
|
1103
|
+
# scalar field, of any declared type, can ever legitimately be
|
|
1104
|
+
# handed an Array or a Hash — that shape is always wrong, and always
|
|
1105
|
+
# was: `InvalidValueGenerator#array_for_scalar`'s own corruption is
|
|
1106
|
+
# deliberately built to be refused (see that file's header), and
|
|
1107
|
+
# until this check existed it sailed straight through for a
|
|
1108
|
+
# String/boolean field the way it never could for an Integer/Float
|
|
1109
|
+
# one (`check_numeric_fields` above already catches an Array offered
|
|
1110
|
+
# for those). Found live via bin/fuzz, seed 17 on the fixtures
|
|
1111
|
+
# domain : an Array standing in for a single-field identity's
|
|
1112
|
+
# declared `String`, `.to_s`'d into a record id downstream.
|
|
705
1113
|
COMPOSITE_SHAPES = [Array, ::Hash].freeze
|
|
706
1114
|
NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
|
|
707
1115
|
private def check_scalar_shapes(value_object, fields)
|
|
708
1116
|
value_object.attributes.each do |attribute|
|
|
709
|
-
|
|
1117
|
+
type = attribute.type.to_s
|
|
1118
|
+
next unless NON_NUMERIC_SCALARS.include?(type)
|
|
710
1119
|
|
|
711
1120
|
given = fields[attribute.name]
|
|
712
|
-
next if given.nil?
|
|
1121
|
+
next if given.nil?
|
|
1122
|
+
|
|
1123
|
+
composite = COMPOSITE_SHAPES.any? { |shape| given.is_a?(shape) }
|
|
1124
|
+
non_string_scalar = type == "String" && !composite && !given.is_a?(String) && !judge_bootstrapping?
|
|
1125
|
+
next unless composite || non_string_scalar
|
|
713
1126
|
|
|
714
1127
|
raise TypeMismatch,
|
|
715
|
-
RefusalWording.
|
|
716
|
-
|
|
717
|
-
|
|
1128
|
+
RefusalWording.render_site("TypeMismatch", "numeric_field",
|
|
1129
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
1130
|
+
expected: attribute.type, offered: Rendering.describe(given))
|
|
718
1131
|
end
|
|
719
1132
|
end
|
|
720
1133
|
|
|
721
|
-
# A field declared with a
|
|
1134
|
+
# A field declared with a pattern must match it.
|
|
722
1135
|
#
|
|
723
1136
|
# Beside check_numeric_fields and for the same reason : a value that does
|
|
724
|
-
# not look like what it claims to be is the
|
|
1137
|
+
# not look like what it claims to be is the domain saying no, and it should
|
|
725
1138
|
# say so here rather than let the wrong shape travel on and surface as a
|
|
726
1139
|
# broken predicate later.
|
|
727
1140
|
#
|
|
@@ -738,9 +1151,9 @@ module Hecks
|
|
|
738
1151
|
next if given.is_a?(String) && Regexp.new(pattern).match?(given)
|
|
739
1152
|
|
|
740
1153
|
raise TypeMismatch,
|
|
741
|
-
RefusalWording.
|
|
742
|
-
|
|
743
|
-
|
|
1154
|
+
RefusalWording.render_site("TypeMismatch", "pattern_mismatch",
|
|
1155
|
+
type: value_object.hecks_name, field: attribute.name,
|
|
1156
|
+
pattern: pattern, offered: Rendering.describe(given))
|
|
744
1157
|
end
|
|
745
1158
|
end
|
|
746
1159
|
end
|