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
|
@@ -79,7 +79,7 @@ module Hecks
|
|
|
79
79
|
rescue Runtime::NotFound, Runtime::TypeMismatch => e
|
|
80
80
|
# A BAD ARGUMENT AND A MISSING RECORD BOTH LAND HERE, and both want the
|
|
81
81
|
# same next step: read what the verb actually takes.
|
|
82
|
-
["#{e.message}\n\n #{program} #{
|
|
82
|
+
["#{e.message}\n\n #{program} #{'ask ' if asking}#{name} --help", 1]
|
|
83
83
|
rescue *Runtime::DOMAIN_REFUSALS => e
|
|
84
84
|
# THE REFUSAL IS THE PRODUCT — the chapter's own sentence, verbatim.
|
|
85
85
|
[e.message, 1]
|
|
@@ -177,7 +177,7 @@ module Hecks
|
|
|
177
177
|
|
|
178
178
|
lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
|
|
179
179
|
lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
|
|
180
|
-
lines += ["", " #{program}#{
|
|
180
|
+
lines += ["", " #{program}#{' ask' if asking} for the full list"]
|
|
181
181
|
lines.join("\n")
|
|
182
182
|
end
|
|
183
183
|
|
|
@@ -18,9 +18,7 @@ module Hecks
|
|
|
18
18
|
|
|
19
19
|
def normalize(input, receiver:, legacy_receiver: nil)
|
|
20
20
|
request = symbolize(input)
|
|
21
|
-
unless request.is_a?(Hash)
|
|
22
|
-
raise Runtime::TypeMismatch, "a command request must be a hash"
|
|
23
|
-
end
|
|
21
|
+
raise Runtime::TypeMismatch, "a command request must be a hash" unless request.is_a?(Hash)
|
|
24
22
|
|
|
25
23
|
route, facts = split(request, receiver: receiver, legacy_receiver: legacy_receiver)
|
|
26
24
|
validate_route!(route, receiver)
|
|
@@ -54,9 +52,7 @@ module Hecks
|
|
|
54
52
|
def take_legacy_route(flat, receiver, legacy_receiver)
|
|
55
53
|
return unless legacy_receiver
|
|
56
54
|
|
|
57
|
-
if receiver == :aggregate
|
|
58
|
-
return flat.delete(legacy_receiver.to_sym)
|
|
59
|
-
end
|
|
55
|
+
return flat.delete(legacy_receiver.to_sym) if receiver == :aggregate
|
|
60
56
|
|
|
61
57
|
return unless receiver == :entity && legacy_receiver.is_a?(Hash)
|
|
62
58
|
|
|
@@ -68,23 +64,27 @@ module Hecks
|
|
|
68
64
|
end
|
|
69
65
|
private_class_method :take_legacy_route
|
|
70
66
|
|
|
67
|
+
# A case dispatch over the three closed receiver kinds (nil,
|
|
68
|
+
# :aggregate, :entity) plus the impossible-kind backstop — each
|
|
69
|
+
# branch is its own self-contained validation for that one kind.
|
|
70
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
71
71
|
def validate_route!(route, receiver)
|
|
72
72
|
case receiver
|
|
73
73
|
when nil
|
|
74
74
|
raise Runtime::TypeMismatch, "this command does not take a receiver in to:" unless route.nil?
|
|
75
75
|
when :aggregate
|
|
76
76
|
if route.nil? || route.to_s.empty? || route.is_a?(Hash)
|
|
77
|
-
raise Runtime::TypeMismatch,
|
|
77
|
+
raise Runtime::TypeMismatch,
|
|
78
|
+
"to: must name the receiving aggregate identity"
|
|
78
79
|
end
|
|
79
80
|
when :entity
|
|
80
|
-
unless route.is_a?(Hash)
|
|
81
|
-
raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:"
|
|
82
|
-
end
|
|
81
|
+
raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:" unless route.is_a?(Hash)
|
|
83
82
|
|
|
84
83
|
extra = route.keys - [:aggregate, :entity]
|
|
85
84
|
missing = [:aggregate, :entity].select { |key| route[key].nil? || route[key].to_s.empty? }
|
|
86
85
|
unless extra.empty? && missing.empty?
|
|
87
|
-
raise Runtime::TypeMismatch,
|
|
86
|
+
raise Runtime::TypeMismatch,
|
|
87
|
+
"to: for an entity command must contain only aggregate: and entity:"
|
|
88
88
|
end
|
|
89
89
|
else
|
|
90
90
|
raise ArgumentError, "unknown receiver kind #{receiver.inspect}"
|
data/lib/hecks/facade/handle.rb
CHANGED
|
@@ -18,10 +18,10 @@ module Hecks
|
|
|
18
18
|
class Handle
|
|
19
19
|
attr_reader :id
|
|
20
20
|
|
|
21
|
-
def initialize(dispatcher:, domain:,
|
|
21
|
+
def initialize(dispatcher:, domain:, aggregate:, instance:)
|
|
22
22
|
@dispatcher = dispatcher
|
|
23
23
|
@domain = domain
|
|
24
|
-
@ir =
|
|
24
|
+
@ir = aggregate
|
|
25
25
|
@id = instance.id
|
|
26
26
|
@state = instance.state
|
|
27
27
|
define_reference_accessors
|
|
@@ -72,7 +72,7 @@ module Hecks
|
|
|
72
72
|
# A declared field not yet written arrives here too (nil, the way a
|
|
73
73
|
# defined reader answered). Verbs are NOT handled here — see
|
|
74
74
|
# `define_verb_methods` for why.
|
|
75
|
-
def method_missing(name, *args, **kwargs, &
|
|
75
|
+
def method_missing(name, *args, **kwargs, &)
|
|
76
76
|
return @state[name] if @state.key?(name) || reader?(name)
|
|
77
77
|
|
|
78
78
|
super
|
|
@@ -13,14 +13,25 @@ module Hecks
|
|
|
13
13
|
# `persisted_by`-style binding collector a `.hecksagon` lands on, and
|
|
14
14
|
# the aggregate-scoped `port` a `.hecksagon` lands on beside it.
|
|
15
15
|
module AggregateDoor
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# One method building one `door` module's ~20 singleton methods
|
|
17
|
+
# looks like it splits along each `define_singleton_method` call,
|
|
18
|
+
# but three of those blocks (`:port`, `:method_missing`,
|
|
19
|
+
# `:const_missing`) are a single cross-referencing essay on the
|
|
20
|
+
# stale-facade-across-boots hazard — each one's comment explicitly
|
|
21
|
+
# points at "below"/"above" as part of the SAME method. Splitting
|
|
22
|
+
# into helper methods wouldn't break anything at runtime (no
|
|
23
|
+
# shared mutable state beyond the closed-over args, which just
|
|
24
|
+
# become parameters), but it would sever that narrative across
|
|
25
|
+
# method boundaries for no functional gain.
|
|
26
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
27
|
+
def aggregate_module(dispatcher, domain, aggregate)
|
|
28
|
+
fqn = "#{domain}::#{aggregate.hecks_name}"
|
|
18
29
|
door = Module.new
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
aggregate.attributes.each do |attribute|
|
|
21
32
|
next unless RESERVED.include?(attribute.name.to_sym)
|
|
22
33
|
|
|
23
|
-
warn "[hecks] #{
|
|
34
|
+
warn "[hecks] #{aggregate.hecks_name}##{attribute.name} shadows a built-in — no reader defined"
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
# A creating verb is a MODULE method returning the new record in hand ;
|
|
@@ -33,9 +44,9 @@ module Hecks
|
|
|
33
44
|
# accounts, a real same-aggregate collision this corpus already
|
|
34
45
|
# has) — the suffix is what makes both nameable at all, not
|
|
35
46
|
# merely a style choice.
|
|
36
|
-
|
|
47
|
+
aggregate.commands.select(&:creates?).each do |command|
|
|
37
48
|
door.define_singleton_method("#{Naming.snake(command.hecks_name)}!") do |**args|
|
|
38
|
-
Handle.new(dispatcher: dispatcher, domain: domain,
|
|
49
|
+
Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate,
|
|
39
50
|
instance: dispatcher.dispatch("#{fqn}.#{command.hecks_name}", with: args).instance)
|
|
40
51
|
end
|
|
41
52
|
end
|
|
@@ -46,14 +57,14 @@ module Hecks
|
|
|
46
57
|
# about the way `!` does for a command. Answers the raw row
|
|
47
58
|
# array `dispatcher.query` itself answers, the same shape
|
|
48
59
|
# `runtime.query("#{fqn}.Name")` already gave.
|
|
49
|
-
|
|
60
|
+
aggregate.queries.each do |query|
|
|
50
61
|
door.define_singleton_method(Naming.snake(query.hecks_name)) do |**args|
|
|
51
62
|
dispatcher.query("#{fqn}.#{query.hecks_name}", **args)
|
|
52
63
|
end
|
|
53
64
|
end
|
|
54
65
|
|
|
55
66
|
door.define_singleton_method(:fqn) { fqn }
|
|
56
|
-
door.define_singleton_method(:ir) {
|
|
67
|
+
door.define_singleton_method(:ir) { aggregate }
|
|
57
68
|
|
|
58
69
|
# THE SAME VERB THE CHAPTER ANSWERS, one level down. Every
|
|
59
70
|
# construct emits its own IR (Hecks::IR), so an aggregate
|
|
@@ -67,39 +78,41 @@ module Hecks
|
|
|
67
78
|
# `{"aggregates" => []}` for an aggregate.
|
|
68
79
|
door.define_singleton_method(:project) do |target, out: nil, **options|
|
|
69
80
|
key = Projector.key_for(target)
|
|
70
|
-
artifact = Projector.call(key, bluebook:
|
|
81
|
+
artifact = Projector.call(key, bluebook: aggregate, options: options)
|
|
71
82
|
return artifact unless out
|
|
72
83
|
|
|
73
84
|
Projector.write(artifact, out, as: Projector.emits_for(key))
|
|
74
85
|
end
|
|
75
|
-
door.define_singleton_method(:repository) { dispatcher.registry.repository(domain,
|
|
76
|
-
door.define_singleton_method(:commands)
|
|
77
|
-
|
|
86
|
+
door.define_singleton_method(:repository) { dispatcher.registry.repository(domain, aggregate) }
|
|
87
|
+
door.define_singleton_method(:commands) do
|
|
88
|
+
aggregate.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort
|
|
89
|
+
end
|
|
90
|
+
door.define_singleton_method(:queries) { aggregate.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
|
|
78
91
|
# ONE AGGREGATE'S USAGE DOCUMENT — the same projection the chapter
|
|
79
92
|
# answers with, narrowed to this head. `commands` above already
|
|
80
93
|
# answers "what can I call"; this answers "and what does each one
|
|
81
94
|
# want, refuse, and guarantee", which is the rest of the question.
|
|
82
95
|
door.define_singleton_method(:docs) do |**options|
|
|
83
96
|
Projector.call(:docs, bluebook: dispatcher.registry.bluebook(domain),
|
|
84
|
-
options: options.merge(aggregate:
|
|
97
|
+
options: options.merge(aggregate: aggregate.hecks_name))
|
|
85
98
|
end
|
|
86
99
|
# ONE AGGREGATE, READ BACK IN ENGLISH — the same narrowing `:docs`
|
|
87
100
|
# takes, aimed at `:narrate` instead.
|
|
88
101
|
door.define_singleton_method(:narrate) do |**options|
|
|
89
102
|
Projector.call(:narrate, bluebook: dispatcher.registry.bluebook(domain),
|
|
90
|
-
options: options.merge(aggregate:
|
|
103
|
+
options: options.merge(aggregate: aggregate.hecks_name))
|
|
91
104
|
end
|
|
92
|
-
door.define_singleton_method(:count) { dispatcher.registry.repository(domain,
|
|
105
|
+
door.define_singleton_method(:count) { dispatcher.registry.repository(domain, aggregate).count }
|
|
93
106
|
door.define_singleton_method(:events) { dispatcher.events.select { |event| event.aggregate == fqn } }
|
|
94
107
|
|
|
95
108
|
door.define_singleton_method(:find) do |id|
|
|
96
|
-
found = dispatcher.registry.repository(domain,
|
|
97
|
-
found && Handle.new(dispatcher: dispatcher, domain: domain,
|
|
109
|
+
found = dispatcher.registry.repository(domain, aggregate).find(id)
|
|
110
|
+
found && Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate, instance: found)
|
|
98
111
|
end
|
|
99
112
|
|
|
100
113
|
door.define_singleton_method(:all) do |**opts|
|
|
101
|
-
dispatcher.registry.repository(domain,
|
|
102
|
-
Handle.new(dispatcher: dispatcher, domain: domain,
|
|
114
|
+
dispatcher.registry.repository(domain, aggregate).all(**opts).map do |instance|
|
|
115
|
+
Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate, instance: instance)
|
|
103
116
|
end
|
|
104
117
|
end
|
|
105
118
|
|
|
@@ -109,20 +122,20 @@ module Hecks
|
|
|
109
122
|
# `ConstShim`/`const_missing`, so `Pizzas::Pizza.port(...)` lands
|
|
110
123
|
# HERE instead once any boot has run before.
|
|
111
124
|
#
|
|
112
|
-
# RE-RESOLVED, NOT THE CLOSED-OVER `
|
|
113
|
-
# one, built by a boot from earlier in this same process,
|
|
114
|
-
# on the `Pizzas`/`Pizza` constants only because nothing
|
|
115
|
-
# re-installed them since. Attaching to this door's OWN
|
|
116
|
-
# attach the port to a discarded aggregate from
|
|
117
|
-
# invisible to the CURRENT one actually being
|
|
118
|
-
# the exact way `method_missing` below already
|
|
119
|
-
# a plain bind, via `HecksagonBuilder.collector`
|
|
120
|
-
# anything this door closes over. `Hecks.current_registry`
|
|
121
|
-
# the same "whichever boot is actually in progress" indirection,
|
|
125
|
+
# RE-RESOLVED, NOT THE CLOSED-OVER `aggregate` — this door can be a
|
|
126
|
+
# STALE one, built by a boot from earlier in this same process,
|
|
127
|
+
# sitting on the `Pizzas`/`Pizza` constants only because nothing
|
|
128
|
+
# has re-installed them since. Attaching to this door's OWN
|
|
129
|
+
# `aggregate` would attach the port to a discarded aggregate from
|
|
130
|
+
# that old boot, invisible to the CURRENT one actually being
|
|
131
|
+
# loaded — silently, the exact way `method_missing` below already
|
|
132
|
+
# has to avoid it for a plain bind, via `HecksagonBuilder.collector`
|
|
133
|
+
# rather than anything this door closes over. `Hecks.current_registry`
|
|
134
|
+
# is the same "whichever boot is actually in progress" indirection,
|
|
122
135
|
# and `BindingProxy#port` already re-resolves through it the same
|
|
123
136
|
# way — this is that door's fallback twin, not a shortcut past it.
|
|
124
137
|
door.define_singleton_method(:port) do |name, &block|
|
|
125
|
-
current = Hecks.current_registry&.bluebook(domain)&.aggregate(
|
|
138
|
+
current = Hecks.current_registry&.bluebook(domain)&.aggregate(aggregate.hecks_name) or
|
|
126
139
|
raise Bluebook::DSL::Malformed, "#{fqn}.port(#{name.inspect}) called outside a boot"
|
|
127
140
|
|
|
128
141
|
domain_port = Bluebook::DSL::ConstShim.with(->(const) { const }) do
|
|
@@ -163,16 +176,17 @@ module Hecks
|
|
|
163
176
|
# const_missing directly, never `Object.const_missing`/`ConstShim::
|
|
164
177
|
# Hook` at all: real modules resolve without ever calling that.
|
|
165
178
|
#
|
|
166
|
-
# `
|
|
167
|
-
# read the SAME either way, whether or not a stale
|
|
168
|
-
# sitting on this process from an earlier boot;
|
|
169
|
-
# would make `Account::Debit`'s own meaning
|
|
170
|
-
# process history, which is the exact
|
|
171
|
-
# remove (docs/dsl-work-slices.md — "two
|
|
172
|
-
# this file's own reason for being
|
|
179
|
+
# `aggregate.hecks_name`, NOT the qualified `fqn` — a scoped
|
|
180
|
+
# reference has to read the SAME either way, whether or not a stale
|
|
181
|
+
# door happens to be sitting on this process from an earlier boot;
|
|
182
|
+
# qualifying it here would make `Account::Debit`'s own meaning
|
|
183
|
+
# depend on incidental process history, which is the exact
|
|
184
|
+
# instability S0b exists to remove (docs/dsl-work-slices.md — "two
|
|
185
|
+
# domains in one registry" is this file's own reason for being
|
|
186
|
+
# owned by that slice).
|
|
173
187
|
door.define_singleton_method(:const_missing) do |name|
|
|
174
188
|
resolver = Bluebook::DSL::ConstShim.resolver
|
|
175
|
-
return resolver.call("#{
|
|
189
|
+
return resolver.call("#{aggregate.hecks_name}::#{name}") if resolver
|
|
176
190
|
|
|
177
191
|
super(name)
|
|
178
192
|
end
|
data/lib/hecks/facade.rb
CHANGED
|
@@ -15,6 +15,10 @@ module Hecks
|
|
|
15
15
|
# what GENERATED retains is one small facade module per name, not a graph
|
|
16
16
|
# per boot.
|
|
17
17
|
module Namespace
|
|
18
|
+
# NOT frozen — a real registry, mutated below
|
|
19
|
+
# (`GENERATED[[container, name]] = value`). False positive for
|
|
20
|
+
# Style/MutableConstant.
|
|
21
|
+
# rubocop:disable-next Style/MutableConstant
|
|
18
22
|
GENERATED = {}
|
|
19
23
|
|
|
20
24
|
module_function
|
data/lib/hecks/forms/app.rb
CHANGED
|
@@ -118,7 +118,8 @@ module Hecks
|
|
|
118
118
|
return respond(405, "text/plain", "GET only") unless request.get?
|
|
119
119
|
|
|
120
120
|
if format == "html"
|
|
121
|
-
html("#{chapter.name}::#{aggregate.hecks_name}",
|
|
121
|
+
html("#{chapter.name}::#{aggregate.hecks_name}",
|
|
122
|
+
RecordRenderer.index(registry: @registry, domain: chapter.name, aggregate: aggregate),
|
|
122
123
|
breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
|
|
123
124
|
else
|
|
124
125
|
instances = @registry.repository(chapter.name, aggregate).all
|
|
@@ -196,7 +197,7 @@ module Hecks
|
|
|
196
197
|
end
|
|
197
198
|
|
|
198
199
|
def command_json(request, domain, aggregate, command)
|
|
199
|
-
return json(200, command.to_h) if request.get? && request.GET.
|
|
200
|
+
return json(200, command.to_h) if request.get? && request.GET.except("format").empty?
|
|
200
201
|
return respond(405, "text/plain", "GET or POST only") unless request.post?
|
|
201
202
|
|
|
202
203
|
_, envelope = submitted_command(request, aggregate, command)
|
|
@@ -213,7 +214,7 @@ module Hecks
|
|
|
213
214
|
return respond(405, "text/plain", "GET only") unless request.get?
|
|
214
215
|
|
|
215
216
|
params = request.GET
|
|
216
|
-
asked = params.
|
|
217
|
+
asked = params.except("format")
|
|
217
218
|
return query_json(domain, aggregate, query, asked) if format != "html"
|
|
218
219
|
|
|
219
220
|
query_html(domain, aggregate, query, asked)
|
|
@@ -252,7 +253,7 @@ module Hecks
|
|
|
252
253
|
def run_query(domain, aggregate, query, fields, asked)
|
|
253
254
|
args = Params.extract(fields, asked)
|
|
254
255
|
rows = @dispatcher.query("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}", **args)
|
|
255
|
-
[rows.map { |row| Record.new(row[:id], row.
|
|
256
|
+
[rows.map { |row| Record.new(row[:id], row.except(:id)) }, nil]
|
|
256
257
|
# L10 (docs/audits/2026-08-10-main-bug-audit.md) — `Params.extract`
|
|
257
258
|
# (params.rb's `extract_list`) reads a list-of-value-object line as
|
|
258
259
|
# JSON (the honest fallback for a multi-attribute list element this
|
|
@@ -275,7 +276,8 @@ module Hecks
|
|
|
275
276
|
# sites in this file (see aggregate_route's own comment).
|
|
276
277
|
return json(200, instance.state.merge(id: instance.id)) if format != "html"
|
|
277
278
|
|
|
278
|
-
html("#{domain}::#{aggregate.hecks_name} #{id}",
|
|
279
|
+
html("#{domain}::#{aggregate.hecks_name} #{id}",
|
|
280
|
+
RecordRenderer.show(registry: @registry, domain: domain, aggregate: aggregate, id: id),
|
|
279
281
|
breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"], [id, nil]])
|
|
280
282
|
end
|
|
281
283
|
|
|
@@ -283,7 +285,9 @@ module Hecks
|
|
|
283
285
|
return json(404, { error: "NotFound", message: "no #{aggregate.hecks_name} #{id}" }) if format != "html"
|
|
284
286
|
|
|
285
287
|
respond(404, "text/html; charset=utf-8",
|
|
286
|
-
Page.render(title: "not found",
|
|
288
|
+
Page.render(title: "not found",
|
|
289
|
+
body: "<h1>Not found</h1><p>No #{Escape.html(aggregate.hecks_name)} " \
|
|
290
|
+
"#{Escape.html(id)}.</p>"))
|
|
287
291
|
end
|
|
288
292
|
|
|
289
293
|
def find_aggregate(chapter, name)
|
|
@@ -57,9 +57,9 @@ module Hecks
|
|
|
57
57
|
def self.header(domain, aggregate, command)
|
|
58
58
|
<<~HTML
|
|
59
59
|
<h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}")}</h1>
|
|
60
|
-
#{
|
|
61
|
-
#{
|
|
62
|
-
#{
|
|
60
|
+
#{%(<span class="badge role" title="Declared on the command; this prototype dispatches with no caller bound, so the check does not run.">role: #{Escape.html(command.role)} (not enforced here)</span>) if command.role}
|
|
61
|
+
#{%(<span class="badge">creates a new #{Escape.html(aggregate.hecks_name)}</span>) if command.creates?}
|
|
62
|
+
#{%(<p class="goal">#{Escape.html(command.goal)}</p>) if command.goal}
|
|
63
63
|
#{givens_callout(command)}
|
|
64
64
|
HTML
|
|
65
65
|
end
|
|
@@ -28,7 +28,7 @@ module Hecks
|
|
|
28
28
|
def self.group(field, values, errors, reference_options, tag:, css: nil)
|
|
29
29
|
inner = field.children.map { |child| render(child, values: values, errors: errors, reference_options: reference_options) }
|
|
30
30
|
<<~HTML
|
|
31
|
-
<#{tag}#{
|
|
31
|
+
<#{tag}#{%( class="#{css}") if css}>
|
|
32
32
|
<legend>#{Escape.html(field.label)}#{required_mark(field)}</legend>
|
|
33
33
|
#{inner.join("\n")}
|
|
34
34
|
</#{tag}>
|
|
@@ -43,7 +43,7 @@ module Hecks
|
|
|
43
43
|
<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>
|
|
44
44
|
<textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}"
|
|
45
45
|
#{aria(field)} placeholder="one #{Escape.attr(item.label.downcase)} per line">#{Escape.html(current)}</textarea>
|
|
46
|
-
<span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{
|
|
46
|
+
<span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{' — each line is one JSON object' unless item.leaf?}.</span>
|
|
47
47
|
</div>
|
|
48
48
|
HTML
|
|
49
49
|
end
|
|
@@ -64,11 +64,11 @@ module Hecks
|
|
|
64
64
|
|
|
65
65
|
def self.wrap(field, body, error)
|
|
66
66
|
<<~HTML
|
|
67
|
-
<div class="field#{
|
|
68
|
-
#{
|
|
67
|
+
<div class="field#{' has-error' if error}">
|
|
68
|
+
#{%(<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>) unless field.kind == :boolean}
|
|
69
69
|
#{body}
|
|
70
|
-
#{
|
|
71
|
-
#{
|
|
70
|
+
#{%(<span class="help" id="#{dom_id(field.path)}-help">#{Escape.html(field.help)}</span>) if field.help}
|
|
71
|
+
#{%(<span class="help" role="alert">#{Escape.html(error)}</span>) if error}
|
|
72
72
|
</div>
|
|
73
73
|
HTML
|
|
74
74
|
end
|
|
@@ -114,7 +114,8 @@ module Hecks
|
|
|
114
114
|
def self.select(field, value)
|
|
115
115
|
selected = value || field.default
|
|
116
116
|
options = field.options.map do |option_value, option_label|
|
|
117
|
-
%(<option value="#{Escape.attr(option_value)}"#{option_value.to_s == selected.to_s
|
|
117
|
+
%(<option value="#{Escape.attr(option_value)}"#{' selected' if option_value.to_s == selected.to_s}>) \
|
|
118
|
+
"#{Escape.html(option_label)}</option>"
|
|
118
119
|
end
|
|
119
120
|
blank = field.optional? ? %(<option value="">—</option>) : ""
|
|
120
121
|
%(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{options.join}</select>)
|
|
@@ -124,9 +125,13 @@ module Hecks
|
|
|
124
125
|
return input(field.tap { |f| f.html_type = "text" }, value) unless options && !options.empty?
|
|
125
126
|
|
|
126
127
|
rendered = options.map do |id, label|
|
|
127
|
-
%(<option value="#{Escape.attr(id)}"#{id.to_s == value.to_s
|
|
128
|
+
%(<option value="#{Escape.attr(id)}"#{' selected' if id.to_s == value.to_s}>#{Escape.html(label)}</option>)
|
|
128
129
|
end
|
|
129
|
-
blank = field.optional?
|
|
130
|
+
blank = if field.optional?
|
|
131
|
+
%(<option value="">—</option>)
|
|
132
|
+
else
|
|
133
|
+
%(<option value="" disabled#{' selected' unless value}>choose one…</option>)
|
|
134
|
+
end
|
|
130
135
|
%(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{rendered.join}</select>)
|
|
131
136
|
end
|
|
132
137
|
|
|
@@ -24,6 +24,8 @@ module Hecks
|
|
|
24
24
|
def required? = !optional
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Turns a wire-spelled field or path segment into the plain-English text
|
|
28
|
+
# `FieldShape` embeds as a `Field`'s label or a group's legend.
|
|
27
29
|
module Humanize
|
|
28
30
|
# "given" -> "Given", "daily_limit" -> "Daily limit", "end_to_end" ->
|
|
29
31
|
# "End to end" — the label a plain reader wants, not the wire spelling.
|
|
@@ -91,8 +93,11 @@ module Hecks
|
|
|
91
93
|
def self.reference_field(attribute, common)
|
|
92
94
|
target = attribute.type.resolve
|
|
93
95
|
Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target,
|
|
94
|
-
help:
|
|
95
|
-
|
|
96
|
+
help: if target
|
|
97
|
+
"References an existing #{target.hecks_name} by id."
|
|
98
|
+
else
|
|
99
|
+
"References an aggregate in another domain — enter its id."
|
|
100
|
+
end)
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
# `admits:` names a closed set declared ELSEWHERE (`"Account::
|
data/lib/hecks/forms/html.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Hecks
|
|
|
69
69
|
next if value.nil? || value == false
|
|
70
70
|
next name.to_s.tr("_", "-") if value == true
|
|
71
71
|
|
|
72
|
-
%(#{name.to_s.tr(
|
|
72
|
+
%(#{name.to_s.tr('_', '-')}="#{Escape.attr(value)}")
|
|
73
73
|
end.join(" ")
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -78,7 +78,10 @@ module Hecks
|
|
|
78
78
|
rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
# `self.open(...)`, not bare `open(...)` — this Html.open (an HTML tag
|
|
82
|
+
# renderer, right above) shadows Kernel#open safely either way, but the
|
|
83
|
+
# explicit receiver also settles Security/Open's static ambiguity.
|
|
84
|
+
def self.void(name, **pairs) = self.open(name, **pairs) # self-closing tags read the same; HTML5 needs no slash
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
end
|
|
@@ -26,7 +26,7 @@ module Hecks
|
|
|
26
26
|
HTML
|
|
27
27
|
end
|
|
28
28
|
<<~HTML
|
|
29
|
-
<h2>#{Escape.html(name)}#{
|
|
29
|
+
<h2>#{Escape.html(name)}#{%( <span class="badge">#{Escape.html(chapter.vision)}</span>) if chapter.vision}</h2>
|
|
30
30
|
<ul class="verb-list">#{items.join}</ul>
|
|
31
31
|
HTML
|
|
32
32
|
end
|
data/lib/hecks/forms/page.rb
CHANGED
|
@@ -40,12 +40,16 @@ module Hecks
|
|
|
40
40
|
return "" if crumbs.empty?
|
|
41
41
|
|
|
42
42
|
items = crumbs.map do |label, href|
|
|
43
|
-
|
|
43
|
+
if href
|
|
44
|
+
%(<a href="#{Escape.attr(href)}">#{Escape.html(label)}</a>)
|
|
45
|
+
else
|
|
46
|
+
%(<span aria-current="page">#{Escape.html(label)}</span>)
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
%(<nav class="breadcrumbs" aria-label="Breadcrumb">#{items.join(' <span class="sep">/</span> ')}</nav>)
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
STYLE = <<~CSS
|
|
52
|
+
STYLE = <<~CSS.freeze
|
|
49
53
|
:root {
|
|
50
54
|
--bg: #fbfaf8; --surface: #ffffff; --border: #e2ddd3; --text: #24211c;
|
|
51
55
|
--muted: #6b6355; --accent: #8a5a2b; --accent-contrast: #fff8ef;
|
|
@@ -131,7 +135,7 @@ module Hecks
|
|
|
131
135
|
# Decorative only — every form here already works with JS disabled
|
|
132
136
|
# (plain `<form method=post>`/`method=get`), matching this repo's own
|
|
133
137
|
# "no ERB, nothing scripted" bar for the domain layer itself.
|
|
134
|
-
SCRIPT = <<~JS
|
|
138
|
+
SCRIPT = <<~JS.freeze
|
|
135
139
|
document.addEventListener("click", (event) => {
|
|
136
140
|
const button = event.target.closest("[data-copy]");
|
|
137
141
|
if (!button) return;
|
|
@@ -38,7 +38,7 @@ module Hecks
|
|
|
38
38
|
def self.header(domain, aggregate, query)
|
|
39
39
|
<<~HTML
|
|
40
40
|
<h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}")}</h1>
|
|
41
|
-
#{
|
|
41
|
+
#{%(<p class="goal">#{Escape.html(query.description)}</p>) if query.description}
|
|
42
42
|
#{badges(query)}
|
|
43
43
|
HTML
|
|
44
44
|
end
|
|
@@ -89,7 +89,8 @@ module Hecks
|
|
|
89
89
|
def self.error_banner(error)
|
|
90
90
|
return "" unless error
|
|
91
91
|
|
|
92
|
-
%(<div class="error-banner" role="alert"><p><strong>#{Escape.html(error.class.name.split('::').last)}</strong> —
|
|
92
|
+
%(<div class="error-banner" role="alert"><p><strong>#{Escape.html(error.class.name.split('::').last)}</strong> — ) \
|
|
93
|
+
"#{Escape.html(error.message)}</p></div>"
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
def self.results_section(aggregate, results, domain)
|
|
@@ -15,7 +15,7 @@ module Hecks
|
|
|
15
15
|
instances = registry.repository(domain, aggregate).all
|
|
16
16
|
<<~HTML
|
|
17
17
|
<h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")}</h1>
|
|
18
|
-
#{
|
|
18
|
+
#{%(<p class="goal">#{Escape.html(aggregate.description)}</p>) if aggregate.description}
|
|
19
19
|
#{creating_links(domain, aggregate)}
|
|
20
20
|
<h2>Records (#{instances.size})</h2>
|
|
21
21
|
#{RecordTable.render(aggregate, instances, domain: domain)}
|
|
@@ -27,9 +27,10 @@ module Hecks
|
|
|
27
27
|
creators = aggregate.commands.select(&:creates?)
|
|
28
28
|
return "" if creators.empty?
|
|
29
29
|
|
|
30
|
-
links = creators.map
|
|
31
|
-
%(<a class="button" href="/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html"
|
|
32
|
-
|
|
30
|
+
links = creators.map do |cmd|
|
|
31
|
+
%(<a class="button" href="/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html">) \
|
|
32
|
+
"+ #{Escape.html(cmd.hecks_name)}</a>"
|
|
33
|
+
end
|
|
33
34
|
%(<div class="actions">#{links.join}</div>)
|
|
34
35
|
end
|
|
35
36
|
|
|
@@ -41,7 +42,7 @@ module Hecks
|
|
|
41
42
|
current = aggregate.lifecycle && state[aggregate.lifecycle.field]
|
|
42
43
|
<<~HTML
|
|
43
44
|
<h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}")} <span class="mono">#{Escape.html(id)}</span></h1>
|
|
44
|
-
#{
|
|
45
|
+
#{%(<span class="badge role">#{Escape.html(aggregate.lifecycle.field)}: #{Escape.html(current)}</span>) if current}
|
|
45
46
|
#{state_table(state)}
|
|
46
47
|
<h2>Commands</h2>
|
|
47
48
|
#{command_links(domain, aggregate, id, current)}
|
|
@@ -50,9 +51,9 @@ module Hecks
|
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def self.state_table(state)
|
|
53
|
-
rows = state.map
|
|
54
|
+
rows = state.map do |key, value|
|
|
54
55
|
"<tr><th>#{Escape.html(Humanize.label(key.to_s))}</th><td>#{Escape.html(render_value(value))}</td></tr>"
|
|
55
|
-
|
|
56
|
+
end
|
|
56
57
|
%(<div class="table-scroll"><table><tbody>#{rows.join}</tbody></table></div>)
|
|
57
58
|
end
|
|
58
59
|
|
|
@@ -81,7 +82,8 @@ module Hecks
|
|
|
81
82
|
# parameter), then the assembled href is attribute-escaped as
|
|
82
83
|
# usual.
|
|
83
84
|
href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
|
|
84
|
-
%(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(cmd.hecks_name)}</span
|
|
85
|
+
%(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(cmd.hecks_name)}</span>) \
|
|
86
|
+
"<span class=\"kind\">#{Escape.html(cmd.goal.to_s)}</span></a></li>"
|
|
85
87
|
end
|
|
86
88
|
%(<ul class="verb-list">#{items.join}</ul>)
|
|
87
89
|
end
|
|
@@ -97,7 +99,8 @@ module Hecks
|
|
|
97
99
|
return "" if aggregate.queries.empty?
|
|
98
100
|
|
|
99
101
|
items = aggregate.queries.map do |query|
|
|
100
|
-
%(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html"
|
|
102
|
+
%(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html">) \
|
|
103
|
+
"<span>#{Escape.html(query.hecks_name)}</span><span class=\"kind\">query</span></a></li>"
|
|
101
104
|
end
|
|
102
105
|
%(<h2>Queries</h2><ul class="verb-list">#{items.join}</ul>)
|
|
103
106
|
end
|
|
@@ -106,13 +109,16 @@ module Hecks
|
|
|
106
109
|
commands = aggregate.commands.reject(&:creates?)
|
|
107
110
|
return "" if commands.empty? && aggregate.queries.empty?
|
|
108
111
|
|
|
109
|
-
cmd_items = commands.map
|
|
110
|
-
%(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{c.hecks_name}.html"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
cmd_items = commands.map do |c|
|
|
113
|
+
%(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{c.hecks_name}.html">) \
|
|
114
|
+
"<span>#{Escape.html(c.hecks_name)}</span><span class=\"kind\">command</span></a></li>"
|
|
115
|
+
end
|
|
116
|
+
query_items = aggregate.queries.map do |q|
|
|
117
|
+
%(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{q.hecks_name}.html">) \
|
|
118
|
+
"<span>#{Escape.html(q.hecks_name)}</span><span class=\"kind\">query</span></a></li>"
|
|
119
|
+
end
|
|
120
|
+
%(<h2>Every command & query on #{Escape.html(aggregate.hecks_name)}</h2>) \
|
|
121
|
+
"<ul class=\"verb-list\">#{(cmd_items + query_items).join}</ul>"
|
|
116
122
|
end
|
|
117
123
|
end
|
|
118
124
|
end
|
data/lib/hecks/forms.rb
CHANGED
|
@@ -13,6 +13,10 @@ require "hecks"
|
|
|
13
13
|
require_relative "forms/app"
|
|
14
14
|
|
|
15
15
|
module Hecks
|
|
16
|
+
# Per-chapter `expose` configuration, keyed by chapter name and looked up
|
|
17
|
+
# by the Rack app at request time (`.config`) to decide which chapters'
|
|
18
|
+
# forms are switched on at all — see the file header above for what
|
|
19
|
+
# `expose` deliberately does and does not cover.
|
|
16
20
|
module Forms
|
|
17
21
|
# `expose`'s own declaration — "which chapters does this app expose" —
|
|
18
22
|
# kept OUTSIDE the `Hecks.*` collector convention (`Hecks.bluebook`,
|