hecks 1.3.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +170 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +180 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +11 -11
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +35 -35
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +22 -2
|
@@ -11,11 +11,24 @@ module Hecks
|
|
|
11
11
|
# for the same input either way; only the surrounding `<form>`'s
|
|
12
12
|
# method differs.
|
|
13
13
|
module FieldRenderer
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# Renders one field, and recursively a group's children, as form markup with any held
|
|
15
|
+
# value filled back in.
|
|
16
|
+
#
|
|
17
|
+
# `values` is the hash a sticky re-render (a rejected command,
|
|
18
|
+
# a submitted query) carries back — read with the same dotted path a
|
|
16
19
|
# Field already carries, via `dig`. `reference_options` maps a
|
|
17
20
|
# `:reference` field's own path to `[[id, label], ...]`, built by the
|
|
18
21
|
# caller (it needs a repository; this module stays pure markup).
|
|
22
|
+
#
|
|
23
|
+
# @param field [Forms::Field] the field to render
|
|
24
|
+
# @param values [Hash{String, Symbol => Object}] held values, either flat and keyed by
|
|
25
|
+
# dotted path or nested by path segment; see `dig`
|
|
26
|
+
# @param errors [Hash{String => String}, nil] message per dotted field path; nil for none
|
|
27
|
+
# @param reference_options [Hash{String => Array<Array>, nil}] `[id, label]` pairs per
|
|
28
|
+
# `:reference` field path, as `ReferenceOptions.collect` builds them; a nil or empty
|
|
29
|
+
# entry renders a plain text input
|
|
30
|
+
# @return [String] HTML for the field: a `<fieldset>` for `:group` and `:money`, a
|
|
31
|
+
# `<div class="field">` otherwise
|
|
19
32
|
def self.render(field, values: {}, errors: nil, reference_options: {})
|
|
20
33
|
case field.kind
|
|
21
34
|
when :group then group(field, values, errors, reference_options, tag: "fieldset")
|
|
@@ -25,6 +38,17 @@ module Hecks
|
|
|
25
38
|
end
|
|
26
39
|
end
|
|
27
40
|
|
|
41
|
+
# Renders a field's children inside one enclosing element, with the field's label as
|
|
42
|
+
# its `<legend>`.
|
|
43
|
+
#
|
|
44
|
+
# @param field [Forms::Field] a `:group` or `:money` field carrying `children`
|
|
45
|
+
# @param values [Hash{String, Symbol => Object}] held values; see `dig`
|
|
46
|
+
# @param errors [Hash{String => String}, nil] message per dotted field path
|
|
47
|
+
# @param reference_options [Hash{String => Array<Array>, nil}] `[id, label]` pairs per
|
|
48
|
+
# `:reference` field path
|
|
49
|
+
# @param tag [String] name of the enclosing element, such as `"fieldset"`
|
|
50
|
+
# @param css [String, nil] class attribute for the enclosing element; nil for none
|
|
51
|
+
# @return [String] HTML for the enclosing element and every child
|
|
28
52
|
def self.group(field, values, errors, reference_options, tag:, css: nil)
|
|
29
53
|
inner = field.children.map { |child| render(child, values: values, errors: errors, reference_options: reference_options) }
|
|
30
54
|
<<~HTML
|
|
@@ -35,6 +59,14 @@ module Hecks
|
|
|
35
59
|
HTML
|
|
36
60
|
end
|
|
37
61
|
|
|
62
|
+
# Renders a `:list` field as one textarea taking an element per line, which is the shape
|
|
63
|
+
# `Params.extract_list` reads back.
|
|
64
|
+
#
|
|
65
|
+
# @param field [Forms::Field] a `:list` field whose first child describes one element
|
|
66
|
+
# @param values [Hash{String, Symbol => Object}] held values; the list's entry may be an
|
|
67
|
+
# Array, a newline-joined String, or absent
|
|
68
|
+
# @param _errors [Hash{String => String}, nil] ignored; a list shows no per-field message
|
|
69
|
+
# @return [String] HTML for the labelled textarea and its help line
|
|
38
70
|
def self.list(field, values, _errors)
|
|
39
71
|
item = field.children.first
|
|
40
72
|
current = Array(dig(values, field.path)).join("\n")
|
|
@@ -48,6 +80,15 @@ module Hecks
|
|
|
48
80
|
HTML
|
|
49
81
|
end
|
|
50
82
|
|
|
83
|
+
# Renders a single-value field with the control its `kind` calls for, wrapped with its
|
|
84
|
+
# label, help text and error.
|
|
85
|
+
#
|
|
86
|
+
# @param field [Forms::Field] a leaf field
|
|
87
|
+
# @param values [Hash{String, Symbol => Object}] held values; see `dig`
|
|
88
|
+
# @param errors [Hash{String => String}, nil] message per dotted field path
|
|
89
|
+
# @param reference_options [Hash{String => Array<Array>, nil}] `[id, label]` pairs per
|
|
90
|
+
# `:reference` field path
|
|
91
|
+
# @return [String] HTML for the `<div class="field">` block
|
|
51
92
|
def self.leaf(field, values, errors, reference_options)
|
|
52
93
|
value = dig(values, field.path)
|
|
53
94
|
error = errors && errors[field.path]
|
|
@@ -62,6 +103,14 @@ module Hecks
|
|
|
62
103
|
wrap(field, body, error)
|
|
63
104
|
end
|
|
64
105
|
|
|
106
|
+
# Wraps a rendered control with its label, help text and error message; a `:boolean`
|
|
107
|
+
# gets no label here because `checkbox` renders its own beside the box.
|
|
108
|
+
#
|
|
109
|
+
# @param field [Forms::Field] the field the control belongs to
|
|
110
|
+
# @param body [String] HTML of the control itself
|
|
111
|
+
# @param error [String, nil] message shown as an alert; nil for none
|
|
112
|
+
# @return [String] HTML for the `<div class="field">` block, classed `has-error` when
|
|
113
|
+
# `error` is given
|
|
65
114
|
def self.wrap(field, body, error)
|
|
66
115
|
<<~HTML
|
|
67
116
|
<div class="field#{' has-error' if error}">
|
|
@@ -73,6 +122,13 @@ module Hecks
|
|
|
73
122
|
HTML
|
|
74
123
|
end
|
|
75
124
|
|
|
125
|
+
# Renders an `<input>` of the field's `html_type`, falling back to the field's default
|
|
126
|
+
# for a missing value; a whole-cents number also gets a live preview span.
|
|
127
|
+
#
|
|
128
|
+
# @param field [Forms::Field] a leaf field
|
|
129
|
+
# @param value [Object, nil] the held value, rendered with `to_s`; nil shows the default
|
|
130
|
+
# @return [String] HTML for the input, followed by the preview `<span>` when the field
|
|
131
|
+
# is a `:number` whose help text mentions cents
|
|
76
132
|
def self.input(field, value)
|
|
77
133
|
money = field.kind == :number && field.help.to_s.include?("cents")
|
|
78
134
|
tag = Tag.void("input", id: dom_id(field.path), name: field.path, type: field.html_type,
|
|
@@ -84,10 +140,23 @@ module Hecks
|
|
|
84
140
|
%(#{tag} <span id="#{dom_id(field.path)}-preview" class="mono help" aria-live="polite"></span>)
|
|
85
141
|
end
|
|
86
142
|
|
|
143
|
+
# Renders a `<textarea>` holding the value as escaped text; unlike `input`, the field's
|
|
144
|
+
# default is not filled in.
|
|
145
|
+
#
|
|
146
|
+
# @param field [Forms::Field] a `:textarea` field
|
|
147
|
+
# @param value [Object, nil] the held value, rendered with `to_s`; nil renders empty
|
|
148
|
+
# @return [String] HTML for the textarea
|
|
87
149
|
def self.textarea(field, value)
|
|
88
150
|
%(<textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{Escape.html(value)}</textarea>)
|
|
89
151
|
end
|
|
90
152
|
|
|
153
|
+
# Renders a checkbox preceded by a hidden `"0"` input of the same name, so an unticked
|
|
154
|
+
# box still submits a value, with the field's label beside it.
|
|
155
|
+
#
|
|
156
|
+
# @param field [Forms::Field] a `:boolean` field
|
|
157
|
+
# @param value [Boolean, String, nil] the held value; `true`, `"true"`, `"on"` and `"1"`
|
|
158
|
+
# tick the box, and nil falls back to a default of exactly `true`
|
|
159
|
+
# @return [String] HTML for the checkbox row
|
|
91
160
|
def self.checkbox(field, value)
|
|
92
161
|
checked = value.nil? ? field.default == true : [true, "true", "on", "1"].include?(value)
|
|
93
162
|
<<~HTML
|
|
@@ -99,6 +168,13 @@ module Hecks
|
|
|
99
168
|
HTML
|
|
100
169
|
end
|
|
101
170
|
|
|
171
|
+
# Renders one radio button per option, pre-checking the one equal to the held value or,
|
|
172
|
+
# failing that, to the field's default.
|
|
173
|
+
#
|
|
174
|
+
# @param field [Forms::Field] a `:radio` field whose `options` are `[value, label]` pairs
|
|
175
|
+
# @param value [Object, nil] the held value, compared with `to_s`; nil falls back to the
|
|
176
|
+
# default
|
|
177
|
+
# @return [String] HTML for the `<div role="radiogroup">` block
|
|
102
178
|
def self.radio_group(field, value)
|
|
103
179
|
selected = value || field.default
|
|
104
180
|
options = field.options.map do |option_value, option_label|
|
|
@@ -111,6 +187,13 @@ module Hecks
|
|
|
111
187
|
%(<div class="radio-group" role="radiogroup">#{options.join}</div>)
|
|
112
188
|
end
|
|
113
189
|
|
|
190
|
+
# Renders a `<select>` over the field's fixed options, with a blank "—" entry first when
|
|
191
|
+
# the field is optional.
|
|
192
|
+
#
|
|
193
|
+
# @param field [Forms::Field] a `:select` field whose `options` are `[value, label]` pairs
|
|
194
|
+
# @param value [Object, nil] the held value, compared with `to_s`; nil falls back to the
|
|
195
|
+
# default
|
|
196
|
+
# @return [String] HTML for the select
|
|
114
197
|
def self.select(field, value)
|
|
115
198
|
selected = value || field.default
|
|
116
199
|
options = field.options.map do |option_value, option_label|
|
|
@@ -121,6 +204,16 @@ module Hecks
|
|
|
121
204
|
%(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{options.join}</select>)
|
|
122
205
|
end
|
|
123
206
|
|
|
207
|
+
# Renders a `<select>` of existing records for a `:reference` field, degrading to a plain
|
|
208
|
+
# text id input when no records are on offer.
|
|
209
|
+
#
|
|
210
|
+
# @param field [Forms::Field] a `:reference` field; its `html_type` is set to `"text"`
|
|
211
|
+
# in place when the text fallback is taken
|
|
212
|
+
# @param value [Object, nil] the held id, compared with `to_s`; nil preselects the
|
|
213
|
+
# disabled "choose one…" entry of a required field
|
|
214
|
+
# @param options [Array<Array>, nil] `[id, label]` pairs to offer; nil or empty takes the
|
|
215
|
+
# text fallback
|
|
216
|
+
# @return [String] HTML for the select, or for the fallback `<input>`
|
|
124
217
|
def self.reference_select(field, value, options)
|
|
125
218
|
return input(field.tap { |f| f.html_type = "text" }, value) unless options && !options.empty?
|
|
126
219
|
|
|
@@ -135,30 +228,73 @@ module Hecks
|
|
|
135
228
|
%(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{rendered.join}</select>)
|
|
136
229
|
end
|
|
137
230
|
|
|
231
|
+
# Renders the asterisk that marks a required field's label or legend.
|
|
232
|
+
#
|
|
233
|
+
# @param field [Forms::Field] the field being labelled
|
|
234
|
+
# @return [String] HTML for the mark, or `""` when the field is optional
|
|
138
235
|
def self.required_mark(field) = field.required? ? %(<span class="required-mark" title="required">*</span>) : ""
|
|
236
|
+
|
|
237
|
+
# Derives the element id of a field's control from its dotted path, such as
|
|
238
|
+
# `"f-amount-cents"` for `"amount.cents"`.
|
|
239
|
+
#
|
|
240
|
+
# @param path [String, Symbol] the field's dotted path
|
|
241
|
+
# @return [String] the id, `"f-"` followed by the path with dots turned to hyphens
|
|
139
242
|
def self.dom_id(path) = "f-#{path.to_s.tr('.', '-')}"
|
|
243
|
+
|
|
244
|
+
# Picks the `pattern` attribute an input carries, withholding it from a `:reference`
|
|
245
|
+
# field, whose input takes a record id rather than the attribute's own value.
|
|
246
|
+
#
|
|
247
|
+
# @param field [Forms::Field] a leaf field
|
|
248
|
+
# @return [String, nil] the regex source the attribute declares as `pattern:`; nil for a
|
|
249
|
+
# `:reference` field or when none is declared
|
|
140
250
|
def self.leaf_pattern(field) = field.kind == :reference ? nil : field.pattern
|
|
251
|
+
|
|
252
|
+
# Names the preview span a whole-cents input's `data-money-cents` attribute points at.
|
|
253
|
+
#
|
|
254
|
+
# @param field [Forms::Field] the cents field
|
|
255
|
+
# @return [String] a CSS id selector, such as `"#f-amount-cents-preview"`
|
|
141
256
|
def self.money_preview_target(field) = "##{dom_id(field.path)}-preview"
|
|
142
257
|
|
|
258
|
+
# Renders the `required` and `aria-describedby` attributes of a control as an
|
|
259
|
+
# attribute string, for the controls assembled by hand rather than through `Tag.void`.
|
|
260
|
+
#
|
|
261
|
+
# @param field [Forms::Field] the field the control belongs to
|
|
262
|
+
# @return [String] the rendered attributes, or `""` when the field is optional and has
|
|
263
|
+
# no help text
|
|
143
264
|
def self.aria(field)
|
|
144
265
|
Tag.attrs(required: field.required?, aria_describedby: field.help ? "#{dom_id(field.path)}-help" : nil)
|
|
145
266
|
end
|
|
146
267
|
|
|
268
|
+
# Builds the `required` and `aria-describedby` attributes of a control as a Hash, to
|
|
269
|
+
# splat into `Tag.void`.
|
|
270
|
+
#
|
|
271
|
+
# @param field [Forms::Field] the field the control belongs to
|
|
272
|
+
# @return [Hash{Symbol => Boolean, String, nil}] `:required` and `:aria_describedby`;
|
|
273
|
+
# the latter is the help span's element id, or nil when the field has no help text
|
|
147
274
|
def self.aria_attrs(field)
|
|
148
275
|
{ required: field.required?, aria_describedby: field.help ? "#{dom_id(field.path)}-help" : nil }
|
|
149
276
|
end
|
|
150
277
|
|
|
151
|
-
#
|
|
152
|
-
#
|
|
278
|
+
# Reads the value held for a dotted field path out of either spelling a form's values
|
|
279
|
+
# arrive in, flat or nested, without losing a stored `false`.
|
|
280
|
+
#
|
|
281
|
+
# `values` is a nested hash; `path` a dotted string using the same
|
|
282
|
+
# segment spelling the hash keys are built from (`Params.nest` in
|
|
153
283
|
# params.rb) — symbols one level down from a group, strings at the
|
|
154
284
|
# flat top when a sticky POST re-render hands raw params back untouched.
|
|
285
|
+
#
|
|
286
|
+
# @param values [Hash{String, Symbol => Object}, Object] held values; anything that is
|
|
287
|
+
# not a Hash, nil included, reads as nil
|
|
288
|
+
# @param path [String, Symbol] the field's dotted path, such as `"amount.cents"`
|
|
289
|
+
# @return [Object, nil] the held value exactly as stored (a raw form String, or a typed
|
|
290
|
+
# value from a record's state); nil when no key on the path answers
|
|
155
291
|
def self.dig(values, path)
|
|
156
292
|
return nil unless values.is_a?(Hash)
|
|
157
293
|
|
|
158
|
-
# A sticky re-render after a refused submission hands back the
|
|
294
|
+
# A sticky re-render after a refused submission hands back the raw
|
|
159
295
|
# flat params (`{"amount.cents"=>"1050"}` — the same shape the form
|
|
160
296
|
# posted, string values and all, dotted key intact) ; a prefill from
|
|
161
|
-
# an existing record's own state hands back a
|
|
297
|
+
# an existing record's own state hands back a nested hash instead
|
|
162
298
|
# (`{amount: {cents: 1050}}`). Flat wins when both would answer,
|
|
163
299
|
# since only the raw form is ever what the caller actually typed.
|
|
164
300
|
# `key?` decides which spelling answers, at every step below —
|
|
@@ -1,37 +1,57 @@
|
|
|
1
|
+
require_relative "../vocabulary"
|
|
1
2
|
require_relative "../bluebook/attribute"
|
|
2
3
|
require_relative "../naming"
|
|
3
4
|
require_relative "value_object_shape"
|
|
4
5
|
|
|
5
6
|
module Hecks
|
|
6
7
|
module Forms
|
|
7
|
-
# One resolved field, ready for a renderer to turn into markup. A
|
|
8
|
+
# One resolved field, ready for a renderer to turn into markup. A leaf
|
|
8
9
|
# scalar carries `kind`/`options`/etc; a `:group` or `:list` carries
|
|
9
10
|
# `children` instead and nothing else on itself.
|
|
10
11
|
#
|
|
11
|
-
# `path` is the
|
|
12
|
+
# `path` is the dotted field path this attribute is reached at —
|
|
12
13
|
# `"email.address"`, `"amount.cents"` — the same convention the language
|
|
13
14
|
# already uses for a query's own cross-object `where` clauses
|
|
14
15
|
# (`where(:"customer.status" => ...)`), not a fresh one invented here. A
|
|
15
16
|
# command receives its arguments as nested hashes
|
|
16
|
-
# (`email: {address: "..."}`), so `Params.
|
|
17
|
+
# (`email: {address: "..."}`), so `Params.extract` (params.rb) walks
|
|
17
18
|
# this same dotted spelling back apart on submit — one convention, two
|
|
18
19
|
# directions.
|
|
19
20
|
Field = Struct.new(
|
|
20
21
|
:path, :label, :kind, :html_type, :options, :children, :default, :optional,
|
|
21
22
|
:pattern, :step, :help, :target_aggregate, keyword_init: true
|
|
22
23
|
) do
|
|
24
|
+
# Tells whether this field holds a value itself rather than carrying `children`.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean] false for a `:group` or `:list`, true for every other kind,
|
|
27
|
+
# `:money` included
|
|
23
28
|
def leaf? = kind != :group && kind != :list
|
|
29
|
+
|
|
30
|
+
# Tells whether a submission may leave this field blank.
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean, nil] the `optional` member as given; nil when the field was built
|
|
33
|
+
# without one, which reads as required
|
|
24
34
|
def optional? = optional
|
|
35
|
+
|
|
36
|
+
# Tells whether a submission must fill this field in.
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] true unless `optional` was set truthy
|
|
25
39
|
def required? = !optional
|
|
26
40
|
end
|
|
27
41
|
|
|
28
42
|
# Turns a wire-spelled field or path segment into the plain-English text
|
|
29
43
|
# `FieldShape` embeds as a `Field`'s label or a group's legend.
|
|
30
44
|
module Humanize
|
|
45
|
+
# Humanizes the last segment of a field name or dotted path into sentence-case words.
|
|
46
|
+
#
|
|
31
47
|
# "given" -> "Given", "daily_limit" -> "Daily limit", "end_to_end" ->
|
|
32
48
|
# "End to end" — the label a plain reader wants, not the wire spelling.
|
|
49
|
+
#
|
|
50
|
+
# @param text [String, Symbol, nil] a field name or dotted path, such as
|
|
51
|
+
# `"amount.cents"`; only the part after the last `"."` is used
|
|
52
|
+
# @return [String] the humanized words, or `""` when `text` is nil or empty
|
|
33
53
|
def self.label(text)
|
|
34
|
-
# Split on "."
|
|
54
|
+
# Split on "." first, alone, to take only the last path segment —
|
|
35
55
|
# "daily_limit" is one segment (the underscore is a word break
|
|
36
56
|
# inside it, not a path hop) and must keep both its words; only a
|
|
37
57
|
# genuinely dotted path ("amount.cents") drops everything before
|
|
@@ -39,14 +59,19 @@ module Hecks
|
|
|
39
59
|
segment = text.to_s.split(".").last.to_s
|
|
40
60
|
return segment if segment.empty?
|
|
41
61
|
|
|
42
|
-
# The word split itself is `Naming.words`
|
|
43
|
-
#
|
|
62
|
+
# The word split itself is `Naming.words` — the one humanizer,
|
|
63
|
+
# shared with the glossary projection.
|
|
44
64
|
Naming.words(segment)
|
|
45
65
|
end
|
|
46
66
|
|
|
67
|
+
# Humanizes every segment of a dotted path and joins them with " → ".
|
|
68
|
+
#
|
|
47
69
|
# The full dotted path, each segment humanized and joined with
|
|
48
70
|
# " → " for a fieldset legend spanning more than one hop
|
|
49
71
|
# ("Amount → Cents") — a group's own legend, not a leaf's label.
|
|
72
|
+
#
|
|
73
|
+
# @param path [String, Symbol, nil] a dotted field path, such as `"amount.cents"`
|
|
74
|
+
# @return [String] the humanized trail, or `""` when `path` is nil or empty
|
|
50
75
|
def self.breadcrumb(path)
|
|
51
76
|
path.to_s.split(".").map { |part| label(part) }.join(" → ")
|
|
52
77
|
end
|
|
@@ -61,12 +86,23 @@ module Hecks
|
|
|
61
86
|
module FieldShape
|
|
62
87
|
PRIMITIVES = Bluebook::Attribute::PRIMITIVES
|
|
63
88
|
|
|
64
|
-
#
|
|
89
|
+
# Resolves one declared attribute into the field a form renders for it, choosing the
|
|
90
|
+
# input kind from the attribute's type, pattern, `admits:` set and value-object shape.
|
|
91
|
+
#
|
|
92
|
+
# `aggregate:` is the Aggregate that owns this attribute (a
|
|
65
93
|
# command's, a query's, or — recursively — a value object's) —
|
|
66
94
|
# needed to resolve `reference_to`, `admits:`, and a same-chapter
|
|
67
95
|
# value object by name. `path:` defaults to the attribute's own name;
|
|
68
96
|
# a caller resolving a nested value object's attribute passes the
|
|
69
97
|
# dotted path so far.
|
|
98
|
+
#
|
|
99
|
+
# @param attribute [Bluebook::Attribute] the attribute to build a field for
|
|
100
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that owns the attribute
|
|
101
|
+
# @param path [String] dotted field path the attribute is reached at
|
|
102
|
+
# @return [Forms::Field] the resolved field; a single-attribute value object resolves
|
|
103
|
+
# to its inner leaf, so the returned path may be longer than `path`
|
|
104
|
+
# @raise [Bluebook::DSL::Malformed] if a `reference_to` attribute cannot say which
|
|
105
|
+
# aggregate declares it
|
|
70
106
|
def self.resolve(attribute, aggregate:, path: attribute.name.to_s)
|
|
71
107
|
return list_field(attribute, aggregate, path) if attribute.list?
|
|
72
108
|
|
|
@@ -80,6 +116,15 @@ module Hecks
|
|
|
80
116
|
primitive_field(attribute, common)
|
|
81
117
|
end
|
|
82
118
|
|
|
119
|
+
# Builds the `:list` field for a `list_of` attribute, carrying as its one child the field
|
|
120
|
+
# a single element would resolve to.
|
|
121
|
+
#
|
|
122
|
+
# @param attribute [Bluebook::Attribute] the list attribute
|
|
123
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that owns the attribute
|
|
124
|
+
# @param path [String] dotted field path the list is reached at
|
|
125
|
+
# @return [Forms::Field] a `:list` field whose `children` holds exactly one element field
|
|
126
|
+
# @raise [Bluebook::DSL::Malformed] if the element is a `reference_to` that cannot say
|
|
127
|
+
# which aggregate declares it
|
|
83
128
|
def self.list_field(attribute, aggregate, path)
|
|
84
129
|
# The scalar shape one element of this list would take, so a
|
|
85
130
|
# renderer can say what belongs on each line without a second
|
|
@@ -92,6 +137,17 @@ module Hecks
|
|
|
92
137
|
children: [resolve(scalar, aggregate: aggregate, path: path)])
|
|
93
138
|
end
|
|
94
139
|
|
|
140
|
+
# Builds the `:reference` field for a `reference_to` attribute, remembering the target
|
|
141
|
+
# aggregate so a renderer can offer its records.
|
|
142
|
+
#
|
|
143
|
+
# @param attribute [Bluebook::Attribute] an attribute whose type is a
|
|
144
|
+
# `Bluebook::Reference`
|
|
145
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares, keyed
|
|
146
|
+
# `:path`, `:label`, `:default`, `:optional`, `:pattern`
|
|
147
|
+
# @return [Forms::Field] a `:reference` field; its `target_aggregate` is nil when the
|
|
148
|
+
# target belongs to a domain that is not loaded
|
|
149
|
+
# @raise [Bluebook::DSL::Malformed] if the reference cannot say which aggregate
|
|
150
|
+
# declares it
|
|
95
151
|
def self.reference_field(attribute, common)
|
|
96
152
|
target = attribute.type.resolve
|
|
97
153
|
Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target,
|
|
@@ -102,27 +158,40 @@ module Hecks
|
|
|
102
158
|
end)
|
|
103
159
|
end
|
|
104
160
|
|
|
105
|
-
# `admits:` names a closed set
|
|
161
|
+
# Builds the radio or select field for an attribute whose `admits:` names a closed set,
|
|
162
|
+
# offering exactly that set's members.
|
|
163
|
+
#
|
|
164
|
+
# `admits:` names a closed set declared elsewhere (`"Account::
|
|
106
165
|
# LedgerDirection"`) that the value must belong to — see
|
|
107
166
|
# Runtime::Value::Admission#admitted_members, which this mirrors
|
|
108
167
|
# exactly (same split, same chapter walk, same discriminant rule) so
|
|
109
168
|
# a rendered `<select>` never offers a member the runtime would then
|
|
110
169
|
# refuse.
|
|
170
|
+
#
|
|
171
|
+
# @param attribute [Bluebook::Attribute] an attribute declaring `admits:`
|
|
172
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that owns the attribute; its
|
|
173
|
+
# chapter is searched for the named set
|
|
174
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares, keyed
|
|
175
|
+
# `:path`, `:label`, `:default`, `:optional`, `:pattern`
|
|
176
|
+
# @return [Forms::Field] a `:radio` or `:select` field, its path extended by one hop
|
|
177
|
+
# when the attribute's own type is a single-attribute value object; a plain
|
|
178
|
+
# primitive field when the named set is not declared
|
|
111
179
|
def self.admitted_field(attribute, aggregate, common)
|
|
112
180
|
set_aggregate_name, set_name = attribute.admits.to_s.split("::", 2)
|
|
113
181
|
chapter = aggregate.hecks_owner
|
|
114
182
|
set = set_name && chapter&.aggregate(set_aggregate_name)&.value_object(set_name)
|
|
115
|
-
|
|
183
|
+
# undeclared — refuse-at-dispatch stays the backstop
|
|
184
|
+
return primitive_field(attribute, common) unless set
|
|
116
185
|
|
|
117
186
|
options = select_or_radio(common, closed_set_options(set))
|
|
118
|
-
# The attribute's
|
|
187
|
+
# The attribute's own type still has to be built the shape coercion
|
|
119
188
|
# expects (`Value::Coercion#fields_for` refuses anything that
|
|
120
189
|
# isn't a Hash or a Value for a value-object-typed attribute) — a
|
|
121
190
|
# plain String attribute stays a bare scalar, but a value object
|
|
122
191
|
# like `MovementDirection { value }` still needs the ".value" hop
|
|
123
|
-
# even though the
|
|
192
|
+
# even though the set it's checked against (`admits:`) is declared
|
|
124
193
|
# somewhere else entirely. Same unwrap `value_object_field` does,
|
|
125
|
-
# kept separate because an admitted set changes the
|
|
194
|
+
# kept separate because an admitted set changes the options, not
|
|
126
195
|
# which field the hop lands on.
|
|
127
196
|
own_shape = own_value_object(attribute, aggregate)
|
|
128
197
|
inner = own_shape && ValueObjectShape.sole_attribute(own_shape)
|
|
@@ -132,10 +201,28 @@ module Hecks
|
|
|
132
201
|
options
|
|
133
202
|
end
|
|
134
203
|
|
|
204
|
+
# Finds the value object an attribute's type names, looking on the owning aggregate
|
|
205
|
+
# first and then on every other aggregate of the same chapter.
|
|
206
|
+
#
|
|
207
|
+
# @param attribute [Bluebook::Attribute] the attribute whose type names a value object
|
|
208
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that owns the attribute
|
|
209
|
+
# @return [Bluebook::ValueObject, nil] the declared shape, or nil when no aggregate in
|
|
210
|
+
# the chapter declares a value object of that name
|
|
135
211
|
def self.own_value_object(attribute, aggregate)
|
|
136
212
|
aggregate.value_object(attribute.type) || cross_aggregate_value_object(aggregate, attribute.type)
|
|
137
213
|
end
|
|
138
214
|
|
|
215
|
+
# Builds the field for an attribute typed as a value object: a closed-set choice, a
|
|
216
|
+
# money pair, the unwrapped inner leaf of a single-attribute shape, or a group.
|
|
217
|
+
#
|
|
218
|
+
# @param attribute [Bluebook::Attribute] an attribute whose type is not a primitive
|
|
219
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that owns the attribute
|
|
220
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares, keyed
|
|
221
|
+
# `:path`, `:label`, `:default`, `:optional`, `:pattern`
|
|
222
|
+
# @return [Forms::Field] the resolved field; a plain primitive field when the named
|
|
223
|
+
# value object is not declared anywhere in the chapter
|
|
224
|
+
# @raise [Bluebook::DSL::Malformed] if a nested `reference_to` attribute cannot say
|
|
225
|
+
# which aggregate declares it
|
|
139
226
|
def self.value_object_field(attribute, aggregate, common)
|
|
140
227
|
shape = own_value_object(attribute, aggregate)
|
|
141
228
|
return primitive_field(attribute, common) unless shape
|
|
@@ -144,9 +231,9 @@ module Hecks
|
|
|
144
231
|
return money_field(shape, common) if ValueObjectShape.money?(shape)
|
|
145
232
|
|
|
146
233
|
# A single-attribute value object (EmailAddress{address}, CustomerNumber{value})
|
|
147
|
-
# is a
|
|
234
|
+
# is a name for a scalar, not a genuine group — unwrap it so the form
|
|
148
235
|
# asks for one thing ("Email address") instead of a one-item fieldset,
|
|
149
|
-
# and so the inner attribute's
|
|
236
|
+
# and so the inner attribute's own pattern (the real email regex,
|
|
150
237
|
# declared on `address`, not on the outer `email` attribute) drives
|
|
151
238
|
# the input type. [[feedback_name_the_scalar_field]] says the same
|
|
152
239
|
# thing about Ruby call sites; a form asks the identical question.
|
|
@@ -158,6 +245,14 @@ module Hecks
|
|
|
158
245
|
group_field(shape, aggregate, common)
|
|
159
246
|
end
|
|
160
247
|
|
|
248
|
+
# Searches every aggregate of the owning chapter for a value object by name, so a shape
|
|
249
|
+
# declared on a sibling aggregate still resolves.
|
|
250
|
+
#
|
|
251
|
+
# @param aggregate [Bluebook::Aggregate] any aggregate of the chapter to search
|
|
252
|
+
# @param type_name [String] name of the value object, as `Bluebook::Attribute#type`
|
|
253
|
+
# spells it
|
|
254
|
+
# @return [Bluebook::ValueObject, nil] the first match in aggregate declaration order, or
|
|
255
|
+
# nil when none declares it or the aggregate has no owning chapter
|
|
161
256
|
def self.cross_aggregate_value_object(aggregate, type_name)
|
|
162
257
|
aggregate.hecks_owner&.aggregates&.each do |sibling|
|
|
163
258
|
found = sibling.value_object(type_name)
|
|
@@ -166,11 +261,31 @@ module Hecks
|
|
|
166
261
|
nil
|
|
167
262
|
end
|
|
168
263
|
|
|
264
|
+
# Builds the `:group` field for a multi-attribute value object, resolving each of its
|
|
265
|
+
# attributes one path hop below the group.
|
|
266
|
+
#
|
|
267
|
+
# @param shape [Bluebook::ValueObject] the value object whose attributes become children
|
|
268
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the nested attributes resolve
|
|
269
|
+
# against
|
|
270
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares; only
|
|
271
|
+
# `:path`, `:label` and `:optional` are read
|
|
272
|
+
# @return [Forms::Field] a `:group` field with one child per attribute of `shape`
|
|
273
|
+
# @raise [Bluebook::DSL::Malformed] if a nested `reference_to` attribute cannot say
|
|
274
|
+
# which aggregate declares it
|
|
169
275
|
def self.group_field(shape, aggregate, common)
|
|
170
276
|
children = shape.attributes.map { |inner| resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}") }
|
|
171
277
|
Field.new(path: common[:path], label: common[:label], kind: :group, optional: common[:optional], children: children)
|
|
172
278
|
end
|
|
173
279
|
|
|
280
|
+
# Builds the `:money` field for a `cents`/`currency` value object: a required-or-not
|
|
281
|
+
# whole-cents number input beside an always-optional currency code.
|
|
282
|
+
#
|
|
283
|
+
# @param shape [Bluebook::ValueObject] the money-shaped value object, read for the
|
|
284
|
+
# declared defaults of `cents` and `currency`
|
|
285
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares; only
|
|
286
|
+
# `:path`, `:label` and `:optional` are read
|
|
287
|
+
# @return [Forms::Field] a `:money` field whose children are the `cents` and `currency`
|
|
288
|
+
# leaves, the currency defaulting to `"USD"` when the shape declares none
|
|
174
289
|
def self.money_field(shape, common)
|
|
175
290
|
cents = Field.new(path: "#{common[:path]}.cents", label: "Amount (cents)", kind: :number,
|
|
176
291
|
html_type: "number", step: "1", optional: common[:optional],
|
|
@@ -182,27 +297,57 @@ module Hecks
|
|
|
182
297
|
children: [cents, currency])
|
|
183
298
|
end
|
|
184
299
|
|
|
300
|
+
# Lists a closed set's members by their discriminant — the value object's first
|
|
301
|
+
# attribute — as the strings a form offers.
|
|
302
|
+
#
|
|
303
|
+
# @param value_object [Bluebook::ValueObject] a closed-set (`one_of`) value object
|
|
304
|
+
# @return [Array<String>] one discriminant value per declared member, in declaration
|
|
305
|
+
# order; `[]` when the shape declares no members
|
|
185
306
|
def self.closed_set_options(value_object)
|
|
186
307
|
discriminant = value_object.attributes.first.name
|
|
187
308
|
value_object.members.map { |member| member[discriminant].to_s }
|
|
188
309
|
end
|
|
189
310
|
|
|
311
|
+
# Builds the radio or select field for an attribute typed as a closed set, its path
|
|
312
|
+
# extended by the set's discriminant attribute.
|
|
313
|
+
#
|
|
190
314
|
# A `one_of` shape (`AccountKind{name}`, `LedgerDirection{value}`) is
|
|
191
|
-
#
|
|
315
|
+
# always single-attribute in this language — the discriminant is the
|
|
192
316
|
# whole value object — so the field's own path always gains that one
|
|
193
317
|
# hop; nothing to branch on the way `admitted_field` has to (a set
|
|
194
318
|
# named by `admits:` may sit on a multi-field value object it doesn't
|
|
195
319
|
# itself define the members of).
|
|
320
|
+
#
|
|
321
|
+
# @param shape [Bluebook::ValueObject] the closed-set value object
|
|
322
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares, keyed
|
|
323
|
+
# `:path`, `:label`, `:default`, `:optional`, `:pattern`
|
|
324
|
+
# @return [Forms::Field] a `:radio` or `:select` field offering the set's members
|
|
196
325
|
def self.closed_set_field(shape, common)
|
|
197
326
|
discriminant = shape.attributes.first.name
|
|
198
327
|
select_or_radio(common.merge(path: "#{common[:path]}.#{discriminant}"), closed_set_options(shape))
|
|
199
328
|
end
|
|
200
329
|
|
|
330
|
+
# Builds a choice field over fixed options, as radio buttons for four or fewer and a
|
|
331
|
+
# `<select>` beyond that.
|
|
332
|
+
#
|
|
333
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares, keyed
|
|
334
|
+
# `:path`, `:label`, `:default`, `:optional`, `:pattern`
|
|
335
|
+
# @param options [Array<String>] the values offered; each is its own label
|
|
336
|
+
# @return [Forms::Field] a `:radio` or `:select` field whose `options` are
|
|
337
|
+
# `[value, label]` pairs
|
|
201
338
|
def self.select_or_radio(common, options)
|
|
202
339
|
kind = options.size <= 4 ? :radio : :select
|
|
203
340
|
Field.new(**common, kind: kind, html_type: "text", options: options.map { |value| [value, value] })
|
|
204
341
|
end
|
|
205
342
|
|
|
343
|
+
# Builds the leaf field for a primitive-typed attribute: a number input for `Integer`
|
|
344
|
+
# and `Float`, a checkbox for a boolean, and a text-family input for anything else.
|
|
345
|
+
#
|
|
346
|
+
# @param attribute [Bluebook::Attribute] the attribute whose type picks the input
|
|
347
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares, keyed
|
|
348
|
+
# `:path`, `:label`, `:default`, `:optional`, `:pattern`
|
|
349
|
+
# @return [Forms::Field] a `:number`, `:boolean`, `:text` or `:textarea` leaf; an
|
|
350
|
+
# `Integer` carries `step` `"1"`, a `Float` `"any"`
|
|
206
351
|
def self.primitive_field(attribute, common)
|
|
207
352
|
case attribute.type.to_s
|
|
208
353
|
when "Integer" then Field.new(**common, kind: :number, html_type: "number", step: "1")
|
|
@@ -214,11 +359,27 @@ module Hecks
|
|
|
214
359
|
end
|
|
215
360
|
end
|
|
216
361
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
362
|
+
# Vocabulary::FieldHint (language/bluebook/vocabulary.bluebook), read
|
|
363
|
+
# off the generated table: `pattern` is the regex source, matched
|
|
364
|
+
# case-insensitively. bin/project_field_hints writes the Rust host's
|
|
365
|
+
# copy from the same rows.
|
|
366
|
+
HINTS = Hecks::Vocabulary.rows("FieldHint")
|
|
367
|
+
.to_h { |row| [row["name"], Regexp.new(row["pattern"], Regexp::IGNORECASE)] }
|
|
368
|
+
.freeze
|
|
369
|
+
EMAIL_HINT = HINTS.fetch("email")
|
|
370
|
+
URL_HINT = HINTS.fetch("url")
|
|
371
|
+
TEL_HINT = HINTS.fetch("tel")
|
|
372
|
+
TEXTAREA_HINT = HINTS.fetch("textarea")
|
|
221
373
|
|
|
374
|
+
# Builds a text-family leaf, picking `email`, `url`, `tel` or plain `text` from the
|
|
375
|
+
# attribute's pattern and the `FieldHint` name patterns, and a textarea for a
|
|
376
|
+
# long-text name.
|
|
377
|
+
#
|
|
378
|
+
# @param attribute [Bluebook::Attribute] the attribute whose name and pattern are read
|
|
379
|
+
# @param common [Hash{Symbol => Object}] the `Field` members every kind shares, keyed
|
|
380
|
+
# `:path`, `:label`, `:default`, `:optional`, `:pattern`
|
|
381
|
+
# @return [Forms::Field] a `:text` leaf, or `:textarea` when the input type is plain
|
|
382
|
+
# `"text"` and the name matches `TEXTAREA_HINT`
|
|
222
383
|
def self.text_field(attribute, common)
|
|
223
384
|
name = attribute.name.to_s
|
|
224
385
|
pattern = attribute.pattern.to_s
|