hecks 1.4.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 +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.hecksagon +19 -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/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 +58 -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/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/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 +29 -5
- data/lib/hecks/projector.rb +74 -6
- 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 +11 -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/naming.rb
CHANGED
|
@@ -18,9 +18,19 @@ module Hecks
|
|
|
18
18
|
|
|
19
19
|
module_function
|
|
20
20
|
|
|
21
|
-
#
|
|
21
|
+
# Joins the parts of an identity in declaration order.
|
|
22
|
+
#
|
|
23
|
+
# @param parts [Array<#to_s>, #to_s] one or more identity segments; a bare
|
|
24
|
+
# value is wrapped in a single-element Array
|
|
25
|
+
# @return [String] the segments joined with `IDENTITY_JOIN`
|
|
22
26
|
def identity(parts) = Array(parts).join(IDENTITY_JOIN)
|
|
23
27
|
|
|
28
|
+
# Strips a namespace path down to its last segment.
|
|
29
|
+
#
|
|
30
|
+
# @param type [Module, String, Symbol, #to_s] a `::`-joined constant path, or
|
|
31
|
+
# anything whose `to_s` is one
|
|
32
|
+
# @return [String] the text after the last `::`, or the whole `to_s` if there
|
|
33
|
+
# is none
|
|
24
34
|
def demodulise(type)
|
|
25
35
|
type.to_s.split("::").last.to_s
|
|
26
36
|
end
|
|
@@ -28,10 +38,17 @@ module Hecks
|
|
|
28
38
|
# snake_case -> PascalCase. The name a synthesised closed-set value object
|
|
29
39
|
# takes when an attribute declares one inline. The derivation is part of
|
|
30
40
|
# the IR contract: the same bluebook must always produce the same name.
|
|
41
|
+
#
|
|
42
|
+
# @param text [String, Symbol, #to_s] a snake_case (or already Pascal) name
|
|
43
|
+
# @return [String] the PascalCase form
|
|
31
44
|
def pascal(text)
|
|
32
45
|
text.to_s.split("_").map { |part| part.sub(/\A(.)/) { Regexp.last_match(1).upcase } }.join
|
|
33
46
|
end
|
|
34
47
|
|
|
48
|
+
# PascalCase or camelCase -> snake_case.
|
|
49
|
+
#
|
|
50
|
+
# @param text [String, Symbol, #to_s] a Pascal-, camel-, or already snake-case name
|
|
51
|
+
# @return [String] the lowercase, underscore-separated form
|
|
35
52
|
def snake(text)
|
|
36
53
|
text.to_s
|
|
37
54
|
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
@@ -48,6 +65,9 @@ module Hecks
|
|
|
48
65
|
# nobody says ("Atm"). First word capitalized, the rest lowercased,
|
|
49
66
|
# so a headword reads as sentence case whatever casing it was
|
|
50
67
|
# declared in.
|
|
68
|
+
#
|
|
69
|
+
# @param text [String, Symbol, #to_s] a Pascal-, camel-, or snake-case identifier
|
|
70
|
+
# @return [String] the space-separated, sentence-cased phrase
|
|
51
71
|
def words(text)
|
|
52
72
|
parts = text.to_s
|
|
53
73
|
.tr("_", " ")
|
|
@@ -61,9 +81,14 @@ module Hecks
|
|
|
61
81
|
end.join(" ")
|
|
62
82
|
end
|
|
63
83
|
|
|
64
|
-
# "A, B, and C" / "A or B" —
|
|
65
|
-
# sentence a projection writes wants; lived in
|
|
66
|
-
# alone until a second projection needed it.
|
|
84
|
+
# Joins items into the Oxford-comma list — "A, B, and C" / "A or B" —
|
|
85
|
+
# every English sentence a projection writes wants; lived in
|
|
86
|
+
# `NarrateProjector` alone until a second projection needed it.
|
|
87
|
+
#
|
|
88
|
+
# @param items [Array<#to_s>] the items to join
|
|
89
|
+
# @param conj [String] the word before the last item, such as `"and"` or `"or"`
|
|
90
|
+
# @return [String] `""` for no items, the item's `to_s` for one, and the
|
|
91
|
+
# Oxford-comma join for more
|
|
67
92
|
def to_sentence_list(items, conj: "and")
|
|
68
93
|
case items.size
|
|
69
94
|
when 0 then ""
|
|
@@ -73,9 +98,12 @@ module Hecks
|
|
|
73
98
|
end
|
|
74
99
|
end
|
|
75
100
|
|
|
76
|
-
#
|
|
77
|
-
# `Projections::Statements#article`
|
|
78
|
-
# plain word, never "hour" or "university".
|
|
101
|
+
# Picks the article for `word` by its first letter — the vowel-letter
|
|
102
|
+
# heuristic, safe here for the same reason `Projections::Statements#article`
|
|
103
|
+
# gives: a construct name is a plain word, never "hour" or "university".
|
|
104
|
+
#
|
|
105
|
+
# @param word [String, Symbol, #to_s] the word the article precedes
|
|
106
|
+
# @return [String] `"an"` if `word` starts with a vowel letter, `"a"` otherwise
|
|
79
107
|
def a_or_an(word)
|
|
80
108
|
%w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
|
|
81
109
|
end
|
|
@@ -89,6 +117,9 @@ module Hecks
|
|
|
89
117
|
# the wrong rule against itself. Agreement is not correctness; it never was.
|
|
90
118
|
#
|
|
91
119
|
# So: one pluraliser, three rules, and every collection name flows through it.
|
|
120
|
+
#
|
|
121
|
+
# @param text [String, Symbol, #to_s] a singular name
|
|
122
|
+
# @return [String] the pluralised name
|
|
92
123
|
def plural(text)
|
|
93
124
|
word = text.to_s
|
|
94
125
|
return "#{word[0..-2]}ies" if word.match?(/[^aeiou]y\z/)
|
|
@@ -104,6 +135,9 @@ module Hecks
|
|
|
104
135
|
# already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
|
|
105
136
|
# whole rule — enough for `has_many Invoices` to resolve to the aggregate
|
|
106
137
|
# actually named Invoice.
|
|
138
|
+
#
|
|
139
|
+
# @param text [String, Symbol, #to_s] a plural name
|
|
140
|
+
# @return [String] the singularised name
|
|
107
141
|
def singularize(text)
|
|
108
142
|
word = text.to_s
|
|
109
143
|
return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
|
|
@@ -124,25 +158,47 @@ module Hecks
|
|
|
124
158
|
word
|
|
125
159
|
end
|
|
126
160
|
|
|
161
|
+
# Derives the attribute name a reference to `type` is stored under.
|
|
162
|
+
#
|
|
163
|
+
# @param type [Module, String, Symbol, #to_s] the referenced construct's name
|
|
164
|
+
# or a `::`-joined path to it
|
|
165
|
+
# @return [Symbol] the snake_case, demodulised name, as a Symbol
|
|
127
166
|
def reference_key(type)
|
|
128
167
|
snake(demodulise(type)).to_sym
|
|
129
168
|
end
|
|
130
169
|
|
|
170
|
+
# Splits a `qualifier.name` string on its first dot.
|
|
171
|
+
#
|
|
172
|
+
# @param dotted [String, Symbol, #to_s] text, optionally containing a dot
|
|
173
|
+
# @return [Array(String, String)] `[before the first dot, after it]`; the
|
|
174
|
+
# second element is `""` when `dotted` has no dot
|
|
131
175
|
def split_dotted(dotted)
|
|
132
176
|
first, second = dotted.to_s.split(".", 2)
|
|
133
177
|
[first.to_s, second.to_s]
|
|
134
178
|
end
|
|
135
179
|
|
|
180
|
+
# The part of a dotted name before its first dot.
|
|
181
|
+
#
|
|
182
|
+
# @param dotted [String, Symbol, #to_s] text, optionally containing a dot
|
|
183
|
+
# @return [String, nil] the text before the first dot, or nil if `dotted`
|
|
184
|
+
# has no dot
|
|
136
185
|
def qualifier(dotted)
|
|
137
186
|
text = dotted.to_s
|
|
138
187
|
text.include?(".") ? text.split(".", 2).first : nil
|
|
139
188
|
end
|
|
140
189
|
|
|
190
|
+
# The part of a dotted name after its first dot.
|
|
191
|
+
#
|
|
192
|
+
# @param dotted [String, Symbol, #to_s] text, optionally containing a dot
|
|
193
|
+
# @return [String] the text after the first dot, or the whole text if
|
|
194
|
+
# `dotted` has no dot
|
|
141
195
|
def unqualified(dotted)
|
|
142
196
|
text = dotted.to_s
|
|
143
197
|
text.include?(".") ? text.split(".", 2).last : text
|
|
144
198
|
end
|
|
145
199
|
|
|
200
|
+
# Splits a domain-qualified verb into its domain, aggregate, and command parts.
|
|
201
|
+
#
|
|
146
202
|
# Domain, aggregate, then the REST dot-joined into one command path.
|
|
147
203
|
#
|
|
148
204
|
# The `::` boundary between domain and aggregate is unambiguous by
|
|
@@ -166,6 +222,11 @@ module Hecks
|
|
|
166
222
|
# .kind_for` and `ReactionInvocation#resolve_target` both already
|
|
167
223
|
# assume this contract on their own end; this is what actually
|
|
168
224
|
# delivers it to them.
|
|
225
|
+
#
|
|
226
|
+
# @param verb [String, Symbol, #to_s] a `Domain::Aggregate.command` (or
|
|
227
|
+
# `.query`) path
|
|
228
|
+
# @return [Array(String, String, String), nil] `[domain, aggregate, command]`,
|
|
229
|
+
# or nil if `verb` has no `.` or no `domain::aggregate` before it
|
|
169
230
|
def split_verb(verb)
|
|
170
231
|
path, command = verb.to_s.split(".", 2)
|
|
171
232
|
return nil unless path && command
|
|
@@ -178,6 +239,9 @@ module Hecks
|
|
|
178
239
|
[domain, aggregate, command]
|
|
179
240
|
end
|
|
180
241
|
|
|
242
|
+
# Rewrites a bare command constant's trailing `::` into `.`, matching a
|
|
243
|
+
# command's own FQN separator; a String or Symbol passes through unchanged.
|
|
244
|
+
#
|
|
181
245
|
# `trigger Account::Debit` / `dispatch Account::Debit` — a bare
|
|
182
246
|
# constant reference (`ConstShim`'s own `ScopedConstant`, S0b), not
|
|
183
247
|
# text (ADR 0025, "events and reactions" — command references become
|
|
@@ -195,6 +259,10 @@ module Hecks
|
|
|
195
259
|
# it (its own last `::` sits between the domain and the aggregate,
|
|
196
260
|
# not the aggregate and the command). Only an actual constant object
|
|
197
261
|
# — never seen holding a `.` of its own — needs the rewrite at all.
|
|
262
|
+
#
|
|
263
|
+
# @param value [Symbol, String, Module] the command, as a bare constant (a
|
|
264
|
+
# `ScopedConstant` module `ConstShim` resolves) or already-dotted text
|
|
265
|
+
# @return [String] the dotted command reference
|
|
198
266
|
def command_ref(value)
|
|
199
267
|
return value.to_s if value.is_a?(::String) || value.is_a?(::Symbol)
|
|
200
268
|
|
|
@@ -203,6 +271,9 @@ module Hecks
|
|
|
203
271
|
path.empty? ? text : "#{path}.#{command}"
|
|
204
272
|
end
|
|
205
273
|
|
|
274
|
+
# The event-side twin of `command_ref`, kept as its own name because an
|
|
275
|
+
# event name is not a command name that happens to share a format.
|
|
276
|
+
#
|
|
206
277
|
# `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
|
|
207
278
|
# event-side twin of `command_ref`, above (ADR 0025, S6 — "events
|
|
208
279
|
# first-class"). Identical transform (a bare `ScopedConstant`'s last
|
|
@@ -212,8 +283,14 @@ module Hecks
|
|
|
212
283
|
# own name because the two references mean different things even
|
|
213
284
|
# though the rewrite is byte-identical: an event name is not a
|
|
214
285
|
# command name that happens to share a format.
|
|
286
|
+
#
|
|
287
|
+
# @param value [Symbol, String, Module] the event, as a bare constant (a
|
|
288
|
+
# `ScopedConstant` module `ConstShim` resolves) or already-dotted text
|
|
289
|
+
# @return [String] the dotted event reference
|
|
215
290
|
def event_ref(value) = command_ref(value)
|
|
216
291
|
|
|
292
|
+
# Strips a process manager's event reference down to its bare event name.
|
|
293
|
+
#
|
|
217
294
|
# `transition Account::AccountDebited => "state"` / `starts_on
|
|
218
295
|
# Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
|
|
219
296
|
# — a process manager's own event references (ADR 0025, S6),
|
|
@@ -240,6 +317,10 @@ module Hecks
|
|
|
240
317
|
# passes through unchanged either way, exactly like `command_ref`'s
|
|
241
318
|
# own legacy branch — this corpus never spelled one dotted to begin
|
|
242
319
|
# with, so there is nothing here to strip.
|
|
320
|
+
#
|
|
321
|
+
# @param value [Symbol, String, Module] the event, as a bare constant (a
|
|
322
|
+
# `ScopedConstant` module `ConstShim` resolves) or already-dotted text
|
|
323
|
+
# @return [String] the bare event name, with any qualifier stripped
|
|
243
324
|
def event_name_ref(value) = demodulise(value)
|
|
244
325
|
end
|
|
245
326
|
end
|
|
@@ -28,8 +28,7 @@ module Hecks
|
|
|
28
28
|
# No adapter or spec double for this port ships in this repository, so every shape
|
|
29
29
|
# below other than `registry` is adapter-defined: the port forwards it untouched.
|
|
30
30
|
#
|
|
31
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
32
|
-
# and handed on to it
|
|
31
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
33
32
|
# @param identity_id [Object] adapter-defined identity key, forwarded unchanged
|
|
34
33
|
# @return [Object] adapter-defined session representation
|
|
35
34
|
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
@@ -44,8 +43,7 @@ module Hecks
|
|
|
44
43
|
# answers `issuer`, `subject` and `email`), but nothing in this repository wires the
|
|
45
44
|
# two together, so their shapes here are adapter-defined.
|
|
46
45
|
#
|
|
47
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
48
|
-
# and handed on to it
|
|
46
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
49
47
|
# @param email [Object] adapter-defined, forwarded unchanged; the person's email
|
|
50
48
|
# @param issuer [Object] adapter-defined, forwarded unchanged; the OIDC issuer that
|
|
51
49
|
# authenticated the person
|
|
@@ -60,8 +58,7 @@ module Hecks
|
|
|
60
58
|
|
|
61
59
|
# Asks the domain's adapter which roles it can grant.
|
|
62
60
|
#
|
|
63
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
64
|
-
# and handed on to it
|
|
61
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
65
62
|
# @return [Object] adapter-defined collection of grantable roles
|
|
66
63
|
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
67
64
|
# (see `adapter`)
|
|
@@ -71,8 +68,7 @@ module Hecks
|
|
|
71
68
|
|
|
72
69
|
# Grants a role to a person, by whatever means the domain's adapter records a grant.
|
|
73
70
|
#
|
|
74
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
75
|
-
# and handed on to it
|
|
71
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
76
72
|
# @param email [Object] adapter-defined, forwarded unchanged; the person receiving the role
|
|
77
73
|
# @param role [Object] adapter-defined, forwarded unchanged; the role to grant
|
|
78
74
|
# @return [Object] adapter-defined representation of the grant
|
|
@@ -84,8 +80,7 @@ module Hecks
|
|
|
84
80
|
|
|
85
81
|
# Lists every person the domain's adapter knows about.
|
|
86
82
|
#
|
|
87
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
88
|
-
# and handed on to it
|
|
83
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
89
84
|
# @return [Object] adapter-defined collection of people
|
|
90
85
|
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
91
86
|
# (see `adapter`)
|
|
@@ -31,8 +31,7 @@ module Hecks
|
|
|
31
31
|
|
|
32
32
|
# Answers whether an actor holds a live (not ended) grant of a role.
|
|
33
33
|
#
|
|
34
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
35
|
-
# and handed on to it
|
|
34
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
36
35
|
# @param actor_id [String] the actor whose grants are checked
|
|
37
36
|
# @param role [String, Symbol] the role name to look for, compared as a String
|
|
38
37
|
# @param as_of [Integer, nil] Unix epoch seconds (from `Ports::Clock.now`); a grant whose
|
|
@@ -51,8 +50,7 @@ module Hecks
|
|
|
51
50
|
|
|
52
51
|
# Answers whether one role may act as another.
|
|
53
52
|
#
|
|
54
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
55
|
-
# and handed on to it
|
|
53
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
56
54
|
# @param from_role [String, Symbol] the role the caller holds, compared as a String
|
|
57
55
|
# @param to_role [String, Symbol] the role the caller wants to act as, compared as a
|
|
58
56
|
# String
|
|
@@ -67,8 +65,7 @@ module Hecks
|
|
|
67
65
|
|
|
68
66
|
# Looks up the role an actor holds right now, rather than checking a guessed one.
|
|
69
67
|
#
|
|
70
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
71
|
-
# and handed on to it
|
|
68
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
72
69
|
# @param actor_id [String] the actor to look up
|
|
73
70
|
# @return [String, nil] the role name of the actor's first live (not ended) grant, or
|
|
74
71
|
# nil if it has none; the caller supplies any fallback
|
|
@@ -27,8 +27,7 @@ module Hecks
|
|
|
27
27
|
# No adapter, spec double or caller of this port ships in this repository, so every
|
|
28
28
|
# shape below other than `registry` is adapter-defined: the port forwards it untouched.
|
|
29
29
|
#
|
|
30
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
31
|
-
# and handed on to it
|
|
30
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
32
31
|
# @param agg_name [Object] adapter-defined, forwarded unchanged; names the aggregate
|
|
33
32
|
# whose identity field is being assigned
|
|
34
33
|
# @param field_name [Object] adapter-defined, forwarded unchanged; names the identity
|
|
@@ -19,8 +19,7 @@ module Hecks
|
|
|
19
19
|
|
|
20
20
|
# Looks up the id of the identity an authenticated (issuer, subject) pair is linked to.
|
|
21
21
|
#
|
|
22
|
-
# @param registry [Runtime::Registry] the booted registry
|
|
23
|
-
# and handed on to it
|
|
22
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
24
23
|
# @param issuer [String] the OIDC issuer that authenticated the caller
|
|
25
24
|
# @param subject [String] the OIDC subject the issuer vouches for
|
|
26
25
|
# @return [String, nil] the linked identity's id, usable as an `actor_id` for
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require_relative "../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Ports
|
|
5
|
+
# A subject's own encryption key lives here, one boundary removed from
|
|
6
|
+
# any event or bluebook attribute — cryptoshredding depends on the key
|
|
7
|
+
# material never being reachable except through this port's opaque
|
|
8
|
+
# reference (see `Privacy::SubjectKey`, `lib/hecks/framework/bluebook/
|
|
9
|
+
# privacy.bluebook`). Resolved the same way `Ports::IdentityGeneration`
|
|
10
|
+
# resolves its own adapter: one adapter registry-wide answers this
|
|
11
|
+
# port, not a per-aggregate binding.
|
|
12
|
+
module KeyVault
|
|
13
|
+
NAME = "key_vault".freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Mints a fresh per-subject encryption key and hands back an opaque reference to it.
|
|
18
|
+
#
|
|
19
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
20
|
+
# @param subject_id [String] the data subject the key is being issued for
|
|
21
|
+
# @return [String] an opaque key reference; the key material itself never leaves the
|
|
22
|
+
# bound adapter, and is never suitable to store on an event or a bluebook attribute
|
|
23
|
+
def issue(registry, subject_id:) = adapter(registry).issue(subject_id: subject_id)
|
|
24
|
+
|
|
25
|
+
# Irrevocably destroys a key that {#issue} already returned a reference for.
|
|
26
|
+
#
|
|
27
|
+
# Ciphertext produced under `key_reference` becomes permanently unrecoverable the
|
|
28
|
+
# moment this returns — the mechanism a right-to-erasure request satisfies without
|
|
29
|
+
# rewriting or deleting any event.
|
|
30
|
+
#
|
|
31
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
32
|
+
# @param key_reference [String] the opaque reference {#issue} returned
|
|
33
|
+
# @return [Boolean] true when a live key was destroyed; false when this reference was
|
|
34
|
+
# already destroyed, or was never issued
|
|
35
|
+
def destroy(registry, key_reference:) = adapter(registry).destroy(key_reference: key_reference)
|
|
36
|
+
|
|
37
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
38
|
+
#
|
|
39
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
40
|
+
# @return [Module] the adapter module or class implementing this port
|
|
41
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port
|
|
42
|
+
def adapter(registry)
|
|
43
|
+
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
44
|
+
|
|
45
|
+
case implementations.size
|
|
46
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
47
|
+
when 0
|
|
48
|
+
raise Runtime::WiringError,
|
|
49
|
+
"no adapter implements the #{NAME} port — nothing can issue or destroy a key"
|
|
50
|
+
else
|
|
51
|
+
raise Runtime::WiringError,
|
|
52
|
+
"#{implementations.size} adapters implement the #{NAME} port " \
|
|
53
|
+
"(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -196,6 +196,12 @@ module Hecks
|
|
|
196
196
|
# duplicate manually after a merge; teaching this intersect
|
|
197
197
|
# about a rekey mapping is real, separate work, deliberately
|
|
198
198
|
# out of scope for rekey's first pass.
|
|
199
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate to check for conflicting ids
|
|
200
|
+
# @param edges [Hash] the fork's own era edges, as `names_by_era` reads them
|
|
201
|
+
# @param era [Integer] the new world's own era
|
|
202
|
+
# @param cut [Integer] the ordinal the fork happened at
|
|
203
|
+
# @return [Array<Array(String, String)>] `[storage_name, aggregate_id]` pairs touched
|
|
204
|
+
# by both worlds since the cut
|
|
199
205
|
def conflict_ids(aggregate, edges, era, cut)
|
|
200
206
|
names = names_by_era(aggregate, edges)
|
|
201
207
|
olds = (1...era).map { |ancestor| text_literal(names[:storage][ancestor - 1]) }.join(", ")
|
|
@@ -139,10 +139,28 @@ module Hecks
|
|
|
139
139
|
"human approved no longer cover the data; re-run bin/translation_audit with --approve"
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
# Refuses a mint that found no translation edge, naming the authoring tools —
|
|
143
|
+
# or, with scaffolding on, the file it just wrote.
|
|
144
|
+
#
|
|
142
145
|
# No edge yet: the boot refuses toward the authoring loop —
|
|
143
146
|
# naming both tools and the era ordinal. With HECKS_SCAFFOLD=1
|
|
144
147
|
# the boot runs the scaffold first (an explicit flag, never a
|
|
145
148
|
# silent side-effect) and the refusal names the file it wrote.
|
|
149
|
+
#
|
|
150
|
+
# @param registry [Runtime::Registry] the registry, forwarded to `scaffold!` when
|
|
151
|
+
# scaffolding is on
|
|
152
|
+
# @param bluebook [Bluebook::Chapter] the domain, named in the refusal
|
|
153
|
+
# @param lineage [Adapters::PostgresEra::Lineage] the domain's lineage, forwarded
|
|
154
|
+
# to `scaffold!` when scaffolding is on
|
|
155
|
+
# @param latest [Hash{Symbol => Object}] the newest held era, forwarded to
|
|
156
|
+
# `scaffold!` when scaffolding is on
|
|
157
|
+
# @param ordinal [Integer] the ordinal of the era about to be minted, named in
|
|
158
|
+
# the refusal
|
|
159
|
+
# @param directory [String, nil] the domain's bluebook directory; scaffolding
|
|
160
|
+
# requires this to be present as well as `HECKS_SCAFFOLD=1`
|
|
161
|
+
# @return [void] never returns
|
|
162
|
+
# @raise [Runtime::WiringError] always; the message names the scaffolded file
|
|
163
|
+
# when `HECKS_SCAFFOLD=1` and `directory` wrote one
|
|
146
164
|
def refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory)
|
|
147
165
|
if ENV["HECKS_SCAFFOLD"] == "1" && directory
|
|
148
166
|
path = scaffold!(registry, bluebook, lineage, latest, directory)
|
|
@@ -157,9 +175,19 @@ module Hecks
|
|
|
157
175
|
"check it with bin/translation_audit, then boot again"
|
|
158
176
|
end
|
|
159
177
|
|
|
160
|
-
#
|
|
178
|
+
# Diffs the held era against the current shape and writes the edge
|
|
161
179
|
# file — confident rules inline, ambiguities as parse-refusing
|
|
162
|
-
# `unresolved` lines.
|
|
180
|
+
# `unresolved` lines.
|
|
181
|
+
#
|
|
182
|
+
# @param _registry [Runtime::Registry] unused; kept so the signature matches its
|
|
183
|
+
# caller's own arguments
|
|
184
|
+
# @param bluebook [Bluebook::Chapter] the domain as currently declared
|
|
185
|
+
# @param lineage [Adapters::PostgresEra::Lineage] the domain's lineage; named so
|
|
186
|
+
# `ensure_named!` can mint a name for `latest` first if it has none
|
|
187
|
+
# @param latest [Hash{Symbol => Object}] the newest held era, as `Lineage#eras`
|
|
188
|
+
# returns it
|
|
189
|
+
# @param directory [String] the domain's bluebook directory to write the edge under
|
|
190
|
+
# @return [String] the path `Translation::Scaffold::Writer#write!` wrote
|
|
163
191
|
def scaffold!(_registry, bluebook, lineage, latest, directory)
|
|
164
192
|
ensure_named!(lineage, latest)
|
|
165
193
|
latest = lineage.eras.last
|
|
@@ -179,9 +207,17 @@ module Hecks
|
|
|
179
207
|
Translation::Scaffold.write!(directory, edge)
|
|
180
208
|
end
|
|
181
209
|
|
|
210
|
+
# Mints a name for an era that has none yet, leaving an already-named era untouched.
|
|
211
|
+
#
|
|
182
212
|
# Era names are minted once. An era held before any drift was
|
|
183
213
|
# seen has no name yet; it gets one the moment an edge needs to
|
|
184
214
|
# leave it.
|
|
215
|
+
#
|
|
216
|
+
# @param lineage [Adapters::PostgresEra::Lineage] the domain's lineage, which
|
|
217
|
+
# records the minted name
|
|
218
|
+
# @param era [Hash{Symbol => Object}] the era to name, as `Lineage#eras` returns it;
|
|
219
|
+
# `:hash` present means it is already named
|
|
220
|
+
# @return [void]
|
|
185
221
|
def ensure_named!(lineage, era)
|
|
186
222
|
return if era[:hash]
|
|
187
223
|
|
|
@@ -92,6 +92,12 @@ module Hecks
|
|
|
92
92
|
# header names. Only reached once `layer_two!` has already ruled
|
|
93
93
|
# out "no declared edge" and "rekeyed" (see its own comment on
|
|
94
94
|
# that second guard); this method assumes both are false.
|
|
95
|
+
# @param violations [Array<String>] mutated in place with one message per divergent id
|
|
96
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate being audited
|
|
97
|
+
# @param declared [Bluebook::Translation] the declared translation to check against
|
|
98
|
+
# @param before [Hash{String => Hash}] the old world's own records, keyed by id
|
|
99
|
+
# @param after [Hash{String => Hash}] the new world's own records, keyed by id
|
|
100
|
+
# @return [void]
|
|
95
101
|
def check_value_preservation!(violations, aggregate, declared, before, after)
|
|
96
102
|
rules = Ports::Persistence::Lineage.from_declared(declared, aggregate.name)
|
|
97
103
|
compute_paths = declared.computes.flat_map { |compute| [compute.from, compute.to] }.map(&:to_s)
|
|
@@ -26,6 +26,13 @@ module Hecks
|
|
|
26
26
|
# in the reference transform's phase order (renames, moves,
|
|
27
27
|
# converts, drops), computes last. `retype` compiles to nothing:
|
|
28
28
|
# stored state never carries a type name.
|
|
29
|
+
# Compiles a declared edge's rename/move/convert/drop/compute rules into one nested SQL
|
|
30
|
+
# expression over the `state` jsonb column.
|
|
31
|
+
#
|
|
32
|
+
# @param declared [Bluebook::TranslationAggregate] this edge's declared rules for one
|
|
33
|
+
# aggregate
|
|
34
|
+
# @return [String] a SQL expression, `"state"` unchanged when `declared` declares none
|
|
35
|
+
# of the five rule kinds
|
|
29
36
|
def compile_rules(declared)
|
|
30
37
|
expression = "state"
|
|
31
38
|
declared.renames.each do |old_name, new_name|
|
|
@@ -57,6 +64,11 @@ module Hecks
|
|
|
57
64
|
# `Ports::Persistence::Lineage` wrapper the app-level consumers
|
|
58
65
|
# (coverage_check.rb, minter.rb, layer_two.rb) go through — this
|
|
59
66
|
# module builds SQL straight off the IR either way.
|
|
67
|
+
# Reports whether an edge's declared rules for an aggregate include a rekey.
|
|
68
|
+
#
|
|
69
|
+
# @param declared [Bluebook::TranslationAggregate, nil] this edge's declared rules for
|
|
70
|
+
# one aggregate; nil for an aggregate the edge declares nothing about
|
|
71
|
+
# @return [Boolean] true when `declared` is present and its `rekeys` is non-empty
|
|
60
72
|
def rekeyed?(declared) = declared && !declared.rekeys.empty?
|
|
61
73
|
|
|
62
74
|
# The only two places `aggregate_id` needs to change — guarded so
|
|
@@ -64,6 +76,12 @@ module Hecks
|
|
|
64
76
|
# declared) stays the bare `aggregate_id` passthrough it always
|
|
65
77
|
# was — this case only appears in an edge that actually declares
|
|
66
78
|
# one.
|
|
79
|
+
#
|
|
80
|
+
# @param guard [String] a SQL boolean expression gating when the rekey applies, such as
|
|
81
|
+
# `"operation = 'save'"`
|
|
82
|
+
# @param declared [Bluebook::TranslationAggregate] this edge's declared rules; must
|
|
83
|
+
# declare a rekey (`rekeyed?(declared)` true)
|
|
84
|
+
# @return [String] a `"CASE WHEN ... END AS aggregate_id"` SQL expression
|
|
67
85
|
def id_case(guard, declared)
|
|
68
86
|
"CASE WHEN #{guard} THEN #{compile_id_expression(declared)} ELSE aggregate_id END AS aggregate_id"
|
|
69
87
|
end
|
|
@@ -75,6 +93,11 @@ module Hecks
|
|
|
75
93
|
# it needs to see first — it reads the record's stored fields
|
|
76
94
|
# exactly as they already are, the same `__s` convention
|
|
77
95
|
# `compile_compute` exposes.
|
|
96
|
+
#
|
|
97
|
+
# @param declared [Bluebook::TranslationAggregate] this edge's declared rules; its
|
|
98
|
+
# first `rekeys` entry supplies the SQL
|
|
99
|
+
# @return [String] a SQL expression evaluating the rekey's own SQL against the record's
|
|
100
|
+
# current `state`
|
|
78
101
|
def compile_id_expression(declared)
|
|
79
102
|
rekey = declared.rekeys.first
|
|
80
103
|
"(SELECT (#{rekey.sql}) FROM (SELECT (state) AS __s) __outer)"
|
|
@@ -84,6 +107,12 @@ module Hecks
|
|
|
84
107
|
# — evaluated exclusively inside the compiled head, never
|
|
85
108
|
# in-process. The old field is exposed under its own name (as
|
|
86
109
|
# text, exactly as the author's expression expects to cast it).
|
|
110
|
+
#
|
|
111
|
+
# @param expression [String] the SQL expression built so far by `compile_rules`, read as
|
|
112
|
+
# `__s` inside the compute's own SQL
|
|
113
|
+
# @param compute [Bluebook::TranslationCompute] the declared compute rule
|
|
114
|
+
# @return [String] `expression` wrapped so the compute's field lands at its declared
|
|
115
|
+
# destination when the source field is present, unchanged otherwise
|
|
87
116
|
def compile_compute(expression, compute)
|
|
88
117
|
from = compute.from.to_s
|
|
89
118
|
to = compute.to.to_s
|
|
@@ -103,13 +132,24 @@ module Hecks
|
|
|
103
132
|
# non-Postgres-bound domain (there are none today, but nothing
|
|
104
133
|
# here should assume there never will be) doesn't gain a hard
|
|
105
134
|
# dependency on `pg` just by loading this file.
|
|
135
|
+
#
|
|
136
|
+
# @param name [String, Symbol] the identifier to quote
|
|
137
|
+
# @return [String] `name` as a double-quoted Postgres identifier
|
|
106
138
|
def quote(name)
|
|
107
139
|
require "pg"
|
|
108
140
|
PG::Connection.quote_ident(name.to_s)
|
|
109
141
|
end
|
|
110
142
|
|
|
143
|
+
# Renders a Ruby value as a single-quoted SQL text literal, escaping embedded quotes.
|
|
144
|
+
#
|
|
145
|
+
# @param text [String, Symbol, Object] the value to render; converted with `to_s`
|
|
146
|
+
# @return [String] a single-quoted SQL literal
|
|
111
147
|
def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
|
|
112
148
|
|
|
149
|
+
# Renders a dotted path as a SQL `text[]` array literal, one element per segment.
|
|
150
|
+
#
|
|
151
|
+
# @param path [String, Symbol] a bare or dotted path, such as `"price.cents"`
|
|
152
|
+
# @return [String] a SQL `ARRAY[...]::text[]` expression
|
|
113
153
|
def path_literal(path)
|
|
114
154
|
segments = path.to_s.split(".").map { |segment| text_literal(segment) }
|
|
115
155
|
"ARRAY[#{segments.join(', ')}]::text[]"
|