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,13 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# The `command "Name" do ... end` receiver — collects a command's
|
|
6
|
+
# `role`/`goal`/`given`/`ensures`/`sets`/`emits`/`delegates_to`/
|
|
7
|
+
# `corrects` declarations, resolves implicit attributes a bare `sets
|
|
8
|
+
# :field` or `append:` self-reference imports from the owner
|
|
9
|
+
# (`#resolve_implicit_attributes!`), and builds the final `Command`.
|
|
5
10
|
class CommandBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Command"
|
|
11
|
+
GRAMMAR_CONTEXT = "Command".freeze
|
|
7
12
|
|
|
8
13
|
include AttributeCollector
|
|
9
14
|
include RuleReference
|
|
@@ -67,10 +72,12 @@ module Hecks
|
|
|
67
72
|
# declares a role), so also named in
|
|
68
73
|
# `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK`.
|
|
69
74
|
def role_impl(value)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
if @role
|
|
76
|
+
raise Malformed,
|
|
77
|
+
"#{@name} declares role twice — a command carries ONE " \
|
|
78
|
+
"responsibility; the second would silently win and the " \
|
|
79
|
+
"first would still look declared"
|
|
80
|
+
end
|
|
74
81
|
|
|
75
82
|
@role = value
|
|
76
83
|
end
|
|
@@ -104,7 +111,7 @@ module Hecks
|
|
|
104
111
|
# one thing it exists to say. Transfer has said
|
|
105
112
|
# `reference_to Account, as: :source` for as long as banking has existed;
|
|
106
113
|
# this is the same sentence when the target happens to be the owner.
|
|
107
|
-
return cross_reference(demodulised, as, optional) if as || demodulised.to_s != @owner.to_s
|
|
114
|
+
return cross_reference(demodulised, as, optional: optional) if as || demodulised.to_s != @owner.to_s
|
|
108
115
|
|
|
109
116
|
if @references
|
|
110
117
|
raise Malformed,
|
|
@@ -118,7 +125,7 @@ module Hecks
|
|
|
118
125
|
|
|
119
126
|
private
|
|
120
127
|
|
|
121
|
-
def cross_reference(target, as, optional
|
|
128
|
+
def cross_reference(target, as, optional: false)
|
|
122
129
|
attribute_impl(as || default_reference_name(target), Reference.new(target), optional: optional)
|
|
123
130
|
end
|
|
124
131
|
|
|
@@ -308,8 +315,8 @@ module Hecks
|
|
|
308
315
|
# dedicated, `status: "deprecated"` Keyword row (syntax.bluebook)
|
|
309
316
|
# rather than living only as `sets`'s own `was:` — see that
|
|
310
317
|
# row's own comment for why.
|
|
311
|
-
def then_set_impl(target, positional_to = UNSET, **
|
|
312
|
-
return legacy_then_set(target, positional_to, **
|
|
318
|
+
def then_set_impl(target, positional_to = UNSET, **)
|
|
319
|
+
return legacy_then_set(target, positional_to, **) if MetaValidator.shadow_parsing?
|
|
313
320
|
|
|
314
321
|
raise Malformed, "#{@name}'s then_set is gone — sets is the word now"
|
|
315
322
|
end
|
|
@@ -478,8 +485,13 @@ module Hecks
|
|
|
478
485
|
source: { as: as&.to_s, reason: reason.to_s, reverses: reverses })
|
|
479
486
|
end
|
|
480
487
|
|
|
488
|
+
# THE EFFECTS THAT WRITE A FIELD OF THE RECORD — `delegate` and
|
|
489
|
+
# `corrects` name a command and an event, never a field.
|
|
490
|
+
FIELD_EFFECTS = %i[set append remove increment decrement multiply clamp].freeze
|
|
491
|
+
|
|
481
492
|
def build
|
|
482
493
|
resolve_implicit_attributes!
|
|
494
|
+
refuse_duplicate_targets!
|
|
483
495
|
|
|
484
496
|
delegation = @mutations.find { |mutation| mutation.op == :delegate }
|
|
485
497
|
if delegation && (@mutations.size > 1 || @emits.any?)
|
|
@@ -515,6 +527,27 @@ module Hecks
|
|
|
515
527
|
|
|
516
528
|
private
|
|
517
529
|
|
|
530
|
+
# C4.2 (docs/semantics/bluebook-semantics.md) — a command's effects
|
|
531
|
+
# are ONE UPDATE SET over the pre-dispatch state, so a field
|
|
532
|
+
# written twice has no meaning to give: last-wins would make
|
|
533
|
+
# declaration order significant, which the update set says it is
|
|
534
|
+
# not. Refused here, where the declaration can still be read whole.
|
|
535
|
+
def refuse_duplicate_targets!
|
|
536
|
+
return if MetaValidator.shadow_parsing? # frozen era text is history
|
|
537
|
+
|
|
538
|
+
seen = {}
|
|
539
|
+
@mutations.each do |mutation|
|
|
540
|
+
next unless FIELD_EFFECTS.include?(mutation.op)
|
|
541
|
+
|
|
542
|
+
if (earlier = seen[mutation.target.to_sym])
|
|
543
|
+
raise Malformed,
|
|
544
|
+
"#{@name} writes #{mutation.target} twice (#{earlier.op} and #{mutation.op}) — a command's " \
|
|
545
|
+
"effects are one update set over the pre-dispatch state, so each field is written at most once"
|
|
546
|
+
end
|
|
547
|
+
seen[mutation.target.to_sym] = mutation
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
518
551
|
# RESOLUTION RULES — see `docs/resolution-rules/README.md` for the
|
|
519
552
|
# precise, language-agnostic algorithm each of `resolve_bare_set!`/
|
|
520
553
|
# `resolve_append_fields!` implements (`implicit-command-attributes.md`
|
|
@@ -563,8 +596,8 @@ module Hecks
|
|
|
563
596
|
# it for an ordering accident this language has never required
|
|
564
597
|
# authors to avoid. rubocop's Style/CombinableLoops can't see
|
|
565
598
|
# that dependency — it only sees two `@mutations.each` calls
|
|
566
|
-
# back to back —
|
|
567
|
-
# rubocop
|
|
599
|
+
# back to back — which is exactly why it's disabled repo-wide
|
|
600
|
+
# (see .rubocop.yml) rather than satisfied.
|
|
568
601
|
@mutations.each { |mutation| refuse_unknown_argument_sources!(mutation) }
|
|
569
602
|
end
|
|
570
603
|
|
|
@@ -674,6 +707,13 @@ module Hecks
|
|
|
674
707
|
# the naive append; every OTHER field in the same hash did not,
|
|
675
708
|
# caught by this codemod's own reboot-and-diff safety net rather
|
|
676
709
|
# than silently landing wrong.
|
|
710
|
+
# `anchor` is a snapshot of `present`'s position, taken BEFORE
|
|
711
|
+
# `attributes.reject!` mutates the array below it — see this
|
|
712
|
+
# method's own header comment for why: a naive append-at-end
|
|
713
|
+
# silently scrambled real corpus field order (Keyword#was/
|
|
714
|
+
# Argument#variadic). Splitting the snapshot from the mutation it
|
|
715
|
+
# guards is exactly the ordering invariant that must not move.
|
|
716
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
677
717
|
def resolve_append_fields!(mutation)
|
|
678
718
|
return unless mutation.source.is_a?(Hash)
|
|
679
719
|
|
|
@@ -755,8 +795,8 @@ module Hecks
|
|
|
755
795
|
# downstream read it — at dispatch (`MutationApplier#appended`),
|
|
756
796
|
# at IR emission (`Mutation#appended_fields`), and in the
|
|
757
797
|
# meta-validator's own Judge (`Readings#mutation_rows`). #138.
|
|
758
|
-
def normalize_append_source(
|
|
759
|
-
return source unless
|
|
798
|
+
def normalize_append_source(oper, source)
|
|
799
|
+
return source unless oper == :append
|
|
760
800
|
return source if source.is_a?(::Hash)
|
|
761
801
|
|
|
762
802
|
{ value: source }
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Bluebook
|
|
3
3
|
module DSL
|
|
4
|
+
# A global (Object-level, via `Hook` prepended below) `const_missing`
|
|
5
|
+
# bridge — `.with(resolver) { ... }` installs a resolver for the
|
|
6
|
+
# dynamic extent of a block, so a bare, not-yet-declared constant
|
|
7
|
+
# inside a DSL block (a bareword type, `Domain::Aggregate`, ...)
|
|
8
|
+
# resolves to whatever that DSL context needs (a name, a
|
|
9
|
+
# `BindingProxy`, a `ScopedConstant`) instead of raising `NameError`.
|
|
4
10
|
module ConstShim
|
|
5
11
|
class << self
|
|
6
12
|
attr_accessor :resolver
|
|
@@ -2,8 +2,15 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `domain_port "Name" do ... end` block into a `DomainPort` —
|
|
6
|
+
# the inbound (`operation`/`tells`) and outbound (`asks`) operations a
|
|
7
|
+
# domain exchanges with the outside world. Falls back to building a
|
|
8
|
+
# plain `Port` (the same object `PortBuilder` builds) when the block is
|
|
9
|
+
# bare-verb shaped instead, so an existing top-level `.port` file can
|
|
10
|
+
# migrate to being parsed by this builder — see `legacy_bare_port:`'s
|
|
11
|
+
# own comment on `#initialize`.
|
|
5
12
|
class DomainPortBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "DomainPort"
|
|
13
|
+
GRAMMAR_CONTEXT = "DomainPort".freeze
|
|
7
14
|
|
|
8
15
|
include WordGate
|
|
9
16
|
|
|
@@ -43,8 +50,8 @@ module Hecks
|
|
|
43
50
|
# — the routing between the two spellings now lives in the table,
|
|
44
51
|
# not in a Ruby `alias`. Not bootstrap-reachable (checked
|
|
45
52
|
# directly), so no BOOTSTRAP_CALLS_FALLBACK entry needed.
|
|
46
|
-
def tells_impl(name, to: nil, &
|
|
47
|
-
@operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :inbound, &
|
|
53
|
+
def tells_impl(name, to: nil, &)
|
|
54
|
+
@operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :inbound, &)
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
# WHAT WE ASK OF THE OUTSIDE — the direction this language did not
|
|
@@ -57,8 +64,8 @@ module Hecks
|
|
|
57
64
|
#
|
|
58
65
|
# RENAMED FROM `asks` — item #13's full metaprogrammed dispatch
|
|
59
66
|
# (slice 4c), same reasoning as tells_impl above.
|
|
60
|
-
def asks_impl(name, to: nil, &
|
|
61
|
-
@operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :outbound, &
|
|
67
|
+
def asks_impl(name, to: nil, &)
|
|
68
|
+
@operations << PortOperationBuilder.build(name, to: to, owner: @owner, direction: :outbound, &)
|
|
62
69
|
end
|
|
63
70
|
|
|
64
71
|
# THE DRIVEN HALF OF THE SAME WORD. `operation`/`emits` translates an
|
|
@@ -101,9 +108,15 @@ module Hecks
|
|
|
101
108
|
def answers(name) = @answers << name.to_sym
|
|
102
109
|
|
|
103
110
|
def build
|
|
104
|
-
|
|
111
|
+
if @verb && !@operations.empty?
|
|
112
|
+
raise Malformed,
|
|
113
|
+
"#{@name} declares both a verb and operations — a port is one or the other, not both"
|
|
114
|
+
end
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
if @verb || (@legacy_bare_port && @operations.empty?)
|
|
117
|
+
return MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal,
|
|
118
|
+
answers: @answers))
|
|
119
|
+
end
|
|
107
120
|
|
|
108
121
|
raise Malformed, "#{@name} declares no verb and no operations" if @operations.empty?
|
|
109
122
|
|
|
@@ -2,8 +2,16 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses an `entity "Name" do ... end` block nested inside an aggregate
|
|
6
|
+
# (or another entity, ADR 0026 — "a piece nested inside a piece") into
|
|
7
|
+
# an `Entity` — attributes, relationships (`has_many`/`has_one`/
|
|
8
|
+
# `belongs_to`), its own nested commands/queries/lifecycle, and
|
|
9
|
+
# preconditions shared across them. Threads the owning aggregate's
|
|
10
|
+
# value-object and cross-entity `given` pools through unchanged, so a
|
|
11
|
+
# nested piece resolves identity and shared rules against the same
|
|
12
|
+
# aggregate-wide state a top-level piece does.
|
|
5
13
|
class EntityBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Entity"
|
|
14
|
+
GRAMMAR_CONTEXT = "Entity".freeze
|
|
7
15
|
|
|
8
16
|
include AttributeCollector
|
|
9
17
|
include IdentityDeclaration
|
|
@@ -80,7 +88,8 @@ module Hecks
|
|
|
80
88
|
# rubocop:disable Naming/PredicatePrefix
|
|
81
89
|
def has_many_impl(type, as: nil, **options)
|
|
82
90
|
unless options.empty?
|
|
83
|
-
raise Malformed,
|
|
91
|
+
raise Malformed,
|
|
92
|
+
"#{@name}.has_many takes no #{options.keys.first}: — an empty list already means none"
|
|
84
93
|
end
|
|
85
94
|
|
|
86
95
|
plural = Naming.demodulise(type)
|
|
@@ -139,8 +148,8 @@ module Hecks
|
|
|
139
148
|
@pending_entities << [name, block]
|
|
140
149
|
end
|
|
141
150
|
|
|
142
|
-
def lifecycle_impl(field, default:, &
|
|
143
|
-
@lifecycle = LifecycleBuilder.build(field, default: default, &
|
|
151
|
+
def lifecycle_impl(field, default:, &)
|
|
152
|
+
@lifecycle = LifecycleBuilder.build(field, default: default, &)
|
|
144
153
|
end
|
|
145
154
|
|
|
146
155
|
# A PRECONDITION SHARED ACROSS THIS PIECE'S OWN COMMANDS, DECLARED
|
|
@@ -400,10 +409,22 @@ module Hecks
|
|
|
400
409
|
# See `AggregateBuilder#seal_lifecycle_guards`'s own comment —
|
|
401
410
|
# the identical check, one level down.
|
|
402
411
|
def seal_lifecycle_guards
|
|
403
|
-
return if @lifecycle
|
|
404
|
-
|
|
405
412
|
@commands.each do |command|
|
|
413
|
+
if @lifecycle && !MetaValidator.shadow_parsing?
|
|
414
|
+
# C5.3 — the same refusal `AggregateBuilder::Sealing` gives
|
|
415
|
+
# an aggregate: a `sets` on the lifecycle field (frozen era
|
|
416
|
+
# text excepted, as there).
|
|
417
|
+
command.mutations.each do |mutation|
|
|
418
|
+
next if [:delegate, :corrects].include?(mutation.op)
|
|
419
|
+
next unless mutation.target.to_sym == @lifecycle.field.to_sym
|
|
420
|
+
|
|
421
|
+
raise Malformed,
|
|
422
|
+
"#{@name}.#{command.hecks_name} sets #{mutation.target}, #{@name}'s lifecycle field — " \
|
|
423
|
+
"a lifecycle field moves only by transition; declare one instead of setting it"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
406
426
|
next unless command.from
|
|
427
|
+
next if @lifecycle
|
|
407
428
|
|
|
408
429
|
raise Malformed,
|
|
409
430
|
"#{@name}.#{command.hecks_name} guards from: #{Array(command.from).inspect}, but " \
|
|
@@ -269,7 +269,8 @@ module Hecks
|
|
|
269
269
|
# would silently execute as a no-op instead of raising the
|
|
270
270
|
# same "not yet implemented" refusal a genuinely unmigrated
|
|
271
271
|
# word already gets.
|
|
272
|
-
raise Runtime::WiringError,
|
|
272
|
+
raise Runtime::WiringError,
|
|
273
|
+
"no dispatcher handles shape #{shape[:kind].inspect} — add one before shape_for can produce it"
|
|
273
274
|
end
|
|
274
275
|
end
|
|
275
276
|
|
|
@@ -277,6 +278,13 @@ module Hecks
|
|
|
277
278
|
# which of the four safe shapes (context, word) is, or `nil` if
|
|
278
279
|
# it falls outside this slice's own verified scope. No argument
|
|
279
280
|
# values are read here; this only ever looks at the table.
|
|
281
|
+
#
|
|
282
|
+
# An early-return classification chain: each `return` depends on
|
|
283
|
+
# locals (`calls`, `fills`, `arguments`, `arg`) computed by the
|
|
284
|
+
# checks before it. Splitting per shape would mean re-deriving or
|
|
285
|
+
# threading those locals across method boundaries for a
|
|
286
|
+
# classification that is only ever read top-to-bottom once, here.
|
|
287
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
280
288
|
def shape_for(context, word, rows)
|
|
281
289
|
keyword = rows[:keywords].find { |k| k[:context] == context && k[:word] == word && k[:status] != "retired" }
|
|
282
290
|
return nil unless keyword
|
|
@@ -333,7 +341,9 @@ module Hecks
|
|
|
333
341
|
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" unless args.empty?
|
|
334
342
|
|
|
335
343
|
rows = MetaValidator::SyntaxBoot.call[:keywords]
|
|
336
|
-
siblings = rows.count
|
|
344
|
+
siblings = rows.count do |k|
|
|
345
|
+
k[:context] == keyword[:context] && k[:fills] == keyword[:fills] && k[:status] != "retired"
|
|
346
|
+
end
|
|
337
347
|
value = siblings > 1 ? keyword[:word].to_sym : true
|
|
338
348
|
|
|
339
349
|
builder.instance_variable_set(:"@#{keyword[:fills]}", value)
|
|
@@ -2,8 +2,15 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `.hecksagon` file's top-level DSL block into a `Hecksagon`
|
|
6
|
+
# — a domain's own WIRING: which adapter binds to which verb
|
|
7
|
+
# (`persisted_by`, `projected_by`, ...), which framework/vendored
|
|
8
|
+
# bluebooks it attaches, which external events it subscribes to, and
|
|
9
|
+
# its bare chapter-root port. Kept separate from the bluebook itself
|
|
10
|
+
# (the domain's own declared MODEL) because wiring is an operational
|
|
11
|
+
# decision, not a fact the domain states about itself.
|
|
5
12
|
class HecksagonBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Hecksagon"
|
|
13
|
+
GRAMMAR_CONTEXT = "Hecksagon".freeze
|
|
7
14
|
|
|
8
15
|
include WordGate
|
|
9
16
|
|
|
@@ -39,14 +39,24 @@ module Hecks
|
|
|
39
39
|
# and Entity Keyword rows name it in `calls:`. Bootstrap-
|
|
40
40
|
# reachable (every self-hosted aggregate/entity declares an
|
|
41
41
|
# identity), so in BOOTSTRAP_CALLS_FALLBACK for both contexts.
|
|
42
|
+
# Dispatches across the three live forms documented above (value-
|
|
43
|
+
# object + block, single type target, single/compound field
|
|
44
|
+
# target), each an early return that sets exactly one pending
|
|
45
|
+
# ivar. Splitting per form would need each branch's own `return`-
|
|
46
|
+
# with-nil semantics and the shared `@name`/`identity_pool`
|
|
47
|
+
# threaded back out as parameters, for no gain beyond what the
|
|
48
|
+
# three-forms comment above already documents.
|
|
49
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
42
50
|
def identified_by_impl(*targets, as: nil, &definition)
|
|
43
51
|
return legacy_identified_by(*targets, as: as, &definition) if MetaValidator.shadow_parsing?
|
|
44
52
|
|
|
45
53
|
refuse_second_identity!
|
|
46
54
|
|
|
47
55
|
if definition
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
unless targets.empty?
|
|
57
|
+
raise Malformed,
|
|
58
|
+
"#{@name}.identified_by cannot combine a value-object type with a block"
|
|
59
|
+
end
|
|
50
60
|
|
|
51
61
|
type_name = identity_value_object_name
|
|
52
62
|
value_object =
|
|
@@ -56,7 +66,7 @@ module Hecks
|
|
|
56
66
|
owner_value_objects: identity_pool,
|
|
57
67
|
&definition
|
|
58
68
|
)
|
|
59
|
-
rescue NameError =>
|
|
69
|
+
rescue NameError => e
|
|
60
70
|
# A REMOVED SPELLING MUST REFUSE LOUDLY, not degrade into a raw
|
|
61
71
|
# Ruby error — the one contract `EraGuard.shadow_parse` leans
|
|
62
72
|
# on to know a normal parse genuinely could not read this text
|
|
@@ -73,11 +83,9 @@ module Hecks
|
|
|
73
83
|
# precisely this text.
|
|
74
84
|
raise Malformed,
|
|
75
85
|
"#{@name}.identified_by do ... end could not be read as a value-object " \
|
|
76
|
-
"definition: #{
|
|
86
|
+
"definition: #{e.message}"
|
|
77
87
|
end
|
|
78
|
-
if value_object.attributes.empty?
|
|
79
|
-
raise Malformed, "#{@name}.identified_by do declares no identity attributes"
|
|
80
|
-
end
|
|
88
|
+
raise Malformed, "#{@name}.identified_by do declares no identity attributes" if value_object.attributes.empty?
|
|
81
89
|
|
|
82
90
|
install_identity_value_object!(value_object)
|
|
83
91
|
@identity_type_pending = [value_object, as || :identity, attributes.size]
|
|
@@ -93,8 +101,10 @@ module Hecks
|
|
|
93
101
|
|
|
94
102
|
if targets.one?
|
|
95
103
|
field = targets.first
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
if as
|
|
105
|
+
raise Malformed,
|
|
106
|
+
"#{@name}.identified_by takes no as: — name the declared field itself"
|
|
107
|
+
end
|
|
98
108
|
# Transitional compatibility: the self-hosted language and live
|
|
99
109
|
# corpus still contain this form. Keep it readable until their
|
|
100
110
|
# exemplar-led migration is complete; the final lifecycle cutover
|
|
@@ -170,8 +180,10 @@ module Hecks
|
|
|
170
180
|
if identity_type?(target)
|
|
171
181
|
@identity_type_pending = [target, as, attributes.size]
|
|
172
182
|
else
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
if as
|
|
184
|
+
raise Malformed,
|
|
185
|
+
"#{@name}.identified_by :#{target} takes no as: — as: only applies to identified_by ValueObject"
|
|
186
|
+
end
|
|
175
187
|
|
|
176
188
|
@identity_field_pending = target
|
|
177
189
|
end
|
|
@@ -180,7 +192,7 @@ module Hecks
|
|
|
180
192
|
|
|
181
193
|
raise Malformed, "#{@name}.identified_by names no field" unless path
|
|
182
194
|
|
|
183
|
-
paths = Ports::Extraction.canonical(path).to_s.split
|
|
195
|
+
paths = Ports::Extraction.canonical(path).to_s.split.reject(&:empty?)
|
|
184
196
|
raise Malformed, "#{@name}.identified_by names no field" if paths.empty?
|
|
185
197
|
|
|
186
198
|
@identity_paths = paths
|
|
@@ -2,8 +2,13 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `lifecycle :field, default: ... do transition ... end`
|
|
6
|
+
# block into a `Lifecycle` — the field an aggregate or entity's own
|
|
7
|
+
# state machine lives on, its starting value, and the `command =>
|
|
8
|
+
# target_state` transition table (with an optional `from:` guard)
|
|
9
|
+
# each `transition_impl` call adds a row to.
|
|
5
10
|
class LifecycleBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Lifecycle"
|
|
11
|
+
GRAMMAR_CONTEXT = "Lifecycle".freeze
|
|
7
12
|
|
|
8
13
|
include WordGate
|
|
9
14
|
|
|
@@ -30,6 +35,7 @@ module Hecks
|
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
def build
|
|
38
|
+
refuse_ambiguity!
|
|
33
39
|
Lifecycle.new(field: @field, default: @default, transitions: @transitions)
|
|
34
40
|
end
|
|
35
41
|
|
|
@@ -38,6 +44,39 @@ module Hecks
|
|
|
38
44
|
builder.instance_eval(&block) if block
|
|
39
45
|
builder.build
|
|
40
46
|
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# C5.3 (docs/semantics/bluebook-semantics.md) — two transitions for
|
|
51
|
+
# one command whose `from:` sets overlap (or where either has no
|
|
52
|
+
# `from:` at all) were silently first-wins; refused where the state
|
|
53
|
+
# machine can be read whole. Two transitions for one command from
|
|
54
|
+
# DISJOINT states are the legitimate shape (`match_transition`
|
|
55
|
+
# picks by the current state) and stay. A `from:` naming a state
|
|
56
|
+
# nothing declares is NOT refused here: it is a reachability
|
|
57
|
+
# finding `bin/model_check` already reports (unreachable state,
|
|
58
|
+
# dead transition), and a bluebook may declare it on purpose.
|
|
59
|
+
def refuse_ambiguity!
|
|
60
|
+
return if MetaValidator.shadow_parsing? # frozen era text is history
|
|
61
|
+
|
|
62
|
+
@transitions.each_with_index do |(command, transition), index|
|
|
63
|
+
earlier = @transitions[0...index].find do |other_command, other|
|
|
64
|
+
other_command == command && overlap?(other, transition)
|
|
65
|
+
end
|
|
66
|
+
next unless earlier
|
|
67
|
+
|
|
68
|
+
raise Malformed,
|
|
69
|
+
"lifecycle :#{@field} declares two transitions for #{command.inspect} from the same state " \
|
|
70
|
+
"(=> #{earlier.last.target.inspect} and => #{transition.target.inspect}) — which one fires " \
|
|
71
|
+
"would be declaration order; give them disjoint from: states"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def overlap?(one, other)
|
|
76
|
+
return true if one.from.nil? || other.from.nil?
|
|
77
|
+
|
|
78
|
+
Array(one.from).map(&:to_s).intersect?(Array(other.from).map(&:to_s))
|
|
79
|
+
end
|
|
41
80
|
end
|
|
42
81
|
end
|
|
43
82
|
end
|
|
@@ -2,8 +2,14 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a `policy "Name" do ... end` block into a `Policy` — a
|
|
6
|
+
# reaction wired at chapter scope: `on` the event it watches, an
|
|
7
|
+
# optional `where` guard evaluated against the event's own payload, an
|
|
8
|
+
# optional `for_each` fan-out query, and the `trigger` command it
|
|
9
|
+
# dispatches (`with:` projecting the event's payload onto the
|
|
10
|
+
# command's own arguments when the two shapes differ).
|
|
5
11
|
class PolicyBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Policy"
|
|
12
|
+
GRAMMAR_CONTEXT = "Policy".freeze
|
|
7
13
|
|
|
8
14
|
include WordGate
|
|
9
15
|
|
|
@@ -100,6 +106,9 @@ module Hecks
|
|
|
100
106
|
"could not be read, so no other runtime could ever evaluate it"
|
|
101
107
|
end
|
|
102
108
|
|
|
109
|
+
# C3.6 — the same PatternSubset check every rule site gets.
|
|
110
|
+
Expression::AstJson.refuse_unshared_patterns!(Expression::AstJson.emit_predicate(canonical),
|
|
111
|
+
owner: @name, word: "where")
|
|
103
112
|
@where = canonical
|
|
104
113
|
end
|
|
105
114
|
|
|
@@ -2,8 +2,13 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses a top-level `.port` file's `Hecks.port "Name" do verb "x";
|
|
6
|
+
# signal :effect end` body into a `Port` — the adapter-facing shape (one
|
|
7
|
+
# verb, one signal, an optional method contract in `answers`) a domain
|
|
8
|
+
# calls OUT through, as opposed to `DomainPortBuilder`'s own inbound/
|
|
9
|
+
# outbound operations.
|
|
5
10
|
class PortBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Port"
|
|
11
|
+
GRAMMAR_CONTEXT = "Port".freeze
|
|
7
12
|
|
|
8
13
|
include WordGate
|
|
9
14
|
|
|
@@ -2,8 +2,14 @@ require_relative "word_gate"
|
|
|
2
2
|
module Hecks
|
|
3
3
|
module Bluebook
|
|
4
4
|
module DSL
|
|
5
|
+
# Parses one `operation`/`tells`/`asks` block inside a `domain_port`
|
|
6
|
+
# into a `PortOperation` — its own attributes (the external fact's
|
|
7
|
+
# payload) plus either the events it `emits` (inbound) or the
|
|
8
|
+
# `answers`/`refuses` pair naming its two possible outcomes (outbound).
|
|
9
|
+
# `to:` records which aggregate the operation routes to as dispatch
|
|
10
|
+
# metadata, never as an attribute — see `to:`'s own comment below.
|
|
5
11
|
class PortOperationBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "PortOperation"
|
|
12
|
+
GRAMMAR_CONTEXT = "PortOperation".freeze
|
|
7
13
|
|
|
8
14
|
include AttributeCollector
|
|
9
15
|
include WordGate
|
|
@@ -93,9 +99,11 @@ module Hecks
|
|
|
93
99
|
# AN INBOUND OPERATION STILL HAS TO SAY SOMETHING. Only inbound: an
|
|
94
100
|
# `asks` says it with `answers`/`refuses` instead, and
|
|
95
101
|
# `refuse_wrong_words!` above has already insisted on both.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
if !outbound && @emits.empty?
|
|
103
|
+
raise Malformed,
|
|
104
|
+
"#{@name} declares no emits — an operation with nothing to say " \
|
|
105
|
+
"afterward is a call into nothing"
|
|
106
|
+
end
|
|
99
107
|
|
|
100
108
|
operation
|
|
101
109
|
end
|
|
@@ -119,18 +127,31 @@ module Hecks
|
|
|
119
127
|
# runtime cannot keep.
|
|
120
128
|
def refuse_wrong_words!(outbound)
|
|
121
129
|
if outbound
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
unless @emits.empty?
|
|
131
|
+
raise Malformed, "#{@name} is an asks and declares emits — name its two endings with " \
|
|
132
|
+
"answers and refuses instead"
|
|
133
|
+
end
|
|
134
|
+
unless @answers
|
|
135
|
+
raise Malformed, "#{@name} declares no answers — an ask with no word for what came " \
|
|
136
|
+
"back cannot be reacted to"
|
|
137
|
+
end
|
|
138
|
+
unless @refuses
|
|
139
|
+
raise Malformed, "#{@name} declares no refuses — an ask that cannot fail is a call " \
|
|
140
|
+
"into a system you do not control, pretending otherwise"
|
|
141
|
+
end
|
|
128
142
|
else
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
if @answers || @refuses
|
|
144
|
+
raise Malformed, "#{@name} is a tells and declares #{@answers ? 'answers' : 'refuses'} — " \
|
|
145
|
+
"an inbound fact has no channel back to whoever sent it"
|
|
146
|
+
end
|
|
131
147
|
end
|
|
132
148
|
end
|
|
133
149
|
|
|
150
|
+
# `private` above (scoping the instance methods between it and here)
|
|
151
|
+
# doesn't reach a singleton method — correctly so: `.build` is this
|
|
152
|
+
# builder's real public entry point (DomainPortBuilder calls it),
|
|
153
|
+
# never meant to be private.
|
|
154
|
+
# rubocop:disable-next Lint/IneffectiveAccessModifier
|
|
134
155
|
def self.build(name, to: nil, owner: nil, direction: :inbound, &block)
|
|
135
156
|
builder = new(name, to: to, owner: owner, direction: direction)
|
|
136
157
|
builder.instance_eval(&block) if block
|