hecks 1.0.2 → 1.1.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 +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +2 -1
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +29 -2
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +207 -48
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +14 -2
|
@@ -72,7 +72,11 @@ module Hecks
|
|
|
72
72
|
parts << identity_sentence(aggregate)
|
|
73
73
|
|
|
74
74
|
refs = aggregate.attributes.select(&:reference?)
|
|
75
|
-
|
|
75
|
+
unless refs.empty?
|
|
76
|
+
parts << "Each one is linked to #{to_sentence_list(refs.map do |r|
|
|
77
|
+
"#{a_or_an(r.type.target_name)} #{r.type.target_name}"
|
|
78
|
+
end)}."
|
|
79
|
+
end
|
|
76
80
|
|
|
77
81
|
parts << lifecycle_narrative(aggregate)
|
|
78
82
|
parts << verbs_narrative(aggregate, depth + 1)
|
|
@@ -124,35 +128,84 @@ module Hecks
|
|
|
124
128
|
"#{header}\n\n#{body}"
|
|
125
129
|
end
|
|
126
130
|
|
|
131
|
+
# ONE PARAGRAPH, BUILT FROM INDEPENDENT SENTENCES — each sentence
|
|
132
|
+
# below states one unrelated fact about `command` (its goal, who
|
|
133
|
+
# issues it, whether it creates the holder, what it takes, what it
|
|
134
|
+
# references, what gates it, what it guarantees, what it emits), in
|
|
135
|
+
# the fixed order a reader expects them; nothing after the first
|
|
136
|
+
# sentence depends on anything before it. Same nil-or-string +
|
|
137
|
+
# `compact.join` shape `aggregate_narrative`/`entity_narrative`
|
|
138
|
+
# already use above for the identical reason.
|
|
127
139
|
def command_paragraph(command, holder)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
[
|
|
141
|
+
command_headline_sentence(command),
|
|
142
|
+
command_role_sentence(command),
|
|
143
|
+
command_creation_sentence(command, holder),
|
|
144
|
+
command_arguments_sentence(command),
|
|
145
|
+
command_references_sentence(command),
|
|
146
|
+
command_conditions_sentence(command, holder),
|
|
147
|
+
command_guarantees_sentence(command),
|
|
148
|
+
command_emits_sentence(command)
|
|
149
|
+
].compact.join(" ")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# THE GOAL, VERBATIM — same rule `DocsProjector` holds to: quoted
|
|
153
|
+
# exactly as declared, not recased to fit mid-sentence, because the
|
|
154
|
+
# promise this whole projector makes is that a sentence here is a
|
|
155
|
+
# sentence the chapter actually wrote.
|
|
156
|
+
def command_headline_sentence(command)
|
|
157
|
+
"**#{command.hecks_name}**#{command.goal ? " — #{command.goal}." : '.'}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def command_role_sentence(command)
|
|
161
|
+
return nil unless command.role
|
|
162
|
+
|
|
163
|
+
"Issued by #{a_or_an(command.role)} #{command.role}."
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# `acts_on.nil?`, NOT `creates?` — `creates?` answers true for every
|
|
167
|
+
# verb an ENTITY declares (it never references itself; see
|
|
168
|
+
# `Command#acts_on`'s own comment), so reading it directly here would
|
|
169
|
+
# tell an SME that `LedgerEntry.Amend` brings a new ledger entry into
|
|
170
|
+
# being, which is exactly backwards.
|
|
171
|
+
def command_creation_sentence(command, holder)
|
|
172
|
+
return nil unless command.acts_on.nil?
|
|
173
|
+
|
|
174
|
+
"This is how a new #{holder.hecks_name} comes into being."
|
|
175
|
+
end
|
|
140
176
|
|
|
177
|
+
def command_arguments_sentence(command)
|
|
141
178
|
arguments = command.attributes.reject(&:reference?)
|
|
142
|
-
|
|
179
|
+
return nil if arguments.empty?
|
|
143
180
|
|
|
181
|
+
"It takes #{to_sentence_list(arguments.map { |a| Forms::Humanize.label(a.name.to_s).downcase })}."
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def command_references_sentence(command)
|
|
144
185
|
refs = command.attributes.select(&:reference?)
|
|
145
|
-
|
|
186
|
+
return nil if refs.empty?
|
|
146
187
|
|
|
188
|
+
"It's aimed at one existing #{to_sentence_list(refs.map { |r| r.type.target_name })}, by id."
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def command_conditions_sentence(command, holder)
|
|
147
192
|
conditions = conditions_of(command, holder)
|
|
148
|
-
|
|
193
|
+
return nil if conditions.empty?
|
|
149
194
|
|
|
195
|
+
"It only goes through if #{conditions.join('; ')}."
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def command_guarantees_sentence(command)
|
|
150
199
|
guarantees = command.ensures.map(&:description)
|
|
151
|
-
|
|
200
|
+
return nil if guarantees.empty?
|
|
201
|
+
|
|
202
|
+
"When it succeeds: #{guarantees.join('; ')}."
|
|
203
|
+
end
|
|
152
204
|
|
|
153
|
-
|
|
205
|
+
def command_emits_sentence(command)
|
|
206
|
+
return nil if command.emits.empty?
|
|
154
207
|
|
|
155
|
-
|
|
208
|
+
"It records `#{command.emits.join('`, `')}` as a fact."
|
|
156
209
|
end
|
|
157
210
|
|
|
158
211
|
# EVERY REQUIRED CONDITION, STATED AS SOMETHING THAT MUST BE TRUE —
|
|
@@ -166,10 +219,14 @@ module Hecks
|
|
|
166
219
|
|
|
167
220
|
lifecycle = holder.lifecycle
|
|
168
221
|
if lifecycle
|
|
169
|
-
froms = lifecycle.transitions.filter_map
|
|
222
|
+
froms = lifecycle.transitions.filter_map do |name, transition|
|
|
170
223
|
Array(transition.from) if name.to_s == command.hecks_name
|
|
171
|
-
|
|
172
|
-
|
|
224
|
+
end.flatten.uniq
|
|
225
|
+
unless froms.empty?
|
|
226
|
+
conditions << "its `#{lifecycle.field}` is currently #{to_sentence_list(froms.map do |f|
|
|
227
|
+
"`#{f}`"
|
|
228
|
+
end, conj: 'or')}"
|
|
229
|
+
end
|
|
173
230
|
end
|
|
174
231
|
|
|
175
232
|
conditions + command.givens.map(&:description)
|
|
@@ -200,8 +257,8 @@ module Hecks
|
|
|
200
257
|
"#{header}\n\n#{lines.join("\n")}"
|
|
201
258
|
end
|
|
202
259
|
|
|
203
|
-
def op_words(
|
|
204
|
-
{ eq: "is", lt: "under", lte: "at most", gt: "over", gte: "at least" }[
|
|
260
|
+
def op_words(comparator)
|
|
261
|
+
{ eq: "is", lt: "under", lte: "at most", gt: "over", gte: "at least" }[comparator.to_s.to_sym] || comparator.to_s
|
|
205
262
|
end
|
|
206
263
|
|
|
207
264
|
# ── what happens on its own ───────────────────────────────────────
|
|
@@ -212,7 +269,8 @@ module Hecks
|
|
|
212
269
|
parts = [DocsProjector.h(depth, "Reactions")]
|
|
213
270
|
bluebook.policies.each do |policy|
|
|
214
271
|
elsewhere = policy.target_domain ? " in #{policy.target_domain}" : ""
|
|
215
|
-
parts << "Whenever `#{policy.on_event}` happens, `#{policy.trigger_command}` fires on its own#{elsewhere} —
|
|
272
|
+
parts << "Whenever `#{policy.on_event}` happens, `#{policy.trigger_command}` fires on its own#{elsewhere} — " \
|
|
273
|
+
"nobody has to ask for it."
|
|
216
274
|
end
|
|
217
275
|
|
|
218
276
|
bluebook.process_managers.each do |saga|
|
data/lib/hecks/projector.rb
CHANGED
|
@@ -70,9 +70,9 @@ module Hecks
|
|
|
70
70
|
# any construct that emits IR, and `admits!` is what decides whether
|
|
71
71
|
# THIS target can actually take the one handed over.
|
|
72
72
|
def call(name, bluebook:, options: {})
|
|
73
|
-
projector = registry.fetch(name.to_sym)
|
|
73
|
+
projector = registry.fetch(name.to_sym) do
|
|
74
74
|
raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
|
|
75
|
-
|
|
75
|
+
end
|
|
76
76
|
admits!(name, projector, bluebook)
|
|
77
77
|
projector.call(bluebook: bluebook, options: options)
|
|
78
78
|
end
|
data/lib/hecks/query_ir.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
require "set"
|
|
2
1
|
require_relative "bluebook"
|
|
3
2
|
require_relative "codemod"
|
|
4
3
|
require_relative "projections/model"
|
|
@@ -49,9 +48,9 @@ module Hecks
|
|
|
49
48
|
# makes, reusing its own Deviations data so this can never silently
|
|
50
49
|
# drift from what that gate actually checks.
|
|
51
50
|
def construct_diff(name)
|
|
52
|
-
klass = CONSTRUCTS.fetch(name)
|
|
51
|
+
klass = CONSTRUCTS.fetch(name) do
|
|
53
52
|
raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}"
|
|
54
|
-
|
|
53
|
+
end
|
|
55
54
|
declared = meta_declared(name)
|
|
56
55
|
emitted = klass.ir_spec.keys
|
|
57
56
|
|
|
@@ -101,33 +100,9 @@ module Hecks
|
|
|
101
100
|
rules = []
|
|
102
101
|
chapters = chapter_name ? [registry.bluebook(chapter_name)] : registry.bluebooks.values
|
|
103
102
|
|
|
104
|
-
walk_construct = lambda do |construct, path|
|
|
105
|
-
(construct.respond_to?(:preconditions) ? construct.preconditions : []).each do |rule|
|
|
106
|
-
rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
|
|
107
|
-
location: "#{path} (declared)")
|
|
108
|
-
end
|
|
109
|
-
(construct.respond_to?(:invariants) ? construct.invariants : []).each do |rule|
|
|
110
|
-
rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
|
|
111
|
-
location: "#{path} (declared)")
|
|
112
|
-
end
|
|
113
|
-
construct.commands.each do |command|
|
|
114
|
-
command.givens.each do |rule|
|
|
115
|
-
rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
|
|
116
|
-
location: "#{path}.#{command.hecks_name}")
|
|
117
|
-
end
|
|
118
|
-
command.ensures.each do |rule|
|
|
119
|
-
rules << Rule.new(kind: "ensures", description: rule.description, canonical: rule.canonical,
|
|
120
|
-
location: "#{path}.#{command.hecks_name}")
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
construct.entities.each { |piece|
|
|
124
|
-
walk_construct.call(piece, "#{path}.#{piece.hecks_name}")
|
|
125
|
-
} if construct.respond_to?(:entities)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
103
|
chapters.each do |chapter|
|
|
129
104
|
chapter.aggregates.each do |aggregate|
|
|
130
|
-
|
|
105
|
+
walk_construct_rules(aggregate, aggregate.hecks_name, rules)
|
|
131
106
|
aggregate.value_objects.each do |vo|
|
|
132
107
|
vo.invariants.each do |rule|
|
|
133
108
|
rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
|
|
@@ -139,6 +114,39 @@ module Hecks
|
|
|
139
114
|
rules
|
|
140
115
|
end
|
|
141
116
|
|
|
117
|
+
# THE RECURSIVE WALK `collect_rules` drives — pulled out of that method
|
|
118
|
+
# (pure extraction, identical traversal and Rule shapes) as its own
|
|
119
|
+
# named, self-recursive method rather than a lambda closing over the
|
|
120
|
+
# same locals. `rules` is the one piece of state every call shares —
|
|
121
|
+
# threaded as a parameter and mutated in place, the same accumulator
|
|
122
|
+
# role the lambda's own closure played.
|
|
123
|
+
def walk_construct_rules(construct, path, rules)
|
|
124
|
+
(construct.respond_to?(:preconditions) ? construct.preconditions : []).each do |rule|
|
|
125
|
+
rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
|
|
126
|
+
location: "#{path} (declared)")
|
|
127
|
+
end
|
|
128
|
+
(construct.respond_to?(:invariants) ? construct.invariants : []).each do |rule|
|
|
129
|
+
rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
|
|
130
|
+
location: "#{path} (declared)")
|
|
131
|
+
end
|
|
132
|
+
construct.commands.each do |command|
|
|
133
|
+
command.givens.each do |rule|
|
|
134
|
+
rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
|
|
135
|
+
location: "#{path}.#{command.hecks_name}")
|
|
136
|
+
end
|
|
137
|
+
command.ensures.each do |rule|
|
|
138
|
+
rules << Rule.new(kind: "ensures", description: rule.description, canonical: rule.canonical,
|
|
139
|
+
location: "#{path}.#{command.hecks_name}")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
return unless construct.respond_to?(:entities)
|
|
143
|
+
|
|
144
|
+
construct.entities.each do |piece|
|
|
145
|
+
walk_construct_rules(piece, "#{path}.#{piece.hecks_name}", rules)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
private_class_method :walk_construct_rules
|
|
149
|
+
|
|
142
150
|
# A rule's OWNER — the construct path a "(declared)" location names
|
|
143
151
|
# directly, or (for a command-level `.givens`/`.ensures` entry) the
|
|
144
152
|
# path with its trailing `.CommandName` segment stripped. Two rules
|
|
@@ -196,10 +204,10 @@ module Hecks
|
|
|
196
204
|
all_rules.group_by { |r| [r.kind, r.description, r.canonical] }
|
|
197
205
|
.map { |key, rules| [key, rules, declaration_count(rules)] }
|
|
198
206
|
.select { |_, _, count| count > 1 }
|
|
199
|
-
.map
|
|
207
|
+
.map do |(kind, description, canonical), rules, _|
|
|
200
208
|
{ kind: kind, description: description, canonical: canonical,
|
|
201
209
|
locations: rules.map(&:location) }
|
|
202
|
-
|
|
210
|
+
end
|
|
203
211
|
end
|
|
204
212
|
|
|
205
213
|
# `given` ONLY (not `invariant`/`ensures`) also covers CROSS-ENTITY
|
|
@@ -254,9 +262,9 @@ module Hecks
|
|
|
254
262
|
# resolved chapter-wide reference.
|
|
255
263
|
def declaration_count(rules)
|
|
256
264
|
declared = rules.select { |r| r.location.end_with?(" (declared)") }
|
|
257
|
-
declared_owners = declared.
|
|
265
|
+
declared_owners = declared.to_set { |r| owner_of(r.location) }
|
|
258
266
|
declared_given_roots = declared.select { |r| r.kind == "given" }
|
|
259
|
-
.
|
|
267
|
+
.to_set { |r| owner_of(r.location).split(".").first }
|
|
260
268
|
|
|
261
269
|
standalone = rules.reject do |r|
|
|
262
270
|
owner = owner_of(r.location)
|
|
@@ -282,8 +290,14 @@ module Hecks
|
|
|
282
290
|
if diff[:missing_from_ruby].empty? && diff[:unaccounted_in_ruby].empty?
|
|
283
291
|
lines << " clean — every declared field is emitted (or a named deviation), nothing emitted is undeclared"
|
|
284
292
|
else
|
|
285
|
-
|
|
286
|
-
|
|
293
|
+
unless diff[:missing_from_ruby].empty?
|
|
294
|
+
lines << " MISSING FROM RUBY (declared, not emitted, not a named deviation): " \
|
|
295
|
+
"#{diff[:missing_from_ruby].join(', ')}"
|
|
296
|
+
end
|
|
297
|
+
unless diff[:unaccounted_in_ruby].empty?
|
|
298
|
+
lines << " UNACCOUNTED IN RUBY (emitted, not declared, not a named deviation): " \
|
|
299
|
+
"#{diff[:unaccounted_in_ruby].join(', ')}"
|
|
300
|
+
end
|
|
287
301
|
end
|
|
288
302
|
lines.join("\n")
|
|
289
303
|
end.join("\n\n")
|
|
@@ -358,6 +372,10 @@ module Hecks
|
|
|
358
372
|
# was never supposed to have this touchpoint at all.
|
|
359
373
|
def reconstruction_reads?(name, field)
|
|
360
374
|
method_name = RECONSTRUCTION_METHODS[name]
|
|
375
|
+
# rubocop:disable-next Style/ReturnNilInPredicateMethodDefinition -- nil vs
|
|
376
|
+
# false is a deliberate distinction here: nil means "not applicable" (no
|
|
377
|
+
# hand-typed method to check), false means "applicable, and it fails" —
|
|
378
|
+
# see the spec's own "not-applicable (nil), not false" example.
|
|
361
379
|
return nil unless method_name
|
|
362
380
|
|
|
363
381
|
file, start_line = Hecks::Bluebook::MetaValidator::Reconstruction.instance_method(method_name).source_location
|
|
@@ -365,7 +383,9 @@ module Hecks
|
|
|
365
383
|
def_line = lines[start_line - 1]
|
|
366
384
|
indent = def_line[/\A\s*/]
|
|
367
385
|
|
|
368
|
-
body = lines[start_line..].take_while
|
|
386
|
+
body = lines[start_line..].take_while do |line|
|
|
387
|
+
line.strip.empty? || line[/\A\s*/].size > indent.size || !line.lstrip.start_with?("def ")
|
|
388
|
+
end
|
|
369
389
|
body.join.include?("#{field}:")
|
|
370
390
|
end
|
|
371
391
|
private_class_method :reconstruction_reads?
|
|
@@ -385,7 +405,11 @@ module Hecks
|
|
|
385
405
|
def format_impact_preview(preview)
|
|
386
406
|
lines = ["== #{preview[:name]}##{preview[:field]} =="]
|
|
387
407
|
preview[:touchpoints].each do |t|
|
|
388
|
-
mark = t[:present].nil?
|
|
408
|
+
mark = if t[:present].nil?
|
|
409
|
+
"n/a"
|
|
410
|
+
else
|
|
411
|
+
(t[:present] ? "yes" : "NOT YET")
|
|
412
|
+
end
|
|
389
413
|
lines << " [#{mark.rjust(7)}] #{t[:touchpoint]}"
|
|
390
414
|
end
|
|
391
415
|
done = preview[:touchpoints].count { |t| t[:present] == true }
|
|
@@ -2,7 +2,7 @@ module Hecks
|
|
|
2
2
|
module QuerySpecification
|
|
3
3
|
module Common
|
|
4
4
|
AuthorizationSpec = Struct.new(:policy, :tenant, keyword_init: true) do
|
|
5
|
-
def to_h = { policy: policy.to_s, tenant: tenant
|
|
5
|
+
def to_h = { policy: policy.to_s, tenant: tenant&.to_s }
|
|
6
6
|
end
|
|
7
7
|
end
|
|
8
8
|
end
|
|
@@ -2,6 +2,10 @@ require_relative "../../literal"
|
|
|
2
2
|
require_relative "../../vocabulary"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
|
+
# Reopened (see query_specification.rb for the namespace's own
|
|
6
|
+
# summary) to add Common::COMPARATORS — the vendored comparator
|
|
7
|
+
# vocabulary — and .render_value, the wire-rendering entry point
|
|
8
|
+
# shared by every literal-bearing spec struct.
|
|
5
9
|
module QuerySpecification
|
|
6
10
|
module Common
|
|
7
11
|
# `none_in_state`, vendored addition not (yet) upstream hecks
|
|
@@ -44,7 +44,7 @@ module Hecks
|
|
|
44
44
|
value = value.to_h if value.is_a?(Runtime::Value)
|
|
45
45
|
return value unless value.is_a?(Hash)
|
|
46
46
|
|
|
47
|
-
numerics = value.values.
|
|
47
|
+
numerics = value.values.grep(Numeric)
|
|
48
48
|
return numerics.first if numerics.size == 1
|
|
49
49
|
return value.values.first if value.size == 1
|
|
50
50
|
|
|
@@ -63,6 +63,13 @@ module Hecks
|
|
|
63
63
|
|
|
64
64
|
NUMERIC_TYPES = %w[Integer Float Numeric].freeze
|
|
65
65
|
|
|
66
|
+
# A `case` over a closed, declared set (Vocabulary::QueryComparator,
|
|
67
|
+
# held equal to this list by spec/vocabulary_table_spec — see the
|
|
68
|
+
# `else` branch's own comment) is the whole point of THE ONE
|
|
69
|
+
# COMPARATOR TABLE this file's header describes: one place naming
|
|
70
|
+
# every comparator, not one method per comparator scattered across
|
|
71
|
+
# a module.
|
|
72
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
66
73
|
def holds?(operation, held, want, registry: nil)
|
|
67
74
|
# A NULL SATISFIES NO COMPARISON — NullPolicy.unmatchable? owns
|
|
68
75
|
# the rule and the reasoning, including why `none_in_state` is
|
|
@@ -11,6 +11,11 @@ require_relative "where_clause"
|
|
|
11
11
|
module Hecks
|
|
12
12
|
module QuerySpecification
|
|
13
13
|
module Common
|
|
14
|
+
# Shared builder-instance methods for the `where`/`order_by`/
|
|
15
|
+
# `limit`/`offset`/`cursor`/`authorize`/`nulls`/`inspect_query`
|
|
16
|
+
# bluebook DSL words, mixed into both QueryBuilder and
|
|
17
|
+
# ReadModelBuilder so the two specification kinds parse the same
|
|
18
|
+
# clauses identically rather than each carrying its own copy.
|
|
14
19
|
module DSL
|
|
15
20
|
# RENAMED FROM `where`/`order_by`/`limit`/`offset`/`authorize`
|
|
16
21
|
# (all below) — item #13's full metaprogrammed dispatch (slice
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module QuerySpecification
|
|
3
3
|
module Common
|
|
4
|
+
# Null-handling shared between the in-memory and SQL query
|
|
5
|
+
# engines: stable null-aware ordering (#order, #sql_order) and
|
|
6
|
+
# which comparators a null value can never satisfy (#unmatchable?,
|
|
7
|
+
# #sql_predicate) — kept in one place because Memory, Postgres and
|
|
8
|
+
# SQLite each default nulls differently and must be made to agree
|
|
9
|
+
# explicitly rather than leaking their own native behavior.
|
|
4
10
|
module NullPolicy
|
|
5
11
|
module_function
|
|
6
12
|
|
|
@@ -82,8 +88,6 @@ module Hecks
|
|
|
82
88
|
["#{expression} IS NULL", []]
|
|
83
89
|
elsif value.nil? && operation.to_s == "ne"
|
|
84
90
|
["#{expression} IS NOT NULL", []]
|
|
85
|
-
else
|
|
86
|
-
nil
|
|
87
91
|
end
|
|
88
92
|
end
|
|
89
93
|
end
|
|
@@ -3,6 +3,12 @@ require_relative "null_semantics"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module QuerySpecification
|
|
5
5
|
module Common
|
|
6
|
+
# The attribute set every query-shaped construct shares — wheres,
|
|
7
|
+
# ordering, paging, cursor, authorization, null semantics,
|
|
8
|
+
# inspection mode — plus `options_to_h`/`extra_options_to_h` for
|
|
9
|
+
# serializing it. Both Bluebook::Query and
|
|
10
|
+
# QuerySpecification::ReadModel::Specification subclass this
|
|
11
|
+
# rather than each declaring the same fields twice.
|
|
6
12
|
class Options
|
|
7
13
|
attr_reader :wheres, :order_by, :limit, :offset, :cursor,
|
|
8
14
|
:authorization, :null_semantics, :inspection
|
|
@@ -10,8 +16,14 @@ module Hecks
|
|
|
10
16
|
def initialize(wheres: [], order_by: nil, limit: nil, offset: nil, cursor: nil,
|
|
11
17
|
authorization: nil,
|
|
12
18
|
inspection: nil, null_semantics: NullSemantics.default)
|
|
13
|
-
@wheres
|
|
14
|
-
@
|
|
19
|
+
@wheres = wheres
|
|
20
|
+
@order_by = order_by
|
|
21
|
+
@limit = limit
|
|
22
|
+
@offset = offset
|
|
23
|
+
@cursor = cursor
|
|
24
|
+
@authorization = authorization
|
|
25
|
+
@null_semantics = null_semantics
|
|
26
|
+
@inspection = inspection
|
|
15
27
|
end
|
|
16
28
|
|
|
17
29
|
def options_to_h
|
|
@@ -21,10 +33,10 @@ module Hecks
|
|
|
21
33
|
end
|
|
22
34
|
|
|
23
35
|
def extra_options_to_h
|
|
24
|
-
options_to_h.reject
|
|
36
|
+
options_to_h.reject do |key, value|
|
|
25
37
|
value.nil? || value == [] || (key == :null_semantics && value == { mode: "native" })
|
|
26
|
-
|
|
27
|
-
.
|
|
38
|
+
end
|
|
39
|
+
.except(:wheres, :order_by, :limit)
|
|
28
40
|
end
|
|
29
41
|
end
|
|
30
42
|
end
|
|
@@ -85,8 +85,8 @@ module Hecks
|
|
|
85
85
|
# or a value object carrying at least one numeric member. A dotted
|
|
86
86
|
# path must land on a numeric primitive itself — the convention does
|
|
87
87
|
# not reach through a named path, it IS the absence of one.
|
|
88
|
-
def numeric?(attribute, segments, &
|
|
89
|
-
leaf = leaf_attribute(attribute, segments, &
|
|
88
|
+
def numeric?(attribute, segments, &)
|
|
89
|
+
leaf = leaf_attribute(attribute, segments, &)
|
|
90
90
|
return false if leaf.nil? || leaf.list? || leaf.reference?
|
|
91
91
|
return true if NUMERIC_PRIMITIVES.include?(leaf.type.to_s)
|
|
92
92
|
return false unless segments.empty?
|
|
@@ -98,8 +98,8 @@ module Hecks
|
|
|
98
98
|
# A dotted path must end on a SCALAR member — landing on a value
|
|
99
99
|
# object would hand SQL a JSON object where the reference
|
|
100
100
|
# interpreter unwraps a hash, and the two would answer differently.
|
|
101
|
-
def scalar_leaf?(attribute, segments, &
|
|
102
|
-
leaf = leaf_attribute(attribute, segments, &
|
|
101
|
+
def scalar_leaf?(attribute, segments, &)
|
|
102
|
+
leaf = leaf_attribute(attribute, segments, &)
|
|
103
103
|
!leaf.nil? && !leaf.list? && !leaf.reference? && SCALAR_PRIMITIVES.include?(leaf.type.to_s)
|
|
104
104
|
end
|
|
105
105
|
end
|
|
@@ -3,11 +3,14 @@ require_relative "../common/options"
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module QuerySpecification
|
|
5
5
|
module ReadModel
|
|
6
|
+
# The read-model-specific specification: Common::Options' shared
|
|
7
|
+
# query attributes plus `joins`, the one thing a read model
|
|
8
|
+
# declares that a plain Query does not.
|
|
6
9
|
class Specification < Common::Options
|
|
7
10
|
attr_reader :joins
|
|
8
11
|
|
|
9
|
-
def initialize(joins: [], **
|
|
10
|
-
super(**
|
|
12
|
+
def initialize(joins: [], **)
|
|
13
|
+
super(**)
|
|
11
14
|
@joins = joins
|
|
12
15
|
end
|
|
13
16
|
|
|
@@ -7,12 +7,19 @@ module Hecks
|
|
|
7
7
|
# resolution stays in Router; this object only adapts it to constants and
|
|
8
8
|
# method calls.
|
|
9
9
|
class NamespaceInstaller
|
|
10
|
+
# Returned by the `.options(version:)` method installed on each
|
|
11
|
+
# namespace module — forwards any command/query verb called on it
|
|
12
|
+
# to the router, pinned to this specific FQN version rather than
|
|
13
|
+
# the router's default resolution.
|
|
10
14
|
class OptionsProxy
|
|
11
15
|
def initialize(router:, realm:, domain:, aggregate:, options:)
|
|
12
16
|
unknown = options.keys - [:version]
|
|
13
17
|
raise ArgumentError, "unknown router options: #{unknown.join(', ')}" unless unknown.empty?
|
|
14
18
|
|
|
15
|
-
@router
|
|
19
|
+
@router = router
|
|
20
|
+
@realm = realm
|
|
21
|
+
@domain = domain
|
|
22
|
+
@aggregate = aggregate
|
|
16
23
|
@version = options[:version]&.to_s
|
|
17
24
|
end
|
|
18
25
|
|
|
@@ -103,12 +110,12 @@ module Hecks
|
|
|
103
110
|
|
|
104
111
|
target.define_singleton_method(:find) do |id|
|
|
105
112
|
found = dispatcher.registry.repository(domain, ir).find(id)
|
|
106
|
-
found && Facade::Handle.new(dispatcher: dispatcher, domain: domain,
|
|
113
|
+
found && Facade::Handle.new(dispatcher: dispatcher, domain: domain, aggregate: ir, instance: found)
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
target.define_singleton_method(:all) do
|
|
110
117
|
dispatcher.registry.repository(domain, ir).all.map do |instance|
|
|
111
|
-
Facade::Handle.new(dispatcher: dispatcher, domain: domain,
|
|
118
|
+
Facade::Handle.new(dispatcher: dispatcher, domain: domain, aggregate: ir, instance: instance)
|
|
112
119
|
end
|
|
113
120
|
end
|
|
114
121
|
end
|
|
@@ -118,16 +125,17 @@ module Hecks
|
|
|
118
125
|
current_entries.reject { |entry| entry.fqn.aggregate.nil? }
|
|
119
126
|
.group_by { |entry| [entry.fqn.aggregate, entry.fqn.verb] }
|
|
120
127
|
.each do |(aggregate, verb), candidates|
|
|
121
|
-
shortcut_target(aggregate).define_singleton_method(verb)
|
|
128
|
+
shortcut_target(aggregate).define_singleton_method(verb) do |**args|
|
|
122
129
|
installer.send(:dispatch_short, candidates, **args)
|
|
123
|
-
|
|
130
|
+
end
|
|
124
131
|
end
|
|
125
132
|
end
|
|
126
133
|
|
|
127
134
|
def dispatch_short(candidates, **args)
|
|
128
135
|
if candidates.length > 1
|
|
129
136
|
shown = candidates.map { |entry| entry.fqn.to_s }.sort.join(", ")
|
|
130
|
-
aggregate
|
|
137
|
+
aggregate = candidates.first.fqn.aggregate
|
|
138
|
+
verb = candidates.first.fqn.verb
|
|
131
139
|
raise AmbiguousShortRoute, "#{aggregate}.#{verb} is ambiguous — choose one of: #{shown}"
|
|
132
140
|
end
|
|
133
141
|
|
|
@@ -137,8 +145,10 @@ module Hecks
|
|
|
137
145
|
|
|
138
146
|
def shortcut_target(aggregate)
|
|
139
147
|
constant = Object.const_get(aggregate, false) if Object.const_defined?(aggregate, false)
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
if constant && !constant.is_a?(Module)
|
|
149
|
+
raise NameError,
|
|
150
|
+
"cannot install Bluebook shortcut #{aggregate}: it is not a module"
|
|
151
|
+
end
|
|
142
152
|
|
|
143
153
|
constant || Object.const_set(aggregate, Module.new)
|
|
144
154
|
end
|
|
@@ -146,8 +156,10 @@ module Hecks
|
|
|
146
156
|
def namespace_for(realm, domain, aggregate)
|
|
147
157
|
[realm, domain, aggregate].compact.reduce(Object) do |parent, name|
|
|
148
158
|
constant = parent.const_get(name, false) if parent.const_defined?(name, false)
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
if constant && !constant.is_a?(Module)
|
|
160
|
+
raise NameError,
|
|
161
|
+
"cannot install Bluebook route under #{parent}::#{name}: it is not a module"
|
|
162
|
+
end
|
|
151
163
|
|
|
152
164
|
constant || parent.const_set(name, Module.new)
|
|
153
165
|
end
|