hecks 1.0.2 → 1.2.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 +100 -0
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
- data/lib/hecks/adapters/driven/postgres.rb +45 -31
- 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 +22 -3
- 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 +15 -4
- 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/read_model.rb +53 -8
- 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 +29 -477
- 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 +142 -19
- 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 +51 -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 +77 -18
- 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/glossary.rb +374 -0
- 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/projections.rb +1 -0
- data/lib/hecks/projector/cli_projector.rb +78 -42
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +34 -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/limit_spec.rb +9 -2
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/common/order_by.rb +9 -2
- data/lib/hecks/query_specification/common/where_clause.rb +15 -2
- 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 +45 -9
- 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 +31 -7
- 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 +271 -49
- 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 +16 -2
|
@@ -2,8 +2,15 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `process_manager "Name" do ... end` block into a
|
|
6
|
+
# `ProcessManager` — its own `starts_on`/`ends_on` events, what
|
|
7
|
+
# correlates its instances (`correlates_by`), and the `transition`-
|
|
8
|
+
# declared state machine whose `dispatch`es (each optionally paired
|
|
9
|
+
# with its own per-dispatch `compensates`) become its handlers. States
|
|
10
|
+
# are DERIVED from the transitions rather than declared separately
|
|
11
|
+
# (S7, ADR 0025).
|
|
5
12
|
class ProcessManagerBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "ProcessManager"
|
|
13
|
+
GRAMMAR_CONTEXT = "ProcessManager".freeze
|
|
7
14
|
|
|
8
15
|
class InvalidProcessManager < StandardError; end
|
|
9
16
|
|
|
@@ -189,8 +196,10 @@ module Hecks
|
|
|
189
196
|
end
|
|
190
197
|
|
|
191
198
|
def validate!
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
unless @correlates_by
|
|
200
|
+
raise InvalidProcessManager, "#{@name} declares no correlates_by — " \
|
|
201
|
+
"nothing would tie its events to one instance"
|
|
202
|
+
end
|
|
194
203
|
|
|
195
204
|
# THE FIELD, NAMED — never the value object that carries it. A bare
|
|
196
205
|
# `correlates_by :end_to_end` reads whatever the payload holds under
|
|
@@ -202,19 +211,54 @@ module Hecks
|
|
|
202
211
|
# holds a head to. This is a syntactic check, not a type check: it
|
|
203
212
|
# does not know or care whether the field IS a value object, only
|
|
204
213
|
# that the declaration cannot leave that question open.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
unless @correlates_by.to_s.include?(".")
|
|
215
|
+
raise InvalidProcessManager, "#{@name} correlates_by #{@correlates_by.inspect}, which names a whole " \
|
|
216
|
+
"field rather than one of its scalars — say which one, e.g. " \
|
|
217
|
+
"#{@correlates_by}.value"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if @starts_on.to_s.empty?
|
|
221
|
+
raise InvalidProcessManager, "#{@name} declares no starts_on — " \
|
|
222
|
+
"nothing would ever begin it"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if @handlers.empty?
|
|
226
|
+
raise InvalidProcessManager, "#{@name} declares no transitions — " \
|
|
227
|
+
"it would start and then ignore every event"
|
|
228
|
+
end
|
|
208
229
|
|
|
209
|
-
|
|
210
|
-
|
|
230
|
+
refuse_ambiguous_legs!
|
|
231
|
+
end
|
|
211
232
|
|
|
212
|
-
|
|
213
|
-
|
|
233
|
+
# C10.3 — a leg is selected by (event, current state), so two
|
|
234
|
+
# legs answering the SAME event from the SAME state would leave
|
|
235
|
+
# the runtime to pick by declaration order, silently. Refused
|
|
236
|
+
# here, where the declaration can still be read whole. (`from:
|
|
237
|
+
# [...]` fan-out counts: `transition E => "a", from: ["x", "y"]`
|
|
238
|
+
# and `transition E => "b", from: "y"` collide on ("E", "y").)
|
|
239
|
+
def refuse_ambiguous_legs!
|
|
240
|
+
return if MetaValidator.shadow_parsing? # frozen era text is history
|
|
241
|
+
|
|
242
|
+
seen = {}
|
|
243
|
+
@handlers.each do |handler|
|
|
244
|
+
key = [handler.event_type, handler.from_state]
|
|
245
|
+
if (earlier = seen[key])
|
|
246
|
+
raise InvalidProcessManager,
|
|
247
|
+
"#{@name} declares two transitions on #{handler.event_type.inspect} from " \
|
|
248
|
+
"#{handler.from_state.inspect} (=> #{earlier.to_state.inspect} and => " \
|
|
249
|
+
"#{handler.to_state.inspect}) — a leg is selected by (event, current state), so " \
|
|
250
|
+
"only one may answer"
|
|
251
|
+
end
|
|
252
|
+
seen[key] = handler
|
|
253
|
+
end
|
|
214
254
|
end
|
|
215
255
|
|
|
256
|
+
# THE BODY OF ONE `transition ... do ... end` block — collects the
|
|
257
|
+
# `dispatch` calls (each optionally opening its own `compensates`
|
|
258
|
+
# via the nested `DispatchBuilder`) that fire when this transition
|
|
259
|
+
# is taken.
|
|
216
260
|
class HandlerBuilder
|
|
217
|
-
GRAMMAR_CONTEXT = "Handler"
|
|
261
|
+
GRAMMAR_CONTEXT = "Handler".freeze
|
|
218
262
|
|
|
219
263
|
attr_reader :dispatches
|
|
220
264
|
|
|
@@ -282,7 +326,7 @@ module Hecks
|
|
|
282
326
|
# any saga dispatch already does
|
|
283
327
|
# (`SagaInterpreter#dispatch_args`).
|
|
284
328
|
class DispatchBuilder
|
|
285
|
-
GRAMMAR_CONTEXT = "Dispatch"
|
|
329
|
+
GRAMMAR_CONTEXT = "Dispatch".freeze
|
|
286
330
|
|
|
287
331
|
include WordGate
|
|
288
332
|
|
|
@@ -2,8 +2,14 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `query "Name" do ... end` block, declared on an aggregate
|
|
6
|
+
# or one of its entities, into a `Query` — its own parameters (plain
|
|
7
|
+
# attributes, including reference-typed ones) plus whatever `where`/
|
|
8
|
+
# `order_by`/`limit`/etc. `QuerySpecification::Common::DSL` contributes.
|
|
9
|
+
# A block parameter naming an already-declared owner attribute has its
|
|
10
|
+
# type derived from the owner rather than restated (`derive_from_owner!`).
|
|
5
11
|
class QueryBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Query"
|
|
12
|
+
GRAMMAR_CONTEXT = "Query".freeze
|
|
7
13
|
|
|
8
14
|
include AttributeCollector
|
|
9
15
|
include QuerySpecification::Common::DSL
|
|
@@ -2,8 +2,14 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `read_model "Name" do ... end` block into a `ReadModel` — a
|
|
6
|
+
# cross-aggregate projection built from an optional `reference_to`
|
|
7
|
+
# root (omitted, it's a rootless "bulk" read model) plus one or more
|
|
8
|
+
# `include`d aggregate heads, with `where`/`order_by`/`limit`/etc, and
|
|
9
|
+
# at most one of `group_by`, `count`, or `median` as its single
|
|
10
|
+
# reduction over the one eligible many-side collection.
|
|
5
11
|
class ReadModelBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "ReadModel"
|
|
12
|
+
GRAMMAR_CONTEXT = "ReadModel".freeze
|
|
7
13
|
|
|
8
14
|
include QuerySpecification::Common::DSL
|
|
9
15
|
include WordGate
|
|
@@ -48,6 +54,72 @@ module Hecks
|
|
|
48
54
|
@includes << [Naming.demodulise(type), as]
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
# `on:` (ADR 0055) — OVERRIDES of `QuerySpecification::Common::DSL`'s
|
|
58
|
+
# shared `where_impl`/`order_by_impl`/`limit_impl`/`offset_impl`,
|
|
59
|
+
# scoped to `ReadModelBuilder` alone rather than added to the shared
|
|
60
|
+
# module `Query` also mixes in: a plain `query` has no
|
|
61
|
+
# `aggregate_heads` at all, so `on:` there would be a silently-
|
|
62
|
+
# ignored no-op rather than a real answer. Overriding only here
|
|
63
|
+
# means a `Query`'s own `where(..., on: X)` gets Ruby's own loud
|
|
64
|
+
# `unknown keyword: :on` instead of quietly doing nothing.
|
|
65
|
+
#
|
|
66
|
+
# `on:` names the target by TYPE (`on: Character`), resolved the
|
|
67
|
+
# same way `reference_to`/`include` already resolve their own type
|
|
68
|
+
# argument (`Naming.demodulise`) — not by the include's own `as:`
|
|
69
|
+
# alias. A read model that `include`s the SAME type twice under two
|
|
70
|
+
# different `as:` has no way to say which one `on:` means today; no
|
|
71
|
+
# real corpus read model does this, so it's a real, deliberate scope
|
|
72
|
+
# limit (see ADR 0055), not an oversight.
|
|
73
|
+
#
|
|
74
|
+
# `*positional, on:, **rest` rather than a plain `(clauses, on: nil)`
|
|
75
|
+
# — found necessary, not stylistic, by reproducing the failure
|
|
76
|
+
# directly: `where(status: "disputed")` reaches here with its
|
|
77
|
+
# `status: "disputed"` captured as `**kwargs` (GenericDispatch's own
|
|
78
|
+
# `builder.send(calls, *args, **kwargs, &block)`), and Ruby stops
|
|
79
|
+
# auto-converting a bare `**hash` call into a plain positional Hash
|
|
80
|
+
# THE MOMENT a method declares any real keyword parameter — so a
|
|
81
|
+
# `(clauses, on: nil)` signature raised "wrong number of arguments
|
|
82
|
+
# (given 0, expected 1)" on every ordinary `where(field: value)`
|
|
83
|
+
# call, never reaching `on:` at all. `**rest` sidesteps this: Ruby
|
|
84
|
+
# still auto-splits `on:` into the declared keyword and gathers
|
|
85
|
+
# every OTHER key into `rest` regardless of how the caller wrote it.
|
|
86
|
+
#
|
|
87
|
+
# `QuerySpecification::Common::WhereClause` etc — FULLY QUALIFIED,
|
|
88
|
+
# not the bare names `dsl.rb`'s own shared `where_impl` gets away
|
|
89
|
+
# with. That file is lexically nested inside `Common` itself, so
|
|
90
|
+
# `WhereClause` resolves directly; this class is nested inside
|
|
91
|
+
# `Bluebook::DSL`, which has no lexical or ancestor path to
|
|
92
|
+
# `QuerySpecification::Common` at all — a bare `WhereClause` here
|
|
93
|
+
# falls through to `const_missing` and, mid-bluebook-load, that's
|
|
94
|
+
# `ConstShim`, which resolves it against the self-hosted grammar
|
|
95
|
+
# domain's OWN unrelated `WhereClause` construct instead (a `Module`,
|
|
96
|
+
# not this `Struct`) — found directly by reproducing "undefined
|
|
97
|
+
# method `new' for module WhereClause" against a real corpus load,
|
|
98
|
+
# not guessed.
|
|
99
|
+
def where_impl(*positional, on: nil, **rest)
|
|
100
|
+
raise ArgumentError, "wrong number of arguments (given #{positional.size}, expected 1)" if positional.size > 1
|
|
101
|
+
|
|
102
|
+
@wheres ||= []
|
|
103
|
+
target = resolve_target(on)
|
|
104
|
+
clauses = (positional.first || {}).merge(rest)
|
|
105
|
+
clauses.each do |field, value|
|
|
106
|
+
op, operand = split_comparator(value)
|
|
107
|
+
@wheres << QuerySpecification::Common::WhereClause.new(field: field, op: op, value: operand, target: target)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def order_by_impl(field, direction = :asc, on: nil)
|
|
112
|
+
@order_by = QuerySpecification::Common::OrderBy.new(field: field, direction: direction, target: resolve_target(on))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def limit_impl(value, on: nil)
|
|
116
|
+
@limit = QuerySpecification::Common::LimitSpec.new(value: value, target: resolve_target(on))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def offset_impl(value, on: nil)
|
|
120
|
+
@offset = QuerySpecification::Common::OffsetSpec.new(value: value, target: resolve_target(on))
|
|
121
|
+
end
|
|
122
|
+
|
|
51
123
|
# NAMES which of the eligible head's own fields to nest its rows
|
|
52
124
|
# under — one level per field, the leaf being that row with the
|
|
53
125
|
# named fields removed (they're already spent, as the keys that
|
|
@@ -107,8 +179,10 @@ module Hecks
|
|
|
107
179
|
# field values) has no root to speak of. Still needs to describe
|
|
108
180
|
# SOMETHING — zero includes AND no reference is refused.
|
|
109
181
|
def build
|
|
110
|
-
|
|
111
|
-
|
|
182
|
+
if !@reference_target && Array(@includes).empty?
|
|
183
|
+
raise Malformed,
|
|
184
|
+
"#{@name} needs an aggregate-head reference or at least one include"
|
|
185
|
+
end
|
|
112
186
|
|
|
113
187
|
Array(@includes).each do |target, as|
|
|
114
188
|
add_aggregate_head(target, as, many: target != @reference_target)
|
|
@@ -134,27 +208,70 @@ module Hecks
|
|
|
134
208
|
|
|
135
209
|
private
|
|
136
210
|
|
|
137
|
-
# where/order_by/limit/offset/authorize's tenant all apply to
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
211
|
+
# where/order_by/limit/offset/authorize's tenant all apply to
|
|
212
|
+
# collections — the `include`d aggregates whose heads are "many"
|
|
213
|
+
# (the "one" side, the reference target itself, is a single row;
|
|
214
|
+
# ordering, paging, or tenant-scoping one row means nothing). ADR
|
|
215
|
+
# 0055 gave `where`/`order_by`/`limit`/`offset` an `on:` to name
|
|
216
|
+
# WHICH many-side collection they mean, so this asks two questions
|
|
217
|
+
# now instead of one:
|
|
218
|
+
#
|
|
219
|
+
# 1. Does every declared `on:` actually name a many-side included
|
|
220
|
+
# aggregate? Checked regardless of how many many-side heads
|
|
221
|
+
# exist — a typo refuses immediately, not only once ambiguity
|
|
222
|
+
# would otherwise bite.
|
|
223
|
+
# 2. Is there still an UNTARGETED option declared (including
|
|
224
|
+
# `authorize`'s own `tenant:`, which has no `on:` of its own —
|
|
225
|
+
# a real, deliberate scope limit, see ADR 0055)? An untargeted
|
|
226
|
+
# option still needs exactly one many-side head to mean
|
|
227
|
+
# anything unambiguous — the ORIGINAL rule, unchanged, and
|
|
228
|
+
# still worded the same way (`spec/runtime/
|
|
229
|
+
# read_model_interpreter_spec.rb`'s existing refusal regex
|
|
230
|
+
# still matches).
|
|
231
|
+
#
|
|
232
|
+
# A read model with several many-side heads is legal precisely when
|
|
233
|
+
# every declared option names one; a read model with a single
|
|
234
|
+
# many-side head is unaffected either way, `on:` or not.
|
|
147
235
|
def seal_query_options
|
|
148
|
-
|
|
149
|
-
return unless declared
|
|
236
|
+
many = Array(@aggregate_heads).select { |head| head[:many] }
|
|
150
237
|
|
|
151
|
-
many
|
|
152
|
-
return
|
|
238
|
+
validate_declared_targets!(many)
|
|
239
|
+
return unless untargeted_option_declared?
|
|
240
|
+
return if many.size == 1
|
|
153
241
|
|
|
154
242
|
raise Malformed,
|
|
155
|
-
"#{@name} declares where/order_by/limit/offset but includes #{many} many-side " \
|
|
243
|
+
"#{@name} declares where/order_by/limit/offset but includes #{many.size} many-side " \
|
|
156
244
|
"aggregates, not exactly one — these options apply to a single collection; " \
|
|
157
|
-
"name which one
|
|
245
|
+
"name which one with `on:` (e.g. `where(field: value, on: Character)`), or drop the options"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Question 1 of `seal_query_options`'s own two, split out to keep
|
|
249
|
+
# both under the same "one job per method" shape every OTHER seal in
|
|
250
|
+
# this file already holds to (each raises its own one Malformed, for
|
|
251
|
+
# its own one reason).
|
|
252
|
+
def validate_declared_targets!(many)
|
|
253
|
+
many_by_aggregate = many.to_h { |head| [head[:aggregate], head] }
|
|
254
|
+
declared_targets = Array(@wheres).map(&:target) + [@order_by&.target, @limit&.target, @offset&.target]
|
|
255
|
+
|
|
256
|
+
declared_targets.compact.uniq.each do |target|
|
|
257
|
+
next if many_by_aggregate.key?(target)
|
|
258
|
+
|
|
259
|
+
raise Malformed,
|
|
260
|
+
"#{@name}'s `on: #{target}` doesn't name one of its own many-side included " \
|
|
261
|
+
"aggregates (it includes #{many.map { |head| head[:aggregate] }.join(', ')} as " \
|
|
262
|
+
"many-side heads)"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Question 2 of `seal_query_options`'s own two — see that method's
|
|
267
|
+
# header. `authorize`'s own `tenant:` has no `on:` at all (ADR 0055's
|
|
268
|
+
# own documented scope limit), so it always counts as untargeted.
|
|
269
|
+
def untargeted_option_declared?
|
|
270
|
+
Array(@wheres).any? { |where| where.target.nil? } ||
|
|
271
|
+
(@order_by && @order_by.target.nil?) ||
|
|
272
|
+
(@limit && @limit.target.nil?) ||
|
|
273
|
+
(@offset && @offset.target.nil?) ||
|
|
274
|
+
@authorization&.tenant
|
|
158
275
|
end
|
|
159
276
|
|
|
160
277
|
# Same shape as `seal_query_options`, same reason — `group_by`
|
|
@@ -219,6 +336,12 @@ module Hecks
|
|
|
219
336
|
"pagination — use limit/offset instead"
|
|
220
337
|
end
|
|
221
338
|
|
|
339
|
+
# `on:`'s own resolution (ADR 0055) — same demodulise `reference_to`/
|
|
340
|
+
# `include` already use for their own type argument. `nil` when `on:`
|
|
341
|
+
# is omitted, matching every other optional field's "absent, not
|
|
342
|
+
# false" reading in this file.
|
|
343
|
+
def resolve_target(on) = on && Naming.demodulise(on)
|
|
344
|
+
|
|
222
345
|
def add_aggregate_head(type, name, many:)
|
|
223
346
|
@aggregate_heads ||= []
|
|
224
347
|
target = Naming.demodulise(type)
|
|
@@ -59,7 +59,9 @@ module Hecks
|
|
|
59
59
|
"extraction — #{extraction_failure}"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
ast = Expression::AstJson.refuse_unshared_patterns!(Expression::AstJson.emit_predicate(canonical),
|
|
63
|
+
owner: owner_name, word: "#{word} #{description.inspect}")
|
|
64
|
+
struct_class.new(description: description, canonical: canonical, predicate: predicate, ast: ast)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
# PRIMITIVE 1 — an ORDERED CHAIN of flat `Hash[description] =>
|
|
@@ -2,8 +2,14 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses an `aggregate "Name" do ... end` block inside a `.translation`
|
|
6
|
+
# file into a `TranslationAggregate` — the per-aggregate rules
|
|
7
|
+
# (`rename`/`move`/`convert`/`retype`/`compute`/`rekey`/`backfill`/
|
|
8
|
+
# `drop`) that carry one era's stored data forward to the next, plus
|
|
9
|
+
# `unresolved` markers the scaffold writes wherever it cannot decide a
|
|
10
|
+
# rule for itself.
|
|
5
11
|
class TranslationAggregateBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "TranslationAggregate"
|
|
12
|
+
GRAMMAR_CONTEXT = "TranslationAggregate".freeze
|
|
7
13
|
|
|
8
14
|
include WordGate
|
|
9
15
|
|
|
@@ -183,8 +189,13 @@ module Hecks
|
|
|
183
189
|
end
|
|
184
190
|
end
|
|
185
191
|
|
|
192
|
+
# Parses a whole `.translation` file into a `Translation` — the
|
|
193
|
+
# domain's own `from:`/`to:` era pair, its list of `aggregate`
|
|
194
|
+
# translation blocks (each built by `TranslationAggregateBuilder`
|
|
195
|
+
# above), and any `retired` aggregates that no longer exist in the
|
|
196
|
+
# destination era.
|
|
186
197
|
class TranslationBuilder
|
|
187
|
-
GRAMMAR_CONTEXT = "Translation"
|
|
198
|
+
GRAMMAR_CONTEXT = "Translation".freeze
|
|
188
199
|
|
|
189
200
|
include WordGate
|
|
190
201
|
|
|
@@ -2,8 +2,15 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `value_object "Name" do ... end` block into a
|
|
6
|
+
# `ValueObject` — its attributes, its own invariants (declared once
|
|
7
|
+
# and referenceable by name from sibling value objects on the same
|
|
8
|
+
# aggregate, `RuleReference`), and its `member` rows when it's a
|
|
9
|
+
# closed set (either bare `one_of` member lines, or the single-field
|
|
10
|
+
# `attribute ..., one_of: [...]` shorthand `install_inline_closed_set`
|
|
11
|
+
# installs).
|
|
5
12
|
class ValueObjectBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "ValueObject"
|
|
13
|
+
GRAMMAR_CONTEXT = "ValueObject".freeze
|
|
7
14
|
|
|
8
15
|
include AttributeCollector
|
|
9
16
|
include RuleReference
|
|
@@ -150,6 +150,11 @@ module Hecks
|
|
|
150
150
|
# admitted-but-unimplemented, or admitted-somewhere-else-only —
|
|
151
151
|
# those are real, useful refusals regardless of which
|
|
152
152
|
# `method_missing` is asking.
|
|
153
|
+
# One ordered admission-then-dispatch pipeline (own context, then
|
|
154
|
+
# "Type" fallback, then admitted-elsewhere check, then dispatch) —
|
|
155
|
+
# see this method's own header comment above for the full,
|
|
156
|
+
# order-dependent account of why each check runs where it does.
|
|
157
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
153
158
|
def word_gate_dispatch(word, args, kwargs, block)
|
|
154
159
|
context = self.class::GRAMMAR_CONTEXT
|
|
155
160
|
rows = MetaValidator::SyntaxBoot.call
|
|
@@ -2,10 +2,15 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# A generic keyword-call sink for one bind's own settings block (e.g.
|
|
6
|
+
# `persisted_by "Heki" do dir :default end`) — every method call made
|
|
7
|
+
# on it inside the block is recorded verbatim by name, with no fixed
|
|
8
|
+
# vocabulary of its own; `WorldBuilder#record_binding` reads `#to_h`
|
|
9
|
+
# back out.
|
|
5
10
|
class SettingsCollector
|
|
6
11
|
def initialize = @values = {}
|
|
7
12
|
|
|
8
|
-
def method_missing(key, *args, &
|
|
13
|
+
def method_missing(key, *args, &)
|
|
9
14
|
@values[key.to_sym] = args.size == 1 ? args.first : args
|
|
10
15
|
end
|
|
11
16
|
|
|
@@ -38,8 +43,13 @@ module Hecks
|
|
|
38
43
|
def respond_to_missing?(_name, _include_private = false) = true
|
|
39
44
|
end
|
|
40
45
|
|
|
46
|
+
# Parses a `.world` file's top-level DSL block into a `World` — a
|
|
47
|
+
# domain's own `realm`/`latest` version markers plus its adapter bind
|
|
48
|
+
# SETTINGS, one entry per `verb("Adapter") do ... end` call (whether
|
|
49
|
+
# written bare or aggregate-qualified through `WorldConstProxy`'s
|
|
50
|
+
# visual mirror of a sibling `.hecksagon` file's own bind).
|
|
41
51
|
class WorldBuilder
|
|
42
|
-
GRAMMAR_CONTEXT = "World"
|
|
52
|
+
GRAMMAR_CONTEXT = "World".freeze
|
|
43
53
|
|
|
44
54
|
include WordGate
|
|
45
55
|
|
|
@@ -107,7 +117,7 @@ module Hecks
|
|
|
107
117
|
|
|
108
118
|
private
|
|
109
119
|
|
|
110
|
-
def required(value,
|
|
120
|
+
def required(value, _label)
|
|
111
121
|
# moved to the language: Realm / Latest invariants, in world.bluebook
|
|
112
122
|
value.to_s
|
|
113
123
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "behaviour/entity"
|
|
2
|
+
require_relative "expression/ast_json"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Bluebook
|
|
@@ -51,7 +52,7 @@ module Hecks
|
|
|
51
52
|
# documentation of what the piece itself declared, the same
|
|
52
53
|
# relationship `Aggregate.preconditions` already has to its own
|
|
53
54
|
# commands.
|
|
54
|
-
preconditions: -> { preconditions.map { |rule|
|
|
55
|
+
preconditions: -> { preconditions.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
55
56
|
# A piece's OWN shape rule, checked against EVERY instance of
|
|
56
57
|
# this piece the aggregate holds (Admissibility#enforce_
|
|
57
58
|
# invariants' own recursive walk) — the SAME relationship
|
|
@@ -61,7 +62,7 @@ module Hecks
|
|
|
61
62
|
# mutation, before save) the aggregate's own invariants always
|
|
62
63
|
# were — see that method's own comment for why this does not
|
|
63
64
|
# contradict "there is no separate entity invariant."
|
|
64
|
-
invariants: -> { invariants.map { |rule|
|
|
65
|
+
invariants: -> { invariants.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
65
66
|
lifecycle: one(:lifecycle)
|
|
66
67
|
)
|
|
67
68
|
|
|
@@ -84,7 +85,8 @@ module Hecks
|
|
|
84
85
|
# Assigns what the language declares, then hands off to the
|
|
85
86
|
# behaviour's own `settle` — derived identity and the name
|
|
86
87
|
# indexes, neither of which the declaration states.
|
|
87
|
-
def absorb(description:, identified_by:, attributes:, commands:, queries:, entities:, preconditions:, invariants:,
|
|
88
|
+
def absorb(description:, identified_by:, attributes:, commands:, queries:, entities:, preconditions:, invariants:,
|
|
89
|
+
lifecycle:)
|
|
88
90
|
@description = description
|
|
89
91
|
@identified_by = identified_by
|
|
90
92
|
@attributes = attributes
|
|
@@ -42,8 +42,8 @@ module Hecks
|
|
|
42
42
|
# `expr_emitter.rb`'s own `emit_bool`/`emit_resolver` already hold
|
|
43
43
|
# to — even though, as of this writing, no real corpus VALUE OBJECT
|
|
44
44
|
# invariant exercises `Include`/`Modulo`/`BlockPredicate`/`Find`/
|
|
45
|
-
# `Array`/`MatchesRegex`/`Presence`/`Split`/`StartsWith
|
|
46
|
-
# `First`/`Last` (only `given`/`ensures` clauses do, elsewhere in
|
|
45
|
+
# `Array`/`MatchesRegex`/`Presence`/`Assignment`/`Split`/`StartsWith`/
|
|
46
|
+
# `EndsWith`/`First`/`Last` (only `given`/`ensures` clauses do, elsewhere in
|
|
47
47
|
# the corpus — a different consumer of this same grammar).
|
|
48
48
|
# `rust/host/src/expr_json.rs`'s own header names exactly which of
|
|
49
49
|
# these its interpreter evaluates for real today versus refuses
|
|
@@ -56,10 +56,75 @@ module Hecks
|
|
|
56
56
|
module AstJson
|
|
57
57
|
module_function
|
|
58
58
|
|
|
59
|
+
# THE CLOSED OP ROSTER — every `"op"` tag the walkers below can
|
|
60
|
+
# emit, pinned so a reader (or a spec) can refuse a tag it does
|
|
61
|
+
# not know instead of guessing. A new node kind is a new entry
|
|
62
|
+
# here, a new arm below, and a new arm in every reader.
|
|
63
|
+
OPS = %w[
|
|
64
|
+
or and not compare include
|
|
65
|
+
int float str bool nil array lookup
|
|
66
|
+
add modulo sign_test empty size to_s
|
|
67
|
+
block_predicate find first last
|
|
68
|
+
matches_regex presence assignment split starts_with ends_with
|
|
69
|
+
].freeze
|
|
70
|
+
|
|
71
|
+
# ONE RULE ROW, THE WAY EVERY RULE SITE EMITS IT — description and
|
|
72
|
+
# canonical text (what every reader has always had) plus the
|
|
73
|
+
# structured form, derived from the same text. `ast` is a pure
|
|
74
|
+
# function of `canonical`: the IR carries both so a reader that
|
|
75
|
+
# only displays keeps the text, and a reader that evaluates never
|
|
76
|
+
# re-parses it.
|
|
77
|
+
def rule_row(rule)
|
|
78
|
+
{ description: rule.description, canonical: rule.canonical, ast: rule.ast || emit_predicate(rule.canonical) }
|
|
79
|
+
end
|
|
80
|
+
|
|
59
81
|
def emit_predicate(canonical)
|
|
60
82
|
emit_bool(Evaluator.parse(canonical))
|
|
61
83
|
end
|
|
62
84
|
|
|
85
|
+
# C3.6 (docs/semantics/bluebook-semantics.md) — every `.match?`
|
|
86
|
+
# pattern a rule carries is held to `PatternSubset`, exactly as an
|
|
87
|
+
# attribute's own `pattern:` already is (`AttributeCollector#
|
|
88
|
+
# refuse_unshared_pattern`): a regex whose meaning depends on the
|
|
89
|
+
# engine reading it is a defect in the bluebook, refused at build.
|
|
90
|
+
# Walks the emitted AST, so every rule site (givens, ensures,
|
|
91
|
+
# invariants, preconditions, a policy's where) gets the one check.
|
|
92
|
+
def refuse_unshared_patterns!(ast, owner:, word:)
|
|
93
|
+
return ast if Hecks::Bluebook::MetaValidator.shadow_parsing? # frozen era text is history
|
|
94
|
+
|
|
95
|
+
each_node(ast) do |node|
|
|
96
|
+
next unless node["op"] == "matches_regex"
|
|
97
|
+
|
|
98
|
+
rejection = PatternSubset.validate(node["pattern"])
|
|
99
|
+
next unless rejection
|
|
100
|
+
|
|
101
|
+
raise DSL::Malformed,
|
|
102
|
+
"#{owner}'s #{word} matches against #{node['pattern'].inspect}, which uses a " \
|
|
103
|
+
"#{rejection.construct} — #{rejection.reason}"
|
|
104
|
+
end
|
|
105
|
+
ast
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Every name a rule resolves at its root — the first segment of
|
|
109
|
+
# each `lookup` path, unique, in first-seen order.
|
|
110
|
+
def lookup_heads(ast)
|
|
111
|
+
heads = []
|
|
112
|
+
each_node(ast) do |node|
|
|
113
|
+
heads << node["path"].first.to_s if node["op"] == "lookup" && node["path"].is_a?(::Array)
|
|
114
|
+
end
|
|
115
|
+
heads.uniq
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def each_node(node, &block)
|
|
119
|
+
case node
|
|
120
|
+
when ::Hash
|
|
121
|
+
yield node
|
|
122
|
+
node.each_value { |child| each_node(child, &block) }
|
|
123
|
+
when ::Array
|
|
124
|
+
node.each { |child| each_node(child, &block) }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
63
128
|
def emit_bool(node)
|
|
64
129
|
case node
|
|
65
130
|
when Evaluator::Or then { "op" => "or", "left" => emit_bool(node.left), "right" => emit_bool(node.right) }
|
|
@@ -73,12 +138,13 @@ module Hecks
|
|
|
73
138
|
when Evaluator::Resolve
|
|
74
139
|
emit_resolver(node.expr)
|
|
75
140
|
else
|
|
76
|
-
raise "unhandled evaluator node #{node.class} — no JSON rendering exists for it
|
|
141
|
+
raise "unhandled evaluator node #{node.class} — no JSON rendering exists for it " \
|
|
142
|
+
"(lib/hecks/bluebook/expression/ast_json.rb#emit_bool)"
|
|
77
143
|
end
|
|
78
144
|
end
|
|
79
145
|
|
|
80
|
-
def emit_comparison(
|
|
81
|
-
{ "less_than" =>
|
|
146
|
+
def emit_comparison(comparator)
|
|
147
|
+
{ "less_than" => comparator.compares_less_than, "equal" => comparator.compares_equal, "negated" => comparator.negated }
|
|
82
148
|
end
|
|
83
149
|
|
|
84
150
|
# The JSON-target sibling of `expr_emitter.rb`'s own
|
|
@@ -99,11 +165,18 @@ module Hecks
|
|
|
99
165
|
return { "op" => "bool", "value" => false } if node.haystack.elements.empty?
|
|
100
166
|
|
|
101
167
|
equalities = node.haystack.elements.map do |element|
|
|
102
|
-
{ "op" => "compare", "cmp" => emit_comparison(EQ), "left" => emit_resolver(node.needle),
|
|
168
|
+
{ "op" => "compare", "cmp" => emit_comparison(EQ), "left" => emit_resolver(node.needle),
|
|
169
|
+
"right" => emit_resolver(element) }
|
|
103
170
|
end
|
|
104
171
|
equalities.reduce { |left, right| { "op" => "or", "left" => left, "right" => right } }
|
|
105
172
|
end
|
|
106
173
|
|
|
174
|
+
# One case arm per Resolver node type — the class header above is
|
|
175
|
+
# explicit that this dispatch must stay COMPLETE and in one place
|
|
176
|
+
# ("every node this grammar admits gets a real arm"); splitting it
|
|
177
|
+
# into several methods would hide whether the set is still
|
|
178
|
+
# exhaustive instead of making that visible at a glance.
|
|
179
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
107
180
|
def emit_resolver(node)
|
|
108
181
|
case node
|
|
109
182
|
when Resolver::IntegerLiteral then { "op" => "int", "value" => node.value }
|
|
@@ -111,7 +184,9 @@ module Hecks
|
|
|
111
184
|
when Resolver::StringLiteral then { "op" => "str", "value" => node.value }
|
|
112
185
|
when Resolver::BoolLiteral then { "op" => "bool", "value" => node.value }
|
|
113
186
|
when Resolver::NilLiteral then { "op" => "nil" }
|
|
114
|
-
|
|
187
|
+
# `path` is the SAME shape `find.path` already has — segments, not
|
|
188
|
+
# a dotted string a reader would have to split by its own rule.
|
|
189
|
+
when Resolver::Lookup then { "op" => "lookup", "path" => node.path.split(".") }
|
|
115
190
|
when Resolver::Addition then { "op" => "add", "left" => emit_resolver(node.left), "right" => emit_resolver(node.right) }
|
|
116
191
|
when Resolver::SignTest
|
|
117
192
|
{ "op" => "sign_test", "cmp" => emit_comparison(node.operator), "receiver" => emit_resolver(node.receiver) }
|
|
@@ -128,9 +203,12 @@ module Hecks
|
|
|
128
203
|
when Resolver::ArrayLiteral
|
|
129
204
|
{ "op" => "array", "elements" => node.elements.map { |element| emit_resolver(element) } }
|
|
130
205
|
when Resolver::MatchesRegex
|
|
131
|
-
{ "op" => "matches_regex", "receiver" => emit_resolver(node.receiver), "pattern" => node.pattern,
|
|
206
|
+
{ "op" => "matches_regex", "receiver" => emit_resolver(node.receiver), "pattern" => node.pattern,
|
|
207
|
+
"flags" => node.flags }
|
|
132
208
|
when Resolver::Presence
|
|
133
209
|
{ "op" => "presence", "receiver" => emit_resolver(node.receiver), "negated" => node.negated }
|
|
210
|
+
when Resolver::Assignment
|
|
211
|
+
{ "op" => "assignment", "receiver" => emit_resolver(node.receiver), "negated" => node.negated }
|
|
134
212
|
when Resolver::Split
|
|
135
213
|
{ "op" => "split", "receiver" => emit_resolver(node.receiver), "separator" => node.separator }
|
|
136
214
|
when Resolver::StartsWith
|
|
@@ -140,7 +218,8 @@ module Hecks
|
|
|
140
218
|
when Resolver::First then { "op" => "first", "receiver" => emit_resolver(node.receiver) }
|
|
141
219
|
when Resolver::Last then { "op" => "last", "receiver" => emit_resolver(node.receiver) }
|
|
142
220
|
else
|
|
143
|
-
raise "unhandled resolver node #{node.class} — no JSON rendering exists for it
|
|
221
|
+
raise "unhandled resolver node #{node.class} — no JSON rendering exists for it " \
|
|
222
|
+
"(lib/hecks/bluebook/expression/ast_json.rb#emit_resolver)"
|
|
144
223
|
end
|
|
145
224
|
end
|
|
146
225
|
end
|