hecks 1.4.0 → 1.5.1
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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- 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 +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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 +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- 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 +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -10,10 +10,11 @@ module Hecks
|
|
|
10
10
|
# Dispatcher, its own adapter instances) rather than one shared
|
|
11
11
|
# process switching connections mid-dispatch.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# ## Why no ambient "current tenant"
|
|
14
|
+
#
|
|
15
|
+
# The project register (Bluebook::ProjectRegister) already resolves an
|
|
16
|
+
# address's realm to a dispatcher at registration time — Router#resolve
|
|
17
|
+
# looks the FQN up in one flat table keyed by realm::domain::aggregate.verb,
|
|
17
18
|
# and each entry already carries its own dispatcher from its own
|
|
18
19
|
# boot. So "which tenant" is decided once, at boot/registration time
|
|
19
20
|
# (which of possibly many boots of the same directory a request's
|
|
@@ -23,8 +24,9 @@ module Hecks
|
|
|
23
24
|
# already gives for free — each tenant's own PostgresEra instance
|
|
24
25
|
# is its own connection, held for the life of that boot.
|
|
25
26
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
27
|
+
# ## What `tenant_capable?` really asks
|
|
28
|
+
#
|
|
29
|
+
# Not "can this adapter switch tenants," but "does booting this
|
|
28
30
|
# adapter twice, with different settings, for the same directory,
|
|
29
31
|
# actually keep the two boots' data apart." Memory answers true
|
|
30
32
|
# trivially — a `@records` Hash is a plain instance variable, and
|
|
@@ -41,6 +43,9 @@ module Hecks
|
|
|
41
43
|
module TenantCheck
|
|
42
44
|
module_function
|
|
43
45
|
|
|
46
|
+
# Refuses to let `domain` boot for more than one tenant unless every
|
|
47
|
+
# aggregate's resolved persistence adapter is `tenant_capable?`.
|
|
48
|
+
#
|
|
44
49
|
# A domain is safe to boot for more than one tenant only if every
|
|
45
50
|
# aggregate's resolved persistence adapter is tenant_capable? — one
|
|
46
51
|
# ungoverned adapter sharing state across two tenant boots is a
|
|
@@ -48,6 +53,12 @@ module Hecks
|
|
|
48
53
|
# a second tenant boot of the same directory is trusted, the same
|
|
49
54
|
# severity EraCheck/refuse_ungoverned_roles! already hold their
|
|
50
55
|
# own gates to.
|
|
56
|
+
#
|
|
57
|
+
# @param registry [Runtime::Registry] the booted registry to check
|
|
58
|
+
# @param domain [String, Symbol] the domain name to check every aggregate of
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @raise [Runtime::WiringError] if any aggregate in `domain` is bound to an adapter
|
|
61
|
+
# that is not `tenant_capable?`
|
|
51
62
|
def refuse_unless_tenant_capable!(registry, domain)
|
|
52
63
|
bluebook = registry.bluebook(domain)
|
|
53
64
|
return unless bluebook
|
|
@@ -67,12 +78,21 @@ module Hecks
|
|
|
67
78
|
"or keep #{domain} single-tenant."
|
|
68
79
|
end
|
|
69
80
|
|
|
81
|
+
# Answers whether `adapter_name`'s Ruby implementation keeps two
|
|
82
|
+
# tenants' boots of the same directory from sharing data.
|
|
83
|
+
#
|
|
70
84
|
# The capability idiom itself — an adapter class that answers
|
|
71
85
|
# tenant_capable? with true keeps two boots' data apart by
|
|
72
86
|
# construction (Memory) or by an explicit per-boot isolation
|
|
73
87
|
# setting (PostgresEra's schema:). Same defensive shape
|
|
74
88
|
# EraCheck#lineage_capable? already uses: a class that doesn't
|
|
75
89
|
# respond at all is false, not an error.
|
|
90
|
+
#
|
|
91
|
+
# @param registry [Runtime::Registry] the booted registry the adapter is wired into
|
|
92
|
+
# @param adapter_name [String] the adapter's declared name, such as `"PostgresEra"`
|
|
93
|
+
# @return [Boolean] true when the adapter is registered, has a Ruby implementation,
|
|
94
|
+
# and answers `tenant_capable?` true; false for any other case, including a
|
|
95
|
+
# missing adapter or one whose lookup raises
|
|
76
96
|
def tenant_capable?(registry, adapter_name)
|
|
77
97
|
adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
|
|
78
98
|
adapter_class.respond_to?(:tenant_capable?) && adapter_class.tenant_capable?
|
|
@@ -29,6 +29,16 @@ module Hecks
|
|
|
29
29
|
module TenantScope
|
|
30
30
|
module_function
|
|
31
31
|
|
|
32
|
+
# Wraps a declared query/read-model spec with its tenant boundary clause, if it has one.
|
|
33
|
+
#
|
|
34
|
+
# @param declared [Bluebook::Query, Bluebook::ReadModel] the declared specification to
|
|
35
|
+
# scope
|
|
36
|
+
# @param args [Hash] the query's arguments, checked for the declared tenant field
|
|
37
|
+
# @return [Bluebook::Query, Bluebook::ReadModel, Runtime::TenantScope::Scoped]
|
|
38
|
+
# `declared` unchanged when it declares no `authorize policy, tenant:`; otherwise a
|
|
39
|
+
# `Scoped` wrapper whose `#wheres` adds the tenant `eq` clause
|
|
40
|
+
# @raise [Runtime::Unauthorized] if `declared` declares a tenant boundary and `args`
|
|
41
|
+
# omits that field
|
|
32
42
|
def apply(declared, args)
|
|
33
43
|
tenant = declared.authorization&.tenant
|
|
34
44
|
return declared unless tenant
|
|
@@ -50,11 +60,19 @@ module Hecks
|
|
|
50
60
|
# own internal `.wheres` read would bypass this override (see this
|
|
51
61
|
# module's own header for why).
|
|
52
62
|
class Scoped < SimpleDelegator
|
|
63
|
+
# @param declared [Bluebook::Query, Bluebook::ReadModel] the specification to wrap,
|
|
64
|
+
# delegated to for everything but `#wheres`
|
|
65
|
+
# @param clause [QuerySpecification::Common::WhereClause] the synthetic tenant `eq`
|
|
66
|
+
# clause to append
|
|
53
67
|
def initialize(declared, clause)
|
|
54
68
|
super(declared)
|
|
55
69
|
@clause = clause
|
|
56
70
|
end
|
|
57
71
|
|
|
72
|
+
# Reads the wrapped specification's where-clauses, with the tenant clause appended.
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<QuerySpecification::Common::WhereClause>] `declared.wheres` with the
|
|
75
|
+
# tenant clause appended
|
|
58
76
|
def wheres = __getobj__.wheres + [@clause]
|
|
59
77
|
end
|
|
60
78
|
end
|
|
@@ -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
|
-
# The four SHAPES an attribute's value can take — named here because
|
|
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?`
|
|
@@ -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
|
|
@@ -71,6 +85,30 @@ module Hecks
|
|
|
71
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?
|
|
@@ -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
|
|
|
@@ -198,8 +256,8 @@ module Hecks
|
|
|
198
256
|
# object, `direct_identity_head`) — an ad hoc wrapper around a
|
|
199
257
|
# bare scalar under some other field name (`VenueHandle`'s own
|
|
200
258
|
# `value`, not `Venue`'s own `code`) fails every path lookup, and
|
|
201
|
-
#
|
|
202
|
-
# storing the wrapped Value. That leaves one reference field on
|
|
259
|
+
# without this would fall through to the unresolved `return value`
|
|
260
|
+
# above, storing the wrapped Value. That leaves one reference field on
|
|
203
261
|
# one aggregate holding two different shapes depending on which
|
|
204
262
|
# command last wrote it — `Open`'s own bare `reference_to Venue`
|
|
205
263
|
# argument was never wrapped in the first place (the top guard
|
|
@@ -218,6 +276,11 @@ module Hecks
|
|
|
218
276
|
# already does for `Value.materialize_unwrapped`'s other callers,
|
|
219
277
|
# landing on the bare scalar `Open`'s own path already produces
|
|
220
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
|
|
221
284
|
def sole_scalar_identity(value, paths)
|
|
222
285
|
return nil unless value.is_a?(self) && paths.one?
|
|
223
286
|
return nil unless value.value_object.sole_attribute
|
|
@@ -228,6 +291,11 @@ module Hecks
|
|
|
228
291
|
# Whether `value` is itself the target's own (single) identity
|
|
229
292
|
# value object — pure, self-contained: reads only `value` and
|
|
230
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
|
|
231
299
|
def direct_identity_head(value, target)
|
|
232
300
|
return nil unless value.is_a?(self) && target.identity_heads.one?
|
|
233
301
|
|
|
@@ -241,6 +309,14 @@ module Hecks
|
|
|
241
309
|
# three inputs; extracted from `reference_identity` alongside
|
|
242
310
|
# `direct_identity_head` above purely to keep that method to its
|
|
243
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
|
|
244
320
|
def identity_part(materialized, path, direct_head)
|
|
245
321
|
segments = path.to_s.split(".")
|
|
246
322
|
segments.shift if direct_head && segments.first == direct_head
|
|
@@ -255,6 +331,12 @@ module Hecks
|
|
|
255
331
|
end
|
|
256
332
|
end
|
|
257
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
|
|
258
340
|
def reference_list(attribute, value)
|
|
259
341
|
unless value.is_a?(Array)
|
|
260
342
|
raise TypeMismatch,
|
|
@@ -264,6 +346,18 @@ module Hecks
|
|
|
264
346
|
Freezer.deep(value.dup)
|
|
265
347
|
end
|
|
266
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
|
|
267
361
|
def fields_for(value_object, name, value)
|
|
268
362
|
return value.transform_keys(&:to_sym) if value.is_a?(Hash)
|
|
269
363
|
# Mutations may legitimately carry a value object into a differently
|
|
@@ -316,11 +410,11 @@ module Hecks
|
|
|
316
410
|
# a value-object-typed field of another value object staying a
|
|
317
411
|
# plain Hash once stored, and this does not change that; it only
|
|
318
412
|
# makes sure that Hash has the shape its own type declares.
|
|
319
|
-
# `aggregate` is the one thing `
|
|
320
|
-
# 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.
|
|
321
415
|
# value_object(name)`, so callers with no aggregate in reach
|
|
322
416
|
# (`Value#with`, always re-setting an already-scalar arithmetic
|
|
323
|
-
# field) simply skip this and keep their
|
|
417
|
+
# field) simply skip this and keep their own behavior unchanged.
|
|
324
418
|
# Recurses into each nested field's own validation too, not only its
|
|
325
419
|
# shape — found live alongside the shape bug this method's header
|
|
326
420
|
# already describes: a nested `Price`/`Size` (a value-object-typed
|
|
@@ -335,6 +429,22 @@ module Hecks
|
|
|
335
429
|
# defaults` runs first, same as the outer value object gets in
|
|
336
430
|
# `build`, so a nested field's own default is filled in before its
|
|
337
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
|
|
338
448
|
def normalize_composite_fields(aggregate, value_object, fields)
|
|
339
449
|
return fields unless aggregate.respond_to?(:value_object)
|
|
340
450
|
|
|
@@ -372,6 +482,13 @@ module Hecks
|
|
|
372
482
|
fields
|
|
373
483
|
end
|
|
374
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
|
|
375
492
|
def apply_defaults(value_object, fields)
|
|
376
493
|
value_object.attributes.each_with_object(fields) do |attribute, completed|
|
|
377
494
|
completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
|
|
@@ -383,6 +500,19 @@ module Hecks
|
|
|
383
500
|
# (every nested one), so a nested `Price`/`Size` is refused exactly
|
|
384
501
|
# the same way, with exactly the same wording, as the identical type
|
|
385
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
|
|
386
516
|
def validate!(value_object, fields)
|
|
387
517
|
# C6.3 (docs/semantics/bluebook-semantics.md) — a value object is
|
|
388
518
|
# validated on construction from input only; state read back from
|
|
@@ -409,6 +539,22 @@ module Hecks
|
|
|
409
539
|
end
|
|
410
540
|
end
|
|
411
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
|
|
412
558
|
def build(value_object, fields, aggregate = nil)
|
|
413
559
|
fields = apply_defaults(value_object, fields.transform_keys(&:to_sym))
|
|
414
560
|
fields = normalize_composite_fields(aggregate, value_object, fields)
|
|
@@ -422,10 +568,17 @@ module Hecks
|
|
|
422
568
|
# the runtime's own callers (entity elements, the remote dispatcher's
|
|
423
569
|
# `symbolize_names:` parse, Era's audit) build symbol-keyed state
|
|
424
570
|
# themselves. A String key here is an adapter or caller that skipped
|
|
425
|
-
# the codec, so it is refused by name rather than respelled:
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
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
|
|
429
582
|
def hydrate(aggregate, state)
|
|
430
583
|
undecoded = state.keys.grep_v(Symbol)
|
|
431
584
|
unless undecoded.empty?
|
|
@@ -444,6 +597,15 @@ module Hecks
|
|
|
444
597
|
|
|
445
598
|
TRUSTED_LOAD_KEY = :hecks_trusting_stored_state
|
|
446
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
|
|
447
609
|
def trusting_stored_state
|
|
448
610
|
previous = Thread.current[TRUSTED_LOAD_KEY]
|
|
449
611
|
Thread.current[TRUSTED_LOAD_KEY] = true
|
|
@@ -452,6 +614,10 @@ module Hecks
|
|
|
452
614
|
Thread.current[TRUSTED_LOAD_KEY] = previous
|
|
453
615
|
end
|
|
454
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
|
|
455
621
|
def trusting_stored_state? = Thread.current[TRUSTED_LOAD_KEY] == true
|
|
456
622
|
|
|
457
623
|
# QualityControl BUG#125 — the one narrow door `check_scalar_shapes`
|
|
@@ -494,6 +660,16 @@ module Hecks
|
|
|
494
660
|
# field.
|
|
495
661
|
BOOTSTRAP_KEY = :hecks_judge_bootstrapping
|
|
496
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
|
|
497
673
|
def judge_bootstrapping
|
|
498
674
|
previous = Thread.current[BOOTSTRAP_KEY]
|
|
499
675
|
Thread.current[BOOTSTRAP_KEY] = true
|
|
@@ -502,23 +678,23 @@ module Hecks
|
|
|
502
678
|
Thread.current[BOOTSTRAP_KEY] = previous
|
|
503
679
|
end
|
|
504
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
|
|
505
685
|
def judge_bootstrapping? = Thread.current[BOOTSTRAP_KEY] == true
|
|
506
686
|
|
|
507
|
-
# `Value.identifier` used to live here: hand it a one-field value object
|
|
508
|
-
# and it opened it, so `identified_by :number` could pass for an identity
|
|
509
|
-
# and the runtime would guess which field was meant. That guess is gone.
|
|
510
687
|
# An identity names its field — `identified_by :number` — and the
|
|
511
|
-
# path is what reaches the scalar
|
|
512
|
-
#
|
|
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.
|
|
513
692
|
#
|
|
514
693
|
# `scalar` below is a different job and stays: rendering a value object
|
|
515
694
|
# into a column or a message, where there is no path to consult.
|
|
516
695
|
|
|
517
|
-
#
|
|
518
|
-
#
|
|
519
|
-
# arrives as anything else — so there is nothing left to open. The comment
|
|
520
|
-
# it carried said retiring it meant changing how references are stored ;
|
|
521
|
-
# 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.
|
|
522
698
|
|
|
523
699
|
# A reference is an ID, so anything else is not one.
|
|
524
700
|
#
|
|
@@ -534,9 +710,9 @@ module Hecks
|
|
|
534
710
|
#
|
|
535
711
|
# Widened past the object shape by BUG#27 (QualityControl ledger,
|
|
536
712
|
# found live on `qa/stress_domains/referral_chain`'s `Member.Join`/
|
|
537
|
-
# `Referral.Issue`).
|
|
538
|
-
# through here untouched — nothing but Hash/Value
|
|
539
|
-
# 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::
|
|
540
716
|
# References#reference_key` ("true", "false", "[8, 8]") and answer
|
|
541
717
|
# NotFound, or, for `null`, skip the lookup outright
|
|
542
718
|
# (`next if held.nil?`, command_rules/references.rb) and let the
|
|
@@ -569,6 +745,16 @@ module Hecks
|
|
|
569
745
|
# today, and inventing a rule for a shape the language cannot
|
|
570
746
|
# declare is how decoration gets written. `reference_list` (below)
|
|
571
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
|
|
572
758
|
def refuse_object_reference(command, attribute, value)
|
|
573
759
|
return unless attribute.reference?
|
|
574
760
|
|
|
@@ -596,6 +782,10 @@ module Hecks
|
|
|
596
782
|
# `false`, `nil`, `[8, 8]` — the same rendering every other
|
|
597
783
|
# TypeMismatch in this file already uses for "here is what you
|
|
598
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)`
|
|
599
789
|
def reference_shape_description(value)
|
|
600
790
|
return "an object" if value.is_a?(Hash) || value.is_a?(self)
|
|
601
791
|
|
|
@@ -613,6 +803,11 @@ module Hecks
|
|
|
613
803
|
# identity has two parts — so a composite target fell through the guard
|
|
614
804
|
# and the refusal went silent exactly where it had the most to say. A
|
|
615
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
|
|
616
811
|
def known_by(attribute)
|
|
617
812
|
heads = Array(attribute.type.resolve&.identity_heads)
|
|
618
813
|
return "" if heads.empty?
|
|
@@ -620,6 +815,15 @@ module Hecks
|
|
|
620
815
|
" (#{attribute.type.target_name} is known by #{heads.join(', ')})"
|
|
621
816
|
end
|
|
622
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
|
|
623
827
|
def scalar(value)
|
|
624
828
|
return value unless value.is_a?(self)
|
|
625
829
|
|
|
@@ -629,6 +833,19 @@ module Hecks
|
|
|
629
833
|
raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "multi_field_scalar", type: value.type_name)
|
|
630
834
|
end
|
|
631
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
|
|
632
849
|
def from_identifier(aggregate, attribute, identifier)
|
|
633
850
|
value_object = value_object_for(aggregate, attribute.type)
|
|
634
851
|
return identifier unless value_object
|
|
@@ -679,6 +896,11 @@ module Hecks
|
|
|
679
896
|
identifier
|
|
680
897
|
end
|
|
681
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
|
|
682
904
|
def canonical_fields(fields)
|
|
683
905
|
JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
|
|
684
906
|
end
|
|
@@ -798,10 +1020,10 @@ module Hecks
|
|
|
798
1020
|
# nil" — the identical string the Rust side's generated `from_json`
|
|
799
1021
|
# gives the same input, so the corpus can pin it on both. Checked
|
|
800
1022
|
# first among the field-content checks (after `check_unknown_fields`'s
|
|
801
|
-
# own structural gate above, BUG#41): an invariant
|
|
802
|
-
# that never arrived
|
|
803
|
-
# "invariant violated" (or nothing at all — `ToppingName`'s
|
|
804
|
-
# `{value: null}`
|
|
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
|
|
805
1027
|
# already been filled in by `apply_defaults`; a list field's absence
|
|
806
1028
|
# is an empty list, never a refusal.
|
|
807
1029
|
private def check_required_fields(value_object, fields)
|
|
@@ -817,7 +1039,7 @@ module Hecks
|
|
|
817
1039
|
end
|
|
818
1040
|
|
|
819
1041
|
# Checked before invariants, because an invariant reading a mistyped field
|
|
820
|
-
# is exactly the thing that
|
|
1042
|
+
# is exactly the thing that would otherwise explode.
|
|
821
1043
|
NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
|
|
822
1044
|
private def check_numeric_fields(value_object, fields)
|
|
823
1045
|
value_object.attributes.each do |attribute|
|
|
@@ -865,12 +1087,12 @@ module Hecks
|
|
|
865
1087
|
# true/false) — QualityControl BUG#125, matching Rust's generated
|
|
866
1088
|
# `from_json`, which requires a JSON string node for a String-typed
|
|
867
1089
|
# field unconditionally and refuses anything else, including a JSON
|
|
868
|
-
# number or boolean. Ruby
|
|
869
|
-
# `Chess::Piece.Capture`'s `PieceId`, String-typed,
|
|
870
|
-
# `id` — Ruby let it pass and failed later on an
|
|
871
|
-
# Rust refused on `id` itself, immediately) —
|
|
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
|
|
872
1094
|
# inside `judge_bootstrapping?` (above), the one caller genuinely
|
|
873
|
-
# relying on
|
|
1095
|
+
# relying on that leniency; see that flag's own comment for why.
|
|
874
1096
|
#
|
|
875
1097
|
# `TrueClass`/`FalseClass` stay laxer than `check_numeric_fields`
|
|
876
1098
|
# above: for those two, this still only enforces that the shape
|