hecks 1.3.0 → 1.5.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 +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- 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 +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -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 +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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/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 +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -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 +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- 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 +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- 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 +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- 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 +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- 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 +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- 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 +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -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 +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- 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 +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- 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/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -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 +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- 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 +34 -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 +95 -6
- 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 +48 -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 +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -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 +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -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 +61 -15
- 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 +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- 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 +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
data/lib/hecks/forms/html.rb
CHANGED
|
@@ -11,8 +11,14 @@ module Hecks
|
|
|
11
11
|
# account number a support rep typed into a form and got wrong — flows
|
|
12
12
|
# through here on every render, so escaping is not optional decoration.
|
|
13
13
|
module Escape
|
|
14
|
+
# Escapes a value for HTML text content, replacing `&`, `<`, `>`, `"` and `'` with
|
|
15
|
+
# entities so nothing in it becomes a tag.
|
|
16
|
+
#
|
|
14
17
|
# Order matters — `&` first, or every escape this method itself just
|
|
15
18
|
# wrote (`&`, `<`, ...) gets re-escaped a second time.
|
|
19
|
+
#
|
|
20
|
+
# @param value [Object, nil] the value to show, rendered with `to_s`; nil renders as `""`
|
|
21
|
+
# @return [String] the escaped text
|
|
16
22
|
def self.html(value)
|
|
17
23
|
value.to_s
|
|
18
24
|
.gsub("&", "&")
|
|
@@ -22,14 +28,22 @@ module Hecks
|
|
|
22
28
|
.gsub("'", "'")
|
|
23
29
|
end
|
|
24
30
|
|
|
31
|
+
# Escapes a value for use inside a quoted HTML attribute.
|
|
32
|
+
#
|
|
25
33
|
# Safe inside a double-quoted HTML attribute specifically — `html`
|
|
26
34
|
# already covers this (it escapes `"`), kept as a named alias so a
|
|
27
35
|
# call site reads "this value fills an attribute" rather than repeating
|
|
28
36
|
# the same escaping and leaving the reader to check they match.
|
|
37
|
+
#
|
|
38
|
+
# @param value [Object, nil] the attribute value, rendered with `to_s`; nil renders
|
|
39
|
+
# as `""`
|
|
40
|
+
# @return [String] the escaped text, without surrounding quotes
|
|
29
41
|
def self.attr(value) = html(value)
|
|
30
42
|
|
|
43
|
+
# Percent-encodes a value for use as a query-string value.
|
|
44
|
+
#
|
|
31
45
|
# L12 (docs/audits/2026-08-10-main-bug-audit.md) — safe as a
|
|
32
|
-
# query-string
|
|
46
|
+
# query-string value. `html`/`attr` guard against the value becoming
|
|
33
47
|
# markup, but say nothing about it staying inside the URL syntax
|
|
34
48
|
# position it was placed in: an aggregate's identity is free-form
|
|
35
49
|
# unless its value object declares a `pattern:` (see S3 in the same
|
|
@@ -38,23 +52,32 @@ module Hecks
|
|
|
38
52
|
# interpolation (a stray `&` smuggles a second query parameter, `#`
|
|
39
53
|
# truncates the path at a fragment, `/` splits the path into an
|
|
40
54
|
# extra segment, ...). Percent-encodes via
|
|
41
|
-
# `application/x-www-form-urlencoded` (`+` for space) — correct
|
|
55
|
+
# `application/x-www-form-urlencoded` (`+` for space) — correct only
|
|
42
56
|
# for a query-string value (query_form_renderer.rb's `quick_links`,
|
|
43
|
-
# record_renderer.rb's `?to=`). For a URL
|
|
57
|
+
# record_renderer.rb's `?to=`). For a URL path segment use `path`
|
|
44
58
|
# below instead — `+` is a literal plus there, not an escaped space,
|
|
45
59
|
# so this method would corrupt any id containing a space. Callers
|
|
46
|
-
# still wrap the
|
|
60
|
+
# still wrap the assembled href/Location in `attr` (or `html`) as
|
|
47
61
|
# usual — this only covers the id's own component, not the
|
|
48
62
|
# surrounding markup.
|
|
63
|
+
#
|
|
64
|
+
# @param value [Object, nil] the value to encode, rendered with `to_s`
|
|
65
|
+
# @return [String] the `application/x-www-form-urlencoded` form, a space rendered as `+`
|
|
49
66
|
def self.url(value) = URI.encode_www_form_component(value.to_s)
|
|
50
67
|
|
|
51
|
-
#
|
|
68
|
+
# Percent-encodes a value for use as one URL path segment.
|
|
69
|
+
#
|
|
70
|
+
# Same guard as `url`, for a URL path segment instead of a
|
|
52
71
|
# query-string value. `encode_www_form_component` renders space as
|
|
53
72
|
# `+`, which is only meaningful inside a query string — in a path
|
|
54
73
|
# segment `+` is a literal plus, so an id like "John Smith" would
|
|
55
74
|
# round-trip to "John+Smith" and 404 against the real id "John
|
|
56
75
|
# Smith". Reuse the same percent-encoding and just correct that one
|
|
57
76
|
# character back to `%20`.
|
|
77
|
+
#
|
|
78
|
+
# @param value [Object, nil] the segment to encode, rendered with `to_s`
|
|
79
|
+
# @return [String] the percent-encoded segment, a space rendered as `%20` and `/` as
|
|
80
|
+
# `%2F`
|
|
58
81
|
def self.path(value) = URI.encode_www_form_component(value.to_s).gsub("+", "%20")
|
|
59
82
|
end
|
|
60
83
|
|
|
@@ -64,6 +87,14 @@ module Hecks
|
|
|
64
87
|
# `true` renders as a bare boolean attribute (`required`, not
|
|
65
88
|
# `required="true"`); `nil`/`false` are dropped entirely.
|
|
66
89
|
module Tag
|
|
90
|
+
# Renders a Hash of attributes as the text that sits inside an opening tag, escaping
|
|
91
|
+
# every value and spelling an underscored name with hyphens.
|
|
92
|
+
#
|
|
93
|
+
# @param pairs [Hash{Symbol => Object}] attribute values by name; `true`
|
|
94
|
+
# renders the bare name, nil and `false` drop the attribute, anything else is
|
|
95
|
+
# rendered with `to_s`
|
|
96
|
+
# @return [String] space-separated attributes, such as `required aria-describedby="x"`;
|
|
97
|
+
# `""` when every pair is dropped
|
|
67
98
|
def self.attrs(pairs)
|
|
68
99
|
pairs.filter_map do |name, value|
|
|
69
100
|
next if value.nil? || value == false
|
|
@@ -73,15 +104,28 @@ module Hecks
|
|
|
73
104
|
end.join(" ")
|
|
74
105
|
end
|
|
75
106
|
|
|
107
|
+
# Renders an opening tag with its attributes. The tag name is interpolated unescaped,
|
|
108
|
+
# so it must come from code, never from domain data.
|
|
109
|
+
#
|
|
110
|
+
# @param name [String] the element name, such as `"input"`
|
|
111
|
+
# @param pairs [Hash{Symbol => Object}] attributes, rendered as `attrs` renders them
|
|
112
|
+
# @return [String] the opening tag, such as `<input type="text" required>`
|
|
76
113
|
def self.open(name, **pairs)
|
|
77
114
|
rendered = attrs(pairs)
|
|
78
115
|
rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
|
|
79
116
|
end
|
|
80
117
|
|
|
81
|
-
#
|
|
118
|
+
# Renders a void element such as `<input>`. A self-closing tag reads the same as an
|
|
119
|
+
# opening one; HTML5 needs no slash.
|
|
120
|
+
#
|
|
121
|
+
# `self.open(...)`, not bare `open(...)` — this `Tag.open` (an HTML tag
|
|
82
122
|
# renderer, right above) shadows Kernel#open safely either way, but the
|
|
83
123
|
# explicit receiver also settles Security/Open's static ambiguity.
|
|
84
|
-
|
|
124
|
+
#
|
|
125
|
+
# @param name [String] the element name, such as `"input"`
|
|
126
|
+
# @param pairs [Hash{Symbol => Object}] attributes, rendered as `attrs` renders them
|
|
127
|
+
# @return [String] the tag, identical to what `open` renders
|
|
128
|
+
def self.void(name, **pairs) = self.open(name, **pairs)
|
|
85
129
|
end
|
|
86
130
|
end
|
|
87
131
|
end
|
|
@@ -4,9 +4,15 @@ module Hecks
|
|
|
4
4
|
module Forms
|
|
5
5
|
# The home page: every exposed chapter, every aggregate on it — the
|
|
6
6
|
# entry point into what would otherwise be a URL you'd have to already
|
|
7
|
-
# know. `chapters` is `{domain_name => Bluebook}`, in `expose`
|
|
8
|
-
# (see
|
|
7
|
+
# know. `chapters` is `{domain_name => Bluebook::Chapter}`, in `expose`
|
|
8
|
+
# order (see `Forms::Config` in forms.rb).
|
|
9
9
|
module IndexRenderer
|
|
10
|
+
# Renders the home page body: one section per exposed chapter, each linking to its
|
|
11
|
+
# aggregates.
|
|
12
|
+
#
|
|
13
|
+
# @param chapters [Hash{String => Bluebook::Chapter}] the loaded chapters by domain
|
|
14
|
+
# name, in `expose` order
|
|
15
|
+
# @return [String] the HTML page body; only the heading when `chapters` is empty
|
|
10
16
|
def self.render(chapters)
|
|
11
17
|
sections = chapters.map { |name, chapter| chapter_section(name, chapter) }
|
|
12
18
|
<<~HTML
|
|
@@ -15,6 +21,12 @@ module Hecks
|
|
|
15
21
|
HTML
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
# Renders one chapter's section: its name, its vision as a badge when it declares one,
|
|
25
|
+
# and a link per aggregate showing how many commands and queries it has.
|
|
26
|
+
#
|
|
27
|
+
# @param name [String] the domain name, used as the heading and the first path segment
|
|
28
|
+
# @param chapter [Bluebook::Chapter] the loaded chapter
|
|
29
|
+
# @return [String] HTML for the section's heading and aggregate list
|
|
18
30
|
def self.chapter_section(name, chapter)
|
|
19
31
|
items = chapter.aggregates.map do |aggregate|
|
|
20
32
|
counts = "#{aggregate.commands.size} command#{'s' unless aggregate.commands.size == 1}, " \
|
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
|
|
data/lib/hecks/forms/params.rb
CHANGED
|
@@ -4,35 +4,59 @@ require_relative "field_renderer"
|
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Forms
|
|
7
|
-
# The two directions between a flat, dotted,
|
|
7
|
+
# The two directions between a flat, dotted, all-strings web payload
|
|
8
8
|
# (`{"amount.cents"=>"1050", "amount.currency"=>"USD"}`, whether it came
|
|
9
9
|
# off a POST form body or a GET query string — Rack hands back the same
|
|
10
10
|
# flat shape for both as long as nothing uses `[]` bracket names) and the
|
|
11
11
|
# nested, typed hash `Dispatcher#dispatch`/`#query` actually take
|
|
12
12
|
# (`{amount: {cents: 1050, currency: "USD"}}`).
|
|
13
13
|
#
|
|
14
|
-
# `extract`
|
|
14
|
+
# `extract` needs the Field tree, not just the raw params — a numeric
|
|
15
15
|
# leaf's own runtime check (`Value::Coercion#check_numeric_fields`)
|
|
16
16
|
# requires an actual `Integer`/`Float`, not a String that merely looks
|
|
17
17
|
# like one (`given.is_a?(expected)`, no coercion attempted there); a web
|
|
18
18
|
# form can only ever hand back strings, so the cast has to happen here,
|
|
19
|
-
# once, using the
|
|
19
|
+
# once, using the same shape `FieldShape` already resolved for
|
|
20
20
|
# rendering the input in the first place — one reading of the IR, not
|
|
21
21
|
# two that could disagree.
|
|
22
22
|
module Params
|
|
23
|
-
#
|
|
23
|
+
# Casts a flat, all-strings web payload into the nested, typed arguments a command or
|
|
24
|
+
# query takes, guided by the field tree the form was rendered from.
|
|
25
|
+
#
|
|
26
|
+
# Every leaf field carries its own full dotted path regardless of how
|
|
24
27
|
# deep `FieldShape` nested it to get there — a single-attribute value
|
|
25
|
-
# object unwraps to a leaf sitting at the
|
|
28
|
+
# object unwraps to a leaf sitting at the top of the fields array with
|
|
26
29
|
# a two-segment path (`"reference.value"`), the exact same shape a
|
|
27
30
|
# `:group`'s own child carries. So this collects every leaf as
|
|
28
|
-
# (full path -> value)
|
|
31
|
+
# (full path -> value) first, and nests by the path's own segments
|
|
29
32
|
# last — one nesting rule, blind to how a field arrived at its path.
|
|
33
|
+
#
|
|
34
|
+
# @param fields [Array<Forms::Field>] the resolved field tree the payload was posted
|
|
35
|
+
# against
|
|
36
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
37
|
+
# @return [Hash{Symbol => Object}] nested, typed arguments keyed by path segment, ready
|
|
38
|
+
# to splat into a dispatch; a blank optional field is left out
|
|
39
|
+
# @raise [ArgumentError] if a number field's text is not numeric, or two fields collide
|
|
40
|
+
# at one path (see `nest`)
|
|
41
|
+
# @raise [TypeError] if a required number field is missing from `raw`
|
|
42
|
+
# @raise [JSON::ParserError] if a line of a list-of-value-objects field is not JSON
|
|
30
43
|
def self.extract(fields, raw)
|
|
31
44
|
pairs = {}
|
|
32
45
|
fields.each { |field| collect(field, raw, pairs) }
|
|
33
46
|
nest(pairs)
|
|
34
47
|
end
|
|
35
48
|
|
|
49
|
+
# Gathers one field's typed value, and recursively its children's, into `pairs` under
|
|
50
|
+
# each leaf's full dotted path; a blank optional leaf adds nothing.
|
|
51
|
+
#
|
|
52
|
+
# @param field [Forms::Field] the field to read
|
|
53
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
54
|
+
# @param pairs [Hash{String => Object}] accumulator of typed values by dotted path,
|
|
55
|
+
# added to in place
|
|
56
|
+
# @return [void]
|
|
57
|
+
# @raise [ArgumentError] if a number field's text is not numeric
|
|
58
|
+
# @raise [TypeError] if a required number field is missing from `raw`
|
|
59
|
+
# @raise [JSON::ParserError] if a line of a list-of-value-objects field is not JSON
|
|
36
60
|
def self.collect(field, raw, pairs)
|
|
37
61
|
case field.kind
|
|
38
62
|
when :group, :money then field.children.each { |child| collect(child, raw, pairs) }
|
|
@@ -41,23 +65,32 @@ module Hecks
|
|
|
41
65
|
end
|
|
42
66
|
end
|
|
43
67
|
|
|
68
|
+
# Nests values keyed by dotted path into a Hash keyed by path segment, refusing two
|
|
69
|
+
# paths that cannot share one result.
|
|
70
|
+
#
|
|
44
71
|
# A path-prefix collision: one field named (say) "price" alongside
|
|
45
|
-
# another named "price.cents" implies "price" is
|
|
72
|
+
# another named "price.cents" implies "price" is both a scalar leaf
|
|
46
73
|
# and the parent of a nested group — the two can never coexist in
|
|
47
74
|
# the same result hash. Depending on which pair `each_with_object`
|
|
48
|
-
# reaches first,
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
75
|
+
# reaches first, an unguarded walk fails in one of two ways: a scalar
|
|
76
|
+
# planted first leaves `acc[segment] ||= {}` seeing a truthy non-Hash
|
|
77
|
+
# and reusing it as `node`, so the next `node[leaf] = value` blows up
|
|
78
|
+
# with a raw `TypeError` from calling `String#[]=` with a Symbol key;
|
|
79
|
+
# a scalar planted after the nested group instead sails through
|
|
80
|
+
# `node[leaf] = value` and silently clobbers the entire nested hash
|
|
81
|
+
# with the scalar, losing every sibling under it with no error at
|
|
82
|
+
# all. Both directions are checked explicitly here so either order
|
|
83
|
+
# raises the same clear `ArgumentError` instead of a confusing crash
|
|
84
|
+
# or silent data loss — this is the family of error every
|
|
85
|
+
# command/query submission path in app.rb already rescues into a 422
|
|
86
|
+
# (`ArgumentError` sits right alongside the domain refusals in every
|
|
87
|
+
# one of those rescue clauses).
|
|
88
|
+
#
|
|
89
|
+
# @param pairs [Hash{String => Object}] values keyed by dotted path, such as
|
|
90
|
+
# `{"amount.cents" => 1050}`
|
|
91
|
+
# @return [Hash{Symbol => Object}] the nested result, such as `{amount: {cents: 1050}}`
|
|
92
|
+
# @raise [ArgumentError] if one path names a plain value where another names a nested
|
|
93
|
+
# group, in either order
|
|
61
94
|
def self.nest(pairs)
|
|
62
95
|
pairs.each_with_object({}) do |(path, value), result|
|
|
63
96
|
segments = path.to_s.split(".").map(&:to_sym)
|
|
@@ -74,6 +107,10 @@ module Hecks
|
|
|
74
107
|
end
|
|
75
108
|
end
|
|
76
109
|
|
|
110
|
+
# Builds, without raising, the error `nest` raises for a path-prefix collision.
|
|
111
|
+
#
|
|
112
|
+
# @param path [String] the dotted path at which the collision was found
|
|
113
|
+
# @return [ArgumentError] an error whose message names the conflicting path
|
|
77
114
|
def self.nesting_collision(path)
|
|
78
115
|
ArgumentError.new("#{path.inspect} conflicts with another field at the same path — " \
|
|
79
116
|
"one names it as a plain value and another as a nested group")
|
|
@@ -82,11 +119,21 @@ module Hecks
|
|
|
82
119
|
SKIP = Object.new.freeze
|
|
83
120
|
private_constant :SKIP
|
|
84
121
|
|
|
122
|
+
# Reads a `:list` field's textarea into an Array, one element per non-blank line.
|
|
123
|
+
#
|
|
85
124
|
# One line of the textarea per element. A line that itself needs
|
|
86
125
|
# several fields (a multi-attribute value object as a list element)
|
|
87
126
|
# is read as JSON on that one line — the honest fallback documented in
|
|
88
127
|
# docs/command-form-and-query-form-bluebook.md rather than a second
|
|
89
128
|
# widget this prototype doesn't build yet.
|
|
129
|
+
#
|
|
130
|
+
# @param field [Forms::Field] a `:list` field whose first child describes one element
|
|
131
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
132
|
+
# @return [Array<Object>, Object] the elements — cast scalars for a leaf element,
|
|
133
|
+
# symbol-keyed parsed JSON otherwise — or the private `SKIP` sentinel when the field
|
|
134
|
+
# is absent or blank
|
|
135
|
+
# @raise [ArgumentError] if a number element's line is not numeric
|
|
136
|
+
# @raise [JSON::ParserError] if a non-leaf element's line is not JSON
|
|
90
137
|
def self.extract_list(field, raw)
|
|
91
138
|
text = raw[field.path]
|
|
92
139
|
return SKIP if text.nil? || text.strip.empty?
|
|
@@ -97,6 +144,16 @@ module Hecks
|
|
|
97
144
|
end
|
|
98
145
|
end
|
|
99
146
|
|
|
147
|
+
# Reads one leaf field's typed value out of the payload; a boolean always answers, since
|
|
148
|
+
# an unticked checkbox is itself a value.
|
|
149
|
+
#
|
|
150
|
+
# @param field [Forms::Field] a leaf field
|
|
151
|
+
# @param raw [Hash{String => String}] the flat web payload, keyed by dotted field path
|
|
152
|
+
# @return [Boolean, Integer, Float, String, nil, Object] the cast value; nil when a
|
|
153
|
+
# required non-number field is missing; the private `SKIP` sentinel when an optional
|
|
154
|
+
# field is absent or empty
|
|
155
|
+
# @raise [ArgumentError] if a number field's text is not numeric
|
|
156
|
+
# @raise [TypeError] if a required number field is missing from `raw`
|
|
100
157
|
def self.extract_leaf(field, raw)
|
|
101
158
|
return checkbox(raw[field.path]) if field.kind == :boolean
|
|
102
159
|
|
|
@@ -106,10 +163,24 @@ module Hecks
|
|
|
106
163
|
cast_scalar(field, text)
|
|
107
164
|
end
|
|
108
165
|
|
|
166
|
+
# Reads a submitted checkbox value as a Boolean.
|
|
167
|
+
#
|
|
168
|
+
# @param raw_value [String, nil] the submitted text; nil when the field was not posted
|
|
169
|
+
# @return [Boolean] true for `"on"`, `"1"` or `"true"` in any letter case, false for
|
|
170
|
+
# anything else
|
|
109
171
|
def self.checkbox(raw_value)
|
|
110
172
|
%w[on 1 true].include?(raw_value.to_s.downcase)
|
|
111
173
|
end
|
|
112
174
|
|
|
175
|
+
# Casts a leaf's submitted text to the Ruby type the runtime's numeric check requires,
|
|
176
|
+
# which accepts an actual `Integer` or `Float` and never a numeric-looking String.
|
|
177
|
+
#
|
|
178
|
+
# @param field [Forms::Field] the leaf the text was submitted for
|
|
179
|
+
# @param text [String, nil] the submitted text
|
|
180
|
+
# @return [Integer, Float, String, nil] an Integer for a `:number` field with `step`
|
|
181
|
+
# `"1"`, a Float for any other `:number` field, and `text` itself for every other kind
|
|
182
|
+
# @raise [ArgumentError] if a number field's text is not numeric
|
|
183
|
+
# @raise [TypeError] if a number field's text is nil
|
|
113
184
|
def self.cast_scalar(field, text)
|
|
114
185
|
case field.kind
|
|
115
186
|
when :number then field.step == "1" ? Integer(text) : Float(text)
|
|
@@ -117,13 +188,23 @@ module Hecks
|
|
|
117
188
|
end
|
|
118
189
|
end
|
|
119
190
|
|
|
191
|
+
# Flattens held values back into the dotted-path, all-strings pairs a query string
|
|
192
|
+
# carries — the reverse of `extract`.
|
|
193
|
+
#
|
|
120
194
|
# The other direction — a nested value hash back to the flat dotted
|
|
121
195
|
# pairs a GET link's query string carries, so a query view's
|
|
122
|
-
# "shareable link" and its filter
|
|
123
|
-
#
|
|
196
|
+
# "shareable link" and its filter form stay two renderings of the
|
|
197
|
+
# same data rather than two formats that can drift. Reads with
|
|
124
198
|
# `FieldRenderer.dig` — the identical full-path lookup a re-rendered
|
|
125
199
|
# input's own value comes from — for the same reason `extract` above
|
|
126
200
|
# nests by full path rather than by tree shape.
|
|
201
|
+
#
|
|
202
|
+
# @param fields [Array<Forms::Field>] the field tree naming which paths to read
|
|
203
|
+
# @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
|
|
204
|
+
# `FieldRenderer.dig`
|
|
205
|
+
# @param into [Hash{String => String}] accumulator the pairs are added to in place
|
|
206
|
+
# @return [Hash{String => String}] `into`, holding one pair per leaf with a non-nil value
|
|
207
|
+
# and one per list (its elements newline-joined, `""` when it holds nothing)
|
|
127
208
|
def self.flatten(fields, values, into: {})
|
|
128
209
|
fields.each do |field|
|
|
129
210
|
case field.kind
|
|
@@ -137,11 +218,19 @@ module Hecks
|
|
|
137
218
|
into
|
|
138
219
|
end
|
|
139
220
|
|
|
221
|
+
# Lists every dotted path a field tree submits under, whether or not anything was
|
|
222
|
+
# entered for it.
|
|
223
|
+
#
|
|
140
224
|
# Every leaf/list path a field tree carries, independent of any
|
|
141
225
|
# values — what `command_form_renderer.rb`'s inspect panel wants
|
|
142
226
|
# ("which fields does this command take"), where `flatten` above wants
|
|
143
|
-
# "what does
|
|
227
|
+
# "what does this one submission look like" and returns nothing for a
|
|
144
228
|
# field nothing was entered for.
|
|
229
|
+
#
|
|
230
|
+
# @param fields [Array<Forms::Field>] the field tree to walk
|
|
231
|
+
# @param into [Array<String>] accumulator the paths are appended to in place
|
|
232
|
+
# @return [Array<String>] `into`, holding each leaf and list path in render order;
|
|
233
|
+
# `:group` and `:money` fields contribute their children's paths, not their own
|
|
145
234
|
def self.paths(fields, into: [])
|
|
146
235
|
fields.each do |field|
|
|
147
236
|
case field.kind
|
|
@@ -152,6 +241,14 @@ module Hecks
|
|
|
152
241
|
into
|
|
153
242
|
end
|
|
154
243
|
|
|
244
|
+
# Renders held values as a URL query string, percent-encoding each flattened path and
|
|
245
|
+
# value.
|
|
246
|
+
#
|
|
247
|
+
# @param fields [Array<Forms::Field>] the field tree naming which paths to read
|
|
248
|
+
# @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
|
|
249
|
+
# `FieldRenderer.dig`
|
|
250
|
+
# @return [String] `key=value` pairs joined with `&`, without a leading `?`; `""` when
|
|
251
|
+
# `flatten` yields no pairs
|
|
155
252
|
def self.to_query_string(fields, values)
|
|
156
253
|
pairs = flatten(fields, values)
|
|
157
254
|
pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
|
|
@@ -5,13 +5,13 @@ module Hecks
|
|
|
5
5
|
# boot. Two spellings the previous inline version got wrong:
|
|
6
6
|
#
|
|
7
7
|
# --port=8080 the equals form — the old `ARGV.each_cons(2)` scan
|
|
8
|
-
# only ever recognized "--port", "8080" as
|
|
8
|
+
# only ever recognized "--port", "8080" as two
|
|
9
9
|
# separate argv entries, so this spelling matched
|
|
10
10
|
# nothing and silently fell through to the default.
|
|
11
11
|
# -p abc a non-numeric value — the old code did `.to_i` on
|
|
12
12
|
# whatever followed unconditionally, so a typo
|
|
13
13
|
# quietly became port 0 (Rackup/WEBrick's actual
|
|
14
|
-
# behavior for `Port: 0` is to bind an
|
|
14
|
+
# behavior for `Port: 0` is to bind an ephemeral
|
|
15
15
|
# port — arguably useful on purpose elsewhere, but
|
|
16
16
|
# never what a mistyped `-p abc` meant to ask for).
|
|
17
17
|
#
|
|
@@ -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/)
|
|
@@ -16,10 +16,25 @@ module Hecks
|
|
|
16
16
|
# hiding that behind a form); a row of ready-made links for any
|
|
17
17
|
# closed-set parameter, since a caller filtering by an enum should not
|
|
18
18
|
# have to fill in a form to get a link they could just click; a filter
|
|
19
|
-
#
|
|
19
|
+
# form for everything else, which — being a GET form — produces
|
|
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('&')}"
|
|
@@ -60,10 +91,16 @@ module Hecks
|
|
|
60
91
|
|
|
61
92
|
# The one enum-shaped parameter (if there is one) rendered as literal,
|
|
62
93
|
# clickable links — no form to fill in for "show me the suspended
|
|
63
|
-
# ones". Capped at the
|
|
94
|
+
# ones". Capped at the first closed-set field on purpose: a second one
|
|
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)
|