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
|
@@ -19,8 +19,15 @@ use std::collections::HashMap;
|
|
|
19
19
|
/// `domain_ir` is the whole `ir.json`: a declared set named by `admits: "Aggregate::Set"` and a
|
|
20
20
|
/// value object another aggregate of the chapter declares are looked up there.
|
|
21
21
|
pub fn validate(domain_ir: &Value, aggregate_ir: &Value, id: &str, state: &Value) -> Vec<String> {
|
|
22
|
-
let name = aggregate_ir
|
|
23
|
-
|
|
22
|
+
let name = aggregate_ir
|
|
23
|
+
.get("name")
|
|
24
|
+
.and_then(Value::as_str)
|
|
25
|
+
.unwrap_or("");
|
|
26
|
+
let attributes = aggregate_ir
|
|
27
|
+
.get("attributes")
|
|
28
|
+
.and_then(Value::as_array)
|
|
29
|
+
.cloned()
|
|
30
|
+
.unwrap_or_default();
|
|
24
31
|
let checker = Checker {
|
|
25
32
|
domain: domain_ir,
|
|
26
33
|
aggregate_name: name,
|
|
@@ -41,12 +48,19 @@ pub fn validate(domain_ir: &Value, aggregate_ir: &Value, id: &str, state: &Value
|
|
|
41
48
|
fn index_by_name<'a>(node: &'a Value, key: &str) -> HashMap<&'a str, &'a Value> {
|
|
42
49
|
node.get(key)
|
|
43
50
|
.and_then(Value::as_array)
|
|
44
|
-
.map(|list|
|
|
51
|
+
.map(|list| {
|
|
52
|
+
list.iter()
|
|
53
|
+
.filter_map(|item| item.get("name").and_then(Value::as_str).map(|n| (n, item)))
|
|
54
|
+
.collect()
|
|
55
|
+
})
|
|
45
56
|
.unwrap_or_default()
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
fn declared_attributes(node: &Value) -> &[Value] {
|
|
49
|
-
node.get("attributes")
|
|
60
|
+
node.get("attributes")
|
|
61
|
+
.and_then(Value::as_array)
|
|
62
|
+
.map(Vec::as_slice)
|
|
63
|
+
.unwrap_or_default()
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
fn flag(node: &Value, key: &str) -> bool {
|
|
@@ -75,8 +89,13 @@ struct Checker<'a> {
|
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
impl<'a> Checker<'a> {
|
|
78
|
-
fn refuse(&self, violations: &mut Vec<String>, message: String) {
|
|
79
|
-
violations.push(format!(
|
|
92
|
+
fn refuse(&self, violations: &mut Vec<String>, message: impl Into<String>) {
|
|
93
|
+
violations.push(format!(
|
|
94
|
+
"{}#{}: {}",
|
|
95
|
+
self.aggregate_name,
|
|
96
|
+
self.id,
|
|
97
|
+
message.into()
|
|
98
|
+
));
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
/// The aggregate's own value object of that name, else the one every aggregate of the chapter
|
|
@@ -92,22 +111,40 @@ impl<'a> Checker<'a> {
|
|
|
92
111
|
.map(|aggregates| {
|
|
93
112
|
aggregates
|
|
94
113
|
.iter()
|
|
95
|
-
.filter_map(|aggregate|
|
|
114
|
+
.filter_map(|aggregate| {
|
|
115
|
+
index_by_name(aggregate, "value_objects")
|
|
116
|
+
.get(type_name)
|
|
117
|
+
.copied()
|
|
118
|
+
})
|
|
96
119
|
.collect()
|
|
97
120
|
})
|
|
98
121
|
.unwrap_or_default();
|
|
99
122
|
let first = *declared.first()?;
|
|
100
|
-
declared
|
|
123
|
+
declared
|
|
124
|
+
.iter()
|
|
125
|
+
.all(|other| same_shape(other, first))
|
|
126
|
+
.then_some(first)
|
|
101
127
|
}
|
|
102
128
|
|
|
103
|
-
fn check_attributes(
|
|
104
|
-
|
|
129
|
+
fn check_attributes(
|
|
130
|
+
&self,
|
|
131
|
+
owner_name: &str,
|
|
132
|
+
owner: Owner,
|
|
133
|
+
state: &Value,
|
|
134
|
+
attributes: &[Value],
|
|
135
|
+
violations: &mut Vec<String>,
|
|
136
|
+
) {
|
|
137
|
+
let Some(state_obj) = state.as_object() else {
|
|
138
|
+
return;
|
|
139
|
+
};
|
|
105
140
|
for attr in attributes {
|
|
106
141
|
let attr_name = attribute_name(attr);
|
|
107
142
|
let type_name = attr.get("type").and_then(Value::as_str).unwrap_or("");
|
|
108
143
|
let list = flag(attr, "list");
|
|
109
144
|
let optional = flag(attr, "optional");
|
|
110
|
-
let Some(raw) = state_obj.get(attr_name) else {
|
|
145
|
+
let Some(raw) = state_obj.get(attr_name) else {
|
|
146
|
+
continue;
|
|
147
|
+
};
|
|
111
148
|
|
|
112
149
|
if raw.is_null() {
|
|
113
150
|
// A value object's required fields were judged as a whole; a list is never
|
|
@@ -122,10 +159,18 @@ impl<'a> Checker<'a> {
|
|
|
122
159
|
match raw.as_array() {
|
|
123
160
|
Some(items) => {
|
|
124
161
|
for item in items {
|
|
125
|
-
self.check_value(
|
|
162
|
+
self.check_value(
|
|
163
|
+
owner_name, attr_name, type_name, item, attr, violations,
|
|
164
|
+
);
|
|
126
165
|
}
|
|
127
166
|
}
|
|
128
|
-
None => self.refuse(
|
|
167
|
+
None => self.refuse(
|
|
168
|
+
violations,
|
|
169
|
+
format!(
|
|
170
|
+
"{owner_name}.{attr_name} expects list_of({type_name}), got {}",
|
|
171
|
+
ruby_inspect(raw)
|
|
172
|
+
),
|
|
173
|
+
),
|
|
129
174
|
}
|
|
130
175
|
} else {
|
|
131
176
|
self.check_value(owner_name, attr_name, type_name, raw, attr, violations);
|
|
@@ -137,17 +182,34 @@ impl<'a> Checker<'a> {
|
|
|
137
182
|
/// A required slot holding null. A value object reads as built from no fields at all, as
|
|
138
183
|
/// `Value::Coercion#nil_argument` builds it: refused for the first required field it lacks,
|
|
139
184
|
/// or accepted when it has none.
|
|
140
|
-
fn refuse_null(
|
|
185
|
+
fn refuse_null(
|
|
186
|
+
&self,
|
|
187
|
+
owner_name: &str,
|
|
188
|
+
attr_name: &str,
|
|
189
|
+
type_name: &str,
|
|
190
|
+
violations: &mut Vec<String>,
|
|
191
|
+
) {
|
|
141
192
|
match self.value_object(type_name) {
|
|
142
|
-
Some(vo) => self.check_value_object(
|
|
143
|
-
|
|
193
|
+
Some(vo) => self.check_value_object(
|
|
194
|
+
attr_name,
|
|
195
|
+
type_name,
|
|
196
|
+
vo,
|
|
197
|
+
&Value::Object(serde_json::Map::new()),
|
|
198
|
+
violations,
|
|
199
|
+
),
|
|
200
|
+
None => self.refuse(
|
|
201
|
+
violations,
|
|
202
|
+
format!("{owner_name}.{attr_name} expects {type_name}, got nil"),
|
|
203
|
+
),
|
|
144
204
|
}
|
|
145
205
|
}
|
|
146
206
|
|
|
147
207
|
/// `admits: "Aggregate::Set"`: the value must be one of the named closed set's first-column
|
|
148
208
|
/// members, compared as text (`Value::Admission#admit_declared_set`).
|
|
149
209
|
fn check_admits(&self, attr: &Value, raw: &Value, violations: &mut Vec<String>) {
|
|
150
|
-
let Some(admits) = attr.get("admits").and_then(Value::as_str) else {
|
|
210
|
+
let Some(admits) = attr.get("admits").and_then(Value::as_str) else {
|
|
211
|
+
return;
|
|
212
|
+
};
|
|
151
213
|
let name = attribute_name(attr);
|
|
152
214
|
let Some(admitted) = self.admitted_members(admits) else {
|
|
153
215
|
self.refuse(
|
|
@@ -167,8 +229,18 @@ impl<'a> Checker<'a> {
|
|
|
167
229
|
if admitted.contains(&ruby_to_s(offered)) {
|
|
168
230
|
return;
|
|
169
231
|
}
|
|
170
|
-
let listed = admitted
|
|
171
|
-
|
|
232
|
+
let listed = admitted
|
|
233
|
+
.iter()
|
|
234
|
+
.map(|member| inspect_text(member))
|
|
235
|
+
.collect::<Vec<_>>()
|
|
236
|
+
.join(", ");
|
|
237
|
+
self.refuse(
|
|
238
|
+
violations,
|
|
239
|
+
format!(
|
|
240
|
+
"{name} admits {admits} — {listed} — got {}",
|
|
241
|
+
ruby_inspect(offered)
|
|
242
|
+
),
|
|
243
|
+
);
|
|
172
244
|
}
|
|
173
245
|
|
|
174
246
|
/// The first column of each member row of the set `"Aggregate::Set"` names, as text.
|
|
@@ -179,19 +251,40 @@ impl<'a> Checker<'a> {
|
|
|
179
251
|
.get("aggregates")
|
|
180
252
|
.and_then(Value::as_array)?
|
|
181
253
|
.iter()
|
|
182
|
-
.find(|aggregate|
|
|
254
|
+
.find(|aggregate| {
|
|
255
|
+
aggregate.get("name").and_then(Value::as_str) == Some(aggregate_name)
|
|
256
|
+
})?;
|
|
183
257
|
let set = *index_by_name(aggregate, "value_objects").get(set_name)?;
|
|
184
258
|
let discriminant = attribute_name(declared_attributes(set).first()?);
|
|
185
|
-
Some(
|
|
259
|
+
Some(
|
|
260
|
+
member_rows(set)
|
|
261
|
+
.iter()
|
|
262
|
+
.map(|row| ruby_to_s(row.get(discriminant).unwrap_or(&Value::Null)))
|
|
263
|
+
.collect(),
|
|
264
|
+
)
|
|
186
265
|
}
|
|
187
266
|
|
|
188
|
-
fn check_value(
|
|
267
|
+
fn check_value(
|
|
268
|
+
&self,
|
|
269
|
+
owner_name: &str,
|
|
270
|
+
attr_name: &str,
|
|
271
|
+
type_name: &str,
|
|
272
|
+
value: &Value,
|
|
273
|
+
attr: &Value,
|
|
274
|
+
violations: &mut Vec<String>,
|
|
275
|
+
) {
|
|
189
276
|
if let Some(vo) = self.value_object(type_name) {
|
|
190
277
|
self.check_value_object(attr_name, type_name, vo, value, violations);
|
|
191
278
|
return;
|
|
192
279
|
}
|
|
193
280
|
if let Some(entity) = self.entities.get(type_name) {
|
|
194
|
-
self.check_attributes(
|
|
281
|
+
self.check_attributes(
|
|
282
|
+
type_name,
|
|
283
|
+
Owner::Record,
|
|
284
|
+
value,
|
|
285
|
+
declared_attributes(entity),
|
|
286
|
+
violations,
|
|
287
|
+
);
|
|
195
288
|
return;
|
|
196
289
|
}
|
|
197
290
|
|
|
@@ -201,15 +294,30 @@ impl<'a> Checker<'a> {
|
|
|
201
294
|
self.refuse(violations, refusal);
|
|
202
295
|
return;
|
|
203
296
|
}
|
|
204
|
-
if let (Some(pattern), Some(text)) =
|
|
297
|
+
if let (Some(pattern), Some(text)) =
|
|
298
|
+
(attr.get("pattern").and_then(Value::as_str), value.as_str())
|
|
299
|
+
{
|
|
205
300
|
if matches!(regex::Regex::new(pattern), Ok(re) if !re.is_match(text)) {
|
|
206
|
-
self.refuse(
|
|
301
|
+
self.refuse(
|
|
302
|
+
violations,
|
|
303
|
+
format!(
|
|
304
|
+
"{owner_name}.{attr_name} must match {pattern}, got {}",
|
|
305
|
+
ruby_inspect(value)
|
|
306
|
+
),
|
|
307
|
+
);
|
|
207
308
|
}
|
|
208
309
|
}
|
|
209
310
|
}
|
|
210
311
|
|
|
211
312
|
/// One stored value object, held to what `Value::Validation#validate!` holds an offered one to.
|
|
212
|
-
fn check_value_object(
|
|
313
|
+
fn check_value_object(
|
|
314
|
+
&self,
|
|
315
|
+
attr_name: &str,
|
|
316
|
+
type_name: &str,
|
|
317
|
+
vo: &Value,
|
|
318
|
+
raw: &Value,
|
|
319
|
+
violations: &mut Vec<String>,
|
|
320
|
+
) {
|
|
213
321
|
let declared = declared_attributes(vo);
|
|
214
322
|
|
|
215
323
|
// A bare scalar stands in for a single-field value object's one field.
|
|
@@ -217,19 +325,43 @@ impl<'a> Checker<'a> {
|
|
|
217
325
|
let fields = if raw.is_object() {
|
|
218
326
|
raw
|
|
219
327
|
} else if declared.len() == 1 {
|
|
220
|
-
wrapped = Value::Object(
|
|
328
|
+
wrapped = Value::Object(
|
|
329
|
+
std::iter::once((attribute_name(&declared[0]).to_string(), raw.clone())).collect(),
|
|
330
|
+
);
|
|
221
331
|
&wrapped
|
|
222
332
|
} else {
|
|
223
|
-
self.refuse(
|
|
333
|
+
self.refuse(
|
|
334
|
+
violations,
|
|
335
|
+
format!(
|
|
336
|
+
"{attr_name} is a {type_name} — pass its fields as an object, not {}",
|
|
337
|
+
ruby_inspect(raw)
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
return;
|
|
341
|
+
};
|
|
342
|
+
let Some(given) = fields.as_object() else {
|
|
224
343
|
return;
|
|
225
344
|
};
|
|
226
|
-
let Some(given) = fields.as_object() else { return };
|
|
227
345
|
|
|
228
|
-
let mut unknown: Vec<&str> = given
|
|
346
|
+
let mut unknown: Vec<&str> = given
|
|
347
|
+
.keys()
|
|
348
|
+
.map(String::as_str)
|
|
349
|
+
.filter(|key| !declared.iter().any(|attr| attribute_name(attr) == *key))
|
|
350
|
+
.collect();
|
|
229
351
|
if !unknown.is_empty() {
|
|
230
|
-
unknown.
|
|
231
|
-
let takes = declared
|
|
232
|
-
|
|
352
|
+
unknown.sort_unstable();
|
|
353
|
+
let takes = declared
|
|
354
|
+
.iter()
|
|
355
|
+
.map(attribute_name)
|
|
356
|
+
.collect::<Vec<_>>()
|
|
357
|
+
.join(", ");
|
|
358
|
+
self.refuse(
|
|
359
|
+
violations,
|
|
360
|
+
format!(
|
|
361
|
+
"{type_name} does not declare {} — it takes {takes}",
|
|
362
|
+
unknown.join(", ")
|
|
363
|
+
),
|
|
364
|
+
);
|
|
233
365
|
return;
|
|
234
366
|
}
|
|
235
367
|
|
|
@@ -237,12 +369,20 @@ impl<'a> Checker<'a> {
|
|
|
237
369
|
let before = violations.len();
|
|
238
370
|
// The first required field left out, as `check_required_fields` refuses it.
|
|
239
371
|
let missing = declared.iter().find(|attr| {
|
|
240
|
-
let present = filled
|
|
372
|
+
let present = filled
|
|
373
|
+
.get(attribute_name(attr))
|
|
374
|
+
.is_some_and(|value| !value.is_null());
|
|
241
375
|
!present && !flag(attr, "optional") && !flag(attr, "list")
|
|
242
376
|
});
|
|
243
377
|
if let Some(attr) = missing {
|
|
244
378
|
let expected = attr.get("type").and_then(Value::as_str).unwrap_or("");
|
|
245
|
-
self.refuse(
|
|
379
|
+
self.refuse(
|
|
380
|
+
violations,
|
|
381
|
+
format!(
|
|
382
|
+
"{type_name}.{} expects {expected}, got nil",
|
|
383
|
+
attribute_name(attr)
|
|
384
|
+
),
|
|
385
|
+
);
|
|
246
386
|
return;
|
|
247
387
|
}
|
|
248
388
|
|
|
@@ -264,31 +404,62 @@ impl<'a> Checker<'a> {
|
|
|
264
404
|
|
|
265
405
|
/// `Value::Admission#admit_member`: some member row matches on every field it names, compared
|
|
266
406
|
/// as text. The refusal quotes the first column only.
|
|
267
|
-
fn check_member(
|
|
407
|
+
fn check_member(
|
|
408
|
+
&self,
|
|
409
|
+
type_name: &str,
|
|
410
|
+
vo: &Value,
|
|
411
|
+
fields: &Value,
|
|
412
|
+
violations: &mut Vec<String>,
|
|
413
|
+
) {
|
|
268
414
|
let rows = member_rows(vo);
|
|
269
415
|
let matches_row = |row: &serde_json::Map<String, Value>| {
|
|
270
|
-
row.iter().all(|(field, member)|
|
|
416
|
+
row.iter().all(|(field, member)| {
|
|
417
|
+
ruby_to_s(fields.get(field).unwrap_or(&Value::Null)) == ruby_to_s(member)
|
|
418
|
+
})
|
|
271
419
|
};
|
|
272
420
|
if rows.iter().any(matches_row) {
|
|
273
421
|
return;
|
|
274
422
|
}
|
|
275
|
-
let Some(first) = declared_attributes(vo).first().map(attribute_name) else {
|
|
276
|
-
|
|
423
|
+
let Some(first) = declared_attributes(vo).first().map(attribute_name) else {
|
|
424
|
+
return;
|
|
425
|
+
};
|
|
426
|
+
let admitted = rows
|
|
427
|
+
.iter()
|
|
428
|
+
.map(|row| ruby_inspect(row.get(first).unwrap_or(&Value::Null)))
|
|
429
|
+
.collect::<Vec<_>>()
|
|
430
|
+
.join(", ");
|
|
277
431
|
let offered = ruby_inspect(fields.get(first).unwrap_or(&Value::Null));
|
|
278
|
-
self.refuse(
|
|
432
|
+
self.refuse(
|
|
433
|
+
violations,
|
|
434
|
+
format!("{type_name} admits {admitted} — got {offered}"),
|
|
435
|
+
);
|
|
279
436
|
}
|
|
280
437
|
|
|
281
438
|
/// Checks a value object's `invariants` through `expr_json`. Fails closed: a malformed or
|
|
282
439
|
/// unsupported `ast` is a violation, since an invariant that cannot be evaluated must not
|
|
283
440
|
/// be minted past.
|
|
284
|
-
fn check_invariants(
|
|
285
|
-
|
|
441
|
+
fn check_invariants(
|
|
442
|
+
&self,
|
|
443
|
+
attr_name: &str,
|
|
444
|
+
type_name: &str,
|
|
445
|
+
value: &Value,
|
|
446
|
+
vo: &Value,
|
|
447
|
+
violations: &mut Vec<String>,
|
|
448
|
+
) {
|
|
449
|
+
let Some(invariants) = vo.get("invariants").and_then(Value::as_array) else {
|
|
450
|
+
return;
|
|
451
|
+
};
|
|
286
452
|
let known = with_absent_optionals(vo, value);
|
|
287
453
|
|
|
288
454
|
for invariant in invariants {
|
|
289
|
-
let description = invariant
|
|
455
|
+
let description = invariant
|
|
456
|
+
.get("description")
|
|
457
|
+
.and_then(Value::as_str)
|
|
458
|
+
.unwrap_or("");
|
|
290
459
|
// An older `ir.json` has no `ast`: nothing to check, so not a violation.
|
|
291
|
-
let Some(ast) = invariant.get("ast") else {
|
|
460
|
+
let Some(ast) = invariant.get("ast") else {
|
|
461
|
+
continue;
|
|
462
|
+
};
|
|
292
463
|
|
|
293
464
|
let expr = match expr_json::parse(ast) {
|
|
294
465
|
Ok(expr) => expr,
|
|
@@ -300,9 +471,12 @@ impl<'a> Checker<'a> {
|
|
|
300
471
|
|
|
301
472
|
match expr_json::interpret(&expr, &known) {
|
|
302
473
|
Ok(result) if result.truthy() => {}
|
|
303
|
-
Ok(_) => self.refuse(
|
|
474
|
+
Ok(_) => self.refuse(
|
|
475
|
+
violations,
|
|
476
|
+
format!("{attr_name} ({type_name}) violates its own invariant — {description}"),
|
|
477
|
+
),
|
|
304
478
|
Err(error) => {
|
|
305
|
-
self.refuse(violations, format!("{attr_name} ({type_name})'s own invariant {description:?} could not be checked — {error}"))
|
|
479
|
+
self.refuse(violations, format!("{attr_name} ({type_name})'s own invariant {description:?} could not be checked — {error}"));
|
|
306
480
|
}
|
|
307
481
|
}
|
|
308
482
|
}
|
|
@@ -318,7 +492,10 @@ fn same_shape(left: &Value, right: &Value) -> bool {
|
|
|
318
492
|
.map(|attr| {
|
|
319
493
|
(
|
|
320
494
|
attribute_name(attr).to_string(),
|
|
321
|
-
attr.get("type")
|
|
495
|
+
attr.get("type")
|
|
496
|
+
.and_then(Value::as_str)
|
|
497
|
+
.unwrap_or("")
|
|
498
|
+
.to_string(),
|
|
322
499
|
flag(attr, "list"),
|
|
323
500
|
flag(attr, "optional"),
|
|
324
501
|
)
|
|
@@ -341,7 +518,12 @@ fn member_rows(vo: &Value) -> Vec<serde_json::Map<String, Value>> {
|
|
|
341
518
|
pairs
|
|
342
519
|
.iter()
|
|
343
520
|
.filter_map(Value::as_array)
|
|
344
|
-
.filter_map(|pair|
|
|
521
|
+
.filter_map(|pair| {
|
|
522
|
+
Some((
|
|
523
|
+
pair.first()?.as_str()?.to_string(),
|
|
524
|
+
pair.get(1)?.clone(),
|
|
525
|
+
))
|
|
526
|
+
})
|
|
345
527
|
.collect()
|
|
346
528
|
})
|
|
347
529
|
.unwrap_or_default()
|
|
@@ -354,7 +536,10 @@ fn member_rows(vo: &Value) -> Vec<serde_json::Map<String, Value>> {
|
|
|
354
536
|
/// The value as the Ruby runtime would hold it once it has defaulted what was left out: every
|
|
355
537
|
/// declared attribute missing from `given` takes its own `default:`, and every required list
|
|
356
538
|
/// reads as empty (`Value::Validation#apply_defaults`). An optional slot stays absent.
|
|
357
|
-
fn with_defaults(
|
|
539
|
+
fn with_defaults(
|
|
540
|
+
declared: &[Value],
|
|
541
|
+
given: &serde_json::Map<String, Value>,
|
|
542
|
+
) -> serde_json::Map<String, Value> {
|
|
358
543
|
let mut filled = given.clone();
|
|
359
544
|
for attr in declared {
|
|
360
545
|
let name = attribute_name(attr);
|
|
@@ -377,12 +562,25 @@ fn with_defaults(declared: &[Value], given: &serde_json::Map<String, Value>) ->
|
|
|
377
562
|
/// A scalar that does not fit its declared type, worded as dispatch words it; `None` when it fits
|
|
378
563
|
/// or the type is not a plain scalar. A leaf is a String, a number or a boolean, never a list or
|
|
379
564
|
/// an object standing in for one.
|
|
380
|
-
fn scalar_refusal(
|
|
381
|
-
|
|
565
|
+
fn scalar_refusal(
|
|
566
|
+
owner_name: &str,
|
|
567
|
+
attr_name: &str,
|
|
568
|
+
type_name: &str,
|
|
569
|
+
value: &Value,
|
|
570
|
+
) -> Option<String> {
|
|
571
|
+
let mismatch = || {
|
|
572
|
+
format!(
|
|
573
|
+
"{owner_name}.{attr_name} expects {type_name}, got {}",
|
|
574
|
+
ruby_inspect(value)
|
|
575
|
+
)
|
|
576
|
+
};
|
|
382
577
|
match type_name {
|
|
383
578
|
"String" if !value.is_string() => Some(mismatch()),
|
|
384
579
|
"Integer" if value.is_i64() => None,
|
|
385
|
-
"Integer" if value.is_u64() => Some(format!(
|
|
580
|
+
"Integer" if value.is_u64() => Some(format!(
|
|
581
|
+
"{owner_name}.{attr_name} must fit in a 64-bit integer, got {}",
|
|
582
|
+
ruby_inspect(value)
|
|
583
|
+
)),
|
|
386
584
|
"Integer" => Some(mismatch()),
|
|
387
585
|
"Float" if value.is_number() => None,
|
|
388
586
|
"Float" => Some(mismatch()),
|
|
@@ -405,7 +603,14 @@ fn ruby_inspect(value: &Value) -> String {
|
|
|
405
603
|
match value {
|
|
406
604
|
Value::Null => "nil".to_string(),
|
|
407
605
|
Value::String(text) => inspect_text(text),
|
|
408
|
-
Value::Array(items) => format!(
|
|
606
|
+
Value::Array(items) => format!(
|
|
607
|
+
"[{}]",
|
|
608
|
+
items
|
|
609
|
+
.iter()
|
|
610
|
+
.map(ruby_inspect)
|
|
611
|
+
.collect::<Vec<_>>()
|
|
612
|
+
.join(", ")
|
|
613
|
+
),
|
|
409
614
|
other => other.to_string(),
|
|
410
615
|
}
|
|
411
616
|
}
|
|
@@ -420,10 +625,14 @@ fn inspect_text(text: &str) -> String {
|
|
|
420
625
|
/// `x.nil?` and a comparison with nil all answer for a slot a writer never sent. A name the value
|
|
421
626
|
/// object does not declare is not filled, so a lookup of it still refuses.
|
|
422
627
|
fn with_absent_optionals(vo: &Value, value: &Value) -> Value {
|
|
423
|
-
let Some(fields) = value.as_object() else {
|
|
628
|
+
let Some(fields) = value.as_object() else {
|
|
629
|
+
return value.clone();
|
|
630
|
+
};
|
|
424
631
|
let mut known = fields.clone();
|
|
425
632
|
for attribute in declared_attributes(vo) {
|
|
426
|
-
let Some(name) = attribute.get("name").and_then(Value::as_str) else {
|
|
633
|
+
let Some(name) = attribute.get("name").and_then(Value::as_str) else {
|
|
634
|
+
continue;
|
|
635
|
+
};
|
|
427
636
|
if flag(attribute, "optional") && !known.contains_key(name) {
|
|
428
637
|
known.insert(name.to_string(), Value::Null);
|
|
429
638
|
}
|
|
@@ -431,7 +640,13 @@ fn with_absent_optionals(vo: &Value, value: &Value) -> Value {
|
|
|
431
640
|
Value::Object(known)
|
|
432
641
|
}
|
|
433
642
|
|
|
434
|
-
fn check_lifecycle(
|
|
643
|
+
fn check_lifecycle(
|
|
644
|
+
aggregate_name: &str,
|
|
645
|
+
id: &str,
|
|
646
|
+
state: &Value,
|
|
647
|
+
lifecycle: &Value,
|
|
648
|
+
violations: &mut Vec<String>,
|
|
649
|
+
) {
|
|
435
650
|
let field = lifecycle.get("field").and_then(Value::as_str).unwrap_or("");
|
|
436
651
|
if field.is_empty() {
|
|
437
652
|
return;
|
|
@@ -444,15 +659,21 @@ fn check_lifecycle(aggregate_name: &str, id: &str, state: &Value, lifecycle: &Va
|
|
|
444
659
|
let mut allowed: Vec<&str> = lifecycle
|
|
445
660
|
.get("transitions")
|
|
446
661
|
.and_then(Value::as_array)
|
|
447
|
-
.map(|ts|
|
|
662
|
+
.map(|ts| {
|
|
663
|
+
ts.iter()
|
|
664
|
+
.filter_map(|t| t.get("to_state").and_then(Value::as_str))
|
|
665
|
+
.collect()
|
|
666
|
+
})
|
|
448
667
|
.unwrap_or_default();
|
|
449
668
|
if let Some(default) = lifecycle.get("default").and_then(Value::as_str) {
|
|
450
669
|
allowed.push(default);
|
|
451
670
|
}
|
|
452
671
|
|
|
453
|
-
let ok = held.as_str().
|
|
672
|
+
let ok = held.as_str().is_some_and(|h| allowed.contains(&h));
|
|
454
673
|
if !ok {
|
|
455
|
-
violations.push(format!(
|
|
674
|
+
violations.push(format!(
|
|
675
|
+
"{aggregate_name}#{id}: {field} is {held}, a state this era's lifecycle never reaches"
|
|
676
|
+
));
|
|
456
677
|
}
|
|
457
678
|
}
|
|
458
679
|
|
|
@@ -463,10 +684,20 @@ mod tests {
|
|
|
463
684
|
|
|
464
685
|
// The aggregate alone is the whole domain here; `domain_validate` takes the domain apart.
|
|
465
686
|
fn validate(aggregate_ir: &Value, id: &str, state: &Value) -> Vec<String> {
|
|
466
|
-
domain_validate(
|
|
687
|
+
domain_validate(
|
|
688
|
+
&json!({"aggregates": [aggregate_ir]}),
|
|
689
|
+
aggregate_ir,
|
|
690
|
+
id,
|
|
691
|
+
state,
|
|
692
|
+
)
|
|
467
693
|
}
|
|
468
694
|
|
|
469
|
-
fn domain_validate(
|
|
695
|
+
fn domain_validate(
|
|
696
|
+
domain_ir: &Value,
|
|
697
|
+
aggregate_ir: &Value,
|
|
698
|
+
id: &str,
|
|
699
|
+
state: &Value,
|
|
700
|
+
) -> Vec<String> {
|
|
470
701
|
super::validate(domain_ir, aggregate_ir, id, state)
|
|
471
702
|
}
|
|
472
703
|
|
|
@@ -525,7 +756,13 @@ mod tests {
|
|
|
525
756
|
fn a_value_outside_the_declared_set_it_admits_is_caught_in_dispatchs_words() {
|
|
526
757
|
let state = json!({ "pizza": { "cents": 1200, "size": "medium" }, "toppings": [], "status": "available" });
|
|
527
758
|
let violations = validate(&order_ir(), "p1", &state);
|
|
528
|
-
assert_eq!(
|
|
759
|
+
assert_eq!(
|
|
760
|
+
violations,
|
|
761
|
+
vec![
|
|
762
|
+
r#"Order#p1: size admits Order::Size — "small", "large" — got "medium""#
|
|
763
|
+
.to_string()
|
|
764
|
+
]
|
|
765
|
+
);
|
|
529
766
|
}
|
|
530
767
|
|
|
531
768
|
#[test]
|
|
@@ -535,7 +772,11 @@ mod tests {
|
|
|
535
772
|
let state = json!({ "pizza": { "cents": 1200, "size": "small" }, "toppings": [], "status": "available" });
|
|
536
773
|
let violations = validate(&ir, "p1", &state);
|
|
537
774
|
assert_eq!(violations.len(), 1);
|
|
538
|
-
assert!(
|
|
775
|
+
assert!(
|
|
776
|
+
violations[0]
|
|
777
|
+
.contains("size admits Order::Missing, which this chapter does not declare"),
|
|
778
|
+
"{violations:?}"
|
|
779
|
+
);
|
|
539
780
|
}
|
|
540
781
|
|
|
541
782
|
#[test]
|
|
@@ -571,7 +812,10 @@ mod tests {
|
|
|
571
812
|
let state = json!({ "pizza": { "cents": -500, "size": "large" }, "toppings": [], "status": "available" });
|
|
572
813
|
let violations = validate(&order_ir(), "p1", &state);
|
|
573
814
|
assert_eq!(violations.len(), 1);
|
|
574
|
-
assert!(
|
|
815
|
+
assert!(
|
|
816
|
+
violations[0].contains("a price is never negative"),
|
|
817
|
+
"{violations:?}"
|
|
818
|
+
);
|
|
575
819
|
}
|
|
576
820
|
|
|
577
821
|
#[test]
|
|
@@ -584,7 +828,10 @@ mod tests {
|
|
|
584
828
|
fn an_invariant_with_no_ast_key_at_all_is_not_checked_and_not_a_violation() {
|
|
585
829
|
// An `ir.json` built before `ast:` existed: nothing to evaluate, so nothing claimed.
|
|
586
830
|
let mut ir = order_ir();
|
|
587
|
-
ir["value_objects"][0]["invariants"][0]
|
|
831
|
+
ir["value_objects"][0]["invariants"][0]
|
|
832
|
+
.as_object_mut()
|
|
833
|
+
.unwrap()
|
|
834
|
+
.remove("ast");
|
|
588
835
|
let state = json!({ "pizza": { "cents": -500, "size": "large" }, "toppings": [], "status": "available" });
|
|
589
836
|
assert_eq!(validate(&ir, "p1", &state), Vec::<String>::new());
|
|
590
837
|
}
|
|
@@ -602,7 +849,10 @@ mod tests {
|
|
|
602
849
|
let bad = json!({ "pizza": { "cents": null, "size": "large" }, "toppings": [], "status": "available" });
|
|
603
850
|
let violations = validate(&ir, "p1", &bad);
|
|
604
851
|
assert_eq!(violations.len(), 1);
|
|
605
|
-
assert!(
|
|
852
|
+
assert!(
|
|
853
|
+
violations[0].contains("violates its own invariant"),
|
|
854
|
+
"{violations:?}"
|
|
855
|
+
);
|
|
606
856
|
}
|
|
607
857
|
|
|
608
858
|
#[test]
|
|
@@ -614,7 +864,11 @@ mod tests {
|
|
|
614
864
|
let state = json!({ "pizza": { "cents": 1200, "size": "large" }, "toppings": [], "status": "available" });
|
|
615
865
|
let violations = validate(&ir, "p1", &state);
|
|
616
866
|
assert_eq!(violations.len(), 1);
|
|
617
|
-
assert!(
|
|
867
|
+
assert!(
|
|
868
|
+
violations[0]
|
|
869
|
+
.contains("could not be checked — empty? expects a list or string, got 1200"),
|
|
870
|
+
"{violations:?}"
|
|
871
|
+
);
|
|
618
872
|
}
|
|
619
873
|
|
|
620
874
|
#[test]
|
|
@@ -622,22 +876,27 @@ mod tests {
|
|
|
622
876
|
let state = json!({ "pizza": null, "toppings": [], "status": "available" });
|
|
623
877
|
let violations = validate(&order_ir(), "p1", &state);
|
|
624
878
|
assert_eq!(violations.len(), 1);
|
|
625
|
-
assert!(
|
|
879
|
+
assert!(
|
|
880
|
+
violations[0].contains("Pizza.cents expects Integer, got nil"),
|
|
881
|
+
"{violations:?}"
|
|
882
|
+
);
|
|
626
883
|
}
|
|
627
884
|
|
|
628
885
|
// A page of panels, each optionally holding a note, which optionally holds a link; a panel
|
|
629
886
|
// also lists cells that may hold a note. Every rule reads a slot a stored value may leave out.
|
|
630
887
|
fn layout_ir() -> Value {
|
|
631
|
-
let attr = |name: &str, ty: &str, list: bool, optional: bool| {
|
|
632
|
-
json!({"name": name, "type": ty, "list": list, "optional": optional})
|
|
633
|
-
};
|
|
888
|
+
let attr = |name: &str, ty: &str, list: bool, optional: bool| json!({"name": name, "type": ty, "list": list, "optional": optional});
|
|
634
889
|
let lookup = |path: &[&str]| json!({"op": "lookup", "path": path});
|
|
635
|
-
let unset =
|
|
636
|
-
|
|
890
|
+
let unset =
|
|
891
|
+
|path: &[&str]| json!({"op": "assignment", "receiver": lookup(path), "negated": true});
|
|
892
|
+
let set =
|
|
893
|
+
|path: &[&str]| json!({"op": "assignment", "receiver": lookup(path), "negated": false});
|
|
637
894
|
let not_empty = |path: &[&str]| json!({"op": "not", "expr": {"op": "empty", "receiver": {"op": "to_s", "receiver": lookup(path)}}});
|
|
638
895
|
let either = |left: Value, right: Value| json!({"op": "or", "left": left, "right": right});
|
|
639
|
-
let differs = |path: &[&str], text: &str|
|
|
640
|
-
"
|
|
896
|
+
let differs = |path: &[&str], text: &str| {
|
|
897
|
+
json!({"op": "compare", "cmp": {"less_than": false, "equal": true, "negated": true},
|
|
898
|
+
"left": lookup(path), "right": {"op": "str", "value": text}})
|
|
899
|
+
};
|
|
641
900
|
let rule = |description: &str, ast: Value| json!({"description": description, "canonical": "", "ast": ast});
|
|
642
901
|
json!({
|
|
643
902
|
"name": "Layout",
|
|
@@ -668,19 +927,29 @@ mod tests {
|
|
|
668
927
|
})
|
|
669
928
|
}
|
|
670
929
|
|
|
930
|
+
#[allow(clippy::needless_pass_by_value)] // reason: test helper called with inline literals
|
|
671
931
|
fn layout_violations(panels: Value) -> Vec<String> {
|
|
672
|
-
validate(
|
|
932
|
+
validate(
|
|
933
|
+
&layout_ir(),
|
|
934
|
+
"l1",
|
|
935
|
+
&json!({"key": {"value": "l1"}, "panels": panels}),
|
|
936
|
+
)
|
|
673
937
|
}
|
|
674
938
|
|
|
675
939
|
#[test]
|
|
676
940
|
fn a_stored_panel_with_every_optional_slot_absent_is_accepted() {
|
|
677
|
-
assert_eq!(
|
|
941
|
+
assert_eq!(
|
|
942
|
+
layout_violations(json!([{"kind": "plain", "cells": [{"label": "c"}]}])),
|
|
943
|
+
Vec::<String>::new()
|
|
944
|
+
);
|
|
678
945
|
}
|
|
679
946
|
|
|
680
947
|
#[test]
|
|
681
948
|
fn an_absent_slot_reads_the_same_as_an_explicit_null() {
|
|
682
949
|
let absent = layout_violations(json!([{"kind": "callout", "cells": []}]));
|
|
683
|
-
let null = layout_violations(
|
|
950
|
+
let null = layout_violations(
|
|
951
|
+
json!([{"kind": "callout", "note": null, "title": null, "cells": []}]),
|
|
952
|
+
);
|
|
684
953
|
assert_eq!(absent.len(), 1, "{absent:?}");
|
|
685
954
|
assert_eq!(absent, null);
|
|
686
955
|
}
|
|
@@ -689,12 +958,18 @@ mod tests {
|
|
|
689
958
|
fn a_rule_that_needs_an_absent_slot_refuses_with_its_description_not_a_lookup_error() {
|
|
690
959
|
let violations = layout_violations(json!([{"kind": "callout", "cells": []}]));
|
|
691
960
|
assert_eq!(violations.len(), 1, "{violations:?}");
|
|
692
|
-
assert!(
|
|
961
|
+
assert!(
|
|
962
|
+
violations[0].contains("violates its own invariant — a callout carries a note"),
|
|
963
|
+
"{violations:?}"
|
|
964
|
+
);
|
|
693
965
|
}
|
|
694
966
|
|
|
695
967
|
#[test]
|
|
696
968
|
fn a_path_through_an_absent_value_object_reads_as_nil() {
|
|
697
|
-
assert_eq!(
|
|
969
|
+
assert_eq!(
|
|
970
|
+
layout_violations(json!([{"kind": "quiet", "cells": []}])),
|
|
971
|
+
Vec::<String>::new()
|
|
972
|
+
);
|
|
698
973
|
}
|
|
699
974
|
|
|
700
975
|
#[test]
|
|
@@ -703,7 +978,10 @@ mod tests {
|
|
|
703
978
|
let blank = json!([{"kind": "plain", "cells": [{"label": "c", "note": {"text": "n", "link": {"href": "/a", "label": ""}}}]}]);
|
|
704
979
|
assert_eq!(layout_violations(absent), Vec::<String>::new());
|
|
705
980
|
let violations = layout_violations(blank);
|
|
706
|
-
assert!(
|
|
981
|
+
assert!(
|
|
982
|
+
violations.len() == 1 && violations[0].contains("a link label is not blank when given"),
|
|
983
|
+
"{violations:?}"
|
|
984
|
+
);
|
|
707
985
|
}
|
|
708
986
|
|
|
709
987
|
#[test]
|
|
@@ -712,27 +990,38 @@ mod tests {
|
|
|
712
990
|
let bad = json!([{"kind": "plain", "cells": [{"label": "a"}, {"label": "b", "note": {"text": ""}}]}]);
|
|
713
991
|
assert_eq!(layout_violations(members), Vec::<String>::new());
|
|
714
992
|
let violations = layout_violations(bad);
|
|
715
|
-
assert!(
|
|
993
|
+
assert!(
|
|
994
|
+
violations
|
|
995
|
+
.iter()
|
|
996
|
+
.any(|v| v.contains("a cell's note has words")),
|
|
997
|
+
"{violations:?}"
|
|
998
|
+
);
|
|
716
999
|
}
|
|
717
1000
|
|
|
718
1001
|
#[test]
|
|
719
1002
|
fn a_name_the_value_object_does_not_declare_still_refuses_to_resolve() {
|
|
720
1003
|
let mut ir = layout_ir();
|
|
721
|
-
ir["value_objects"][1]["invariants"] =
|
|
722
|
-
json!([{"description": "a typo", "canonical": "", "ast": {"op": "lookup", "path": ["tittle"]}}]);
|
|
1004
|
+
ir["value_objects"][1]["invariants"] = json!([{"description": "a typo", "canonical": "", "ast": {"op": "lookup", "path": ["tittle"]}}]);
|
|
723
1005
|
let state = json!({"key": {"value": "l1"}, "panels": [{"kind": "plain", "cells": []}]});
|
|
724
1006
|
let violations = validate(&ir, "l1", &state);
|
|
725
1007
|
assert_eq!(violations.len(), 1, "{violations:?}");
|
|
726
|
-
assert!(
|
|
1008
|
+
assert!(
|
|
1009
|
+
violations[0].contains(
|
|
1010
|
+
"could not be checked — cannot resolve \"tittle\" — no such attribute or argument"
|
|
1011
|
+
),
|
|
1012
|
+
"{violations:?}"
|
|
1013
|
+
);
|
|
727
1014
|
}
|
|
728
1015
|
|
|
729
1016
|
#[test]
|
|
730
1017
|
fn a_required_slot_left_out_is_refused_by_name_not_filled_in_for_an_invariant_to_read() {
|
|
731
1018
|
let mut ir = layout_ir();
|
|
732
|
-
ir["value_objects"][1]["invariants"] =
|
|
733
|
-
json!([{"description": "a kind", "canonical": "", "ast": {"op": "lookup", "path": ["kind"]}}]);
|
|
1019
|
+
ir["value_objects"][1]["invariants"] = json!([{"description": "a kind", "canonical": "", "ast": {"op": "lookup", "path": ["kind"]}}]);
|
|
734
1020
|
let state = json!({"key": {"value": "l1"}, "panels": [{"cells": []}]});
|
|
735
|
-
assert_eq!(
|
|
1021
|
+
assert_eq!(
|
|
1022
|
+
validate(&ir, "l1", &state),
|
|
1023
|
+
vec!["Layout#l1: Panel.kind expects String, got nil".to_string()]
|
|
1024
|
+
);
|
|
736
1025
|
}
|
|
737
1026
|
|
|
738
1027
|
// A closed set, a multi-field closed set and a value object that holds both, three deep and in a
|
|
@@ -765,10 +1054,16 @@ mod tests {
|
|
|
765
1054
|
json!({"band": {"size": "small"}, "count": 1, "slug": "ok", "labels": ["a", "b"]})
|
|
766
1055
|
}
|
|
767
1056
|
|
|
1057
|
+
#[allow(clippy::needless_pass_by_value)] // reason: test helper called with inline literals
|
|
768
1058
|
fn shelf_violations(panels: Value) -> Vec<String> {
|
|
769
|
-
validate(
|
|
1059
|
+
validate(
|
|
1060
|
+
&constrained_ir(),
|
|
1061
|
+
"s1",
|
|
1062
|
+
&json!({"band": {"size": "large"}, "panels": panels}),
|
|
1063
|
+
)
|
|
770
1064
|
}
|
|
771
1065
|
|
|
1066
|
+
#[allow(clippy::needless_pass_by_value)] // reason: test helper called with inline literals
|
|
772
1067
|
fn assert_refused_as(violations: Vec<String>, expected: &str) {
|
|
773
1068
|
assert_eq!(violations, vec![format!("Shelf#s1: {expected}")]);
|
|
774
1069
|
}
|
|
@@ -779,21 +1074,34 @@ mod tests {
|
|
|
779
1074
|
inner["pairing"] = json!({"code": "q", "label": "Cue"});
|
|
780
1075
|
let mut outer = panel();
|
|
781
1076
|
outer["inner"] = json!({"band": {"size": "large"}, "count": 2, "slug": "in", "labels": [], "inner": inner});
|
|
782
|
-
assert_eq!(
|
|
1077
|
+
assert_eq!(
|
|
1078
|
+
shelf_violations(json!([outer, panel()])),
|
|
1079
|
+
Vec::<String>::new()
|
|
1080
|
+
);
|
|
783
1081
|
}
|
|
784
1082
|
|
|
785
1083
|
#[test]
|
|
786
1084
|
fn a_closed_set_refuses_a_non_member_alone_in_a_list_member_and_three_deep() {
|
|
787
|
-
let violations = validate(
|
|
1085
|
+
let violations = validate(
|
|
1086
|
+
&constrained_ir(),
|
|
1087
|
+
"s1",
|
|
1088
|
+
&json!({"band": {"size": "huge"}, "panels": []}),
|
|
1089
|
+
);
|
|
788
1090
|
assert_refused_as(violations, r#"Band admits "small", "large" — got "huge""#);
|
|
789
1091
|
|
|
790
1092
|
let mut bad = panel();
|
|
791
1093
|
bad["band"] = json!({"size": "huge"});
|
|
792
|
-
assert_refused_as(
|
|
1094
|
+
assert_refused_as(
|
|
1095
|
+
shelf_violations(json!([panel(), bad.clone()])),
|
|
1096
|
+
r#"Band admits "small", "large" — got "huge""#,
|
|
1097
|
+
);
|
|
793
1098
|
|
|
794
1099
|
let mut deep = panel();
|
|
795
1100
|
deep["inner"] = json!({"band": {"size": "small"}, "count": 2, "slug": "in", "labels": [], "inner": {"band": {"size": "small"}, "count": 3, "slug": "x", "labels": [], "inner": bad}});
|
|
796
|
-
assert_refused_as(
|
|
1101
|
+
assert_refused_as(
|
|
1102
|
+
shelf_violations(json!([deep])),
|
|
1103
|
+
r#"Band admits "small", "large" — got "huge""#,
|
|
1104
|
+
);
|
|
797
1105
|
}
|
|
798
1106
|
|
|
799
1107
|
#[test]
|
|
@@ -803,14 +1111,20 @@ mod tests {
|
|
|
803
1111
|
assert_eq!(shelf_violations(json!([bare])), Vec::<String>::new());
|
|
804
1112
|
let mut wrong = panel();
|
|
805
1113
|
wrong["band"] = json!("huge");
|
|
806
|
-
assert_refused_as(
|
|
1114
|
+
assert_refused_as(
|
|
1115
|
+
shelf_violations(json!([wrong])),
|
|
1116
|
+
r#"Band admits "small", "large" — got "huge""#,
|
|
1117
|
+
);
|
|
807
1118
|
}
|
|
808
1119
|
|
|
809
1120
|
#[test]
|
|
810
1121
|
fn a_multi_field_member_must_match_on_every_column_and_quotes_the_first() {
|
|
811
1122
|
let mut mixed = panel();
|
|
812
1123
|
mixed["pairing"] = json!({"code": "p", "label": "Cue"});
|
|
813
|
-
assert_refused_as(
|
|
1124
|
+
assert_refused_as(
|
|
1125
|
+
shelf_violations(json!([mixed])),
|
|
1126
|
+
r#"Pairing admits "p", "q" — got "p""#,
|
|
1127
|
+
);
|
|
814
1128
|
}
|
|
815
1129
|
|
|
816
1130
|
#[test]
|
|
@@ -819,7 +1133,11 @@ mod tests {
|
|
|
819
1133
|
extra["bogus"] = json!(1);
|
|
820
1134
|
assert_refused_as(shelf_violations(json!([extra])), "Panel does not declare bogus — it takes band, count, slug, labels, pairing, tone, inner");
|
|
821
1135
|
|
|
822
|
-
let violations = validate(
|
|
1136
|
+
let violations = validate(
|
|
1137
|
+
&constrained_ir(),
|
|
1138
|
+
"s1",
|
|
1139
|
+
&json!({"band": {"size": "small", "bogus": 1}, "panels": []}),
|
|
1140
|
+
);
|
|
823
1141
|
assert_refused_as(violations, "Band does not declare bogus — it takes size");
|
|
824
1142
|
}
|
|
825
1143
|
|
|
@@ -827,7 +1145,10 @@ mod tests {
|
|
|
827
1145
|
fn a_required_field_left_out_is_refused_while_an_optional_one_a_default_and_a_list_are_not() {
|
|
828
1146
|
let mut absent = panel();
|
|
829
1147
|
absent.as_object_mut().unwrap().remove("count");
|
|
830
|
-
assert_refused_as(
|
|
1148
|
+
assert_refused_as(
|
|
1149
|
+
shelf_violations(json!([absent])),
|
|
1150
|
+
"Panel.count expects Integer, got nil",
|
|
1151
|
+
);
|
|
831
1152
|
|
|
832
1153
|
let mut sparse = panel();
|
|
833
1154
|
sparse.as_object_mut().unwrap().remove("labels");
|
|
@@ -838,22 +1159,34 @@ mod tests {
|
|
|
838
1159
|
fn a_pattern_is_held_by_every_member_of_a_list_and_by_the_whole_value() {
|
|
839
1160
|
let mut bad_label = panel();
|
|
840
1161
|
bad_label["labels"] = json!(["a", "B"]);
|
|
841
|
-
assert_refused_as(
|
|
1162
|
+
assert_refused_as(
|
|
1163
|
+
shelf_violations(json!([bad_label])),
|
|
1164
|
+
r#"Panel.labels must match ^[a-z]+$, got "B""#,
|
|
1165
|
+
);
|
|
842
1166
|
|
|
843
1167
|
let mut newline = panel();
|
|
844
1168
|
newline["slug"] = json!("ok\n");
|
|
845
|
-
assert_refused_as(
|
|
1169
|
+
assert_refused_as(
|
|
1170
|
+
shelf_violations(json!([newline])),
|
|
1171
|
+
r#"Panel.slug must match ^[a-z]+$, got "ok\n""#,
|
|
1172
|
+
);
|
|
846
1173
|
}
|
|
847
1174
|
|
|
848
1175
|
#[test]
|
|
849
1176
|
fn a_number_past_signed_64_bits_and_a_whole_float_are_not_integers() {
|
|
850
1177
|
let mut big = panel();
|
|
851
|
-
big["count"] = json!(
|
|
852
|
-
assert_refused_as(
|
|
1178
|
+
big["count"] = json!(9_223_372_036_854_775_808_u64);
|
|
1179
|
+
assert_refused_as(
|
|
1180
|
+
shelf_violations(json!([big])),
|
|
1181
|
+
"Panel.count must fit in a 64-bit integer, got 9223372036854775808",
|
|
1182
|
+
);
|
|
853
1183
|
|
|
854
1184
|
let mut whole = panel();
|
|
855
1185
|
whole["count"] = json!(3.0);
|
|
856
|
-
assert_refused_as(
|
|
1186
|
+
assert_refused_as(
|
|
1187
|
+
shelf_violations(json!([whole])),
|
|
1188
|
+
"Panel.count expects Integer, got 3.0",
|
|
1189
|
+
);
|
|
857
1190
|
|
|
858
1191
|
let mut largest = panel();
|
|
859
1192
|
largest["count"] = json!(i64::MAX);
|
|
@@ -862,7 +1195,10 @@ mod tests {
|
|
|
862
1195
|
|
|
863
1196
|
#[test]
|
|
864
1197
|
fn a_list_offered_a_lone_value_is_a_shape_error_and_a_null_list_is_not() {
|
|
865
|
-
assert_refused_as(
|
|
1198
|
+
assert_refused_as(
|
|
1199
|
+
shelf_violations(json!("x")),
|
|
1200
|
+
r#"Shelf.panels expects list_of(Panel), got "x""#,
|
|
1201
|
+
);
|
|
866
1202
|
let mut unset = panel();
|
|
867
1203
|
unset["labels"] = Value::Null;
|
|
868
1204
|
assert_eq!(shelf_violations(json!([unset])), Vec::<String>::new());
|
|
@@ -878,6 +1214,9 @@ mod tests {
|
|
|
878
1214
|
let domain = json!({"aggregates": [shelf, vocabulary]});
|
|
879
1215
|
let refused = domain_validate(&domain, &shelf, "s1", &json!({"band": {"size": "huge"}}));
|
|
880
1216
|
assert_refused_as(refused, r#"Band admits "small" — got "huge""#);
|
|
881
|
-
assert_eq!(
|
|
1217
|
+
assert_eq!(
|
|
1218
|
+
domain_validate(&domain, &shelf, "s1", &json!({"band": {"size": "small"}})),
|
|
1219
|
+
Vec::<String>::new()
|
|
1220
|
+
);
|
|
882
1221
|
}
|
|
883
1222
|
}
|