hecks 3.13.0 → 4.0.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/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/named_pattern.rb +33 -0
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +3 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/framework/bluebook/change.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +58 -37
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +15 -92
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +10 -16
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +51 -51
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value.rb +13 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/site/bluebook/site.bluebook +5 -5
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +300 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +145 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +990 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +191 -0
- data/rust/host/src/payments/tests.rs +1445 -275
- data/rust/host/src/payments.rs +409 -121
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +483 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +26 -0
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -510
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +43 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +229 -112
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +230 -148
- data/rust/src/kernel/mod.rs +46 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +85 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
- data/rust/host/src/approval.rs +0 -652
|
@@ -28,11 +28,10 @@ module Hecks
|
|
|
28
28
|
# @param page [Hash] optionally `status:`, the submitted `values:`, the `error:` to show and
|
|
29
29
|
# the `prefill:` values
|
|
30
30
|
def command_form(domain, aggregate, command, action, **page)
|
|
31
|
-
html(
|
|
31
|
+
html(Humanize.phrase(command.hecks_name),
|
|
32
32
|
CommandFormRenderer.render(registry: @registry, domain: domain, aggregate: aggregate, command: command,
|
|
33
33
|
action: action, **page.slice(:values, :error, :prefill)),
|
|
34
|
-
breadcrumbs:
|
|
35
|
-
[command.hecks_name, nil]],
|
|
34
|
+
breadcrumbs: verb_breadcrumbs(domain, aggregate, command),
|
|
36
35
|
status: page.fetch(:status, 200))
|
|
37
36
|
end
|
|
38
37
|
|
|
@@ -25,11 +25,10 @@ module Hecks
|
|
|
25
25
|
def query_html(domain, aggregate, query, asked)
|
|
26
26
|
action = "/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html"
|
|
27
27
|
results, error = asked.empty? ? [nil, nil] : run_query(domain, aggregate, query, asked)
|
|
28
|
-
html(
|
|
28
|
+
html(Humanize.phrase(query.hecks_name),
|
|
29
29
|
QueryFormRenderer.render(registry: @registry, domain: domain, aggregate: aggregate, query: query,
|
|
30
30
|
action: action, params: asked, results: results, error: error),
|
|
31
|
-
breadcrumbs:
|
|
32
|
-
[query.hecks_name, nil]],
|
|
31
|
+
breadcrumbs: verb_breadcrumbs(domain, aggregate, query),
|
|
33
32
|
status: error ? 422 : 200)
|
|
34
33
|
end
|
|
35
34
|
|
|
@@ -12,9 +12,9 @@ module Hecks
|
|
|
12
12
|
return respond(405, "text/plain", "GET only") unless request.get?
|
|
13
13
|
return aggregate_json(chapter, aggregate) unless format == "html"
|
|
14
14
|
|
|
15
|
-
html(
|
|
15
|
+
html(Naming.plural(Humanize.phrase(aggregate.hecks_name)),
|
|
16
16
|
RecordRenderer.index(registry: @registry, domain: chapter.name, aggregate: aggregate),
|
|
17
|
-
breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
|
|
17
|
+
breadcrumbs: [[chapter.name, "/"], [Humanize.phrase(aggregate.hecks_name), nil]])
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def aggregate_json(chapter, aggregate)
|
|
@@ -52,10 +52,22 @@ module Hecks
|
|
|
52
52
|
# `id:` last, as in `aggregate_route`.
|
|
53
53
|
return json(200, instance.state.merge(id: instance.id)) if format != "html"
|
|
54
54
|
|
|
55
|
-
html("#{
|
|
55
|
+
html("#{Humanize.phrase(aggregate.hecks_name)} #{id}",
|
|
56
56
|
RecordRenderer.show(registry: @registry, domain: domain, aggregate: aggregate, id: id),
|
|
57
|
-
breadcrumbs:
|
|
58
|
-
|
|
57
|
+
breadcrumbs: aggregate_crumbs(domain, aggregate) + [[id, nil]])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Array<Array(String, String)>] the domain and aggregate crumbs every page
|
|
61
|
+
# under an aggregate starts with, the aggregate named in words
|
|
62
|
+
def aggregate_crumbs(domain, aggregate)
|
|
63
|
+
[[domain, "/"], [Humanize.phrase(aggregate.hecks_name), "/#{domain}/#{aggregate.hecks_name}.html"]]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param verb [Bluebook::Command, Bluebook::Query] the command or query the page is for
|
|
67
|
+
# @return [Array<Array(String, String), Array(String, nil)>] the aggregate crumbs, then
|
|
68
|
+
# the verb named in words as the current page
|
|
69
|
+
def verb_breadcrumbs(domain, aggregate, verb)
|
|
70
|
+
aggregate_crumbs(domain, aggregate) + [[Humanize.phrase(verb.hecks_name), nil]]
|
|
59
71
|
end
|
|
60
72
|
|
|
61
73
|
def find_aggregate(chapter, name)
|
|
@@ -43,7 +43,7 @@ module Hecks
|
|
|
43
43
|
fields_html = fields_markup(registry, domain, all_fields, form)
|
|
44
44
|
action = form.fetch(:action)
|
|
45
45
|
cancel = "/#{domain}/#{aggregate.hecks_name}"
|
|
46
|
-
[header(
|
|
46
|
+
[header(aggregate, command), error_banner(form[:error]),
|
|
47
47
|
form_markup(action, fields_html, command, cancel).chomp,
|
|
48
48
|
inspect_panel(domain, aggregate, command, action, all_fields)].join("\n").concat("\n")
|
|
49
49
|
end
|
|
@@ -64,7 +64,7 @@ module Hecks
|
|
|
64
64
|
<form method="post" action="#{Escape.attr(action)}" novalidate>
|
|
65
65
|
#{fields_html}
|
|
66
66
|
<div class="actions">
|
|
67
|
-
<button type="submit">#{Escape.html(command.hecks_name)}</button>
|
|
67
|
+
<button type="submit">#{Escape.html(Humanize.phrase(command.hecks_name))}</button>
|
|
68
68
|
<a class="button secondary" href="#{Escape.attr(cancel)}">Cancel</a>
|
|
69
69
|
</div>
|
|
70
70
|
</form>
|
|
@@ -72,33 +72,58 @@ module Hecks
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
# Builds the `to` field a non-creating command uses to name the record it acts on,
|
|
75
|
-
# labelled with the aggregate's
|
|
75
|
+
# labelled with the aggregate's name and helped by what identifies one ("the order's
|
|
76
|
+
# name").
|
|
76
77
|
#
|
|
77
78
|
# @param aggregate [Bluebook::Aggregate] the aggregate whose records the picker offers
|
|
78
79
|
# @return [Forms::Field] a `:reference` field at path `"to"` targeting `aggregate`
|
|
79
80
|
def self.identity_field(aggregate)
|
|
80
|
-
|
|
81
|
+
noun = Humanize.noun(aggregate.hecks_name)
|
|
82
|
+
Field.new(path: "to", label: Humanize.phrase(aggregate.hecks_name),
|
|
81
83
|
kind: :reference, html_type: "text", target_aggregate: aggregate,
|
|
82
|
-
help: "
|
|
84
|
+
help: "Which #{noun} this is for, by the #{noun}'s #{identity_words(aggregate)}.")
|
|
83
85
|
end
|
|
84
86
|
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
+
# Names what identifies a record in words: `["name.value"]` -> "name", a composite
|
|
88
|
+
# `["bank", "number"]` -> "bank and number". A dotted path names its first segment,
|
|
89
|
+
# since the hops after it reach inside a value object.
|
|
90
|
+
#
|
|
91
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose identity is named
|
|
92
|
+
# @return [String] the identity's field names as lowercase words; "id" when it has none
|
|
93
|
+
def self.identity_words(aggregate)
|
|
94
|
+
names = aggregate.identity_paths.map { |path| Humanize.noun(path.to_s.split(".").first) }.uniq
|
|
95
|
+
names.empty? ? "id" : Naming.to_sentence_list(names)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Renders the form's heading: the command's name in words, its role and creates
|
|
99
|
+
# badges, its goal, and its preconditions callout. The code name stays in the inspect
|
|
100
|
+
# panel.
|
|
87
101
|
#
|
|
88
|
-
# @param domain [String] name of the domain (chapter) the aggregate belongs to
|
|
89
102
|
# @param aggregate [Bluebook::Aggregate] the aggregate that declares the command
|
|
90
103
|
# @param command [Bluebook::Command] the command being described
|
|
91
104
|
# @return [String] HTML for the heading block
|
|
92
|
-
def self.header(
|
|
105
|
+
def self.header(aggregate, command)
|
|
93
106
|
<<~HTML
|
|
94
|
-
<h1>#{Escape.html(
|
|
95
|
-
#{
|
|
96
|
-
#{%(<span class="badge">
|
|
107
|
+
<h1>#{Escape.html(Humanize.phrase(command.hecks_name))}</h1>
|
|
108
|
+
#{role_badge(command)}
|
|
109
|
+
#{%(<span class="badge">Creates a new #{Escape.html(Humanize.noun(aggregate.hecks_name))}</span>) if command.creates?}
|
|
97
110
|
#{%(<p class="goal">#{Escape.html(command.goal)}</p>) if command.goal}
|
|
98
111
|
#{givens_callout(command)}
|
|
99
112
|
HTML
|
|
100
113
|
end
|
|
101
114
|
|
|
115
|
+
# Names who the command is for. The preview dispatches with no caller bound, so the role
|
|
116
|
+
# is shown but not checked, and the badge says so.
|
|
117
|
+
#
|
|
118
|
+
# @param command [Bluebook::Command] the command whose declared role is shown
|
|
119
|
+
# @return [String] HTML for the badge, or `""` when the command declares no role
|
|
120
|
+
def self.role_badge(command)
|
|
121
|
+
return "" unless command.role
|
|
122
|
+
|
|
123
|
+
title = "The bluebook declares who does this. This preview signs nobody in, so it does not check."
|
|
124
|
+
%(<span class="badge role" title="#{title}">Done by: #{Escape.html(command.role)} (not checked in this preview)</span>)
|
|
125
|
+
end
|
|
126
|
+
|
|
102
127
|
# Renders the command's `given` preconditions as a callout, so a caller sees what
|
|
103
128
|
# would refuse the submission before making it.
|
|
104
129
|
#
|
|
@@ -108,7 +133,8 @@ module Hecks
|
|
|
108
133
|
return "" if command.givens.empty?
|
|
109
134
|
|
|
110
135
|
items = command.givens.map { |given| "<li>#{Escape.html(given.description)}</li>" }
|
|
111
|
-
%(<div class="callout"><strong>
|
|
136
|
+
%(<div class="callout"><strong>Checked first.</strong> The action is refused if any of these fail:) \
|
|
137
|
+
"<ul>#{items.join}</ul></div>"
|
|
112
138
|
end
|
|
113
139
|
|
|
114
140
|
# Renders a refused submission as an alert banner carrying the refusal's class name
|
|
@@ -121,7 +147,7 @@ module Hecks
|
|
|
121
147
|
|
|
122
148
|
<<~HTML
|
|
123
149
|
<div class="error-banner" role="alert">
|
|
124
|
-
<p><strong
|
|
150
|
+
<p><strong>Refused (#{Escape.html(error.class.name.split("::").last)}):</strong> #{Escape.html(error.message)}</p>
|
|
125
151
|
</div>
|
|
126
152
|
HTML
|
|
127
153
|
end
|
|
@@ -174,9 +200,9 @@ module Hecks
|
|
|
174
200
|
def self.inspect_head(verb, command, curl)
|
|
175
201
|
<<~HTML
|
|
176
202
|
<details class="inspect">
|
|
177
|
-
<summary>Inspect
|
|
203
|
+
<summary>Inspect: #{Escape.html(verb)}</summary>
|
|
178
204
|
<p>Emits: #{emitted_events(command)}</p>
|
|
179
|
-
<p>
|
|
205
|
+
<p>The same request as <code>curl</code>. Every field is sent by its dotted path (<code>#{Escape.html("name.path")}</code>), as a form or as JSON:</p>
|
|
180
206
|
<div class="link-row"><code id="curl-snippet">#{Escape.html(curl)}</code><button type="button" class="copy" data-copy="#curl-snippet">copy</button></div>
|
|
181
207
|
HTML
|
|
182
208
|
end
|
|
@@ -65,7 +65,7 @@ module Hecks
|
|
|
65
65
|
%(<option value="#{Escape.attr(option_value)}"#{" selected" if option_value.to_s == selected.to_s}>) \
|
|
66
66
|
"#{Escape.html(option_label)}</option>"
|
|
67
67
|
end
|
|
68
|
-
blank = field.optional? ? %(<option value=""
|
|
68
|
+
blank = field.optional? ? %(<option value="">(none)</option>) : ""
|
|
69
69
|
select_tag(field, "#{blank}#{options.join}")
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -80,7 +80,7 @@ module Hecks
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def self.reference_blank(field, value)
|
|
83
|
-
return %(<option value=""
|
|
83
|
+
return %(<option value="">(none)</option>) if field.optional?
|
|
84
84
|
|
|
85
85
|
%(<option value="" disabled#{" selected" unless value}>choose one…</option>)
|
|
86
86
|
end
|
|
@@ -51,7 +51,7 @@ module Hecks
|
|
|
51
51
|
|
|
52
52
|
def self.list_tail(field, item)
|
|
53
53
|
<<~HTML
|
|
54
|
-
<span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{"
|
|
54
|
+
<span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{", each line one JSON object" unless item.leaf?}.</span>
|
|
55
55
|
</div>
|
|
56
56
|
HTML
|
|
57
57
|
end
|
|
@@ -3,6 +3,7 @@ require_relative "../bluebook/attribute"
|
|
|
3
3
|
require_relative "../naming"
|
|
4
4
|
require_relative "value_object_shape"
|
|
5
5
|
require_relative "text_shape"
|
|
6
|
+
require_relative "rule_words"
|
|
6
7
|
|
|
7
8
|
module Hecks
|
|
8
9
|
# The forms surface: the `expose` DSL, the IR->HTML renderers and the Rack app (see forms.rb).
|
|
@@ -32,9 +33,20 @@ module Hecks
|
|
|
32
33
|
Naming.words(segment)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
# "
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# A code name as a phrase: "AddTopping" -> "Add topping", "Order" -> "Order".
|
|
37
|
+
#
|
|
38
|
+
# @param name [String, Symbol, #to_s] a command, query or aggregate name
|
|
39
|
+
# @return [String] the sentence-cased words
|
|
40
|
+
def self.phrase(name) = Naming.words(name)
|
|
41
|
+
|
|
42
|
+
# A code name as words inside a sentence: "BankAccount" -> "bank account", "ATMCard" ->
|
|
43
|
+
# "ATM card".
|
|
44
|
+
#
|
|
45
|
+
# @param name [String, Symbol, #to_s] an aggregate or field name
|
|
46
|
+
# @return [String] the words, the first lowercased unless it is an acronym
|
|
47
|
+
def self.noun(name)
|
|
48
|
+
words = Naming.words(name)
|
|
49
|
+
words.match?(/\A[A-Z]{2,}\b/) ? words : words.sub(/\A./, &:downcase)
|
|
38
50
|
end
|
|
39
51
|
end
|
|
40
52
|
|
|
@@ -79,12 +91,8 @@ module Hecks
|
|
|
79
91
|
|
|
80
92
|
def self.reference_field(attribute, common)
|
|
81
93
|
target = attribute.type.resolve
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"References an existing #{target.hecks_name} by id."
|
|
85
|
-
else
|
|
86
|
-
"References an aggregate in another domain — enter its id."
|
|
87
|
-
end)
|
|
94
|
+
help = target ? "An existing #{Humanize.noun(target.hecks_name)}, by its id." : "A record in another domain, by its id."
|
|
95
|
+
Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target, help: help)
|
|
88
96
|
end
|
|
89
97
|
|
|
90
98
|
# Mirrors Runtime::Value::Admission#admitted_members (same split, chapter walk and
|
|
@@ -119,16 +127,26 @@ module Hecks
|
|
|
119
127
|
return closed_set_field(shape, common) if shape.closed_set?
|
|
120
128
|
return money_field(shape, common) if ValueObjectShape.money?(shape)
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (inner = ValueObjectShape.sole_attribute(shape))
|
|
125
|
-
return resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}")
|
|
126
|
-
.tap { |field| field.optional = common[:optional] || field.optional }
|
|
127
|
-
end
|
|
130
|
+
inner = ValueObjectShape.sole_attribute(shape)
|
|
131
|
+
return sole_attribute_field(shape, inner, aggregate, common) if inner
|
|
128
132
|
|
|
129
133
|
group_field(shape, aggregate, common)
|
|
130
134
|
end
|
|
131
135
|
|
|
136
|
+
# A single-attribute value object names a scalar, not a group: unwrap it so the form
|
|
137
|
+
# asks for one thing and the inner attribute's own pattern drives the input type. The
|
|
138
|
+
# label stays the command's own name for it (`topping`), never the inner `value`, and
|
|
139
|
+
# the help says what the value object's rules accept.
|
|
140
|
+
#
|
|
141
|
+
# @return [Forms::Field] the inner attribute's field at the dotted path
|
|
142
|
+
def self.sole_attribute_field(shape, inner, aggregate, common)
|
|
143
|
+
field = resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}")
|
|
144
|
+
field.label = common[:label]
|
|
145
|
+
field.optional = common[:optional] || field.optional
|
|
146
|
+
field.help = RuleWords.describe(inner, shape.invariants) if field.leaf? && field.kind != :reference
|
|
147
|
+
field
|
|
148
|
+
end
|
|
149
|
+
|
|
132
150
|
def self.cross_aggregate_value_object(aggregate, type_name)
|
|
133
151
|
aggregate.hecks_owner&.aggregates&.each do |sibling|
|
|
134
152
|
found = sibling.value_object(type_name)
|
|
@@ -145,10 +163,10 @@ module Hecks
|
|
|
145
163
|
def self.money_field(shape, common)
|
|
146
164
|
cents = Field.new(path: "#{common[:path]}.cents", label: "Amount (cents)", kind: :number,
|
|
147
165
|
html_type: "number", step: "1", optional: common[:optional],
|
|
148
|
-
default: shape.attribute(:cents)&.default, help: "
|
|
166
|
+
default: shape.attribute(:cents)&.default, help: "In cents: 1050 is $10.50.")
|
|
149
167
|
currency = Field.new(path: "#{common[:path]}.currency", label: "Currency", kind: :text, html_type: "text",
|
|
150
168
|
optional: true, default: shape.attribute(:currency)&.default || "USD",
|
|
151
|
-
help: "Three-letter code.")
|
|
169
|
+
help: "Three-letter code, such as USD.")
|
|
152
170
|
Field.new(path: common[:path], label: common[:label], kind: :money, optional: common[:optional],
|
|
153
171
|
children: [cents, currency])
|
|
154
172
|
end
|
|
@@ -173,6 +191,7 @@ module Hecks
|
|
|
173
191
|
end
|
|
174
192
|
|
|
175
193
|
def self.primitive_field(attribute, common)
|
|
194
|
+
common = common.merge(help: RuleWords.describe(attribute))
|
|
176
195
|
case attribute.type.to_s
|
|
177
196
|
when "Integer" then Field.new(**common, kind: :number, html_type: "number", step: "1")
|
|
178
197
|
when "Float" then Field.new(**common, kind: :number, html_type: "number", step: "any")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "html"
|
|
2
|
+
require_relative "field_shape"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Forms
|
|
@@ -40,7 +41,7 @@ module Hecks
|
|
|
40
41
|
"#{aggregate.queries.size} quer#{aggregate.queries.size == 1 ? "y" : "ies"}"
|
|
41
42
|
<<~HTML
|
|
42
43
|
<li><a href="/#{Escape.attr(name)}/#{Escape.attr(aggregate.hecks_name)}.html">
|
|
43
|
-
<span>#{Escape.html(aggregate.hecks_name)}</span><span class="kind">#{Escape.html(counts)}</span>
|
|
44
|
+
<span>#{Escape.html(Humanize.phrase(aggregate.hecks_name))}</span><span class="kind">#{Escape.html(counts)}</span>
|
|
44
45
|
</a></li>
|
|
45
46
|
HTML
|
|
46
47
|
end
|
data/lib/hecks/forms/page.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Hecks
|
|
|
35
35
|
#{body}
|
|
36
36
|
</main>
|
|
37
37
|
<footer class="site-footer">
|
|
38
|
-
<p>
|
|
38
|
+
<p>Every page here is generated from the domain's description (its bluebook). None is written by hand.</p>
|
|
39
39
|
</footer>
|
|
40
40
|
<script>#{SCRIPT}</script>
|
|
41
41
|
</body>
|
data/lib/hecks/forms/params.rb
CHANGED
|
@@ -203,19 +203,6 @@ module Hecks
|
|
|
203
203
|
end
|
|
204
204
|
into
|
|
205
205
|
end
|
|
206
|
-
|
|
207
|
-
# Renders held values as a URL query string, percent-encoding each flattened path and
|
|
208
|
-
# value.
|
|
209
|
-
#
|
|
210
|
-
# @param fields [Array<Forms::Field>] the field tree naming which paths to read
|
|
211
|
-
# @param values [Hash{String, Symbol => Object}] held values, flat or nested; see
|
|
212
|
-
# `FieldRenderer.dig`
|
|
213
|
-
# @return [String] `key=value` pairs joined with `&`, without a leading `?`; `""` when
|
|
214
|
-
# `flatten` yields no pairs
|
|
215
|
-
def self.to_query_string(fields, values)
|
|
216
|
-
pairs = flatten(fields, values)
|
|
217
|
-
pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
|
|
218
|
-
end
|
|
219
206
|
end
|
|
220
207
|
end
|
|
221
208
|
end
|
|
@@ -22,7 +22,7 @@ module Hecks
|
|
|
22
22
|
reference_options = ReferenceOptions.collect(registry, domain, fields)
|
|
23
23
|
|
|
24
24
|
<<~HTML
|
|
25
|
-
#{header(
|
|
25
|
+
#{header(query)}
|
|
26
26
|
#{canonical_link(action, fields)}
|
|
27
27
|
#{quick_links(action, fields)}
|
|
28
28
|
#{filter_form(action, fields, view.fetch(:params, {}), reference_options)}
|
|
@@ -32,9 +32,9 @@ module Hecks
|
|
|
32
32
|
HTML
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def self.header(
|
|
35
|
+
def self.header(query)
|
|
36
36
|
<<~HTML
|
|
37
|
-
<h1>#{Escape.html(
|
|
37
|
+
<h1>#{Escape.html(Humanize.phrase(query.hecks_name))}</h1>
|
|
38
38
|
#{%(<p class="goal">#{Escape.html(query.description)}</p>) if query.description}
|
|
39
39
|
#{badges(query)}
|
|
40
40
|
HTML
|
|
@@ -50,7 +50,7 @@ module Hecks
|
|
|
50
50
|
paths = Params.paths(fields)
|
|
51
51
|
template = paths.empty? ? action : "#{action}?#{paths.map { |path| "#{path}={#{path}}" }.join("&")}"
|
|
52
52
|
<<~HTML
|
|
53
|
-
<p class="help">
|
|
53
|
+
<p class="help">This query is a plain link. Bookmark it, share it, or put it on a dashboard:</p>
|
|
54
54
|
<div class="link-row"><code id="canonical-link">#{Escape.html(template)}</code><button type="button" class="copy" data-copy="#canonical-link">copy</button></div>
|
|
55
55
|
HTML
|
|
56
56
|
end
|
|
@@ -81,7 +81,8 @@ module Hecks
|
|
|
81
81
|
def self.error_banner(error)
|
|
82
82
|
return "" unless error
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
kind = Escape.html(error.class.name.split("::").last)
|
|
85
|
+
%(<div class="error-banner" role="alert"><p><strong>Refused (#{kind}):</strong> ) \
|
|
85
86
|
"#{Escape.html(error.message)}</p></div>"
|
|
86
87
|
end
|
|
87
88
|
|
|
@@ -96,7 +97,7 @@ module Hecks
|
|
|
96
97
|
paths = Params.paths(fields)
|
|
97
98
|
<<~HTML
|
|
98
99
|
<details class="inspect">
|
|
99
|
-
<summary>Inspect
|
|
100
|
+
<summary>Inspect: #{Escape.html(verb)}</summary>
|
|
100
101
|
<p>Parameters: #{paths.empty? ? "<em>none</em>" : paths.map { |p| "<code>#{Escape.html(p)}</code>" }.join(", ")}</p>
|
|
101
102
|
<pre>#{Escape.html(JSON.pretty_generate(query.to_h))}</pre>
|
|
102
103
|
</details>
|
|
@@ -11,7 +11,7 @@ module Hecks
|
|
|
11
11
|
def self.index(registry:, domain:, aggregate:)
|
|
12
12
|
instances = registry.repository(domain, aggregate).all
|
|
13
13
|
<<~HTML
|
|
14
|
-
<h1>#{Escape.html(
|
|
14
|
+
<h1>#{Escape.html(Naming.plural(Humanize.phrase(aggregate.hecks_name)))}</h1>
|
|
15
15
|
#{%(<p class="goal">#{Escape.html(aggregate.description)}</p>) if aggregate.description}
|
|
16
16
|
#{creating_links(domain, aggregate)}
|
|
17
17
|
<h2>Records (#{instances.size})</h2>
|
|
@@ -26,7 +26,7 @@ module Hecks
|
|
|
26
26
|
|
|
27
27
|
links = creators.map do |cmd|
|
|
28
28
|
%(<a class="button" href="/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html">) \
|
|
29
|
-
"+ #{Escape.html(cmd.hecks_name)}</a>"
|
|
29
|
+
"+ #{Escape.html(Humanize.phrase(cmd.hecks_name))}</a>"
|
|
30
30
|
end
|
|
31
31
|
%(<div class="actions">#{links.join}</div>)
|
|
32
32
|
end
|
|
@@ -40,7 +40,7 @@ module Hecks
|
|
|
40
40
|
state = instance.state
|
|
41
41
|
current = aggregate.lifecycle && state[aggregate.lifecycle.field]
|
|
42
42
|
<<~HTML
|
|
43
|
-
<h1>#{Escape.html(
|
|
43
|
+
<h1>#{Escape.html(Humanize.phrase(aggregate.hecks_name))} <span class="mono">#{Escape.html(id)}</span></h1>
|
|
44
44
|
#{lifecycle_badge(aggregate, current)}
|
|
45
45
|
#{state_table(state)}
|
|
46
46
|
<h2>Commands</h2>
|
|
@@ -76,7 +76,8 @@ module Hecks
|
|
|
76
76
|
# Omits lifecycle transitions that dispatch would refuse from the current state.
|
|
77
77
|
def self.command_links(domain, aggregate, id, current_state)
|
|
78
78
|
commands = aggregate.commands.reject(&:creates?).select { |cmd| applies?(aggregate, cmd, current_state) }
|
|
79
|
-
|
|
79
|
+
noun = Escape.html(Humanize.noun(aggregate.hecks_name))
|
|
80
|
+
return "<p><em>No commands act on an existing #{noun}.</em></p>" if commands.empty?
|
|
80
81
|
|
|
81
82
|
items = commands.map { |cmd| command_item(domain, aggregate, cmd, id) }
|
|
82
83
|
%(<ul class="verb-list">#{items.join}</ul>)
|
|
@@ -86,7 +87,7 @@ module Hecks
|
|
|
86
87
|
def self.command_item(domain, aggregate, cmd, id)
|
|
87
88
|
# The id is free-form, so it is percent-encoded as a query value.
|
|
88
89
|
href = "/#{domain}/#{aggregate.hecks_name}/#{cmd.hecks_name}.html?to=#{Escape.url(id)}"
|
|
89
|
-
%(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(cmd.hecks_name)}</span>) \
|
|
90
|
+
%(<li><a href="#{Escape.attr(href)}"><span>#{Escape.html(Humanize.phrase(cmd.hecks_name))}</span>) \
|
|
90
91
|
"<span class=\"kind\">#{Escape.html(cmd.goal.to_s)}</span></a></li>"
|
|
91
92
|
end
|
|
92
93
|
|
|
@@ -110,15 +111,18 @@ module Hecks
|
|
|
110
111
|
|
|
111
112
|
cmd_items = commands.map { |c| verb_item(domain, aggregate, c, "command") }
|
|
112
113
|
query_items = aggregate.queries.map { |q| verb_item(domain, aggregate, q, "query") }
|
|
113
|
-
%(<h2>
|
|
114
|
+
%(<h2>Commands and queries for #{plural_noun(aggregate)}</h2>) \
|
|
114
115
|
"<ul class=\"verb-list\">#{(cmd_items + query_items).join}</ul>"
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
# @param kind [String] the word shown beside the verb, `"command"` or `"query"`
|
|
118
119
|
# @return [String] the list item linking to the verb's own page
|
|
120
|
+
# @return [String] the aggregate's name as a lowercase plural, HTML-escaped: "orders"
|
|
121
|
+
def self.plural_noun(aggregate) = Escape.html(Naming.plural(Humanize.noun(aggregate.hecks_name)))
|
|
122
|
+
|
|
119
123
|
def self.verb_item(domain, aggregate, verb, kind)
|
|
120
124
|
%(<li><a href="/#{domain}/#{aggregate.hecks_name}/#{verb.hecks_name}.html">) \
|
|
121
|
-
"<span>#{Escape.html(verb.hecks_name)}</span><span class=\"kind\">#{kind}</span></a></li>"
|
|
125
|
+
"<span>#{Escape.html(Humanize.phrase(verb.hecks_name))}</span><span class=\"kind\">#{kind}</span></a></li>"
|
|
122
126
|
end
|
|
123
127
|
end
|
|
124
128
|
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require_relative "../bluebook/named_pattern"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Forms
|
|
5
|
+
module FieldShape
|
|
6
|
+
# Says in plain words what a field accepts, read only from what the bluebook declares:
|
|
7
|
+
# the attribute's type, its named `matches:` pattern, and the invariants of the value
|
|
8
|
+
# object it sits in. An invariant this module cannot read is quoted by its own
|
|
9
|
+
# description rather than guessed at.
|
|
10
|
+
module RuleWords
|
|
11
|
+
# Words for each `Bluebook::NamedPattern` name; a raw regex has none and is not shown.
|
|
12
|
+
PATTERN_WORDS = {
|
|
13
|
+
present: "Must not be blank.",
|
|
14
|
+
not_empty: "Must not be empty.",
|
|
15
|
+
no_whitespace: "No spaces.",
|
|
16
|
+
snake_name: "Lowercase letters, digits and underscores, starting with a letter.",
|
|
17
|
+
git_sha: "A commit id: 7 to 40 hexadecimal characters.",
|
|
18
|
+
digits: "Digits only."
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# The noun a numeric type reads as, for "A whole number above zero."
|
|
22
|
+
NUMBER_NOUNS = { "Integer" => "A whole number", "Float" => "A number" }.freeze
|
|
23
|
+
|
|
24
|
+
# One comparison of the attribute against a literal number, as its canonical source
|
|
25
|
+
# spells it: `value > 0`, `cents >= 0`.
|
|
26
|
+
COMPARISON = /\A(?<name>\w+) (?<op>>=|<=|>|<) (?<bound>-?\d+(?:\.\d+)?)\z/
|
|
27
|
+
|
|
28
|
+
# How each comparison reads after the noun: "above 5", "10 or less".
|
|
29
|
+
BOUND_WORDS = { ">" => "above %s", ">=" => "%s or more", "<" => "below %s", "<=" => "%s or less" }.freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Describes what `attribute` accepts, or nil when nothing declared can be said in words.
|
|
34
|
+
#
|
|
35
|
+
# @param attribute [Bluebook::Attribute] the scalar attribute the field asks for
|
|
36
|
+
# @param invariants [Array<Bluebook::Invariant>] the rules of the value object that
|
|
37
|
+
# wraps `attribute`; empty for a bare attribute
|
|
38
|
+
# @return [String, nil] one or more sentences, such as "A whole number above zero."
|
|
39
|
+
def describe(attribute, invariants = [])
|
|
40
|
+
readable, quoted = invariants.partition { |rule| reading(attribute, rule) }
|
|
41
|
+
sentences = [type_sentence(attribute, readable), pattern_sentence(attribute, readable)]
|
|
42
|
+
sentences += quoted.map { |rule| "Rule: #{rule.description}." }
|
|
43
|
+
text = sentences.compact.uniq.join(" ")
|
|
44
|
+
text.empty? ? nil : text
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String, nil] the numeric noun with every readable bound, such as
|
|
48
|
+
# "A whole number above zero." or "A whole number, zero or more."; nil for a
|
|
49
|
+
# non-numeric attribute
|
|
50
|
+
def type_sentence(attribute, readable)
|
|
51
|
+
noun = NUMBER_NOUNS[attribute.type.to_s]
|
|
52
|
+
return nil unless noun
|
|
53
|
+
|
|
54
|
+
bounds = Naming.to_sentence_list(readable.map { |rule| reading(attribute, rule) }.grep_v(:not_blank))
|
|
55
|
+
return "#{noun}." if bounds.empty?
|
|
56
|
+
|
|
57
|
+
"#{noun}#{bounds.start_with?("above", "below") ? " " : ", "}#{bounds}."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [String, nil] "Must not be blank." when the named pattern or a readable
|
|
61
|
+
# invariant says so, or the named pattern's own words
|
|
62
|
+
def pattern_sentence(attribute, readable)
|
|
63
|
+
named = Bluebook::NamedPattern::TABLE.key(attribute.pattern.to_s)
|
|
64
|
+
return PATTERN_WORDS.fetch(:present) if readable.any? { |rule| reading(attribute, rule) == :not_blank }
|
|
65
|
+
|
|
66
|
+
named && PATTERN_WORDS[named]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reads one invariant over the attribute alone.
|
|
70
|
+
#
|
|
71
|
+
# @return [String, Symbol, nil] a bound's words ("above zero"), `:not_blank`, or nil
|
|
72
|
+
# when the rule is not one this module can read
|
|
73
|
+
def reading(attribute, rule)
|
|
74
|
+
name = attribute.name.to_s
|
|
75
|
+
canonical = rule.canonical.to_s.strip
|
|
76
|
+
return :not_blank if blank_check?(name, canonical)
|
|
77
|
+
return "above zero" if canonical == "#{name}.positive?"
|
|
78
|
+
|
|
79
|
+
comparison_words(name, canonical)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] whether the rule refuses an empty or all-whitespace value
|
|
83
|
+
def blank_check?(name, canonical)
|
|
84
|
+
["!#{name}.to_s.empty?", "!#{name}.to_s.strip.empty?", "!#{name}.empty?", "!#{name}.strip.empty?"]
|
|
85
|
+
.include?(canonical)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [String, nil] the comparison's words, with zero spelled out; nil when the
|
|
89
|
+
# rule is not one comparison of `name` against a number
|
|
90
|
+
def comparison_words(name, canonical)
|
|
91
|
+
match = COMPARISON.match(canonical)
|
|
92
|
+
return nil unless match && match[:name] == name
|
|
93
|
+
|
|
94
|
+
bound = match[:bound] == "0" ? "zero" : match[:bound]
|
|
95
|
+
format(BOUND_WORDS.fetch(match[:op]), bound)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Forms
|
|
3
3
|
# Shared value-object shape classification, so callers such as
|
|
4
|
-
# field_shape.rb
|
|
4
|
+
# field_shape.rb and sql_query_builder.rb agree on it. There is no Ruby ui_schema; the Rust
|
|
5
|
+
# host builds the UI schema (rust/host/src/ui_schema.rs) from the same field-shape rules.
|
|
5
6
|
module ValueObjectShape
|
|
6
7
|
module_function
|
|
7
8
|
|
|
@@ -18,14 +18,14 @@ Hecks.bluebook "Change" do
|
|
|
18
18
|
identified_by :run
|
|
19
19
|
|
|
20
20
|
value_object "RunKey" do
|
|
21
|
-
attribute :value, String,
|
|
21
|
+
attribute :value, String, matches: :present
|
|
22
22
|
|
|
23
23
|
invariant("a change is named") { !value.to_s.empty? }
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# The name the script is registered under with the adapter.
|
|
27
27
|
value_object "ScriptName" do
|
|
28
|
-
attribute :value, String,
|
|
28
|
+
attribute :value, String, matches: :present
|
|
29
29
|
|
|
30
30
|
invariant("a change names its script") { !value.to_s.empty? }
|
|
31
31
|
end
|
|
@@ -35,9 +35,6 @@ module Hecks
|
|
|
35
35
|
array: %w[arr_num arr_str].freeze
|
|
36
36
|
}.freeze
|
|
37
37
|
|
|
38
|
-
# Element type per array attribute, so a block parameter gets the right leaf set.
|
|
39
|
-
ARRAY_ELEMENT_TYPE = { "arr_num" => :numeric, "arr_str" => :string }.freeze
|
|
40
|
-
|
|
41
38
|
BLOCK_PARAM = "el".freeze
|
|
42
39
|
|
|
43
40
|
MAX_DEPTH = 3
|