hecks 1.4.0 → 1.5.1
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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
data/lib/hecks/facade/handle.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require_relative "../naming"
|
|
2
|
+
require_relative "../runtime/caller"
|
|
3
|
+
require_relative "../ports/authorization"
|
|
2
4
|
|
|
3
5
|
module Hecks
|
|
4
6
|
module Facade
|
|
@@ -41,7 +43,7 @@ module Hecks
|
|
|
41
43
|
# @param key [Symbol, String] the attribute name
|
|
42
44
|
# @return [Object, nil] the value held in state (a scalar, a value object, a list,
|
|
43
45
|
# or a referenced record's id); `nil` when the field is unset or not in state
|
|
44
|
-
def [](key) =
|
|
46
|
+
def [](key) = redacted(key.to_sym)
|
|
45
47
|
|
|
46
48
|
# Answers the record's state as a plain Hash with the bare identity under `:id`.
|
|
47
49
|
#
|
|
@@ -59,7 +61,7 @@ module Hecks
|
|
|
59
61
|
#
|
|
60
62
|
# @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
|
|
61
63
|
# name, plus `:id` holding the identity String
|
|
62
|
-
def to_h = @state.merge(id: @id)
|
|
64
|
+
def to_h = @state.to_h { |key, _| [key, redacted(key)] }.merge(id: @id)
|
|
63
65
|
|
|
64
66
|
# Names the aggregate this record belongs to, in the form every dispatch verb and
|
|
65
67
|
# event is addressed by.
|
|
@@ -120,7 +122,7 @@ module Hecks
|
|
|
120
122
|
# written yet
|
|
121
123
|
# @raise [NoMethodError] if `name` is neither a key in state nor a declared field
|
|
122
124
|
def method_missing(name, *args, **kwargs, &)
|
|
123
|
-
return
|
|
125
|
+
return redacted(name) if @state.key?(name) || reader?(name)
|
|
124
126
|
|
|
125
127
|
super
|
|
126
128
|
end
|
|
@@ -133,6 +135,77 @@ module Hecks
|
|
|
133
135
|
|
|
134
136
|
def repository = @dispatcher.registry.repository(@domain, @ir)
|
|
135
137
|
|
|
138
|
+
# One field's value, with any Privacy::Marking-flagged leaf masked
|
|
139
|
+
# out unless the ambient caller holds a live Governance grant of
|
|
140
|
+
# the marking's own `readable_by` — the read-side half of the
|
|
141
|
+
# Privacy framework member (lib/hecks/framework/bluebook/
|
|
142
|
+
# privacy.bluebook): a marking's presence is what makes a read
|
|
143
|
+
# redacted, not a separate flag this class carries itself.
|
|
144
|
+
#
|
|
145
|
+
# ALWAYS THE STRONG CHECK, never the weak string-only fallback
|
|
146
|
+
# `CommandRules::Authorization#refuse_role_mismatch` allows an
|
|
147
|
+
# unidentified caller — a read gate gone wrong is a leak, not a
|
|
148
|
+
# refused command, so an ambient caller with no `actor_id` (or no
|
|
149
|
+
# caller at all) is masked here, full stop, rather than waved
|
|
150
|
+
# through the way a self-asserted `role` string is for a command.
|
|
151
|
+
#
|
|
152
|
+
# ONE LEVEL OF NESTING ONLY — `attendee.medications` masks inside
|
|
153
|
+
# the returned `Runtime::Value` via its own `#with`; a marking two
|
|
154
|
+
# levels deep is not supported and is left unmasked rather than
|
|
155
|
+
# silently mishandled, since nothing in this corpus needs it yet.
|
|
156
|
+
#
|
|
157
|
+
# @param field [Symbol] the state key being read
|
|
158
|
+
# @return [Object, nil] `@state[field]`, or a copy with the marked leaf replaced by
|
|
159
|
+
# the literal String `"[redacted]"` when the caller is not authorized to see it
|
|
160
|
+
def redacted(field)
|
|
161
|
+
raw = @state[field]
|
|
162
|
+
rows = marked_paths.select { |row| row[:attribute_path][:value].to_s.split(".", 2).first == field.to_s }
|
|
163
|
+
return raw if rows.empty?
|
|
164
|
+
|
|
165
|
+
rows.each do |row|
|
|
166
|
+
path = row[:attribute_path][:value].to_s
|
|
167
|
+
next if authorized_for?(row[:readable_by][:value].to_s)
|
|
168
|
+
|
|
169
|
+
segments = path.split(".", 2)
|
|
170
|
+
if segments.size == 1
|
|
171
|
+
raw = "[redacted]"
|
|
172
|
+
elsif raw.is_a?(Runtime::Value)
|
|
173
|
+
raw = raw.with(segments[1], "[redacted]")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
raw
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Every Privacy::Marking declared for this record's own aggregate — `[]` when the
|
|
181
|
+
# Privacy framework member is not attached in this boot at all, checked once and
|
|
182
|
+
# cheaply rather than paying for a query dispatch every read on a domain that never
|
|
183
|
+
# attached Privacy.
|
|
184
|
+
#
|
|
185
|
+
# @return [Array<Hash>] `Privacy::Marking.ForDomain`'s own rows for this `fqn`
|
|
186
|
+
def marked_paths
|
|
187
|
+
return @marked_paths if defined?(@marked_paths)
|
|
188
|
+
return @marked_paths = [] unless @dispatcher.registry.bluebook("Privacy")
|
|
189
|
+
|
|
190
|
+
@marked_paths = @dispatcher.query("Privacy::Marking.ForDomain", domain: fqn)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Whether the ambient caller holds a live Governance grant of `role`, over this
|
|
194
|
+
# record's own domain — `false` outright for an unidentified caller or a domain
|
|
195
|
+
# with no authorization provider attached, never the weak fallback a command's own
|
|
196
|
+
# role check allows (see `redacted`'s own header for why).
|
|
197
|
+
#
|
|
198
|
+
# @param role [String] the marking's own `readable_by`
|
|
199
|
+
# @return [Boolean] true only for an identified caller holding a live grant
|
|
200
|
+
def authorized_for?(role)
|
|
201
|
+
caller = Runtime::Caller.current
|
|
202
|
+
return false unless caller&.actor_id
|
|
203
|
+
return false unless @dispatcher.registry.authorization_provider_for(@domain)
|
|
204
|
+
|
|
205
|
+
Ports::Authorization.holds_role?(@dispatcher.registry, actor_id: caller.actor_id, role: role,
|
|
206
|
+
as_of: caller.as_of, scope: caller.scope)
|
|
207
|
+
end
|
|
208
|
+
|
|
136
209
|
def reader?(name)
|
|
137
210
|
!@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
|
|
138
211
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "../../bluebook/dsl/hecksagon_builder"
|
|
2
2
|
require_relative "../../bluebook/dsl/domain_port_builder"
|
|
3
3
|
require_relative "../../bluebook/dsl/const_shim"
|
|
4
|
+
require_relative "../../bluebook/dsl/binding_proxy"
|
|
4
5
|
require_relative "../../bluebook/hexagon"
|
|
5
6
|
require_relative "../handle"
|
|
6
7
|
require_relative "../../naming"
|
|
@@ -164,6 +165,13 @@ module Hecks
|
|
|
164
165
|
end
|
|
165
166
|
|
|
166
167
|
door.define_singleton_method(:method_missing) do |verb, *args, **kwargs, &block|
|
|
168
|
+
# A BARE CALL starts a Privacy marking chain — see
|
|
169
|
+
# `Bluebook::DSL::BindingProxy#method_missing`'s own header;
|
|
170
|
+
# this is the same mechanism, reached when the constant is
|
|
171
|
+
# already a real, installed door (a second boot in-process)
|
|
172
|
+
# rather than a `.hecksagon`-parse-time `BindingProxy`.
|
|
173
|
+
return Bluebook::DSL::AttributePath.new(fqn, [verb.to_s]) if args.empty? && kwargs.empty? && !block
|
|
174
|
+
|
|
167
175
|
collector = Bluebook::DSL::HecksagonBuilder.collector
|
|
168
176
|
return super(verb, *args, **kwargs, &block) unless collector
|
|
169
177
|
|
|
@@ -4,6 +4,9 @@ require_relative "../naming"
|
|
|
4
4
|
require_relative "value_object_shape"
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
|
+
# The forms surface's own namespace — the `expose` DSL, the IR->HTML
|
|
8
|
+
# renderers, and the Rack app bridging them; see `forms.rb`'s own header,
|
|
9
|
+
# its primary opening, for the fuller design this implements.
|
|
7
10
|
module Forms
|
|
8
11
|
# One resolved field, ready for a renderer to turn into markup. A leaf
|
|
9
12
|
# scalar carries `kind`/`options`/etc; a `:group` or `:list` carries
|
data/lib/hecks/forms/page.rb
CHANGED
|
@@ -9,6 +9,15 @@ module Hecks
|
|
|
9
9
|
# `prefers-color-scheme` alone; nothing here reads a cookie or a query
|
|
10
10
|
# param for it, so it is never wrong for the browser rendering it.
|
|
11
11
|
module Page
|
|
12
|
+
# Wraps one page's own body HTML in the shared shell: doctype, head, nav, footer,
|
|
13
|
+
# inline styles and script.
|
|
14
|
+
#
|
|
15
|
+
# @param title [String] the page title, escaped into `<title>` and the browser tab
|
|
16
|
+
# @param body [String] the page's own body markup, inserted unescaped inside `<main>`
|
|
17
|
+
# @param breadcrumbs [Array<Array(String, String), Array(String, nil)>] each
|
|
18
|
+
# `[label, href]` pair, in order; the last pair's `href` should be `nil` for the
|
|
19
|
+
# current page
|
|
20
|
+
# @return [String] the complete HTML document
|
|
12
21
|
def self.render(title:, body:, breadcrumbs: [])
|
|
13
22
|
<<~HTML
|
|
14
23
|
<!doctype html>
|
|
@@ -36,6 +45,11 @@ module Hecks
|
|
|
36
45
|
HTML
|
|
37
46
|
end
|
|
38
47
|
|
|
48
|
+
# Renders the breadcrumb trail as a `<nav>`, each crumb a link except the last.
|
|
49
|
+
#
|
|
50
|
+
# @param crumbs [Array<Array(String, String), Array(String, nil)>] each
|
|
51
|
+
# `[label, href]` pair; a `nil` href renders as the current, unlinked page
|
|
52
|
+
# @return [String] the `<nav>` markup, HTML-escaped; `""` when `crumbs` is empty
|
|
39
53
|
def self.breadcrumbs_html(crumbs)
|
|
40
54
|
return "" if crumbs.empty?
|
|
41
55
|
|
|
@@ -22,6 +22,13 @@ module Hecks
|
|
|
22
22
|
module PortArgument
|
|
23
23
|
module_function
|
|
24
24
|
|
|
25
|
+
# Reads the `-p`/`--port` argument out of an argv array.
|
|
26
|
+
#
|
|
27
|
+
# @param argv [Array<String>] the command-line arguments, ARGV-shaped
|
|
28
|
+
# @param default [Integer] the port to use when neither spelling appears at all
|
|
29
|
+
# @return [Array(Integer, nil), Array(nil, String)] `[port, nil]` on a clean
|
|
30
|
+
# parse, or `[nil, message]` when an explicit port was given but isn't a
|
|
31
|
+
# real port number
|
|
25
32
|
def parse(argv, default: 4567)
|
|
26
33
|
equals = argv.find { |arg| arg.start_with?("--port=") }
|
|
27
34
|
return resolve(equals.split("=", 2).last) if equals
|
|
@@ -32,6 +39,11 @@ module Hecks
|
|
|
32
39
|
resolve(argv[index + 1])
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# Validates and converts a raw `-p`/`--port` argument value.
|
|
43
|
+
#
|
|
44
|
+
# @param value [String, nil] the text following `-p`/`--port`, or nil if none
|
|
45
|
+
# @return [Array(Integer, nil), Array(nil, String)] `[port, nil]` if `value`
|
|
46
|
+
# is a whole number between 1 and 65535, or `[nil, message]` otherwise
|
|
35
47
|
def resolve(value)
|
|
36
48
|
return [nil, "-p/--port requires a value"] if value.nil? || value.empty?
|
|
37
49
|
return [nil, "-p/--port must be a whole number, got #{value.inspect}"] unless value.match?(/\A\d+\z/)
|
|
@@ -20,6 +20,21 @@ module Hecks
|
|
|
20
20
|
# exactly the same kind of link on submit; and the results table once
|
|
21
21
|
# a request actually supplies parameters.
|
|
22
22
|
module QueryFormRenderer
|
|
23
|
+
# Renders one query's whole GET view: header, canonical link, quick links, filter
|
|
24
|
+
# form, any error, results, and the inspect panel.
|
|
25
|
+
#
|
|
26
|
+
# @param registry [Runtime::Registry] the booted registry, for resolving reference
|
|
27
|
+
# fields' target aggregates
|
|
28
|
+
# @param domain [String] the owning chapter's name
|
|
29
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the query belongs to
|
|
30
|
+
# @param query [Bluebook::Query] the query being rendered
|
|
31
|
+
# @param action [String] the form's `action` URL and the canonical link's base
|
|
32
|
+
# @param params [Hash] the request's own query params, pre-filling the filter form
|
|
33
|
+
# @param results [Array<Hash>, nil] the query's own answer rows; `nil` renders no
|
|
34
|
+
# results section
|
|
35
|
+
# @param error [StandardError, nil] a raised error to show in the error banner;
|
|
36
|
+
# `nil` renders none
|
|
37
|
+
# @return [String] the page body markup
|
|
23
38
|
def self.render(registry:, domain:, aggregate:, query:, action:, params: {}, results: nil, error: nil)
|
|
24
39
|
fields = query.attributes.map { |a| FieldShape.resolve(a, aggregate: aggregate) }
|
|
25
40
|
reference_options = ReferenceOptions.collect(registry, domain, fields)
|
|
@@ -35,6 +50,12 @@ module Hecks
|
|
|
35
50
|
HTML
|
|
36
51
|
end
|
|
37
52
|
|
|
53
|
+
# Renders the query's title, description and badges.
|
|
54
|
+
#
|
|
55
|
+
# @param domain [String] the owning chapter's name
|
|
56
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the query belongs to
|
|
57
|
+
# @param query [Bluebook::Query] the query being rendered
|
|
58
|
+
# @return [String] the header markup
|
|
38
59
|
def self.header(domain, aggregate, query)
|
|
39
60
|
<<~HTML
|
|
40
61
|
<h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}")}</h1>
|
|
@@ -43,12 +64,22 @@ module Hecks
|
|
|
43
64
|
HTML
|
|
44
65
|
end
|
|
45
66
|
|
|
67
|
+
# Renders the query's declared badges — today, just its `limit` when it declares one.
|
|
68
|
+
#
|
|
69
|
+
# @param query [Bluebook::Query] the query being rendered
|
|
70
|
+
# @return [String] the badge markup; `""` when the query declares no `limit`
|
|
46
71
|
def self.badges(query)
|
|
47
72
|
parts = []
|
|
48
73
|
parts << %(<span class="badge">limit #{Escape.html(query.limit.to_h[:value])}</span>) if query.limit
|
|
49
74
|
parts.join
|
|
50
75
|
end
|
|
51
76
|
|
|
77
|
+
# Renders the query's plain-GET canonical link template, one `path={path}` per
|
|
78
|
+
# parameter.
|
|
79
|
+
#
|
|
80
|
+
# @param action [String] the query's base URL
|
|
81
|
+
# @param fields [Array<Forms::Field>] the resolved field tree naming the parameters
|
|
82
|
+
# @return [String] the help text and the templated link markup
|
|
52
83
|
def self.canonical_link(action, fields)
|
|
53
84
|
paths = Params.paths(fields)
|
|
54
85
|
template = paths.empty? ? action : "#{action}?#{paths.map { |path| "#{path}={#{path}}" }.join('&')}"
|
|
@@ -64,6 +95,12 @@ module Hecks
|
|
|
64
95
|
# would mean a cross product of links, which reads as noise rather
|
|
65
96
|
# than help. A query with more than one is still fully reachable
|
|
66
97
|
# through the filter form below.
|
|
98
|
+
#
|
|
99
|
+
# @param action [String] the query's base URL
|
|
100
|
+
# @param fields [Array<Forms::Field>] the resolved field tree to search for a
|
|
101
|
+
# `:select` or `:radio` field
|
|
102
|
+
# @return [String] one clickable link per option of the first closed-set field
|
|
103
|
+
# found; `""` when no field is `:select` or `:radio`
|
|
67
104
|
def self.quick_links(action, fields)
|
|
68
105
|
field = fields.find { |f| %i[select radio].include?(f.kind) }
|
|
69
106
|
return "" unless field
|
|
@@ -75,6 +112,14 @@ module Hecks
|
|
|
75
112
|
%(<div class="example-links">#{links.join}</div>)
|
|
76
113
|
end
|
|
77
114
|
|
|
115
|
+
# Renders the GET filter form covering every declared parameter.
|
|
116
|
+
#
|
|
117
|
+
# @param action [String] the form's `action` URL
|
|
118
|
+
# @param fields [Array<Forms::Field>] the resolved field tree to render
|
|
119
|
+
# @param params [Hash] the request's own query params, pre-filling each field
|
|
120
|
+
# @param reference_options [Hash{String => Array<Array(String, String)>, nil}] each
|
|
121
|
+
# `:reference` field's own dropdown options, as `ReferenceOptions.collect` returns
|
|
122
|
+
# @return [String] the form markup; `""` when the query declares no parameters
|
|
78
123
|
def self.filter_form(action, fields, params, reference_options)
|
|
79
124
|
return "" if fields.empty?
|
|
80
125
|
|
|
@@ -86,6 +131,10 @@ module Hecks
|
|
|
86
131
|
HTML
|
|
87
132
|
end
|
|
88
133
|
|
|
134
|
+
# Renders a raised error as a dismissable-looking alert banner.
|
|
135
|
+
#
|
|
136
|
+
# @param error [StandardError, nil] the error to show
|
|
137
|
+
# @return [String] the banner markup; `""` when `error` is `nil`
|
|
89
138
|
def self.error_banner(error)
|
|
90
139
|
return "" unless error
|
|
91
140
|
|
|
@@ -93,12 +142,26 @@ module Hecks
|
|
|
93
142
|
"#{Escape.html(error.message)}</p></div>"
|
|
94
143
|
end
|
|
95
144
|
|
|
145
|
+
# Renders the results heading and table, once a request actually supplied
|
|
146
|
+
# parameters.
|
|
147
|
+
#
|
|
148
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose fields shape the table
|
|
149
|
+
# @param results [Array<Hash>, nil] the query's own answer rows
|
|
150
|
+
# @param domain [String] the owning chapter's name, for building record links
|
|
151
|
+
# @return [String] the results section markup; `""` when `results` is `nil`
|
|
96
152
|
def self.results_section(aggregate, results, domain)
|
|
97
153
|
return "" unless results
|
|
98
154
|
|
|
99
155
|
"<h2>Results (#{results.size})</h2>#{RecordTable.render(aggregate, results, domain: domain)}"
|
|
100
156
|
end
|
|
101
157
|
|
|
158
|
+
# Renders a collapsed panel with the query's own parameter list and raw IR JSON.
|
|
159
|
+
#
|
|
160
|
+
# @param domain [String] the owning chapter's name
|
|
161
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the query belongs to
|
|
162
|
+
# @param query [Bluebook::Query] the query being rendered
|
|
163
|
+
# @param fields [Array<Forms::Field>] the resolved field tree, for the parameter list
|
|
164
|
+
# @return [String] the `<details>` panel markup
|
|
102
165
|
def self.inspect_panel(domain, aggregate, query, fields)
|
|
103
166
|
verb = "#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}"
|
|
104
167
|
paths = Params.paths(fields)
|
|
@@ -11,6 +11,12 @@ module Hecks
|
|
|
11
11
|
# do (facade/surface/aggregate_door.rb) — so they exist for every
|
|
12
12
|
# aggregate whether or not its bluebook declared a query at all.
|
|
13
13
|
module RecordRenderer
|
|
14
|
+
# Renders the index page listing every record of one aggregate.
|
|
15
|
+
#
|
|
16
|
+
# @param registry [Runtime::Registry] the booted registry holding the domain
|
|
17
|
+
# @param domain [String, Symbol] the domain name
|
|
18
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose records are listed
|
|
19
|
+
# @return [String] the index page's rendered HTML
|
|
14
20
|
def self.index(registry:, domain:, aggregate:)
|
|
15
21
|
instances = registry.repository(domain, aggregate).all
|
|
16
22
|
<<~HTML
|
|
@@ -23,6 +29,12 @@ module Hecks
|
|
|
23
29
|
HTML
|
|
24
30
|
end
|
|
25
31
|
|
|
32
|
+
# Renders the "+ Command" buttons for every creating command.
|
|
33
|
+
#
|
|
34
|
+
# @param domain [String, Symbol] the domain name
|
|
35
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose creating commands
|
|
36
|
+
# are linked
|
|
37
|
+
# @return [String] HTML action buttons, or `""` if the aggregate declares none
|
|
26
38
|
def self.creating_links(domain, aggregate)
|
|
27
39
|
creators = aggregate.commands.select(&:creates?)
|
|
28
40
|
return "" if creators.empty?
|
|
@@ -34,6 +46,15 @@ module Hecks
|
|
|
34
46
|
%(<div class="actions">#{links.join}</div>)
|
|
35
47
|
end
|
|
36
48
|
|
|
49
|
+
# Renders one record's state page: its current fields, plus the commands and
|
|
50
|
+
# queries it can dispatch next.
|
|
51
|
+
#
|
|
52
|
+
# @param registry [Runtime::Registry] the booted registry holding the domain
|
|
53
|
+
# @param domain [String, Symbol] the domain name
|
|
54
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
|
|
55
|
+
# @param id [String, Object] the record's identity
|
|
56
|
+
# @return [String, nil] the record page's rendered HTML, or nil if no record
|
|
57
|
+
# has that id
|
|
37
58
|
def self.show(registry:, domain:, aggregate:, id:)
|
|
38
59
|
instance = registry.repository(domain, aggregate).find(id)
|
|
39
60
|
return nil unless instance
|
|
@@ -50,6 +71,10 @@ module Hecks
|
|
|
50
71
|
HTML
|
|
51
72
|
end
|
|
52
73
|
|
|
74
|
+
# Renders a record's state as an HTML field table.
|
|
75
|
+
#
|
|
76
|
+
# @param state [Hash{Symbol => Object}] the record's stored attribute values
|
|
77
|
+
# @return [String] an HTML table of the state's fields, values escaped
|
|
53
78
|
def self.state_table(state)
|
|
54
79
|
rows = state.map do |key, value|
|
|
55
80
|
"<tr><th>#{Escape.html(Humanize.label(key.to_s))}</th><td>#{Escape.html(render_value(value))}</td></tr>"
|
|
@@ -60,6 +85,10 @@ module Hecks
|
|
|
60
85
|
# `Value.materialize` first — a stored field is a `Runtime::Value`
|
|
61
86
|
# wherever its attribute is a value object, not a plain Hash (see
|
|
62
87
|
# record_table.rb's own note on the same read).
|
|
88
|
+
#
|
|
89
|
+
# @param value [Object] a stored field's value, possibly a `Runtime::Value`
|
|
90
|
+
# @return [String] the value rendered as text: a Hash as `"key: value, ..."`
|
|
91
|
+
# pairs, an Array `;`-joined, anything else its own `to_s`
|
|
63
92
|
def self.render_value(value)
|
|
64
93
|
case (value = Runtime::Value.materialize(value))
|
|
65
94
|
when Hash then value.map { |k, v| "#{k}: #{render_value(v)}" }.join(", ")
|
|
@@ -72,6 +101,14 @@ module Hecks
|
|
|
72
101
|
# not apply from the record's current state — the same rule
|
|
73
102
|
# `Rules#admissible_transition` enforces at dispatch, read here so a
|
|
74
103
|
# link that would only refuse is never offered in the first place.
|
|
104
|
+
#
|
|
105
|
+
# @param domain [String, Symbol] the domain name
|
|
106
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
|
|
107
|
+
# @param id [String, Object] the record's identity
|
|
108
|
+
# @param current_state [String, nil] the record's current lifecycle state, or
|
|
109
|
+
# nil if the aggregate has no lifecycle field or it is unset
|
|
110
|
+
# @return [String] an HTML list of admissible command links, or a message
|
|
111
|
+
# when no command currently applies
|
|
75
112
|
def self.command_links(domain, aggregate, id, current_state)
|
|
76
113
|
commands = aggregate.commands.reject(&:creates?).select { |cmd| applies?(aggregate, cmd, current_state) }
|
|
77
114
|
return "<p><em>No commands act on an existing #{Escape.html(aggregate.hecks_name)}.</em></p>" if commands.empty?
|
|
@@ -88,6 +125,14 @@ module Hecks
|
|
|
88
125
|
%(<ul class="verb-list">#{items.join}</ul>)
|
|
89
126
|
end
|
|
90
127
|
|
|
128
|
+
# Whether the aggregate's lifecycle admits dispatching `command` from
|
|
129
|
+
# `current_state`.
|
|
130
|
+
#
|
|
131
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the command belongs to
|
|
132
|
+
# @param command [Bluebook::Command] the command to check
|
|
133
|
+
# @param current_state [String, nil] the record's current lifecycle state
|
|
134
|
+
# @return [Boolean] true if the aggregate declares no lifecycle transitions for
|
|
135
|
+
# this command, or one of them admits `current_state`
|
|
91
136
|
def self.applies?(aggregate, command, current_state)
|
|
92
137
|
transitions = aggregate.lifecycle&.transitions_for(command.hecks_name) || []
|
|
93
138
|
return true if transitions.empty?
|
|
@@ -95,6 +140,12 @@ module Hecks
|
|
|
95
140
|
transitions.any? { |t| t.from.nil? || Array(t.from).map(&:to_s).include?(current_state.to_s) }
|
|
96
141
|
end
|
|
97
142
|
|
|
143
|
+
# Renders the "Queries" link list for an aggregate's own queries.
|
|
144
|
+
#
|
|
145
|
+
# @param domain [String, Symbol] the domain name
|
|
146
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose queries are linked
|
|
147
|
+
# @return [String] an HTML section listing every query, or `""` if the
|
|
148
|
+
# aggregate declares none
|
|
98
149
|
def self.query_links(domain, aggregate)
|
|
99
150
|
return "" if aggregate.queries.empty?
|
|
100
151
|
|
|
@@ -105,6 +156,13 @@ module Hecks
|
|
|
105
156
|
%(<h2>Queries</h2><ul class="verb-list">#{items.join}</ul>)
|
|
106
157
|
end
|
|
107
158
|
|
|
159
|
+
# Renders the full command-and-query index for an aggregate, independent of
|
|
160
|
+
# any one record's state.
|
|
161
|
+
#
|
|
162
|
+
# @param domain [String, Symbol] the domain name
|
|
163
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose verbs are listed
|
|
164
|
+
# @return [String] an HTML section listing every non-creating command and every
|
|
165
|
+
# query, or `""` if the aggregate declares neither
|
|
108
166
|
def self.verb_list(domain, aggregate)
|
|
109
167
|
commands = aggregate.commands.reject(&:creates?)
|
|
110
168
|
return "" if commands.empty? && aggregate.queries.empty?
|
|
@@ -19,12 +19,26 @@ module Hecks
|
|
|
19
19
|
# query's own results (query_form_renderer.rb) — the same records
|
|
20
20
|
# read the same columns either way.
|
|
21
21
|
module RecordTable
|
|
22
|
+
# Picks which fields to show as columns: identity, lifecycle state, then scalar
|
|
23
|
+
# attributes, capped at seven.
|
|
24
|
+
#
|
|
25
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose records will be shown
|
|
26
|
+
# @return [Array<Symbol>] up to 7 unique field names, in that order; excludes
|
|
27
|
+
# reference and list attributes
|
|
22
28
|
def self.columns(aggregate)
|
|
23
29
|
lifecycle = aggregate.lifecycle&.field
|
|
24
30
|
scalars = aggregate.attributes.reject { |a| a.reference? || a.list? }.map(&:name)
|
|
25
31
|
[*aggregate.identity_heads, lifecycle, *scalars].compact.uniq.first(7)
|
|
26
32
|
end
|
|
27
33
|
|
|
34
|
+
# Renders a list of records as an HTML table, one row per record.
|
|
35
|
+
#
|
|
36
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the records belong to
|
|
37
|
+
# @param instances [Array<Runtime::Instance, Forms::Record>] the records to render;
|
|
38
|
+
# anything answering `#id`/`#state` works
|
|
39
|
+
# @param domain [String] the owning chapter's name, for building each row's link
|
|
40
|
+
# @return [String] the table markup; `"<p><em>No records.</em></p>"` when
|
|
41
|
+
# `instances` is empty
|
|
28
42
|
def self.render(aggregate, instances, domain:)
|
|
29
43
|
cols = columns(aggregate)
|
|
30
44
|
head = (["id"] + cols).map { |name| "<th>#{Escape.html(Humanize.label(name.to_s))}</th>" }.join
|
|
@@ -39,6 +53,13 @@ module Hecks
|
|
|
39
53
|
HTML
|
|
40
54
|
end
|
|
41
55
|
|
|
56
|
+
# Renders one record's own table row, linked to its show page.
|
|
57
|
+
#
|
|
58
|
+
# @param instance [Runtime::Instance, Forms::Record] the record to render
|
|
59
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
|
|
60
|
+
# @param cols [Array<Symbol>] the column field names, as `columns` returns them
|
|
61
|
+
# @param domain [String] the owning chapter's name, for building the row's link
|
|
62
|
+
# @return [String] the `<tr>` markup
|
|
42
63
|
def self.row(instance, aggregate, cols, domain)
|
|
43
64
|
cells = cols.map { |name| "<td>#{Escape.html(cell(instance, name))}</td>" }.join
|
|
44
65
|
# L12 — the id is free-form (S3): percent-encoded as the path
|
|
@@ -50,6 +71,12 @@ module Hecks
|
|
|
50
71
|
"<tr><td><a href=\"#{Escape.attr(href)}\">#{Escape.html(instance.id)}</a></td>#{cells}</tr>"
|
|
51
72
|
end
|
|
52
73
|
|
|
74
|
+
# Reads one field's value for display, unwrapped to a single cell value.
|
|
75
|
+
#
|
|
76
|
+
# @param instance [Runtime::Instance, Forms::Record] the record to read
|
|
77
|
+
# @param name [Symbol] the field name to read
|
|
78
|
+
# @return [Object] the field's own value; for a Hash-shaped value object, its
|
|
79
|
+
# first member's value; `""` for `nil`
|
|
53
80
|
def self.cell(instance, name)
|
|
54
81
|
# `state` holds `Runtime::Value` wherever an attribute is a value
|
|
55
82
|
# object, not a plain Hash — `.materialize` is the runtime's own
|
|
@@ -7,12 +7,27 @@ module Hecks
|
|
|
7
7
|
# resolve against the same repository the same way, and neither word
|
|
8
8
|
# owns this more than the other.
|
|
9
9
|
module ReferenceOptions
|
|
10
|
+
# Collects every `:reference` field's own dropdown options out of a resolved field
|
|
11
|
+
# tree.
|
|
12
|
+
#
|
|
13
|
+
# @param registry [Runtime::Registry] the booted registry to read repositories from
|
|
14
|
+
# @param domain [String] the owning chapter's name
|
|
15
|
+
# @param fields [Array<Forms::Field>] the resolved field tree to search
|
|
16
|
+
# @return [Hash{String => Array<Array(String, String)>, nil}] one entry per
|
|
17
|
+
# `:reference` field found, keyed by its `path`; `nil` when the field's target
|
|
18
|
+
# could not be resolved or its options could not be read
|
|
10
19
|
def self.collect(registry, domain, fields)
|
|
11
20
|
targets = {}
|
|
12
21
|
walk(fields) { |field| targets[field.path] = field.target_aggregate if field.kind == :reference }
|
|
13
22
|
targets.transform_values { |aggregate| aggregate && options_for(registry, domain, aggregate) }
|
|
14
23
|
end
|
|
15
24
|
|
|
25
|
+
# Visits every field in a tree, depth-first, including each `:list`/`:group`
|
|
26
|
+
# field's own children.
|
|
27
|
+
#
|
|
28
|
+
# @param fields [Array<Forms::Field>] the field tree to walk
|
|
29
|
+
# @yieldparam field [Forms::Field] each field visited, parent before children
|
|
30
|
+
# @return [void]
|
|
16
31
|
def self.walk(fields, &block)
|
|
17
32
|
fields.each do |field|
|
|
18
33
|
block.call(field)
|
|
@@ -20,6 +35,15 @@ module Hecks
|
|
|
20
35
|
end
|
|
21
36
|
end
|
|
22
37
|
|
|
38
|
+
# Reads up to 200 records' ids as `<select>` options, for one reference field's
|
|
39
|
+
# target aggregate.
|
|
40
|
+
#
|
|
41
|
+
# @param registry [Runtime::Registry] the booted registry to read the repository from
|
|
42
|
+
# @param domain [String] the owning chapter's name
|
|
43
|
+
# @param aggregate [Bluebook::Aggregate] the referenced aggregate to list
|
|
44
|
+
# @return [Array<Array(String, String)>, nil] up to 200 `[id, id]` pairs; `nil` when
|
|
45
|
+
# the repository cannot be resolved (a wiring gap degrades to a plain text id
|
|
46
|
+
# input, not a 500)
|
|
23
47
|
def self.options_for(registry, domain, aggregate)
|
|
24
48
|
registry.repository(domain, aggregate).all.first(200).map { |instance| [instance.id, instance.id] }
|
|
25
49
|
rescue StandardError
|
|
@@ -18,6 +18,8 @@ module Hecks
|
|
|
18
18
|
# every money-shaped VO in the corpus spells it exactly this way,
|
|
19
19
|
# the two-attribute convention the language itself never enforces
|
|
20
20
|
# but every real chapter follows.
|
|
21
|
+
# @param value_object [Bluebook::ValueObject] the value object to classify
|
|
22
|
+
# @return [Boolean] true if its attributes are exactly `cents` and `currency`
|
|
21
23
|
def money?(value_object)
|
|
22
24
|
value_object.attributes.map { |a| a.name.to_s }.sort == %w[cents currency]
|
|
23
25
|
end
|
|
@@ -27,6 +29,10 @@ module Hecks
|
|
|
27
29
|
# [[feedback_name_the_scalar_field]]'s own reasoning, shared here
|
|
28
30
|
# rather than re-decided per caller. Returns the sole attribute,
|
|
29
31
|
# or nil for anything else.
|
|
32
|
+
#
|
|
33
|
+
# @param value_object [Bluebook::ValueObject] the value object to inspect
|
|
34
|
+
# @return [Bluebook::Attribute, nil] its one declared attribute, or nil if
|
|
35
|
+
# it declares zero or more than one
|
|
30
36
|
def sole_attribute(value_object)
|
|
31
37
|
return nil unless value_object.attributes.size == 1
|
|
32
38
|
|
|
@@ -38,6 +44,10 @@ module Hecks
|
|
|
38
44
|
# money-shaped (money's own two members are handled by `money?`
|
|
39
45
|
# instead, since which one governs ordering is a money-specific
|
|
40
46
|
# decision, not a general "pick the first number" one).
|
|
47
|
+
#
|
|
48
|
+
# @param value_object [Bluebook::ValueObject] the value object to inspect
|
|
49
|
+
# @return [Bluebook::Attribute, nil] its first Integer- or Float-typed
|
|
50
|
+
# attribute, or nil if it declares none
|
|
41
51
|
def numeric_member(value_object)
|
|
42
52
|
value_object.attributes.find { |a| %w[Integer Float].include?(a.type.to_s) }
|
|
43
53
|
end
|
data/lib/hecks/fqn.rb
CHANGED
|
@@ -8,10 +8,35 @@ module Hecks
|
|
|
8
8
|
|
|
9
9
|
attr_reader :realm, :domain, :version, :aggregate, :verb, :kind
|
|
10
10
|
|
|
11
|
+
# Builds the FQN of one command on an aggregate.
|
|
12
|
+
#
|
|
13
|
+
# @param realm [String, nil] deployment identity from the world, or nil for an
|
|
14
|
+
# unrealmed address
|
|
15
|
+
# @param domain [String] the domain name
|
|
16
|
+
# @param aggregate [String] the aggregate name the command belongs to
|
|
17
|
+
# @param command [String] the command's PascalCase verb
|
|
18
|
+
# @param version [String, nil] the pinned domain version, or nil for the
|
|
19
|
+
# world's configured latest alias
|
|
20
|
+
# @return [Fqn] the command address
|
|
21
|
+
# @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
|
|
22
|
+
# `command` is not a valid PascalCase command name
|
|
11
23
|
def self.command(realm:, domain:, aggregate:, command:, version: nil)
|
|
12
24
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: command, kind: :command)
|
|
13
25
|
end
|
|
14
26
|
|
|
27
|
+
# Builds the FQN of one query, either on an aggregate or domain-level.
|
|
28
|
+
#
|
|
29
|
+
# @param realm [String, nil] deployment identity from the world, or nil for an
|
|
30
|
+
# unrealmed address
|
|
31
|
+
# @param domain [String] the domain name
|
|
32
|
+
# @param query [String] the query's snake_case verb
|
|
33
|
+
# @param aggregate [String, nil] the aggregate the query belongs to, or nil for
|
|
34
|
+
# a domain-level read model
|
|
35
|
+
# @param version [String, nil] the pinned domain version, or nil for the
|
|
36
|
+
# world's configured latest alias
|
|
37
|
+
# @return [Fqn] the query address
|
|
38
|
+
# @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
|
|
39
|
+
# `query` is not a valid snake_case query name
|
|
15
40
|
def self.query(realm:, domain:, query:, aggregate: nil, version: nil)
|
|
16
41
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: query, kind: :query)
|
|
17
42
|
end
|
|
@@ -22,6 +47,13 @@ module Hecks
|
|
|
22
47
|
# kind) the step before it derived; splitting would mean threading all of
|
|
23
48
|
# them back out as parameters/returns between new methods, for no
|
|
24
49
|
# readability gain over reading the pipeline top to bottom once.
|
|
50
|
+
#
|
|
51
|
+
# @param text [String] a `Realm::Domain::Aggregate.verb` address, with domain
|
|
52
|
+
# optionally `@version`-pinned and aggregate optional for a domain-level query
|
|
53
|
+
# @return [Fqn] the parsed address
|
|
54
|
+
# @raise [Fqn::Invalid] if `text` is not shaped like a FQN, its verb is neither
|
|
55
|
+
# PascalCase nor snake_case, its domain version is malformed, or it names a
|
|
56
|
+
# domain-level command
|
|
25
57
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
26
58
|
def self.parse(text)
|
|
27
59
|
head, separator, verb = text.to_s.rpartition(".")
|
|
@@ -53,9 +85,28 @@ module Hecks
|
|
|
53
85
|
new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: verb, kind: kind)
|
|
54
86
|
end
|
|
55
87
|
|
|
88
|
+
# Whether `name` is a valid command verb: PascalCase.
|
|
89
|
+
#
|
|
90
|
+
# @param name [String, Symbol, #to_s] the candidate verb
|
|
91
|
+
# @return [Boolean] true if `name` matches the PascalCase command shape
|
|
56
92
|
def self.command_name?(name) = /\A[A-Z][A-Za-z0-9]*\z/.match?(name.to_s)
|
|
93
|
+
|
|
94
|
+
# Whether `name` is a valid query verb: snake_case.
|
|
95
|
+
#
|
|
96
|
+
# @param name [String, Symbol, #to_s] the candidate verb
|
|
97
|
+
# @return [Boolean] true if `name` matches the snake_case query shape
|
|
57
98
|
def self.query_name?(name) = /\A[a-z][a-z0-9_]*\z/.match?(name.to_s)
|
|
58
99
|
|
|
100
|
+
# @param realm [String, nil] deployment identity from the world, or nil for an
|
|
101
|
+
# unrealmed address
|
|
102
|
+
# @param domain [String] the domain name
|
|
103
|
+
# @param aggregate [String, nil] the aggregate name, or nil for a domain-level query
|
|
104
|
+
# @param verb [String] the command or query verb
|
|
105
|
+
# @param kind [Symbol, String] `:command` or `:query`
|
|
106
|
+
# @param version [String, nil] the pinned domain version, or nil for the
|
|
107
|
+
# world's configured latest alias
|
|
108
|
+
# @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
|
|
109
|
+
# `verb` does not match the shape required by `kind`
|
|
59
110
|
def initialize(realm:, domain:, aggregate:, verb:, kind:, version: nil)
|
|
60
111
|
@realm = realm && segment(realm, "realm")
|
|
61
112
|
@domain = segment(domain, "domain")
|
|
@@ -68,7 +119,14 @@ module Hecks
|
|
|
68
119
|
raise Invalid, "#{@kind} FQN has an invalid verb #{@verb.inspect}" unless valid
|
|
69
120
|
end
|
|
70
121
|
|
|
122
|
+
# Whether this address names a command.
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean] true if this address's kind is `:command`
|
|
71
125
|
def command? = @kind == :command
|
|
126
|
+
|
|
127
|
+
# Whether this address names a query.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] true if this address's kind is `:query`
|
|
72
130
|
def query? = @kind == :query
|
|
73
131
|
|
|
74
132
|
def to_s
|