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
|
@@ -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
|
|
@@ -107,8 +113,10 @@ module Hecks
|
|
|
107
113
|
# field values) has no root to speak of. Still needs to describe
|
|
108
114
|
# SOMETHING — zero includes AND no reference is refused.
|
|
109
115
|
def build
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
if !@reference_target && Array(@includes).empty?
|
|
117
|
+
raise Malformed,
|
|
118
|
+
"#{@name} needs an aggregate-head reference or at least one include"
|
|
119
|
+
end
|
|
112
120
|
|
|
113
121
|
Array(@includes).each do |target, as|
|
|
114
122
|
add_aggregate_head(target, as, many: target != @reference_target)
|
|
@@ -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
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require_relative "evaluator"
|
|
2
|
+
require_relative "resolver"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Bluebook
|
|
6
|
+
module Expression
|
|
7
|
+
# THE INVERSE OF `AstJson` — reads the `"op"`-tagged JSON a rule row
|
|
8
|
+
# carries as `ast` back into the SAME `Evaluator`/`Resolver` node
|
|
9
|
+
# Structs `Evaluator.parse` builds from `canonical`. This is how the
|
|
10
|
+
# runtime evaluates a rule without re-parsing its text: the one
|
|
11
|
+
# parse happened at DSL-build time, behind `AstJson`; dispatch walks
|
|
12
|
+
# the structured form (`Evaluator.call_rule`), and `canonical` stays
|
|
13
|
+
# what it always displayed as — text for humans and refusal wording.
|
|
14
|
+
#
|
|
15
|
+
# Written as the obvious mirror of `ast_json.rb`'s two walkers, arm
|
|
16
|
+
# for arm, so a new op there is a new arm here and
|
|
17
|
+
# spec/expression_ast_spec.rb's roster contract fails until it
|
|
18
|
+
# lands. Promoted from spec/support once the runtime switched —
|
|
19
|
+
# the spec proved the round trip first, then the runtime adopted it.
|
|
20
|
+
#
|
|
21
|
+
# One deliberate asymmetry, inherited: `AstJson` rewrites a
|
|
22
|
+
# LITERAL-array `.include?` into an OR of equalities (see
|
|
23
|
+
# `emit_include`), so reading never produces an `Include` over an
|
|
24
|
+
# `ArrayLiteral`. Evaluation is unchanged by that rewrite, which is
|
|
25
|
+
# exactly what the equivalence spec pins.
|
|
26
|
+
module AstReader
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def read_predicate(json) = read_bool(json)
|
|
30
|
+
|
|
31
|
+
def read_bool(json)
|
|
32
|
+
case json.fetch("op")
|
|
33
|
+
when "or" then Evaluator::Or.new(left: read_bool(json["left"]), right: read_bool(json["right"]))
|
|
34
|
+
when "and" then Evaluator::And.new(left: read_bool(json["left"]), right: read_bool(json["right"]))
|
|
35
|
+
when "not" then Evaluator::Not.new(node: read_bool(json["expr"]))
|
|
36
|
+
when "compare"
|
|
37
|
+
Evaluator::Compare.new(operator: operator(json["cmp"]),
|
|
38
|
+
left: read_resolver(json["left"]), right: read_resolver(json["right"]))
|
|
39
|
+
when "include"
|
|
40
|
+
Evaluator::Include.new(haystack: read_resolver(json["haystack"]), needle: read_resolver(json["needle"]))
|
|
41
|
+
else Evaluator::Resolve.new(expr: read_resolver(json))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The comparator algebra travels as its three-flag triple; the
|
|
46
|
+
# `Operator` carrying that exact triple is the one `parse` would
|
|
47
|
+
# have chosen, because the roster (`expression/projection.json`)
|
|
48
|
+
# holds one symbol per triple.
|
|
49
|
+
def operator(cmp)
|
|
50
|
+
Evaluator::OPERATORS.find do |op|
|
|
51
|
+
op.compares_less_than == cmp.fetch("less_than") &&
|
|
52
|
+
op.compares_equal == cmp.fetch("equal") &&
|
|
53
|
+
op.negated == cmp.fetch("negated")
|
|
54
|
+
end or raise "no comparison operator has the triple #{cmp.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity -- one arm
|
|
58
|
+
# per AstJson op is the point; splitting the case would hide the roster.
|
|
59
|
+
def read_resolver(json)
|
|
60
|
+
recv = -> { read_resolver(json["receiver"]) }
|
|
61
|
+
case json.fetch("op")
|
|
62
|
+
when "int" then Resolver::IntegerLiteral.new(value: json["value"])
|
|
63
|
+
when "float" then Resolver::FloatLiteral.new(value: json["value"])
|
|
64
|
+
when "str" then Resolver::StringLiteral.new(value: json["value"])
|
|
65
|
+
when "bool" then Resolver::BoolLiteral.new(value: json["value"])
|
|
66
|
+
when "nil" then Resolver::NilLiteral.new
|
|
67
|
+
when "array" then Resolver::ArrayLiteral.new(elements: json["elements"].map { |e| read_resolver(e) })
|
|
68
|
+
when "lookup" then Resolver::Lookup.new(path: json["path"].join("."))
|
|
69
|
+
when "add" then Resolver::Addition.new(left: read_resolver(json["left"]), right: read_resolver(json["right"]))
|
|
70
|
+
when "sign_test"
|
|
71
|
+
op = operator(json["cmp"])
|
|
72
|
+
Resolver::SignTest.new(operator: op, test: sign_test_name(op), receiver: recv.call)
|
|
73
|
+
when "empty" then Resolver::Empty.new(receiver: recv.call)
|
|
74
|
+
when "to_s" then Resolver::ToS.new(receiver: recv.call)
|
|
75
|
+
when "modulo" then Resolver::Modulo.new(receiver: recv.call, divisor: read_resolver(json["divisor"]))
|
|
76
|
+
when "size" then Resolver::Size.new(receiver: recv.call)
|
|
77
|
+
when "first" then Resolver::First.new(receiver: recv.call)
|
|
78
|
+
when "last" then Resolver::Last.new(receiver: recv.call)
|
|
79
|
+
when "block_predicate"
|
|
80
|
+
Resolver::BlockPredicate.new(mode: json["mode"].to_sym, receiver: recv.call, param: json["param"],
|
|
81
|
+
predicate: read_bool(json["predicate"]))
|
|
82
|
+
when "find"
|
|
83
|
+
Resolver::Find.new(receiver: recv.call, param: json["param"],
|
|
84
|
+
predicate: read_bool(json["predicate"]), path: json["path"])
|
|
85
|
+
when "matches_regex" then Resolver::MatchesRegex.new(receiver: recv.call, pattern: json["pattern"],
|
|
86
|
+
flags: json["flags"])
|
|
87
|
+
when "presence" then Resolver::Presence.new(receiver: recv.call, negated: json["negated"])
|
|
88
|
+
when "assignment" then Resolver::Assignment.new(receiver: recv.call, negated: json["negated"])
|
|
89
|
+
when "split" then Resolver::Split.new(receiver: recv.call, separator: json["separator"])
|
|
90
|
+
when "starts_with" then Resolver::StartsWith.new(receiver: recv.call, substring: json["substring"])
|
|
91
|
+
when "ends_with" then Resolver::EndsWith.new(receiver: recv.call, substring: json["substring"])
|
|
92
|
+
else raise "no reader handles op #{json['op'].inspect} — add an arm before AstJson can emit it"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
96
|
+
|
|
97
|
+
# `SignTest#test` is only wording (the refusal names it); the
|
|
98
|
+
# triple is what evaluates. Recover the spelling from the
|
|
99
|
+
# vocabulary so a rebuilt node refuses with the same message the
|
|
100
|
+
# parsed one would.
|
|
101
|
+
def sign_test_name(operator)
|
|
102
|
+
Resolver::SIGN_TEST_OPERATORS.key(operator.symbol) || operator.symbol
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -4,6 +4,13 @@ require_relative "../../vocabulary"
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Bluebook
|
|
6
6
|
module Expression
|
|
7
|
+
# Rewrites a predicate's SOURCE TEXT into one canonical spelling
|
|
8
|
+
# before it's ever parsed or hashed — collapsing whitespace and
|
|
9
|
+
# folding admitted synonyms (e.g. `.length` → `.size`) via the
|
|
10
|
+
# `RULES` table projected from the grammar chapter, so two byte-
|
|
11
|
+
# different but equivalent predicates compare and cache identically.
|
|
12
|
+
# Never touches text inside quoted string literals
|
|
13
|
+
# (`map_outside_strings`).
|
|
7
14
|
module CanonicalForm
|
|
8
15
|
Rule = Struct.new(:strategy, :source_token, :replacement, :boundary, :position, keyword_init: true)
|
|
9
16
|
|