hecks 0.2.0 → 0.3.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 +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
require_relative "../bluebook/attribute"
|
|
2
|
+
require_relative "value_object_shape"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Forms
|
|
6
|
+
# One resolved field, ready for a renderer to turn into markup. A LEAF
|
|
7
|
+
# scalar carries `kind`/`options`/etc; a `:group` or `:list` carries
|
|
8
|
+
# `children` instead and nothing else on itself.
|
|
9
|
+
#
|
|
10
|
+
# `path` is the DOTTED field path this attribute is reached at —
|
|
11
|
+
# `"email.address"`, `"amount.cents"` — the same convention the language
|
|
12
|
+
# already uses for a query's own cross-object `where` clauses
|
|
13
|
+
# (`where(:"customer.status" => ...)`), not a fresh one invented here. A
|
|
14
|
+
# command receives its arguments as nested hashes
|
|
15
|
+
# (`email: {address: "..."}`), so `Params.unflatten` (params.rb) walks
|
|
16
|
+
# this same dotted spelling back apart on submit — one convention, two
|
|
17
|
+
# directions.
|
|
18
|
+
Field = Struct.new(
|
|
19
|
+
:path, :label, :kind, :html_type, :options, :children, :default, :optional,
|
|
20
|
+
:pattern, :step, :help, :target_aggregate, keyword_init: true
|
|
21
|
+
) do
|
|
22
|
+
def leaf? = kind != :group && kind != :list
|
|
23
|
+
def optional? = optional
|
|
24
|
+
def required? = !optional
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module Humanize
|
|
28
|
+
# "given" -> "Given", "daily_limit" -> "Daily limit", "end_to_end" ->
|
|
29
|
+
# "End to end" — the label a plain reader wants, not the wire spelling.
|
|
30
|
+
def self.label(text)
|
|
31
|
+
# Split on "." FIRST, alone, to take only the last path segment —
|
|
32
|
+
# "daily_limit" is one segment (the underscore is a word break
|
|
33
|
+
# inside it, not a path hop) and must keep both its words; only a
|
|
34
|
+
# genuinely dotted path ("amount.cents") drops everything before
|
|
35
|
+
# the last ".".
|
|
36
|
+
segment = text.to_s.split(".").last.to_s
|
|
37
|
+
words = segment.split("_")
|
|
38
|
+
return segment if words.empty?
|
|
39
|
+
|
|
40
|
+
([words.first.capitalize] + words.drop(1)).join(" ")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The full dotted path, each segment humanized and joined with
|
|
44
|
+
# " → " for a fieldset legend spanning more than one hop
|
|
45
|
+
# ("Amount → Cents") — a group's own legend, not a leaf's label.
|
|
46
|
+
def self.breadcrumb(path)
|
|
47
|
+
path.to_s.split(".").map { |part| label(part) }.join(" → ")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Attribute -> Field. The one mapping every renderer in this
|
|
52
|
+
# directory reads instead of re-deriving its own — see
|
|
53
|
+
# docs/command-form-and-query-form-bluebook.md's survey of the gap this closes (the
|
|
54
|
+
# prior generic-console attempt fell back to `type="text"` for anything
|
|
55
|
+
# that wasn't a number or an enum; a `pattern` naming an email shape, or a
|
|
56
|
+
# closed set another aggregate declares, went unread).
|
|
57
|
+
module FieldShape
|
|
58
|
+
PRIMITIVES = Bluebook::Attribute::PRIMITIVES
|
|
59
|
+
|
|
60
|
+
# `aggregate:` is the Aggregate that OWNS this attribute (a
|
|
61
|
+
# command's, a query's, or — recursively — a value object's) —
|
|
62
|
+
# needed to resolve `reference_to`, `admits:`, and a same-chapter
|
|
63
|
+
# value object by name. `path:` defaults to the attribute's own name;
|
|
64
|
+
# a caller resolving a nested value object's attribute passes the
|
|
65
|
+
# dotted path so far.
|
|
66
|
+
def self.resolve(attribute, aggregate:, path: attribute.name.to_s)
|
|
67
|
+
return list_field(attribute, aggregate, path) if attribute.list?
|
|
68
|
+
|
|
69
|
+
common = { path: path, label: Humanize.label(path), default: attribute.default,
|
|
70
|
+
optional: attribute.optional?, pattern: attribute.pattern }
|
|
71
|
+
|
|
72
|
+
return reference_field(attribute, common) if attribute.reference?
|
|
73
|
+
return admitted_field(attribute, aggregate, common) if attribute.admits
|
|
74
|
+
return value_object_field(attribute, aggregate, common) unless PRIMITIVES.include?(attribute.type)
|
|
75
|
+
|
|
76
|
+
primitive_field(attribute, common)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.list_field(attribute, aggregate, path)
|
|
80
|
+
# The scalar shape one element of this list would take, so a
|
|
81
|
+
# renderer can say what belongs on each line without a second
|
|
82
|
+
# mapping table. `list:` is the only thing that differs.
|
|
83
|
+
scalar = Bluebook::Attribute.new(
|
|
84
|
+
name: attribute.name, type: attribute.type, list: false,
|
|
85
|
+
default: nil, optional: true, pattern: attribute.pattern, admits: attribute.admits
|
|
86
|
+
)
|
|
87
|
+
Field.new(path: path, label: Humanize.label(path), kind: :list, optional: attribute.optional?,
|
|
88
|
+
children: [resolve(scalar, aggregate: aggregate, path: path)])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.reference_field(attribute, common)
|
|
92
|
+
target = attribute.type.resolve
|
|
93
|
+
Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target,
|
|
94
|
+
help: target ? "References an existing #{target.hecks_name} by id." :
|
|
95
|
+
"References an aggregate in another domain — enter its id.")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# `admits:` names a closed set declared ELSEWHERE (`"Account::
|
|
99
|
+
# LedgerDirection"`) that the value must belong to — see
|
|
100
|
+
# Runtime::Value::Admission#admitted_members, which this mirrors
|
|
101
|
+
# exactly (same split, same chapter walk, same discriminant rule) so
|
|
102
|
+
# a rendered `<select>` never offers a member the runtime would then
|
|
103
|
+
# refuse.
|
|
104
|
+
def self.admitted_field(attribute, aggregate, common)
|
|
105
|
+
set_aggregate_name, set_name = attribute.admits.to_s.split("::", 2)
|
|
106
|
+
chapter = aggregate.hecks_owner
|
|
107
|
+
set = set_name && chapter&.aggregate(set_aggregate_name)&.value_object(set_name)
|
|
108
|
+
return primitive_field(attribute, common) unless set # undeclared — refuse-at-dispatch stays the backstop
|
|
109
|
+
|
|
110
|
+
options = select_or_radio(common, closed_set_options(set))
|
|
111
|
+
# The attribute's OWN type still has to be built the shape coercion
|
|
112
|
+
# expects (`Value::Coercion#fields_for` refuses anything that
|
|
113
|
+
# isn't a Hash or a Value for a value-object-typed attribute) — a
|
|
114
|
+
# plain String attribute stays a bare scalar, but a value object
|
|
115
|
+
# like `MovementDirection { value }` still needs the ".value" hop
|
|
116
|
+
# even though the SET it's checked against (`admits:`) is declared
|
|
117
|
+
# somewhere else entirely. Same unwrap `value_object_field` does,
|
|
118
|
+
# kept separate because an admitted set changes the OPTIONS, not
|
|
119
|
+
# which field the hop lands on.
|
|
120
|
+
own_shape = own_value_object(attribute, aggregate)
|
|
121
|
+
inner = own_shape && ValueObjectShape.sole_attribute(own_shape)
|
|
122
|
+
return options unless inner
|
|
123
|
+
|
|
124
|
+
options.path = "#{common[:path]}.#{inner.name}"
|
|
125
|
+
options
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.own_value_object(attribute, aggregate)
|
|
129
|
+
aggregate.value_object(attribute.type) || cross_aggregate_value_object(aggregate, attribute.type)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.value_object_field(attribute, aggregate, common)
|
|
133
|
+
shape = own_value_object(attribute, aggregate)
|
|
134
|
+
return primitive_field(attribute, common) unless shape
|
|
135
|
+
|
|
136
|
+
return closed_set_field(shape, common) if shape.closed_set?
|
|
137
|
+
return money_field(shape, common) if ValueObjectShape.money?(shape)
|
|
138
|
+
|
|
139
|
+
# A single-attribute value object (EmailAddress{address}, CustomerNumber{value})
|
|
140
|
+
# is a NAME for a scalar, not a genuine group — unwrap it so the form
|
|
141
|
+
# asks for one thing ("Email address") instead of a one-item fieldset,
|
|
142
|
+
# and so the inner attribute's OWN pattern (the real email regex,
|
|
143
|
+
# declared on `address`, not on the outer `email` attribute) drives
|
|
144
|
+
# the input type. [[feedback_name_the_scalar_field]] says the same
|
|
145
|
+
# thing about Ruby call sites; a form asks the identical question.
|
|
146
|
+
if (inner = ValueObjectShape.sole_attribute(shape))
|
|
147
|
+
return resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}")
|
|
148
|
+
.tap { |field| field.optional = common[:optional] || field.optional }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
group_field(shape, aggregate, common)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.cross_aggregate_value_object(aggregate, type_name)
|
|
155
|
+
aggregate.hecks_owner&.aggregates&.each do |sibling|
|
|
156
|
+
found = sibling.value_object(type_name)
|
|
157
|
+
return found if found
|
|
158
|
+
end
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.group_field(shape, aggregate, common)
|
|
163
|
+
children = shape.attributes.map { |inner| resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}") }
|
|
164
|
+
Field.new(path: common[:path], label: common[:label], kind: :group, optional: common[:optional], children: children)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.money_field(shape, common)
|
|
168
|
+
cents = Field.new(path: "#{common[:path]}.cents", label: "Amount (cents)", kind: :number,
|
|
169
|
+
html_type: "number", step: "1", optional: common[:optional],
|
|
170
|
+
default: shape.attribute(:cents)&.default, help: "Whole cents — 1050 is $10.50.")
|
|
171
|
+
currency = Field.new(path: "#{common[:path]}.currency", label: "Currency", kind: :text, html_type: "text",
|
|
172
|
+
optional: true, default: shape.attribute(:currency)&.default || "USD",
|
|
173
|
+
help: "Three-letter code.")
|
|
174
|
+
Field.new(path: common[:path], label: common[:label], kind: :money, optional: common[:optional],
|
|
175
|
+
children: [cents, currency])
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.closed_set_options(value_object)
|
|
179
|
+
discriminant = value_object.attributes.first.name
|
|
180
|
+
value_object.members.map { |member| member[discriminant].to_s }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A `one_of` shape (`AccountKind{name}`, `LedgerDirection{value}`) is
|
|
184
|
+
# ALWAYS single-attribute in this language — the discriminant IS the
|
|
185
|
+
# whole value object — so the field's own path always gains that one
|
|
186
|
+
# hop; nothing to branch on the way `admitted_field` has to (a set
|
|
187
|
+
# named by `admits:` may sit on a multi-field value object it doesn't
|
|
188
|
+
# itself define the members of).
|
|
189
|
+
def self.closed_set_field(shape, common)
|
|
190
|
+
discriminant = shape.attributes.first.name
|
|
191
|
+
select_or_radio(common.merge(path: "#{common[:path]}.#{discriminant}"), closed_set_options(shape))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.select_or_radio(common, options)
|
|
195
|
+
kind = options.size <= 4 ? :radio : :select
|
|
196
|
+
Field.new(**common, kind: kind, html_type: "text", options: options.map { |value| [value, value] })
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def self.primitive_field(attribute, common)
|
|
200
|
+
case attribute.type.to_s
|
|
201
|
+
when "Integer" then Field.new(**common, kind: :number, html_type: "number", step: "1")
|
|
202
|
+
when "Float" then Field.new(**common, kind: :number, html_type: "number", step: "any")
|
|
203
|
+
when "TrueClass", "FalseClass"
|
|
204
|
+
Field.new(**common, kind: :boolean, html_type: "checkbox")
|
|
205
|
+
else
|
|
206
|
+
text_field(attribute, common)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
EMAIL_HINT = /email/i
|
|
211
|
+
URL_HINT = /\b(url|uri|website|link)\b/i
|
|
212
|
+
TEL_HINT = /phone|\btel(ephone)?\b/i
|
|
213
|
+
TEXTAREA_HINT = /\b(text|body|note|notes|description|message|comment)\b/i
|
|
214
|
+
|
|
215
|
+
def self.text_field(attribute, common)
|
|
216
|
+
name = attribute.name.to_s
|
|
217
|
+
pattern = attribute.pattern.to_s
|
|
218
|
+
html_type = if pattern.include?("@") || name.match?(EMAIL_HINT)
|
|
219
|
+
"email"
|
|
220
|
+
elsif pattern.match?(/https?/i) || name.match?(URL_HINT)
|
|
221
|
+
"url"
|
|
222
|
+
elsif name.match?(TEL_HINT)
|
|
223
|
+
"tel"
|
|
224
|
+
else
|
|
225
|
+
"text"
|
|
226
|
+
end
|
|
227
|
+
kind = html_type == "text" && name.match?(TEXTAREA_HINT) ? :textarea : :text
|
|
228
|
+
Field.new(**common, kind: kind, html_type: html_type)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Forms
|
|
3
|
+
# Hand-rolled, on purpose — the repo has no ERB anywhere and no template
|
|
4
|
+
# engine dependency (see docs/command-form-and-query-form-bluebook.md's survey). Every
|
|
5
|
+
# other generator in this codebase (bin/reference's markdown, the IR's own
|
|
6
|
+
# `to_h`) builds output as plain Ruby strings; this does the same for HTML,
|
|
7
|
+
# with exactly one job: nothing that reaches `Escape.html` ever becomes a
|
|
8
|
+
# tag. A feature developer's own domain data — a customer's name, an
|
|
9
|
+
# account number a support rep typed into a form and got wrong — flows
|
|
10
|
+
# through here on every render, so escaping is not optional decoration.
|
|
11
|
+
module Escape
|
|
12
|
+
# Order matters — `&` first, or every escape this method itself just
|
|
13
|
+
# wrote (`&`, `<`, ...) gets re-escaped a second time.
|
|
14
|
+
def self.html(value)
|
|
15
|
+
value.to_s
|
|
16
|
+
.gsub("&", "&")
|
|
17
|
+
.gsub("<", "<")
|
|
18
|
+
.gsub(">", ">")
|
|
19
|
+
.gsub('"', """)
|
|
20
|
+
.gsub("'", "'")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Safe inside a double-quoted HTML attribute specifically — `html`
|
|
24
|
+
# already covers this (it escapes `"`), kept as a named alias so a
|
|
25
|
+
# call site reads "this value fills an attribute" rather than repeating
|
|
26
|
+
# the same escaping and leaving the reader to check they match.
|
|
27
|
+
def self.attr(value) = html(value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A tiny attribute-hash -> string helper, shared by every renderer in
|
|
31
|
+
# this directory so `<input ...>` doesn't get hand-assembled five
|
|
32
|
+
# different ways with five different escaping bugs waiting in each.
|
|
33
|
+
# `true` renders as a bare boolean attribute (`required`, not
|
|
34
|
+
# `required="true"`); `nil`/`false` are dropped entirely.
|
|
35
|
+
module Tag
|
|
36
|
+
def self.attrs(pairs)
|
|
37
|
+
pairs.filter_map do |name, value|
|
|
38
|
+
next if value.nil? || value == false
|
|
39
|
+
next name.to_s.tr("_", "-") if value == true
|
|
40
|
+
|
|
41
|
+
%(#{name.to_s.tr("_", "-")}="#{Escape.attr(value)}")
|
|
42
|
+
end.join(" ")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.open(name, **pairs)
|
|
46
|
+
rendered = attrs(pairs)
|
|
47
|
+
rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.void(name, **pairs) = open(name, **pairs) # self-closing tags read the same; HTML5 needs no slash
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require_relative "html"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Forms
|
|
5
|
+
# The home page: every exposed chapter, every aggregate on it — the
|
|
6
|
+
# entry point into what would otherwise be a URL you'd have to already
|
|
7
|
+
# know. `chapters` is `{domain_name => Bluebook}`, in `expose` order
|
|
8
|
+
# (see config.rb).
|
|
9
|
+
module IndexRenderer
|
|
10
|
+
def self.render(chapters)
|
|
11
|
+
sections = chapters.map { |name, chapter| chapter_section(name, chapter) }
|
|
12
|
+
<<~HTML
|
|
13
|
+
<h1>Exposed domains</h1>
|
|
14
|
+
#{sections.join}
|
|
15
|
+
HTML
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.chapter_section(name, chapter)
|
|
19
|
+
items = chapter.aggregates.map do |aggregate|
|
|
20
|
+
counts = "#{aggregate.commands.size} command#{'s' unless aggregate.commands.size == 1}, " \
|
|
21
|
+
"#{aggregate.queries.size} quer#{aggregate.queries.size == 1 ? 'y' : 'ies'}"
|
|
22
|
+
<<~HTML
|
|
23
|
+
<li><a href="/#{Escape.attr(name)}/#{Escape.attr(aggregate.hecks_name)}.html">
|
|
24
|
+
<span>#{Escape.html(aggregate.hecks_name)}</span><span class="kind">#{Escape.html(counts)}</span>
|
|
25
|
+
</a></li>
|
|
26
|
+
HTML
|
|
27
|
+
end
|
|
28
|
+
<<~HTML
|
|
29
|
+
<h2>#{Escape.html(name)}#{chapter.vision ? %( <span class="badge">#{Escape.html(chapter.vision)}</span>) : ''}</h2>
|
|
30
|
+
<ul class="verb-list">#{items.join}</ul>
|
|
31
|
+
HTML
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require_relative "html"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Forms
|
|
5
|
+
# The one HTML shell every page in this app renders inside — nav,
|
|
6
|
+
# typography, form/table/badge styles, all inline (no CDN, no build
|
|
7
|
+
# step: this ships inside the `hecks` gem, and a project that
|
|
8
|
+
# embeds it gets one dependency-free response per request). Themed via
|
|
9
|
+
# `prefers-color-scheme` alone; nothing here reads a cookie or a query
|
|
10
|
+
# param for it, so it is never wrong for the browser rendering it.
|
|
11
|
+
module Page
|
|
12
|
+
def self.render(title:, body:, breadcrumbs: [])
|
|
13
|
+
<<~HTML
|
|
14
|
+
<!doctype html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
+
<title>#{Escape.html(title)} · hecks forms</title>
|
|
20
|
+
<style>#{STYLE}</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<header class="site-header">
|
|
24
|
+
<a class="brand" href="/">hecks forms</a>
|
|
25
|
+
#{breadcrumbs_html(breadcrumbs)}
|
|
26
|
+
</header>
|
|
27
|
+
<main>
|
|
28
|
+
#{body}
|
|
29
|
+
</main>
|
|
30
|
+
<footer class="site-footer">
|
|
31
|
+
<p>Generated straight from the loaded bluebook IR — nothing here is hand-authored per page. See <code>docs/command-form-and-query-form-bluebook.md</code>.</p>
|
|
32
|
+
</footer>
|
|
33
|
+
<script>#{SCRIPT}</script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
HTML
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.breadcrumbs_html(crumbs)
|
|
40
|
+
return "" if crumbs.empty?
|
|
41
|
+
|
|
42
|
+
items = crumbs.map do |label, href|
|
|
43
|
+
href ? %(<a href="#{Escape.attr(href)}">#{Escape.html(label)}</a>) : %(<span aria-current="page">#{Escape.html(label)}</span>)
|
|
44
|
+
end
|
|
45
|
+
%(<nav class="breadcrumbs" aria-label="Breadcrumb">#{items.join(' <span class="sep">/</span> ')}</nav>)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
STYLE = <<~CSS
|
|
49
|
+
:root {
|
|
50
|
+
--bg: #fbfaf8; --surface: #ffffff; --border: #e2ddd3; --text: #24211c;
|
|
51
|
+
--muted: #6b6355; --accent: #8a5a2b; --accent-contrast: #fff8ef;
|
|
52
|
+
--danger-bg: #fdeceb; --danger-border: #e2a49c; --danger-text: #7a2419;
|
|
53
|
+
--focus: #2b6cb0; --code-bg: #f1ede4; --radius: 8px;
|
|
54
|
+
--mono: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
|
|
55
|
+
--sans: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
56
|
+
}
|
|
57
|
+
@media (prefers-color-scheme: dark) {
|
|
58
|
+
:root {
|
|
59
|
+
--bg: #17140f; --surface: #201c15; --border: #3a3327; --text: #ece6d9;
|
|
60
|
+
--muted: #a89d87; --accent: #d99a52; --accent-contrast: #241a0c;
|
|
61
|
+
--danger-bg: #341613; --danger-border: #7a352b; --danger-text: #f3a99c;
|
|
62
|
+
--focus: #6ab0ff; --code-bg: #241f17;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
* { box-sizing: border-box; }
|
|
66
|
+
body { background: var(--bg); color: var(--text); font-family: var(--sans); margin: 0; line-height: 1.5; }
|
|
67
|
+
a { color: var(--accent); }
|
|
68
|
+
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
|
|
69
|
+
outline: 2px solid var(--focus); outline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
.site-header { display: flex; align-items: baseline; gap: 1rem; padding: .9rem 1.5rem;
|
|
72
|
+
border-bottom: 1px solid var(--border); background: var(--surface); flex-wrap: wrap; }
|
|
73
|
+
.brand { font-family: var(--mono); font-weight: 600; text-decoration: none; color: var(--text); }
|
|
74
|
+
.breadcrumbs { font-size: .9rem; color: var(--muted); }
|
|
75
|
+
.breadcrumbs .sep { padding: 0 .3rem; }
|
|
76
|
+
main { max-width: 46rem; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
|
77
|
+
.site-footer { max-width: 46rem; margin: 0 auto; padding: 1rem 1.5rem 3rem; color: var(--muted); font-size: .82rem; }
|
|
78
|
+
h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
|
|
79
|
+
h2 { font-size: 1.1rem; margin: 2rem 0 .6rem; }
|
|
80
|
+
.goal { color: var(--muted); margin: 0 0 1rem; }
|
|
81
|
+
.badge { display: inline-block; font-family: var(--mono); font-size: .72rem; padding: .15rem .5rem;
|
|
82
|
+
border-radius: 999px; border: 1px solid var(--border); color: var(--muted); margin: 0 .3rem .3rem 0; }
|
|
83
|
+
.badge.role { color: var(--accent); border-color: var(--accent); }
|
|
84
|
+
.callout { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
|
85
|
+
padding: .8rem 1rem; margin: 0 0 1.2rem; font-size: .92rem; }
|
|
86
|
+
.callout ul { margin: .3rem 0 0; padding-left: 1.2rem; }
|
|
87
|
+
.error-banner { background: var(--danger-bg); border: 1px solid var(--danger-border); color: var(--danger-text);
|
|
88
|
+
border-radius: var(--radius); padding: .8rem 1rem; margin: 0 0 1.2rem; }
|
|
89
|
+
.error-banner p { margin: 0; }
|
|
90
|
+
fieldset { border: 1px solid var(--border); border-radius: var(--radius); margin: 0 0 1.1rem; padding: .8rem 1rem 1rem; }
|
|
91
|
+
legend { font-size: .85rem; color: var(--muted); padding: 0 .4rem; }
|
|
92
|
+
.field { margin: 0 0 1.1rem; }
|
|
93
|
+
.field > label { display: block; font-weight: 600; font-size: .9rem; margin-bottom: .3rem; }
|
|
94
|
+
.field .required-mark { color: var(--accent); }
|
|
95
|
+
.field .help { display: block; color: var(--muted); font-size: .8rem; margin-top: .3rem; }
|
|
96
|
+
.field input[type=text], .field input[type=email], .field input[type=url], .field input[type=tel],
|
|
97
|
+
.field input[type=number], .field select, .field textarea {
|
|
98
|
+
width: 100%; padding: .5rem .6rem; border: 1px solid var(--border); border-radius: var(--radius);
|
|
99
|
+
background: var(--surface); color: var(--text); font: inherit;
|
|
100
|
+
}
|
|
101
|
+
.field textarea { min-height: 4.5rem; font-family: var(--mono); font-size: .85rem; }
|
|
102
|
+
.radio-group { display: flex; flex-wrap: wrap; gap: .3rem 1rem; }
|
|
103
|
+
.radio-group label { font-weight: 400; display: flex; align-items: center; gap: .35rem; }
|
|
104
|
+
.checkbox-row { display: flex; align-items: center; gap: .5rem; }
|
|
105
|
+
button, .button { font: inherit; border-radius: var(--radius); border: 1px solid var(--accent);
|
|
106
|
+
background: var(--accent); color: var(--accent-contrast); padding: .55rem 1.1rem; cursor: pointer; }
|
|
107
|
+
button.secondary, .button.secondary { background: transparent; color: var(--text); border-color: var(--border); }
|
|
108
|
+
button.copy { font-size: .75rem; padding: .2rem .5rem; }
|
|
109
|
+
table { width: 100%; border-collapse: collapse; font-size: .88rem; }
|
|
110
|
+
.table-scroll { overflow-x: auto; margin: 0 0 1.2rem; }
|
|
111
|
+
th, td { text-align: left; padding: .45rem .6rem; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
112
|
+
thead th { color: var(--muted); font-weight: 600; position: sticky; top: 0; background: var(--bg); }
|
|
113
|
+
tbody tr:hover { background: var(--surface); }
|
|
114
|
+
code, pre, .mono { font-family: var(--mono); font-size: .82rem; }
|
|
115
|
+
pre { background: var(--code-bg); border-radius: var(--radius); padding: .8rem 1rem; overflow-x: auto; margin: 0; }
|
|
116
|
+
.link-row { display: flex; gap: .5rem; align-items: center; margin: 0 0 .8rem; }
|
|
117
|
+
.link-row code { flex: 1; overflow-x: auto; padding: .4rem .6rem; background: var(--code-bg); border-radius: var(--radius); }
|
|
118
|
+
.example-links { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; }
|
|
119
|
+
.example-links a { border: 1px solid var(--border); border-radius: 999px; padding: .25rem .7rem;
|
|
120
|
+
text-decoration: none; font-family: var(--mono); font-size: .82rem; }
|
|
121
|
+
details.inspect { margin: 2rem 0 0; border-top: 1px dashed var(--border); padding-top: 1rem; }
|
|
122
|
+
details.inspect summary { cursor: pointer; color: var(--muted); font-size: .85rem; }
|
|
123
|
+
.verb-list { list-style: none; padding: 0; margin: 0; }
|
|
124
|
+
.verb-list li { border-bottom: 1px solid var(--border); }
|
|
125
|
+
.verb-list a { display: flex; justify-content: space-between; gap: 1rem; padding: .7rem .2rem;
|
|
126
|
+
text-decoration: none; color: var(--text); }
|
|
127
|
+
.verb-list .kind { color: var(--muted); font-family: var(--mono); font-size: .78rem; }
|
|
128
|
+
.actions { display: flex; flex-wrap: wrap; gap: .5rem; margin: .6rem 0 1.4rem; }
|
|
129
|
+
CSS
|
|
130
|
+
|
|
131
|
+
# Decorative only — every form here already works with JS disabled
|
|
132
|
+
# (plain `<form method=post>`/`method=get`), matching this repo's own
|
|
133
|
+
# "no ERB, nothing scripted" bar for the domain layer itself.
|
|
134
|
+
SCRIPT = <<~JS
|
|
135
|
+
document.addEventListener("click", (event) => {
|
|
136
|
+
const button = event.target.closest("[data-copy]");
|
|
137
|
+
if (!button) return;
|
|
138
|
+
const text = document.querySelector(button.getAttribute("data-copy"))?.textContent ?? "";
|
|
139
|
+
navigator.clipboard?.writeText(text.trim());
|
|
140
|
+
const original = button.textContent;
|
|
141
|
+
button.textContent = "copied";
|
|
142
|
+
setTimeout(() => { button.textContent = original; }, 1200);
|
|
143
|
+
});
|
|
144
|
+
document.querySelectorAll("[data-money-cents]").forEach((input) => {
|
|
145
|
+
const preview = document.querySelector(input.getAttribute("data-money-cents"));
|
|
146
|
+
if (!preview) return;
|
|
147
|
+
const update = () => {
|
|
148
|
+
const cents = parseInt(input.value, 10);
|
|
149
|
+
preview.textContent = Number.isFinite(cents) ? "= " + (cents / 100).toFixed(2) : "";
|
|
150
|
+
};
|
|
151
|
+
input.addEventListener("input", update);
|
|
152
|
+
update();
|
|
153
|
+
});
|
|
154
|
+
JS
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "uri"
|
|
3
|
+
require_relative "field_renderer"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Forms
|
|
7
|
+
# The two directions between a flat, dotted, ALL-STRINGS web payload
|
|
8
|
+
# (`{"amount.cents"=>"1050", "amount.currency"=>"USD"}`, whether it came
|
|
9
|
+
# off a POST form body or a GET query string — Rack hands back the same
|
|
10
|
+
# flat shape for both as long as nothing uses `[]` bracket names) and the
|
|
11
|
+
# nested, typed hash `Dispatcher#dispatch`/`#query` actually take
|
|
12
|
+
# (`{amount: {cents: 1050, currency: "USD"}}`).
|
|
13
|
+
#
|
|
14
|
+
# `extract` NEEDS the Field tree, not just the raw params — a numeric
|
|
15
|
+
# leaf's own runtime check (`Value::Coercion#check_numeric_fields`)
|
|
16
|
+
# requires an actual `Integer`/`Float`, not a String that merely looks
|
|
17
|
+
# like one (`given.is_a?(expected)`, no coercion attempted there); a web
|
|
18
|
+
# form can only ever hand back strings, so the cast has to happen here,
|
|
19
|
+
# once, using the SAME shape `FieldShape` already resolved for
|
|
20
|
+
# rendering the input in the first place — one reading of the IR, not
|
|
21
|
+
# two that could disagree.
|
|
22
|
+
module Params
|
|
23
|
+
# Every LEAF field carries its own full dotted path regardless of how
|
|
24
|
+
# deep `FieldShape` nested it to get there — a single-attribute value
|
|
25
|
+
# object unwraps to a leaf sitting at the TOP of the fields array with
|
|
26
|
+
# a two-segment path (`"reference.value"`), the exact same shape a
|
|
27
|
+
# `:group`'s own child carries. So this collects every leaf as
|
|
28
|
+
# (full path -> value) FIRST, and nests by the path's OWN segments
|
|
29
|
+
# last — one nesting rule, blind to how a field arrived at its path.
|
|
30
|
+
def self.extract(fields, raw)
|
|
31
|
+
pairs = {}
|
|
32
|
+
fields.each { |field| collect(field, raw, pairs) }
|
|
33
|
+
nest(pairs)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.collect(field, raw, pairs)
|
|
37
|
+
case field.kind
|
|
38
|
+
when :group, :money then field.children.each { |child| collect(child, raw, pairs) }
|
|
39
|
+
when :list then (value = extract_list(field, raw)) == SKIP || pairs[field.path] = value
|
|
40
|
+
else (value = extract_leaf(field, raw)) == SKIP || pairs[field.path] = value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.nest(pairs)
|
|
45
|
+
pairs.each_with_object({}) do |(path, value), result|
|
|
46
|
+
segments = path.to_s.split(".").map(&:to_sym)
|
|
47
|
+
leaf = segments.pop
|
|
48
|
+
node = segments.reduce(result) { |acc, segment| acc[segment] ||= {} }
|
|
49
|
+
node[leaf] = value
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
SKIP = Object.new.freeze
|
|
54
|
+
private_constant :SKIP
|
|
55
|
+
|
|
56
|
+
# One line of the textarea per element. A line that itself needs
|
|
57
|
+
# several fields (a multi-attribute value object as a list element)
|
|
58
|
+
# is read as JSON on that one line — the honest fallback documented in
|
|
59
|
+
# docs/command-form-and-query-form-bluebook.md rather than a second
|
|
60
|
+
# widget this prototype doesn't build yet.
|
|
61
|
+
def self.extract_list(field, raw)
|
|
62
|
+
text = raw[field.path]
|
|
63
|
+
return SKIP if text.nil? || text.strip.empty?
|
|
64
|
+
|
|
65
|
+
item = field.children.first
|
|
66
|
+
text.each_line.map(&:strip).reject(&:empty?).map do |line|
|
|
67
|
+
item.leaf? ? cast_scalar(item, line) : JSON.parse(line, symbolize_names: true)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.extract_leaf(field, raw)
|
|
72
|
+
return checkbox(raw[field.path]) if field.kind == :boolean
|
|
73
|
+
|
|
74
|
+
text = raw[field.path]
|
|
75
|
+
return SKIP if (text.nil? || text.empty?) && field.optional?
|
|
76
|
+
|
|
77
|
+
cast_scalar(field, text)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.checkbox(raw_value)
|
|
81
|
+
%w[on 1 true].include?(raw_value.to_s.downcase)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.cast_scalar(field, text)
|
|
85
|
+
case field.kind
|
|
86
|
+
when :number then field.step == "1" ? Integer(text) : Float(text)
|
|
87
|
+
else text
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The other direction — a nested value hash back to the flat dotted
|
|
92
|
+
# pairs a GET link's query string carries, so a query view's
|
|
93
|
+
# "shareable link" and its filter FORM stay two renderings of the
|
|
94
|
+
# SAME data rather than two formats that can drift. Reads with
|
|
95
|
+
# `FieldRenderer.dig` — the identical full-path lookup a re-rendered
|
|
96
|
+
# input's own value comes from — for the same reason `extract` above
|
|
97
|
+
# nests by full path rather than by tree shape.
|
|
98
|
+
def self.flatten(fields, values, into: {})
|
|
99
|
+
fields.each do |field|
|
|
100
|
+
case field.kind
|
|
101
|
+
when :group, :money then flatten(field.children, values, into: into)
|
|
102
|
+
when :list then into[field.path] = Array(FieldRenderer.dig(values, field.path)).join("\n")
|
|
103
|
+
else
|
|
104
|
+
value = FieldRenderer.dig(values, field.path)
|
|
105
|
+
into[field.path] = value.to_s unless value.nil?
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
into
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Every leaf/list path a field tree carries, independent of any
|
|
112
|
+
# values — what `command_form_renderer.rb`'s inspect panel wants
|
|
113
|
+
# ("which fields does this command take"), where `flatten` above wants
|
|
114
|
+
# "what does THIS submission look like" and returns nothing for a
|
|
115
|
+
# field nothing was entered for.
|
|
116
|
+
def self.paths(fields, into: [])
|
|
117
|
+
fields.each do |field|
|
|
118
|
+
case field.kind
|
|
119
|
+
when :group, :money then paths(field.children, into: into)
|
|
120
|
+
else into << field.path
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
into
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.to_query_string(fields, values)
|
|
127
|
+
pairs = flatten(fields, values)
|
|
128
|
+
pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|