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
|
@@ -52,7 +52,7 @@ module Hecks
|
|
|
52
52
|
out << chapter_header(bluebook, depth) unless only
|
|
53
53
|
Array(aggregates(bluebook, only)).each { |aggregate| out << aggregate_section(aggregate, only ? depth : depth + 1) }
|
|
54
54
|
out << closing(bluebook, depth + 1) unless only
|
|
55
|
-
out.compact.join("\n").rstrip
|
|
55
|
+
"#{out.compact.join("\n").rstrip}\n"
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# A NAME THAT NAMES NOTHING IS REFUSED, not answered with an empty
|
|
@@ -179,7 +179,7 @@ module Hecks
|
|
|
179
179
|
value_object = value_object_for(attribute, holder)
|
|
180
180
|
inner =
|
|
181
181
|
if value_object
|
|
182
|
-
"{
|
|
182
|
+
"{ #{value_object.attributes.map { |f| "#{f.name}: #{f.type}" }.join(', ')} }"
|
|
183
183
|
else
|
|
184
184
|
attribute.type.to_s
|
|
185
185
|
end
|
|
@@ -244,19 +244,13 @@ module Hecks
|
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
def command_entry(command, holder, depth)
|
|
247
|
-
out = [h(depth, "#{command.hecks_name}#{
|
|
247
|
+
out = [h(depth, "#{command.hecks_name}#{' *(creates)*' if command.creates?}"), ""]
|
|
248
248
|
out += [command.goal, ""] if command.goal
|
|
249
249
|
out << "Issued by: **#{command.role}**." if command.role
|
|
250
250
|
out << ""
|
|
251
251
|
|
|
252
252
|
arguments = command.attributes
|
|
253
|
-
unless arguments.empty?
|
|
254
|
-
rows = arguments.map do |attribute|
|
|
255
|
-
shape = attribute.reference? ? "id of a `#{attribute.type.target_name}`" : shape_of(attribute, holder)
|
|
256
|
-
["`#{attribute.name}`", shape, attribute.optional? ? "" : "required"]
|
|
257
|
-
end
|
|
258
|
-
out << table(%w[argument shape needed], rows)
|
|
259
|
-
end
|
|
253
|
+
out << table(%w[argument shape needed], command_argument_rows(arguments, holder)) unless arguments.empty?
|
|
260
254
|
|
|
261
255
|
refusals = refusals_of(command, holder)
|
|
262
256
|
unless refusals.empty?
|
|
@@ -272,6 +266,17 @@ module Hecks
|
|
|
272
266
|
out.join("\n")
|
|
273
267
|
end
|
|
274
268
|
|
|
269
|
+
# The argument table's own rows — a pure per-attribute mapping with
|
|
270
|
+
# nothing to share with `command_entry`'s other sections, extracted
|
|
271
|
+
# only to keep that method to the one shape every section there
|
|
272
|
+
# follows: build a chunk, append it if non-empty.
|
|
273
|
+
def command_argument_rows(arguments, holder)
|
|
274
|
+
arguments.map do |attribute|
|
|
275
|
+
shape = attribute.reference? ? "id of a `#{attribute.type.target_name}`" : shape_of(attribute, holder)
|
|
276
|
+
["`#{attribute.name}`", shape, attribute.optional? ? "" : "required"]
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
275
280
|
# EVERY WAY THIS VERB CAN SAY NO, gathered from the three places a
|
|
276
281
|
# chapter states them — the lifecycle it is an edge of, its own
|
|
277
282
|
# `given`s, and the fact that a reference has to resolve. A caller
|
|
@@ -284,7 +289,11 @@ module Hecks
|
|
|
284
289
|
froms = lifecycle && lifecycle.transitions.filter_map do |name, transition|
|
|
285
290
|
Array(transition.from) if name.to_s == command.hecks_name
|
|
286
291
|
end.flatten.uniq
|
|
287
|
-
|
|
292
|
+
if froms && !froms.empty?
|
|
293
|
+
refusals << "`#{lifecycle.field}` is anything other than #{froms.map do |f|
|
|
294
|
+
"`#{f}`"
|
|
295
|
+
end.join(' or ')}"
|
|
296
|
+
end
|
|
288
297
|
|
|
289
298
|
command.attributes.select(&:reference?).each do |reference|
|
|
290
299
|
refusals << "no `#{reference.type.target_name}` exists for the id given as `#{reference.name}`"
|
|
@@ -302,7 +311,7 @@ module Hecks
|
|
|
302
311
|
queries.each do |query|
|
|
303
312
|
shape = query.to_h
|
|
304
313
|
takes = Array(shape[:attributes]).map { |a| "`#{a[:name]}`" }.join(", ")
|
|
305
|
-
out << "**#{query.hecks_name}**#{
|
|
314
|
+
out << "**#{query.hecks_name}**#{" (#{takes})" unless takes.empty?} "
|
|
306
315
|
out << (query.description ? "#{query.description} " : "")
|
|
307
316
|
filters = Array(shape[:wheres]).map { |w| "`#{w[:field]} #{w[:op]} #{w[:value].inspect}`" }
|
|
308
317
|
out << "Filters: #{filters.join(', ')}." unless filters.empty?
|
|
@@ -3,6 +3,13 @@ require_relative "../ports/persistence"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Projector
|
|
6
|
+
# Registry-wide serialization to Hash/JSON: bluebook IR
|
|
7
|
+
# (`call`/`json`), era-adapter lineage-capability flags (`lineage`),
|
|
8
|
+
# and translation edges in both their DIGEST-relevant declared shape
|
|
9
|
+
# (`translation_hash`, what ApprovalDigest hashes) and their
|
|
10
|
+
# consumer-ready COMPILED shape with precompiled SQL attached
|
|
11
|
+
# (`translations`/`compiled_translation_aggregate`). Read directly by
|
|
12
|
+
# bin/ir, bin/project_rust, and the translation/audit approval digest.
|
|
6
13
|
module Exporter
|
|
7
14
|
module_function
|
|
8
15
|
|
|
@@ -47,6 +54,30 @@ module Hecks
|
|
|
47
54
|
{ capable_aggregates: capable.map { |aggregate| { name: aggregate.name, storage_name: aggregate.storage_name } } }
|
|
48
55
|
end
|
|
49
56
|
|
|
57
|
+
# A BINDING fact, same shape/reasoning as `lineage` above: every
|
|
58
|
+
# aggregate's DECLARED persistence adapter name (`persisted_by`),
|
|
59
|
+
# not part of the canonical bluebook shape `call` exports (ADR
|
|
60
|
+
# 0001 — the IR describes what's declared, never which adapter a
|
|
61
|
+
# deployment binds it to). Unlike `lineage`, this needs no era
|
|
62
|
+
# plugin — `BindingPolicy` is core, always loaded — and covers
|
|
63
|
+
# EVERY aggregate, not just lineage-capable ones: `rust/host`
|
|
64
|
+
# (`ir.rs`'s own `refuse_unsupported_persistence_adapters`) reads
|
|
65
|
+
# this to refuse loudly, at boot, against a domain bound to an
|
|
66
|
+
# adapter it has no backend for (Heki, Memory, Sqlite, D1,
|
|
67
|
+
# LocalStorage — `rust/host` understands only Postgres/PostgresEra
|
|
68
|
+
# today), rather than silently building up a second, disjoint
|
|
69
|
+
# history nothing but Rust ever reads while the real state stays
|
|
70
|
+
# wherever its own adapter actually wrote it.
|
|
71
|
+
def persistence(registry, domain_name)
|
|
72
|
+
bluebook = registry.bluebooks.fetch(domain_name)
|
|
73
|
+
aggregates = bluebook.aggregates.map do |aggregate|
|
|
74
|
+
adapter = Ports::Persistence::BindingPolicy.resolve(registry, domain_name, aggregate).adapter
|
|
75
|
+
{ name: aggregate.name, storage_name: aggregate.storage_name, adapter: adapter }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
{ aggregates: aggregates }
|
|
79
|
+
end
|
|
80
|
+
|
|
50
81
|
# Translation IR, always as an array, WITH each aggregate's
|
|
51
82
|
# precompiled SQL attached (`compiled_translation_aggregate`) —
|
|
52
83
|
# this is the export a consumer embeds (`ir.json`'s `translations`
|
|
@@ -150,7 +181,9 @@ module Hecks
|
|
|
150
181
|
|
|
151
182
|
translation_aggregate(aggregate).merge(
|
|
152
183
|
compiled_state_expression: Translation::RuleCompiler.compile_rules(aggregate),
|
|
153
|
-
compiled_id_expression: Translation::RuleCompiler.rekeyed?(aggregate)
|
|
184
|
+
compiled_id_expression: (if Translation::RuleCompiler.rekeyed?(aggregate)
|
|
185
|
+
Translation::RuleCompiler.compile_id_expression(aggregate)
|
|
186
|
+
end)
|
|
154
187
|
)
|
|
155
188
|
end
|
|
156
189
|
end
|
|
@@ -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,8 +1,15 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module QuerySpecification
|
|
3
3
|
module Common
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# `target` — see `WhereClause`'s own header (ADR 0055): which
|
|
5
|
+
# many-side `include`d aggregate this `limit` applies to, on a
|
|
6
|
+
# `read_model` declaring more than one. `nil` for a plain `Query`,
|
|
7
|
+
# and for a `read_model` with a single many-side head.
|
|
8
|
+
LimitSpec = Struct.new(:value, :target, keyword_init: true) do
|
|
9
|
+
def to_h
|
|
10
|
+
base = { value: QuerySpecification.render_value(value) }
|
|
11
|
+
target ? base.merge(target: target.to_s) : base
|
|
12
|
+
end
|
|
6
13
|
end
|
|
7
14
|
end
|
|
8
15
|
end
|
|
@@ -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
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module QuerySpecification
|
|
3
3
|
module Common
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# `target` — see `WhereClause`'s own header (ADR 0055): which
|
|
5
|
+
# many-side `include`d aggregate this `offset` applies to, on a
|
|
6
|
+
# `read_model` declaring more than one. `nil` for a plain `Query`,
|
|
7
|
+
# and for a `read_model` with a single many-side head.
|
|
8
|
+
OffsetSpec = Struct.new(:value, :target, keyword_init: true) do
|
|
9
|
+
def to_h
|
|
10
|
+
base = { value: QuerySpecification.render_value(value) }
|
|
11
|
+
target ? base.merge(target: target.to_s) : base
|
|
12
|
+
end
|
|
6
13
|
end
|
|
7
14
|
end
|
|
8
15
|
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
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module QuerySpecification
|
|
3
3
|
module Common
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# `target` — see `WhereClause`'s own header (ADR 0055): which
|
|
5
|
+
# many-side `include`d aggregate this `order_by` applies to, on a
|
|
6
|
+
# `read_model` declaring more than one. `nil` for a plain `Query`,
|
|
7
|
+
# and for a `read_model` with a single many-side head.
|
|
8
|
+
OrderBy = Struct.new(:field, :direction, :target, keyword_init: true) do
|
|
9
|
+
def to_h
|
|
10
|
+
base = { field: field.to_s, direction: direction.to_s }
|
|
11
|
+
target ? base.merge(target: target.to_s) : base
|
|
12
|
+
end
|
|
6
13
|
end
|
|
7
14
|
end
|
|
8
15
|
end
|