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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
require_relative "../projector"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Projections
|
|
5
|
+
# A CHAPTER, PROJECTED AS ITS UBIQUITOUS LANGUAGE — one glossary
|
|
6
|
+
# entry per term the business actually uses: the Domain itself, and
|
|
7
|
+
# every Aggregate, Entity, Value Object, Command (a verb), Query (a
|
|
8
|
+
# question), Read Model, Event, Role, Lifecycle, Saga, and Policy it
|
|
9
|
+
# declares — every BUSINESS-domain construct the language's own §30
|
|
10
|
+
# reference distinguishes. Deliberately not the deployment/wiring
|
|
11
|
+
# half (World, Hecksagon, Port, Adapter, Translation): Evans'
|
|
12
|
+
# Ubiquitous Language is the business model, not the plumbing that
|
|
13
|
+
# runs it.
|
|
14
|
+
#
|
|
15
|
+
# GROUPED UNDER THE AGGREGATE EACH TERM BELONGS TO — a reader thinks
|
|
16
|
+
# "what does Account mean" before they think "what starts with A",
|
|
17
|
+
# so a term's home is the aggregate that declares it (an Entity's
|
|
18
|
+
# own Commands/Queries/Lifecycle nest under ITS aggregate, not
|
|
19
|
+
# listed as their own peer); an Event or a Policy that reacts to one
|
|
20
|
+
# nests under the aggregate whose command actually raises it — the
|
|
21
|
+
# same "who does this" a reader would ask by hand. Only what
|
|
22
|
+
# genuinely belongs to no single aggregate (a Role, issued across
|
|
23
|
+
# several; a Read Model, joined across several) gets its own
|
|
24
|
+
# trailing section. Within a section, still one short sentence per
|
|
25
|
+
# term, dictionary-plain — this is not `DocsProjector` re-sorted
|
|
26
|
+
# (verbs, refusals, wire shapes, walked for a caller); it is what
|
|
27
|
+
# each term MEANS, walked for a domain expert.
|
|
28
|
+
#
|
|
29
|
+
# WHAT IT DELIBERATELY DOES NOT DO: invent, same discipline as
|
|
30
|
+
# `DocsProjector`. A term with no declared prose (most value objects;
|
|
31
|
+
# every event and role, which the language only ever spells as bare
|
|
32
|
+
# strings — a command's `emits:`/`role:`) gets a definition DERIVED
|
|
33
|
+
# from the graph around it — what it's shaped like, what emits it,
|
|
34
|
+
# who issues it — never a restated name standing in for a sentence
|
|
35
|
+
# nobody wrote.
|
|
36
|
+
#
|
|
37
|
+
# Projector.call(:glossary, bluebook: <the Bluebook chapter>)
|
|
38
|
+
module Glossary
|
|
39
|
+
extend Projector::Target
|
|
40
|
+
|
|
41
|
+
projects_as :glossary
|
|
42
|
+
|
|
43
|
+
Entry = Struct.new(:term, :kind, :definition, :within, :section, keyword_init: true)
|
|
44
|
+
|
|
45
|
+
# DISPLAY ORDER WITHIN A SECTION — the aggregate's own row first
|
|
46
|
+
# (what it IS), then the shapes it can be in and hold, then what
|
|
47
|
+
# you can ask of it, then what happens on its own.
|
|
48
|
+
KIND_RANK = {
|
|
49
|
+
"Aggregate" => 0, "Lifecycle" => 1, "Entity" => 2, "Value Object" => 3,
|
|
50
|
+
"Command" => 4, "Query" => 5, "Event" => 6, "Policy" => 7, "Saga" => 8
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
ROLES_SECTION = "Roles".freeze
|
|
54
|
+
READ_MODELS_SECTION = "Read Models".freeze
|
|
55
|
+
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
def call(bluebook:, options: {}) = render(bluebook)
|
|
59
|
+
|
|
60
|
+
# ── gathering ─────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
# AN ENTITY CAN CARRY ITS OWN COMMANDS, QUERIES, AND VALUE OBJECTS
|
|
63
|
+
# TOO — walked the same one level down `DocsProjector` and
|
|
64
|
+
# `Projections::Diagrams` already walk it, so a domain's entity
|
|
65
|
+
# gaining any of these needs no change here either.
|
|
66
|
+
def holders(bluebook)
|
|
67
|
+
bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# EVERY HOLDER'S OWN AGGREGATE, ONE HOP OR ZERO — an aggregate
|
|
71
|
+
# maps to itself, an entity to whichever aggregate declared it.
|
|
72
|
+
# This is the single fact the whole grouping is built on: a
|
|
73
|
+
# Command/Query/Lifecycle's `within` already names its holder, so
|
|
74
|
+
# looking that name up here is enough to find its SECTION even
|
|
75
|
+
# when the holder is an entity two levels down from the chapter.
|
|
76
|
+
def holder_aggregate(bluebook)
|
|
77
|
+
bluebook.aggregates.each_with_object({}) do |aggregate, map|
|
|
78
|
+
map[aggregate.hecks_name] = aggregate.hecks_name
|
|
79
|
+
aggregate.entities.each { |entity| map[entity.hecks_name] = aggregate.hecks_name }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# EVERY EVENT'S OWN AGGREGATE — an event is never declared, only
|
|
84
|
+
# emitted, so its home is whichever aggregate the FIRST command
|
|
85
|
+
# that raises it belongs to. A policy or saga reacting to that
|
|
86
|
+
# event inherits the same home, the same way a reader tracing the
|
|
87
|
+
# reaction by hand would: "what raises this, and whose is that."
|
|
88
|
+
def event_aggregate(bluebook, holder_aggregate)
|
|
89
|
+
map = {}
|
|
90
|
+
holders(bluebook).each do |holder|
|
|
91
|
+
home = holder_aggregate[holder.hecks_name]
|
|
92
|
+
holder.commands.each { |command| command.emits.each { |event| map[event] ||= home } }
|
|
93
|
+
end
|
|
94
|
+
map
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def entries(bluebook)
|
|
98
|
+
holder_agg = holder_aggregate(bluebook)
|
|
99
|
+
event_agg = event_aggregate(bluebook, holder_agg)
|
|
100
|
+
|
|
101
|
+
entries = []
|
|
102
|
+
entries += domain_entries(bluebook)
|
|
103
|
+
entries += aggregate_entries(bluebook)
|
|
104
|
+
entries += entity_entries(bluebook, holder_agg)
|
|
105
|
+
entries += value_object_entries(bluebook, holder_agg)
|
|
106
|
+
entries += command_entries(bluebook, holder_agg)
|
|
107
|
+
entries += query_entries(bluebook, holder_agg)
|
|
108
|
+
entries += read_model_entries(bluebook)
|
|
109
|
+
entries += event_entries(bluebook, event_agg)
|
|
110
|
+
entries += role_entries(bluebook)
|
|
111
|
+
entries += lifecycle_entries(bluebook, holder_agg)
|
|
112
|
+
entries += saga_entries(bluebook, event_agg)
|
|
113
|
+
entries += policy_entries(bluebook, event_agg)
|
|
114
|
+
entries
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# THE CHAPTER ITSELF, AS A TERM — the one construct every other
|
|
118
|
+
# entry here lives inside (`Hecks.bluebook "Banking" do ... end`),
|
|
119
|
+
# and the one place `vision` — the single sentence written for a
|
|
120
|
+
# reader who doesn't know the domain yet — actually lives. Given
|
|
121
|
+
# no `section` (rendered once, at the very top, above every
|
|
122
|
+
# aggregate) since it is the one term that belongs to no aggregate
|
|
123
|
+
# by being the thing that holds them all.
|
|
124
|
+
def domain_entries(bluebook)
|
|
125
|
+
[Entry.new(term: bluebook.name, kind: "Domain", definition: bluebook.vision)]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def aggregate_entries(bluebook)
|
|
129
|
+
bluebook.aggregates.map do |aggregate|
|
|
130
|
+
Entry.new(term: aggregate.hecks_name, kind: "Aggregate", definition: aggregate.description,
|
|
131
|
+
section: aggregate.hecks_name)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def entity_entries(bluebook, holder_agg)
|
|
136
|
+
bluebook.aggregates.flat_map do |aggregate|
|
|
137
|
+
aggregate.entities.map do |entity|
|
|
138
|
+
Entry.new(term: entity.hecks_name, kind: "Entity", within: aggregate.hecks_name,
|
|
139
|
+
section: holder_agg[aggregate.hecks_name], definition: entity.description)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# A VALUE OBJECT CARRIES NO `description` — the language never gave
|
|
145
|
+
# it one (`Bluebook::ValueObject` declares `attributes`,
|
|
146
|
+
# `invariants`, `members`, `closed_set`, nothing else). Its
|
|
147
|
+
# definition is derived from its own shape instead: a closed set
|
|
148
|
+
# states its members, an open one its fields — the same
|
|
149
|
+
# distinction `DocsProjector#shape_of` draws.
|
|
150
|
+
#
|
|
151
|
+
# AGGREGATES ONLY, not `holders` — unlike commands and queries, an
|
|
152
|
+
# entity declares no value objects of its own (`Bluebook::Entity`
|
|
153
|
+
# deliberately does not answer `value_objects`; its argument types
|
|
154
|
+
# live on the aggregate above it, same as `DocsProjector#value_object_for`
|
|
155
|
+
# already has to account for).
|
|
156
|
+
def value_object_entries(bluebook, holder_agg)
|
|
157
|
+
bluebook.aggregates.flat_map do |aggregate|
|
|
158
|
+
aggregate.value_objects.map do |vo|
|
|
159
|
+
Entry.new(term: vo.hecks_name, kind: "Value Object", within: aggregate.hecks_name,
|
|
160
|
+
section: holder_agg[aggregate.hecks_name], definition: value_object_definition(vo))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# THE SHAPE, THEN THE RULES — a value object's own `invariant`
|
|
166
|
+
# bodies are real authored prose ("a currency is a three-letter
|
|
167
|
+
# code"), not derived the way the shape above is, and were
|
|
168
|
+
# sitting unread by this projector until now: `to_h`'s own
|
|
169
|
+
# `invariants` field already carries them, one `description` per
|
|
170
|
+
# `Bluebook::Invariant`.
|
|
171
|
+
def value_object_definition(value_object)
|
|
172
|
+
sentence = value_object_shape(value_object)
|
|
173
|
+
rules = value_object.invariants.map(&:description)
|
|
174
|
+
sentence += " Must satisfy: #{rules.join('; ')}." unless rules.empty?
|
|
175
|
+
sentence
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# A CLOSED SET'S ROWS, not just their first field — the same
|
|
179
|
+
# correction `Projections::Vocabulary` already had to make
|
|
180
|
+
# (`StatementFrequency` names retention months and a paper fee
|
|
181
|
+
# alongside its cadence; flattening every field into one list
|
|
182
|
+
# produces well-formed nonsense, same as it did there).
|
|
183
|
+
def value_object_shape(value_object)
|
|
184
|
+
if value_object.closed_set?
|
|
185
|
+
if value_object.members.first && value_object.members.first.size > 1
|
|
186
|
+
rows = value_object.members.map { |row| "{ #{row.map { |f, v| "#{f}: #{v.inspect}" }.join(', ')} }" }
|
|
187
|
+
"One of: #{rows.join('; ')}."
|
|
188
|
+
else
|
|
189
|
+
"One of #{value_object.members.flat_map(&:values).uniq.map { |m| "`#{m}`" }.join(', ')}."
|
|
190
|
+
end
|
|
191
|
+
elsif value_object.attributes.empty?
|
|
192
|
+
"A marker with no fields of its own."
|
|
193
|
+
else
|
|
194
|
+
"{ #{value_object.attributes.map { |f| "#{f.name}: #{f.type}" }.join(', ')} }"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def command_entries(bluebook, holder_agg)
|
|
199
|
+
holders(bluebook).flat_map do |holder|
|
|
200
|
+
holder.commands.map do |command|
|
|
201
|
+
Entry.new(term: command.hecks_name, kind: "Command", within: holder.hecks_name,
|
|
202
|
+
section: holder_agg[holder.hecks_name], definition: command.goal)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def query_entries(bluebook, holder_agg)
|
|
208
|
+
holders(bluebook).flat_map do |holder|
|
|
209
|
+
holder.queries.map do |query|
|
|
210
|
+
Entry.new(term: query.hecks_name, kind: "Query", within: holder.hecks_name,
|
|
211
|
+
section: holder_agg[holder.hecks_name], definition: query.description)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# CROSS-AGGREGATE, ON PURPOSE — a read model joins heads from more
|
|
217
|
+
# than one aggregate (`ReadModel`'s own header: "an ask that
|
|
218
|
+
# gathers heads from more than one aggregate"), so it belongs to
|
|
219
|
+
# no single one and gets its own trailing section instead.
|
|
220
|
+
def read_model_entries(bluebook)
|
|
221
|
+
bluebook.read_models.map do |read_model|
|
|
222
|
+
Entry.new(term: read_model.name, kind: "Read Model", section: READ_MODELS_SECTION,
|
|
223
|
+
definition: read_model.description)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# AN EVENT IS NEVER ITS OWN DECLARATION — the language only ever
|
|
228
|
+
# spells one as a bare string in a command's `emits:`. So it earns
|
|
229
|
+
# a glossary entry by appearing there, and its "definition" is the
|
|
230
|
+
# one fact the graph actually holds about it: which verb(s) raise
|
|
231
|
+
# it, and — the other half nothing raises without a reader
|
|
232
|
+
# crossing over to check `bluebook.policies` by hand — which
|
|
233
|
+
# policy (if any) reacts to it. That is derived, not invented —
|
|
234
|
+
# nobody wrote a sentence this restates.
|
|
235
|
+
def event_entries(bluebook, event_agg)
|
|
236
|
+
by_event = Hash.new { |h, k| h[k] = [] }
|
|
237
|
+
holders(bluebook).each do |holder|
|
|
238
|
+
holder.commands.each do |command|
|
|
239
|
+
command.emits.each { |event| by_event[event] << command.hecks_name }
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
by_event.map do |event, commands|
|
|
243
|
+
sentence = "Raised by #{commands.uniq.map { |c| "`#{c}`" }.join(', ')}."
|
|
244
|
+
reactions = policies_on(bluebook, event)
|
|
245
|
+
sentence += " #{reactions.join(' ')}" unless reactions.empty?
|
|
246
|
+
Entry.new(term: event, kind: "Event", section: event_agg[event], definition: sentence)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# A POLICY NAMES ITS EVENT QUALIFIED (`"Account.AccountFrozen"`) —
|
|
251
|
+
# `bare` strips the aggregate a policy's own `on_event`/
|
|
252
|
+
# `trigger_command` always carries but an event's own glossary
|
|
253
|
+
# term (keyed off `emits:`, which is never qualified) never does.
|
|
254
|
+
def bare(qualified) = qualified.to_s.split(".").last
|
|
255
|
+
|
|
256
|
+
def policies_on(bluebook, event)
|
|
257
|
+
bluebook.policies.select { |policy| bare(policy.on_event) == event }.map do |policy|
|
|
258
|
+
domain = " in #{policy.target_domain}" if policy.target_domain
|
|
259
|
+
"Triggers policy `#{policy.name}` → `#{policy.trigger_command}`#{domain}."
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# A ROLE IS THE SAME SHAPE OF FACT AS AN EVENT — free text on a
|
|
264
|
+
# command's `role:`, never declared on its own — so it gets the
|
|
265
|
+
# same treatment: who it is, told by what it does. CROSS-CUTTING
|
|
266
|
+
# BY NATURE (`System`/`Customer` issue commands across half the
|
|
267
|
+
# aggregates in this corpus), so — like a Read Model — it belongs
|
|
268
|
+
# to no single aggregate.
|
|
269
|
+
def role_entries(bluebook)
|
|
270
|
+
by_role = Hash.new { |h, k| h[k] = [] }
|
|
271
|
+
holders(bluebook).each do |holder|
|
|
272
|
+
holder.commands.select(&:role).each { |command| by_role[command.role] << command.hecks_name }
|
|
273
|
+
end
|
|
274
|
+
by_role.map do |role, commands|
|
|
275
|
+
Entry.new(term: role, kind: "Role", section: ROLES_SECTION,
|
|
276
|
+
definition: "Issues #{commands.uniq.map { |c| "`#{c}`" }.join(', ')}.")
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# A LIFECYCLE HAS NO NAME OF ITS OWN — it is a body nested inside
|
|
281
|
+
# the aggregate or entity it governs (`lifecycle :status do ...
|
|
282
|
+
# end`), so it earns a glossary entry under ITS HOLDER's term,
|
|
283
|
+
# the same way `Command`/`Query` do — a reader who looks up
|
|
284
|
+
# `Account` and a reader who looks up what states an Account can
|
|
285
|
+
# hold are asking two different questions of the same name.
|
|
286
|
+
def lifecycle_entries(bluebook, holder_agg)
|
|
287
|
+
holders(bluebook).select(&:lifecycle).map do |holder|
|
|
288
|
+
lifecycle = holder.lifecycle
|
|
289
|
+
states = ([lifecycle.default] + lifecycle.transitions.map { |_name, transition| transition.target }).uniq
|
|
290
|
+
Entry.new(term: holder.hecks_name, kind: "Lifecycle", section: holder_agg[holder.hecks_name],
|
|
291
|
+
definition: "Starts at `#{lifecycle.default}`. States: #{states.map { |s| "`#{s}`" }.join(', ')}.")
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# A SAGA'S OWN HANDLERS/DISPATCHES ARE NAMELESS TOO — the same
|
|
296
|
+
# shape as a Lifecycle's transitions, one level up: a sequence of
|
|
297
|
+
# states no individual step is worth its own glossary row for.
|
|
298
|
+
# `states`, from the builder's own precomputed leg order
|
|
299
|
+
# (`Behaviour::ProcessManager#saga`, the same field
|
|
300
|
+
# `DocsProjector#closing` already reads), says what a Lifecycle
|
|
301
|
+
# entry says for an aggregate — the shape of the whole journey,
|
|
302
|
+
# not just where it starts and ends. HOMED ON ITS STARTING EVENT —
|
|
303
|
+
# a saga usually ends somewhere else entirely (that is most of
|
|
304
|
+
# what makes it a saga), so "where it begins" is the only single
|
|
305
|
+
# aggregate honestly its own.
|
|
306
|
+
def saga_entries(bluebook, event_agg)
|
|
307
|
+
bluebook.process_managers.map do |saga|
|
|
308
|
+
shape = saga.to_h
|
|
309
|
+
sentence = "Starts on `#{shape[:starts_on]}`, ends on `#{shape[:ends_on]}`."
|
|
310
|
+
states = Array(shape[:states])
|
|
311
|
+
sentence += " States: #{states.map { |s| "`#{s}`" }.join(' → ')}." unless states.empty?
|
|
312
|
+
Entry.new(term: shape[:name], kind: "Saga", section: event_agg[bare(shape[:starts_on])],
|
|
313
|
+
definition: sentence)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A POLICY IS A NAMED CONSTRUCT TOO (`policy "ReviewOnFreeze" do
|
|
318
|
+
# ... end`), the same as a Saga — so it earns its own glossary
|
|
319
|
+
# entry rather than surfacing only as a parenthetical on the
|
|
320
|
+
# event that triggers it. HOMED ON THE EVENT IT REACTS TO, not
|
|
321
|
+
# what it dispatches — `on_event` is always local to this
|
|
322
|
+
# bluebook, `trigger_command` sometimes is not (a cross-domain
|
|
323
|
+
# policy's own `across`). `for_each`, when declared, fans the
|
|
324
|
+
# dispatch out over more than one record; stated here because a
|
|
325
|
+
# reader who only sees "dispatches X" would otherwise assume one.
|
|
326
|
+
def policy_entries(bluebook, event_agg)
|
|
327
|
+
bluebook.policies.map do |policy|
|
|
328
|
+
sentence = "On `#{bare(policy.on_event)}`, dispatches `#{policy.trigger_command}`"
|
|
329
|
+
sentence += " in #{policy.target_domain}" if policy.target_domain
|
|
330
|
+
sentence += ", once per `#{policy.for_each}` row" if policy.for_each
|
|
331
|
+
Entry.new(term: policy.name, kind: "Policy", section: event_agg[bare(policy.on_event)],
|
|
332
|
+
definition: "#{sentence}.")
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# ── rendering ─────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
def render(bluebook)
|
|
339
|
+
all = entries(bluebook)
|
|
340
|
+
grouped = all.reject { |entry| entry.kind == "Domain" }.group_by { |entry| entry.section || "Cross-Domain Reactions" }
|
|
341
|
+
|
|
342
|
+
out = [chapter_header(bluebook)]
|
|
343
|
+
bluebook.aggregates.map(&:hecks_name).sort.each { |name| out << section(name, grouped[name]) if grouped[name] }
|
|
344
|
+
[ROLES_SECTION, READ_MODELS_SECTION, "Cross-Domain Reactions"].each do |name|
|
|
345
|
+
out << section(name, grouped[name]) if grouped[name]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
"#{out.compact.join("\n\n")}\n"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def chapter_header(bluebook)
|
|
352
|
+
out = ["# #{bluebook.name} — Glossary", ""]
|
|
353
|
+
out += ["> #{bluebook.vision}", ""] if bluebook.vision
|
|
354
|
+
out << "The ubiquitous language: every term #{bluebook.name} declares, grouped under the aggregate " \
|
|
355
|
+
"it belongs to, in the domain's own words. Generated from `#{bluebook.name}`'s bluebook — a " \
|
|
356
|
+
"term missing here is a term the bluebook does not yet declare, and a definition missing here " \
|
|
357
|
+
"is a sentence nobody has written yet."
|
|
358
|
+
out.join("\n")
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def section(title, rows)
|
|
362
|
+
return nil unless rows
|
|
363
|
+
|
|
364
|
+
sorted = rows.sort_by { |entry| [KIND_RANK[entry.kind] || 99, entry.term.downcase] }
|
|
365
|
+
"## #{title}\n\n| Term | Kind | Definition |\n|---|---|---|\n#{sorted.map { |entry| entry_row(entry) }.join("\n")}"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def entry_row(entry)
|
|
369
|
+
term = entry.within ? "**#{entry.term}** *(#{entry.within})*" : "**#{entry.term}**"
|
|
370
|
+
"| #{term} | #{entry.kind} | #{entry.definition || '—'} |"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
data/lib/hecks/projections/ir.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require_relative "../projector"
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
|
+
# Reopened from projections.rb (see there for the namespace's full
|
|
5
|
+
# rationale) to define and register the :ir target below.
|
|
4
6
|
module Projections
|
|
5
7
|
# The canonical IR, as a constant. The implementation already existed
|
|
6
8
|
# and is already golden-tested (`Projector::IRProjector`, registered
|
|
@@ -60,7 +60,8 @@ module Hecks
|
|
|
60
60
|
"Bluebook" => { ir_version: "the EMISSION's own version, not the domain's",
|
|
61
61
|
canonical_form: "the normalisation table every reader needs beside the IR" },
|
|
62
62
|
"ValueObject" => { closed_set: "an empty one_of and no one_of are otherwise indistinguishable" },
|
|
63
|
-
"Aggregate" => { ports: "declared in the hecksagon, attached after the aggregate exists" }
|
|
63
|
+
"Aggregate" => { ports: "declared in the hecksagon, attached after the aggregate exists" },
|
|
64
|
+
"Policy" => { where_ast: "the structured form of `where`, derived from the same text at emission" }
|
|
64
65
|
}.freeze
|
|
65
66
|
|
|
66
67
|
# DECLARED, AND DELIBERATELY NOT EMITTED. The category that had no
|
|
@@ -50,7 +50,11 @@ module Hecks
|
|
|
50
50
|
# a binding that reads an event field and one that supplies a
|
|
51
51
|
# literal string are otherwise indistinguishable once written
|
|
52
52
|
# down (see `MetaValidator::Readings`' own note on exactly that).
|
|
53
|
-
renders: { with_spec: "-> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }"
|
|
53
|
+
renders: { with_spec: "-> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }",
|
|
54
|
+
# COMPUTED (Deviations::COMPUTED["Policy"]): the structured
|
|
55
|
+
# form of `where`, a pure function of that text, the same
|
|
56
|
+
# `ast` every rule row carries beside its `canonical`.
|
|
57
|
+
where_ast: "-> { where_ast }" },
|
|
54
58
|
settles: false
|
|
55
59
|
}
|
|
56
60
|
}.freeze
|
|
@@ -66,7 +70,7 @@ module Hecks
|
|
|
66
70
|
# GENERATED — projected from the language's own #{name} aggregate.
|
|
67
71
|
# DO NOT EDIT: the holding half is rendered, and #{host.fetch(:behaviour)}
|
|
68
72
|
# is where anything hand-written belongs.
|
|
69
|
-
require_relative "behaviour/#{File.basename(host.fetch(:file),
|
|
73
|
+
require_relative "behaviour/#{File.basename(host.fetch(:file), '.rb')}"
|
|
70
74
|
|
|
71
75
|
module Hecks
|
|
72
76
|
module Bluebook
|
|
@@ -87,14 +91,15 @@ module Hecks
|
|
|
87
91
|
|
|
88
92
|
# The emission, keyed as the model spells it and sourced as the
|
|
89
93
|
# language declares it.
|
|
94
|
+
# A COMPUTED field (Deviations::COMPUTED) is emitted too — it is
|
|
95
|
+
# model-only by definition, so it rides after the declared fields
|
|
96
|
+
# and MUST have a `renders` entry, there being nothing to `send`.
|
|
90
97
|
def emits(bluebook, name)
|
|
91
|
-
fields = emitted_fields(bluebook, name)
|
|
98
|
+
fields = emitted_fields(bluebook, name) + Deviations.computed(name)
|
|
92
99
|
renders = HOST.fetch(name).fetch(:renders, {})
|
|
93
100
|
width = fields.map { |f| f.to_s.length }.max.to_i
|
|
94
101
|
|
|
95
|
-
"emits_ir(\n" +
|
|
96
|
-
fields.map { |f| " #{"#{f}:".ljust(width + 1)} #{renders.fetch(f, ":#{f}")}" }.join(",\n") +
|
|
97
|
-
"\n)"
|
|
102
|
+
"emits_ir(\n#{fields.map { |f| " #{"#{f}:".ljust(width + 1)} #{renders.fetch(f, ":#{f}")}" }.join(",\n")}\n)"
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
# WHAT THE CONSTRUCT EMITS: what the language declares, less every
|
|
@@ -121,18 +126,18 @@ module Hecks
|
|
|
121
126
|
# here rather than typed in — a comment that survives
|
|
122
127
|
# regeneration is one the generator writes.
|
|
123
128
|
reasons = Deviations::OFF_THE_WIRE.fetch(host.fetch(:construct, ""), {})
|
|
124
|
-
(lines + accessors.map
|
|
129
|
+
(lines + accessors.map do |a|
|
|
125
130
|
why = reasons[a]
|
|
126
131
|
(why ? "\n# #{a.upcase}, DECLARED AND DELIBERATELY OFF THE WIRE\n# #{wrap(why)}\n" : "") +
|
|
127
132
|
"attr_accessor :#{a}"
|
|
128
|
-
|
|
133
|
+
end).join("\n")
|
|
129
134
|
end
|
|
130
135
|
|
|
131
136
|
def constructor(host)
|
|
132
137
|
args = host.fetch(:defaults).map { |f, d| d ? "#{f}: #{d}" : "#{f}:" }.join(", ")
|
|
133
|
-
body = host.fetch(:defaults).keys.map
|
|
138
|
+
body = host.fetch(:defaults).keys.map do |f|
|
|
134
139
|
" @#{f} = #{f}#{host.fetch(:coerce, {})[f]}"
|
|
135
|
-
|
|
140
|
+
end
|
|
136
141
|
body << "\n settle" if host.fetch(:settles, true)
|
|
137
142
|
|
|
138
143
|
"def initialize(#{args})\n#{body.join("\n")}\nend"
|
|
@@ -58,9 +58,7 @@ module Hecks
|
|
|
58
58
|
|
|
59
59
|
def render(bluebook)
|
|
60
60
|
rows = tables(bluebook).sort_by(&:first).map do |name, members|
|
|
61
|
-
" #{name.inspect} => [\n"
|
|
62
|
-
members.map { |row| " #{row.inspect}.freeze" }.join(",\n") +
|
|
63
|
-
"\n ].freeze"
|
|
61
|
+
" #{name.inspect} => [\n#{members.map { |row| " #{row.inspect}.freeze" }.join(",\n")}\n ].freeze"
|
|
64
62
|
end
|
|
65
63
|
|
|
66
64
|
<<~RUBY
|
data/lib/hecks/projections.rb
CHANGED
|
@@ -54,12 +54,12 @@ module Hecks
|
|
|
54
54
|
aggregate.queries.each { |q| claim(questions, name_for(aggregate, q), query_spec(bluebook, aggregate, nil, q)) }
|
|
55
55
|
|
|
56
56
|
aggregate.entities.each do |entity|
|
|
57
|
-
entity.commands.each
|
|
57
|
+
entity.commands.each do |c|
|
|
58
58
|
claim(verbs, name_for(aggregate, c, entity), command_spec(bluebook, aggregate, entity, c))
|
|
59
|
-
|
|
60
|
-
entity.queries.each
|
|
59
|
+
end
|
|
60
|
+
entity.queries.each do |q|
|
|
61
61
|
claim(questions, name_for(aggregate, q, entity), query_spec(bluebook, aggregate, entity, q))
|
|
62
|
-
|
|
62
|
+
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# A PORT IS A VERB TOO, and leaving it off the map was a real gap
|
|
@@ -156,6 +156,33 @@ module Hecks
|
|
|
156
156
|
entity ? "#{entity.hecks_name}." : "", verb.hecks_name].join
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
+
# THE ARGUMENTS A RECEIVER ADDS, BEFORE ANY VERB-SPECIFIC ONE. Shared by
|
|
160
|
+
# `command_spec` and `port_spec` — a port operation always addresses an
|
|
161
|
+
# aggregate record (`port_spec` passes `receiver: :aggregate`, never
|
|
162
|
+
# `:entity` or `nil`, because a port is declared on an aggregate, never
|
|
163
|
+
# an entity), and this is exactly the same "how do I name the record"
|
|
164
|
+
# question a command with a receiver already answers, so it is answered
|
|
165
|
+
# once, here, rather than duplicated.
|
|
166
|
+
#
|
|
167
|
+
# `nil` — a creating command — takes none: there is no existing record
|
|
168
|
+
# yet for `to=` to name.
|
|
169
|
+
def receiver_options(receiver, aggregate, entity)
|
|
170
|
+
case receiver
|
|
171
|
+
when :entity
|
|
172
|
+
[
|
|
173
|
+
{ path: "to.aggregate", type: "String", required: true,
|
|
174
|
+
note: "id of the #{aggregate.hecks_name} holding the #{entity.hecks_name}" },
|
|
175
|
+
{ path: "to.entity", type: "String", required: true,
|
|
176
|
+
note: "id of the #{entity.hecks_name} to act on" }
|
|
177
|
+
]
|
|
178
|
+
when :aggregate
|
|
179
|
+
[{ path: "to", type: "String", required: true,
|
|
180
|
+
note: "id of the #{aggregate.hecks_name} to act on" }]
|
|
181
|
+
else
|
|
182
|
+
[]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
159
186
|
# ── one verb ──────────────────────────────────────────────────────
|
|
160
187
|
|
|
161
188
|
def command_spec(bluebook, aggregate, entity, command)
|
|
@@ -166,7 +193,6 @@ module Hecks
|
|
|
166
193
|
else
|
|
167
194
|
(command.creates? ? nil : :aggregate)
|
|
168
195
|
end
|
|
169
|
-
legacy_arguments = []
|
|
170
196
|
|
|
171
197
|
# THE RECEIVER IS NOT A COMMAND ARGUMENT. An aggregate command names
|
|
172
198
|
# its record through to; an entity command needs both the aggregate
|
|
@@ -178,18 +204,8 @@ module Hecks
|
|
|
178
204
|
# Existing aggregate scripts may still spell the receiver id=... .
|
|
179
205
|
# That alias is deliberately hidden from help and recorded separately
|
|
180
206
|
# as legacy_arguments; new help and examples teach only to=... .
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
{ path: "to.aggregate", type: "String", required: true,
|
|
184
|
-
note: "id of the #{aggregate.hecks_name} holding the #{entity.hecks_name}" },
|
|
185
|
-
{ path: "to.entity", type: "String", required: true,
|
|
186
|
-
note: "id of the #{entity.hecks_name} to act on" }
|
|
187
|
-
] + arguments
|
|
188
|
-
elsif receiver == :aggregate
|
|
189
|
-
arguments = [{ path: "to", type: "String", required: true,
|
|
190
|
-
note: "id of the #{aggregate.hecks_name} to act on" }] + arguments
|
|
191
|
-
legacy_arguments = [{ path: "id", type: "String", required: true }]
|
|
192
|
-
end
|
|
207
|
+
arguments = receiver_options(receiver, aggregate, entity) + arguments
|
|
208
|
+
legacy_arguments = receiver == :aggregate ? [{ path: "id", type: "String", required: true }] : []
|
|
193
209
|
|
|
194
210
|
{ verb: fqn(bluebook, aggregate, command, entity), kind: :command,
|
|
195
211
|
summary: command.goal, role: command.role, role_gated: !command.role.to_s.empty?,
|
|
@@ -235,7 +251,11 @@ module Hecks
|
|
|
235
251
|
# `CommandInterpreter`/`EntityInterpreter` call it, never the port
|
|
236
252
|
# dispatch path), so `role_gated: false` always, unlike
|
|
237
253
|
# `command_spec` where the same key name means a real one.
|
|
238
|
-
role: operation.outbound?
|
|
254
|
+
role: if operation.outbound?
|
|
255
|
+
"#{aggregate.hecks_name} asking #{port.name}"
|
|
256
|
+
else
|
|
257
|
+
"#{port.name} telling #{aggregate.hecks_name}"
|
|
258
|
+
end,
|
|
239
259
|
role_gated: false,
|
|
240
260
|
summary: port_summary(port, operation), arguments: arguments }
|
|
241
261
|
end
|
|
@@ -289,8 +309,8 @@ module Hecks
|
|
|
289
309
|
# malformed records into a real store, which is what it did on its first
|
|
290
310
|
# run against the pizzas database.
|
|
291
311
|
def options_for(attribute, holder, aggregate, prefix = nil, optional = nil)
|
|
292
|
-
path
|
|
293
|
-
optional
|
|
312
|
+
path = [prefix, attribute.name].compact.join(".")
|
|
313
|
+
optional ||= attribute.optional?
|
|
294
314
|
return [reference_option(attribute)] if attribute.reference?
|
|
295
315
|
|
|
296
316
|
value_object = value_object_for(attribute, holder, aggregate)
|
|
@@ -408,38 +428,54 @@ module Hecks
|
|
|
408
428
|
text.to_s.split(/(?<=\.)\s/).first.to_s
|
|
409
429
|
end
|
|
410
430
|
|
|
431
|
+
# FOUR TEXT BLOCKS, IN FIXED DISPLAY ORDER — meta (name/kind/role),
|
|
432
|
+
# invocation, arguments, refusals. Each block is independent of the
|
|
433
|
+
# others' content (only the OUTPUT ORDER is fixed, and stays fixed
|
|
434
|
+
# below), so each is its own method returning the lines it
|
|
435
|
+
# contributes — `[]` when it contributes none — concatenated in the
|
|
436
|
+
# same order the original inline version built them in.
|
|
411
437
|
def verb_help(program, name, spec, ask: false)
|
|
438
|
+
out = verb_help_meta_lines(name, spec)
|
|
439
|
+
out.concat(verb_help_invocation_lines(program, name, spec, ask))
|
|
440
|
+
out.concat(verb_help_argument_lines(spec))
|
|
441
|
+
out.concat(verb_help_refusal_lines(spec))
|
|
442
|
+
out.join("\n")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def verb_help_meta_lines(name, spec)
|
|
412
446
|
out = ["#{name} — #{spec[:summary]}", ""]
|
|
413
447
|
out << "dispatches #{spec[:verb]}" if spec[:kind] == :command
|
|
414
448
|
out << "reads #{spec[:verb]}" if spec[:kind] == :query
|
|
415
449
|
out << "issued by #{spec[:role]}" if spec[:role]
|
|
416
|
-
out
|
|
450
|
+
out
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def verb_help_invocation_lines(program, name, spec, ask)
|
|
417
454
|
invocation = ask ? "#{program} ask #{name}" : "#{program} #{name}"
|
|
418
|
-
|
|
419
|
-
|
|
455
|
+
["", " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}", ""]
|
|
456
|
+
end
|
|
420
457
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
458
|
+
def verb_help_argument_lines(spec)
|
|
459
|
+
return [] if spec[:arguments].empty?
|
|
460
|
+
|
|
461
|
+
width = spec[:arguments].map { |a| a[:path].length }.max
|
|
462
|
+
lines = spec[:arguments].map do |argument|
|
|
463
|
+
notes = []
|
|
464
|
+
notes << argument[:type]
|
|
465
|
+
notes << "one of #{argument[:enum].join(', ')}" if argument[:enum]
|
|
466
|
+
notes << "matches #{argument[:pattern]}" if argument[:pattern]
|
|
467
|
+
notes << "defaults to #{argument[:default].inspect}" unless argument[:default].nil?
|
|
468
|
+
notes << argument[:note] if argument[:note]
|
|
469
|
+
notes << "optional" unless argument[:required]
|
|
470
|
+
" #{argument[:path].ljust(width)} #{notes.join('; ')}"
|
|
434
471
|
end
|
|
472
|
+
lines << ""
|
|
473
|
+
end
|
|
435
474
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
spec[:refusals].each { |refusal| out << " #{refusal}" }
|
|
439
|
-
out << ""
|
|
440
|
-
end
|
|
475
|
+
def verb_help_refusal_lines(spec)
|
|
476
|
+
return [] if Array(spec[:refusals]).empty?
|
|
441
477
|
|
|
442
|
-
|
|
478
|
+
["refused when:", *spec[:refusals].map { |refusal| " #{refusal}" }, ""]
|
|
443
479
|
end
|
|
444
480
|
end
|
|
445
481
|
end
|