hecks 3.14.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.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/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/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 +2 -0
- data/lib/hecks/chapters.rb +3 -3
- 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/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/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- 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 +21 -0
- 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 +3 -80
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +7 -13
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- 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/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/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/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
- 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 +39 -7
- data/rust/host/src/payments/tests.rs +1427 -282
- data/rust/host/src/payments.rs +391 -112
- 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 -1
- data/rust/parser/src/build/named_pattern.rs +4 -1
- 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 -516
- 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 +22 -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 +82 -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/rust/host/src/approval.rs +0 -652
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
// `PUT /api/presentation`: validates against the live domain IR, then dispatches real commands.
|
|
3
3
|
|
|
4
4
|
use crate::dispatch;
|
|
5
|
-
use crate::
|
|
6
|
-
use crate::journal::LineageConfig;
|
|
5
|
+
use crate::host::Host;
|
|
7
6
|
use crate::presentation::{self, KernelRows};
|
|
8
7
|
use crate::ui_schema;
|
|
9
8
|
use serde_json::{json, Map, Value};
|
|
10
9
|
use std::collections::BTreeSet;
|
|
11
|
-
use std::path::Path;
|
|
12
|
-
use tokio::sync::Mutex;
|
|
13
|
-
use tokio_postgres::Client;
|
|
14
10
|
|
|
15
11
|
// Duplicates the chapter's own `one_of` so this refuses before any dispatch starts.
|
|
16
12
|
const VALID_TONES: &[&str] = &["good", "warn", "danger", "muted", "accent"];
|
|
@@ -35,14 +31,11 @@ pub(crate) enum SaveRefusal {
|
|
|
35
31
|
pub(crate) async fn save(
|
|
36
32
|
domain_ir: &Value,
|
|
37
33
|
submitted: &Value,
|
|
38
|
-
|
|
39
|
-
wasm_path: &Path,
|
|
40
|
-
lineage: &LineageConfig,
|
|
41
|
-
invoker: &dyn LambdaInvoker,
|
|
34
|
+
host: Host<'_>,
|
|
42
35
|
) -> Result<Value, SaveRefusal> {
|
|
43
36
|
// Asked before validated: a host with no such chapter cannot honour this request at all,
|
|
44
37
|
// regardless of which rule the submitted config would otherwise break.
|
|
45
|
-
let existing = match presentation::kernel_rows(client, wasm_path).await {
|
|
38
|
+
let existing = match presentation::kernel_rows(host.client, host.wasm_path).await {
|
|
46
39
|
Ok(Some(rows)) => rows,
|
|
47
40
|
Ok(None) => return Err(SaveRefusal::NoKernel),
|
|
48
41
|
Err(e) => return Err(SaveRefusal::Internal(format!("{e:#}"))),
|
|
@@ -50,12 +43,20 @@ pub(crate) async fn save(
|
|
|
50
43
|
|
|
51
44
|
validate(submitted, domain_ir).map_err(SaveRefusal::Malformed)?;
|
|
52
45
|
|
|
53
|
-
let
|
|
54
|
-
writer
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
let writer = Writer { host };
|
|
47
|
+
writer
|
|
48
|
+
.write_states(section(submitted, "states"), &existing)
|
|
49
|
+
.await?;
|
|
50
|
+
writer
|
|
51
|
+
.write_collections(section(submitted, "collections"), &existing)
|
|
52
|
+
.await?;
|
|
53
|
+
writer
|
|
54
|
+
.write_overview(section(submitted, "overview"), &existing)
|
|
55
|
+
.await?;
|
|
56
|
+
|
|
57
|
+
presentation::load(host.client, host.wasm_path, host.config)
|
|
58
|
+
.await
|
|
59
|
+
.map_err(|e| SaveRefusal::Internal(format!("{e:#}")))
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// A missing, null, or non-object section is the empty one rather than an error: `validate` has
|
|
@@ -66,46 +67,47 @@ fn section<'a>(config: &'a Value, key: &str) -> &'a Value {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
fn entries(value: &Value) -> Vec<(&String, &Value)> {
|
|
69
|
-
value
|
|
70
|
+
value
|
|
71
|
+
.as_object()
|
|
72
|
+
.map(|map| map.iter().collect())
|
|
73
|
+
.unwrap_or_default()
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
// A null, a missing key, or anything that isn't a list is the empty list.
|
|
73
77
|
fn list(value: Option<&Value>) -> &[Value] {
|
|
74
|
-
value
|
|
78
|
+
value
|
|
79
|
+
.and_then(|v| v.as_array())
|
|
80
|
+
.map_or(&[][..], std::vec::Vec::as_slice)
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
// Every present field is re-dispatched on every save, even to an unchanged value, so the two
|
|
78
84
|
// engines produce the same history for the same request. A field once set and later omitted is
|
|
79
85
|
// never cleared: the chapter declares no `Clear*` command.
|
|
80
86
|
struct Writer<'a> {
|
|
81
|
-
|
|
82
|
-
wasm_path: &'a Path,
|
|
83
|
-
lineage: &'a LineageConfig,
|
|
84
|
-
invoker: &'a dyn LambdaInvoker,
|
|
87
|
+
host: Host<'a>,
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
impl Writer<'_> {
|
|
88
91
|
// A creating command — facts only, no identity to route to.
|
|
89
|
-
async fn declare(&
|
|
90
|
-
let outcome = dispatch::handle_facts(self.
|
|
92
|
+
async fn declare(&self, verb: &str, facts: Value) -> Result<(), SaveRefusal> {
|
|
93
|
+
let outcome = dispatch::handle_facts(self.host, verb, facts, None)
|
|
91
94
|
.await
|
|
92
95
|
.map_err(|e| SaveRefusal::Internal(format!("{e:#}")))?;
|
|
93
|
-
|
|
96
|
+
Self::accepted(outcome)
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
// A command acting on an existing row, routed by that row's own identity: "Agg:state" for
|
|
97
100
|
// StateStyle, the aggregate name for Collection, "overview" for the singleton.
|
|
98
|
-
async fn route(&
|
|
99
|
-
let outcome =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.accepted(outcome)
|
|
101
|
+
async fn route(&self, verb: &str, id: &str, facts: Value) -> Result<(), SaveRefusal> {
|
|
102
|
+
let outcome = dispatch::handle_routed(self.host, verb, json!(id), facts, None)
|
|
103
|
+
.await
|
|
104
|
+
.map_err(|e| SaveRefusal::Internal(format!("{e:#}")))?;
|
|
105
|
+
Self::accepted(outcome)
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
// A refusal stops the whole save where it happened: several independent dispatches, not one
|
|
107
109
|
// atomic write. `validate` above is what makes this rare rather than routine.
|
|
108
|
-
fn accepted(
|
|
110
|
+
fn accepted(outcome: dispatch::Outcome) -> Result<(), SaveRefusal> {
|
|
109
111
|
if outcome.accepted {
|
|
110
112
|
Ok(())
|
|
111
113
|
} else {
|
|
@@ -113,7 +115,7 @@ impl Writer<'_> {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
async fn write_states(&
|
|
118
|
+
async fn write_states(&self, states: &Value, existing: &KernelRows) -> Result<(), SaveRefusal> {
|
|
117
119
|
let known: BTreeSet<&str> = existing.state_ids();
|
|
118
120
|
for (agg, per_state) in entries(states) {
|
|
119
121
|
for (state, entry) in entries(per_state) {
|
|
@@ -127,13 +129,26 @@ impl Writer<'_> {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
if let Some(tone) = present(entry.get("tone")) {
|
|
130
|
-
self.route(
|
|
132
|
+
self.route(
|
|
133
|
+
"ConsoleSettings::StateStyle.SetTone",
|
|
134
|
+
&id,
|
|
135
|
+
json!({"tone": {"value": tone}}),
|
|
136
|
+
)
|
|
137
|
+
.await?;
|
|
131
138
|
}
|
|
132
139
|
// Key presence, not truthiness: an explicit `false` is a real answer to store.
|
|
133
140
|
if let Some(attention) = entry.get("attention") {
|
|
134
|
-
let flag = if attention.as_bool() == Some(true) {
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
let flag = if attention.as_bool() == Some(true) {
|
|
142
|
+
"true"
|
|
143
|
+
} else {
|
|
144
|
+
"false"
|
|
145
|
+
};
|
|
146
|
+
self.route(
|
|
147
|
+
"ConsoleSettings::StateStyle.SetAttention",
|
|
148
|
+
&id,
|
|
149
|
+
json!({"attention": {"value": flag}}),
|
|
150
|
+
)
|
|
151
|
+
.await?;
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
let extra = extra_fields(entry, KNOWN_STATE_KEYS);
|
|
@@ -150,11 +165,19 @@ impl Writer<'_> {
|
|
|
150
165
|
Ok(())
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
async fn write_collections(
|
|
168
|
+
async fn write_collections(
|
|
169
|
+
&self,
|
|
170
|
+
collections: &Value,
|
|
171
|
+
existing: &KernelRows,
|
|
172
|
+
) -> Result<(), SaveRefusal> {
|
|
154
173
|
let known: BTreeSet<&str> = existing.collection_ids();
|
|
155
174
|
for (agg, entry) in entries(collections) {
|
|
156
175
|
if !known.contains(agg.as_str()) {
|
|
157
|
-
self.declare(
|
|
176
|
+
self.declare(
|
|
177
|
+
"ConsoleSettings::Collection.Declare",
|
|
178
|
+
json!({"agg": {"value": agg}}),
|
|
179
|
+
)
|
|
180
|
+
.await?;
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
for (field, command) in [
|
|
@@ -166,7 +189,8 @@ impl Writer<'_> {
|
|
|
166
189
|
] {
|
|
167
190
|
if let Some(value) = present(entry.get(field)) {
|
|
168
191
|
let verb = format!("ConsoleSettings::Collection.{command}");
|
|
169
|
-
self.route(&verb, agg, json!({ field: {"value": value} }))
|
|
192
|
+
self.route(&verb, agg, json!({ field: {"value": value} }))
|
|
193
|
+
.await?;
|
|
170
194
|
}
|
|
171
195
|
}
|
|
172
196
|
|
|
@@ -220,10 +244,16 @@ impl Writer<'_> {
|
|
|
220
244
|
Ok(())
|
|
221
245
|
}
|
|
222
246
|
|
|
223
|
-
async fn write_identity(&
|
|
247
|
+
async fn write_identity(&self, agg: &str, identity: &Value) -> Result<(), SaveRefusal> {
|
|
224
248
|
let mut facts = Map::new();
|
|
225
|
-
facts.insert(
|
|
226
|
-
|
|
249
|
+
facts.insert(
|
|
250
|
+
"identity_field".to_string(),
|
|
251
|
+
json!({"value": identity.get("field")}),
|
|
252
|
+
);
|
|
253
|
+
facts.insert(
|
|
254
|
+
"identity_strategy".to_string(),
|
|
255
|
+
json!({"value": identity.get("strategy")}),
|
|
256
|
+
);
|
|
227
257
|
if let Some(source) = present(identity.get("source")) {
|
|
228
258
|
facts.insert("identity_source".to_string(), json!({ "value": source }));
|
|
229
259
|
}
|
|
@@ -232,15 +262,31 @@ impl Writer<'_> {
|
|
|
232
262
|
}
|
|
233
263
|
let extra = extra_fields(identity, KNOWN_IDENTITY_KEYS);
|
|
234
264
|
if !extra.is_empty() {
|
|
235
|
-
facts.insert(
|
|
265
|
+
facts.insert(
|
|
266
|
+
"identity_extra_json".to_string(),
|
|
267
|
+
json!({"value": Value::Object(extra).to_string()}),
|
|
268
|
+
);
|
|
236
269
|
}
|
|
237
|
-
self.route(
|
|
270
|
+
self.route(
|
|
271
|
+
"ConsoleSettings::Collection.SetIdentity",
|
|
272
|
+
agg,
|
|
273
|
+
Value::Object(facts),
|
|
274
|
+
)
|
|
275
|
+
.await
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
// The one row: `Declare`d once, then `ReplaceStats` whole on every save, even an empty list.
|
|
241
|
-
async fn write_overview(
|
|
279
|
+
async fn write_overview(
|
|
280
|
+
&self,
|
|
281
|
+
overview: &Value,
|
|
282
|
+
existing: &KernelRows,
|
|
283
|
+
) -> Result<(), SaveRefusal> {
|
|
242
284
|
if existing.overview.is_empty() {
|
|
243
|
-
self.declare(
|
|
285
|
+
self.declare(
|
|
286
|
+
"ConsoleSettings::Overview.Declare",
|
|
287
|
+
json!({"key": {"value": "overview"}}),
|
|
288
|
+
)
|
|
289
|
+
.await?;
|
|
244
290
|
}
|
|
245
291
|
self.route(
|
|
246
292
|
"ConsoleSettings::Overview.ReplaceStats",
|
|
@@ -296,9 +342,14 @@ fn columns_for_dispatch(columns: Option<&Value>) -> Vec<Value> {
|
|
|
296
342
|
.map(|column| {
|
|
297
343
|
// A column is either a bare field name or a descriptor hash — both shapes
|
|
298
344
|
// `validate_collection!` reads.
|
|
299
|
-
let Some(object) = column.as_object() else {
|
|
345
|
+
let Some(object) = column.as_object() else {
|
|
346
|
+
return json!({ "field": column });
|
|
347
|
+
};
|
|
300
348
|
let mut row = Map::new();
|
|
301
|
-
row.insert(
|
|
349
|
+
row.insert(
|
|
350
|
+
"field".to_string(),
|
|
351
|
+
object.get("field").cloned().unwrap_or(Value::Null),
|
|
352
|
+
);
|
|
302
353
|
// "true"/"false" as strings: `Column#sortable` is a String attribute, not a boolean.
|
|
303
354
|
if let Some(sortable) = object.get("sortable") {
|
|
304
355
|
row.insert("sortable".to_string(), json!(ruby_to_s(sortable)));
|
|
@@ -308,7 +359,10 @@ fn columns_for_dispatch(columns: Option<&Value>) -> Vec<Value> {
|
|
|
308
359
|
}
|
|
309
360
|
let extra = extra_fields(column, KNOWN_COLUMN_KEYS);
|
|
310
361
|
if !extra.is_empty() {
|
|
311
|
-
row.insert(
|
|
362
|
+
row.insert(
|
|
363
|
+
"extra_json".to_string(),
|
|
364
|
+
json!(Value::Object(extra).to_string()),
|
|
365
|
+
);
|
|
312
366
|
}
|
|
313
367
|
Value::Object(row)
|
|
314
368
|
})
|
|
@@ -319,9 +373,14 @@ fn detail_fields_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
|
|
|
319
373
|
list(detail_fields)
|
|
320
374
|
.iter()
|
|
321
375
|
.map(|field| {
|
|
322
|
-
let Some(object) = field.as_object() else {
|
|
376
|
+
let Some(object) = field.as_object() else {
|
|
377
|
+
return json!({ "field": field });
|
|
378
|
+
};
|
|
323
379
|
let mut row = Map::new();
|
|
324
|
-
row.insert(
|
|
380
|
+
row.insert(
|
|
381
|
+
"field".to_string(),
|
|
382
|
+
object.get("field").cloned().unwrap_or(Value::Null),
|
|
383
|
+
);
|
|
325
384
|
if let Some(display) = present(object.get("display")) {
|
|
326
385
|
row.insert("display".to_string(), display.clone());
|
|
327
386
|
}
|
|
@@ -330,7 +389,10 @@ fn detail_fields_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
|
|
|
330
389
|
}
|
|
331
390
|
let extra = extra_fields(field, KNOWN_DETAIL_FIELD_KEYS);
|
|
332
391
|
if !extra.is_empty() {
|
|
333
|
-
row.insert(
|
|
392
|
+
row.insert(
|
|
393
|
+
"extra_json".to_string(),
|
|
394
|
+
json!(Value::Object(extra).to_string()),
|
|
395
|
+
);
|
|
334
396
|
}
|
|
335
397
|
Value::Object(row)
|
|
336
398
|
})
|
|
@@ -341,8 +403,12 @@ fn detail_fields_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
|
|
|
341
403
|
fn detail_field_columns_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
|
|
342
404
|
let mut rows = Vec::new();
|
|
343
405
|
for field in list(detail_fields) {
|
|
344
|
-
let Some(object) = field.as_object() else {
|
|
345
|
-
|
|
406
|
+
let Some(object) = field.as_object() else {
|
|
407
|
+
continue;
|
|
408
|
+
};
|
|
409
|
+
let Some(columns) = object.get("columns") else {
|
|
410
|
+
continue;
|
|
411
|
+
};
|
|
346
412
|
let name = object.get("field").cloned().unwrap_or(Value::Null);
|
|
347
413
|
for column in list(Some(columns)) {
|
|
348
414
|
rows.push(json!({ "field": name, "column": column }));
|
|
@@ -356,11 +422,20 @@ fn preconditions_for_dispatch(preconditions: Option<&Value>) -> Vec<Value> {
|
|
|
356
422
|
.iter()
|
|
357
423
|
.map(|rule| {
|
|
358
424
|
let mut row = Map::new();
|
|
359
|
-
row.insert(
|
|
360
|
-
|
|
425
|
+
row.insert(
|
|
426
|
+
"field".to_string(),
|
|
427
|
+
rule.get("field").cloned().unwrap_or(Value::Null),
|
|
428
|
+
);
|
|
429
|
+
row.insert(
|
|
430
|
+
"state".to_string(),
|
|
431
|
+
rule.get("state").cloned().unwrap_or(Value::Null),
|
|
432
|
+
);
|
|
361
433
|
let extra = extra_fields(rule, KNOWN_PRECONDITION_KEYS);
|
|
362
434
|
if !extra.is_empty() {
|
|
363
|
-
row.insert(
|
|
435
|
+
row.insert(
|
|
436
|
+
"extra_json".to_string(),
|
|
437
|
+
json!(Value::Object(extra).to_string()),
|
|
438
|
+
);
|
|
364
439
|
}
|
|
365
440
|
Value::Object(row)
|
|
366
441
|
})
|
|
@@ -372,7 +447,11 @@ fn preconditions_for_dispatch(preconditions: Option<&Value>) -> Vec<Value> {
|
|
|
372
447
|
fn field_formats_for_dispatch(field_formats: Option<&Value>) -> Vec<Value> {
|
|
373
448
|
field_formats
|
|
374
449
|
.and_then(|v| v.as_object())
|
|
375
|
-
.map(|map|
|
|
450
|
+
.map(|map| {
|
|
451
|
+
map.iter()
|
|
452
|
+
.map(|(field, format)| json!({"field": field, "format": format}))
|
|
453
|
+
.collect()
|
|
454
|
+
})
|
|
376
455
|
.unwrap_or_default()
|
|
377
456
|
}
|
|
378
457
|
|
|
@@ -381,8 +460,14 @@ fn stats_for_dispatch(stats: Option<&Value>) -> Vec<Value> {
|
|
|
381
460
|
.iter()
|
|
382
461
|
.map(|stat| {
|
|
383
462
|
let mut row = Map::new();
|
|
384
|
-
row.insert(
|
|
385
|
-
|
|
463
|
+
row.insert(
|
|
464
|
+
"label".to_string(),
|
|
465
|
+
stat.get("label").cloned().unwrap_or(Value::Null),
|
|
466
|
+
);
|
|
467
|
+
row.insert(
|
|
468
|
+
"collection".to_string(),
|
|
469
|
+
stat.get("collection").cloned().unwrap_or(Value::Null),
|
|
470
|
+
);
|
|
386
471
|
if let Some(fold) = present(stat.get("as")) {
|
|
387
472
|
row.insert("as".to_string(), fold.clone());
|
|
388
473
|
}
|
|
@@ -445,11 +530,15 @@ fn refuse_unsupported_yet(collections: &Value) -> Refusal {
|
|
|
445
530
|
}
|
|
446
531
|
|
|
447
532
|
fn find_aggregate<'a>(aggregates: &[&'a Value], name: &str) -> Option<&'a Value> {
|
|
448
|
-
aggregates
|
|
533
|
+
aggregates
|
|
534
|
+
.iter()
|
|
535
|
+
.copied()
|
|
536
|
+
.find(|a| ui_schema::agg_name(a) == name)
|
|
449
537
|
}
|
|
450
538
|
|
|
451
539
|
fn require_aggregate<'a>(aggregates: &[&'a Value], name: &str) -> Result<&'a Value, String> {
|
|
452
|
-
find_aggregate(aggregates, name)
|
|
540
|
+
find_aggregate(aggregates, name)
|
|
541
|
+
.ok_or_else(|| format!("{name} is not a real aggregate this domain declares"))
|
|
453
542
|
}
|
|
454
543
|
|
|
455
544
|
fn validate_states(agg_name: &str, per_state: &Value, aggregates: &[&Value]) -> Refusal {
|
|
@@ -478,10 +567,18 @@ fn missing_state_entries(states: &Value, aggregates: &[&Value]) -> Refusal {
|
|
|
478
567
|
if missing.is_empty() {
|
|
479
568
|
return Ok(());
|
|
480
569
|
}
|
|
481
|
-
Err(format!(
|
|
570
|
+
Err(format!(
|
|
571
|
+
"no presentation entry for {} — every real state needs one, even a bare one",
|
|
572
|
+
missing.join(", ")
|
|
573
|
+
))
|
|
482
574
|
}
|
|
483
575
|
|
|
484
|
-
fn validate_state_entry(
|
|
576
|
+
fn validate_state_entry(
|
|
577
|
+
agg_name: &str,
|
|
578
|
+
state_name: &str,
|
|
579
|
+
entry: &Value,
|
|
580
|
+
known: &[String],
|
|
581
|
+
) -> Refusal {
|
|
485
582
|
if !known.iter().any(|s| s == state_name) {
|
|
486
583
|
return Err(format!(
|
|
487
584
|
"{agg_name} has no state {} — it declares {}",
|
|
@@ -501,14 +598,20 @@ fn validate_state_entry(agg_name: &str, state_name: &str, entry: &Value, known:
|
|
|
501
598
|
}
|
|
502
599
|
|
|
503
600
|
match entry.get("attention") {
|
|
504
|
-
None | Some(Value::Null
|
|
505
|
-
Some(other) =>
|
|
506
|
-
|
|
507
|
-
|
|
601
|
+
None | Some(Value::Null | Value::Bool(_)) => Ok(()),
|
|
602
|
+
Some(other) => Err(format!(
|
|
603
|
+
"{agg_name}.{state_name}'s attention must be true or false, not {}",
|
|
604
|
+
inspect(other)
|
|
605
|
+
)),
|
|
508
606
|
}
|
|
509
607
|
}
|
|
510
608
|
|
|
511
|
-
fn validate_collection(
|
|
609
|
+
fn validate_collection(
|
|
610
|
+
agg_name: &str,
|
|
611
|
+
entry: &Value,
|
|
612
|
+
aggregates: &[&Value],
|
|
613
|
+
domain_ir: &Value,
|
|
614
|
+
) -> Refusal {
|
|
512
615
|
let aggregate = require_aggregate(aggregates, agg_name)?;
|
|
513
616
|
|
|
514
617
|
validate_columns(agg_name, aggregate, entry.get("columns"))?;
|
|
@@ -518,7 +621,10 @@ fn validate_collection(agg_name: &str, entry: &Value, aggregates: &[&Value], dom
|
|
|
518
621
|
// than a vocabulary.
|
|
519
622
|
if let Some(nav_order) = entry.get("nav_order") {
|
|
520
623
|
if !nav_order.is_number() {
|
|
521
|
-
return Err(format!(
|
|
624
|
+
return Err(format!(
|
|
625
|
+
"{agg_name}'s nav_order must be a number, not {}",
|
|
626
|
+
inspect(nav_order)
|
|
627
|
+
));
|
|
522
628
|
}
|
|
523
629
|
}
|
|
524
630
|
|
|
@@ -543,7 +649,10 @@ fn validate_collection(agg_name: &str, entry: &Value, aggregates: &[&Value], dom
|
|
|
543
649
|
// "does this aggregate declare it", not a closed vocabulary.
|
|
544
650
|
fn validate_columns(agg_name: &str, aggregate: &Value, columns: Option<&Value>) -> Refusal {
|
|
545
651
|
for column in list(columns) {
|
|
546
|
-
let key = column
|
|
652
|
+
let key = column
|
|
653
|
+
.get("field")
|
|
654
|
+
.filter(|_| column.is_object())
|
|
655
|
+
.unwrap_or(column);
|
|
547
656
|
let name = ruby_to_s(key);
|
|
548
657
|
if name != "__state__" && ui_schema::find_attribute(aggregate, &name).is_none() {
|
|
549
658
|
return Err(format!(
|
|
@@ -553,7 +662,9 @@ fn validate_columns(agg_name: &str, aggregate: &Value, columns: Option<&Value>)
|
|
|
553
662
|
));
|
|
554
663
|
}
|
|
555
664
|
|
|
556
|
-
let Some(object) = column.as_object() else {
|
|
665
|
+
let Some(object) = column.as_object() else {
|
|
666
|
+
continue;
|
|
667
|
+
};
|
|
557
668
|
if let Some(sortable) = object.get("sortable") {
|
|
558
669
|
if !sortable.is_boolean() {
|
|
559
670
|
return Err(format!(
|
|
@@ -577,29 +688,44 @@ fn validate_columns(agg_name: &str, aggregate: &Value, columns: Option<&Value>)
|
|
|
577
688
|
}
|
|
578
689
|
|
|
579
690
|
fn attribute_names(aggregate: &Value) -> String {
|
|
580
|
-
ui_schema::array(aggregate, "attributes")
|
|
691
|
+
ui_schema::array(aggregate, "attributes")
|
|
692
|
+
.iter()
|
|
693
|
+
.map(ui_schema::attr_name)
|
|
694
|
+
.collect::<Vec<_>>()
|
|
695
|
+
.join(", ")
|
|
581
696
|
}
|
|
582
697
|
|
|
583
698
|
// A non-reference field is caught here first, since otherwise the state check below would
|
|
584
699
|
// simply never fire.
|
|
585
|
-
fn validate_precondition(
|
|
700
|
+
fn validate_precondition(
|
|
701
|
+
agg_name: &str,
|
|
702
|
+
aggregate: &Value,
|
|
703
|
+
rule: &Value,
|
|
704
|
+
aggregates: &[&Value],
|
|
705
|
+
) -> Refusal {
|
|
586
706
|
let Some(field) = present(rule.get("field")) else {
|
|
587
707
|
return Err(format!("{agg_name} has a precondition with no field named"));
|
|
588
708
|
};
|
|
589
709
|
let name = ruby_to_s(field);
|
|
590
|
-
let attribute =
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return Err(format!(
|
|
594
|
-
|
|
710
|
+
let Some(attribute) =
|
|
711
|
+
ui_schema::find_attribute(aggregate, &name).filter(|a| ui_schema::is_reference(a))
|
|
712
|
+
else {
|
|
713
|
+
return Err(format!(
|
|
714
|
+
"{agg_name}.{name} isn't a reference — a precondition only makes sense against one"
|
|
715
|
+
));
|
|
716
|
+
};
|
|
595
717
|
|
|
596
|
-
let Some(state) = present(rule.get("state")) else {
|
|
718
|
+
let Some(state) = present(rule.get("state")) else {
|
|
719
|
+
return Ok(());
|
|
720
|
+
};
|
|
597
721
|
// A cross-domain or otherwise unresolvable target — nothing more to
|
|
598
722
|
// check here, exactly as Ruby's own `return unless target` says.
|
|
599
|
-
let Some(target_name) = ui_schema::reference_target(ui_schema::type_of(attribute
|
|
723
|
+
let Some(target_name) = ui_schema::reference_target(ui_schema::type_of(attribute)) else {
|
|
724
|
+
return Ok(());
|
|
725
|
+
};
|
|
726
|
+
let Some(target) = find_aggregate(aggregates, &target_name) else {
|
|
600
727
|
return Ok(());
|
|
601
728
|
};
|
|
602
|
-
let Some(target) = find_aggregate(aggregates, &target_name) else { return Ok(()) };
|
|
603
729
|
|
|
604
730
|
let known = ui_schema::lifecycle_states(target);
|
|
605
731
|
if known.iter().any(|s| *s == ruby_to_s(state)) {
|
|
@@ -615,10 +741,17 @@ fn validate_precondition(agg_name: &str, aggregate: &Value, rule: &Value, aggreg
|
|
|
615
741
|
// Keyed by attribute name, not path: a nested compound field is found the same way, since
|
|
616
742
|
// `ui_schema` threads the same map down through every level of recursion.
|
|
617
743
|
fn validate_field_formats(agg_name: &str, aggregate: &Value, formats: Option<&Value>) -> Refusal {
|
|
618
|
-
let Some(map) = formats.and_then(|v| v.as_object()) else {
|
|
744
|
+
let Some(map) = formats.and_then(|v| v.as_object()) else {
|
|
745
|
+
return Ok(());
|
|
746
|
+
};
|
|
619
747
|
for (field_name, format) in map {
|
|
620
|
-
if ui_schema::find_attribute(aggregate, field_name).is_none()
|
|
621
|
-
|
|
748
|
+
if ui_schema::find_attribute(aggregate, field_name).is_none()
|
|
749
|
+
&& !nested_attribute(aggregate, field_name)
|
|
750
|
+
{
|
|
751
|
+
return Err(format!(
|
|
752
|
+
"{agg_name} has no attribute {} to format",
|
|
753
|
+
inspect(&json!(field_name))
|
|
754
|
+
));
|
|
622
755
|
}
|
|
623
756
|
if !VALID_FORMATS.contains(&ruby_to_s(format).as_str()) {
|
|
624
757
|
return Err(format!(
|
|
@@ -632,43 +765,68 @@ fn validate_field_formats(agg_name: &str, aggregate: &Value, formats: Option<&Va
|
|
|
632
765
|
}
|
|
633
766
|
|
|
634
767
|
fn nested_attribute(aggregate: &Value, field_name: &str) -> bool {
|
|
635
|
-
ui_schema::array(aggregate, "attributes")
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
768
|
+
ui_schema::array(aggregate, "attributes")
|
|
769
|
+
.iter()
|
|
770
|
+
.any(|attribute| {
|
|
771
|
+
if ui_schema::is_reference(attribute) {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
let Some(value_object) =
|
|
775
|
+
ui_schema::find_value_object(aggregate, ui_schema::type_of(attribute))
|
|
776
|
+
else {
|
|
777
|
+
return false;
|
|
778
|
+
};
|
|
779
|
+
ui_schema::array(value_object, "attributes")
|
|
780
|
+
.iter()
|
|
781
|
+
.any(|sub| ui_schema::attr_name(sub) == field_name)
|
|
782
|
+
})
|
|
644
783
|
}
|
|
645
784
|
|
|
646
785
|
// Never a reference: there is nothing to derive from another aggregate's identity.
|
|
647
786
|
fn validate_identity(agg_name: &str, aggregate: &Value, rule: &Value) -> Refusal {
|
|
648
787
|
let Some(field) = present(rule.get("field")) else {
|
|
649
|
-
return Err(format!(
|
|
788
|
+
return Err(format!(
|
|
789
|
+
"{agg_name} has an identity rule with no field named"
|
|
790
|
+
));
|
|
650
791
|
};
|
|
651
792
|
let name = ruby_to_s(field);
|
|
652
793
|
let Some(attribute) = ui_schema::find_attribute(aggregate, &name) else {
|
|
653
|
-
return Err(format!(
|
|
794
|
+
return Err(format!(
|
|
795
|
+
"{agg_name} has no attribute {} to auto-derive",
|
|
796
|
+
inspect(field)
|
|
797
|
+
));
|
|
654
798
|
};
|
|
655
799
|
if ui_schema::is_reference(attribute) {
|
|
656
|
-
return Err(format!(
|
|
800
|
+
return Err(format!(
|
|
801
|
+
"{agg_name}.{name} is a reference — identity can only derive a plain field"
|
|
802
|
+
));
|
|
657
803
|
}
|
|
658
804
|
|
|
659
|
-
match rule
|
|
805
|
+
match rule
|
|
806
|
+
.get("strategy")
|
|
807
|
+
.map(ruby_to_s)
|
|
808
|
+
.unwrap_or_default()
|
|
809
|
+
.as_str()
|
|
810
|
+
{
|
|
660
811
|
"slug" => {
|
|
661
812
|
let Some(source) = present(rule.get("source")) else {
|
|
662
|
-
return Err(format!(
|
|
813
|
+
return Err(format!(
|
|
814
|
+
"{agg_name}'s identity slug strategy needs a source field"
|
|
815
|
+
));
|
|
663
816
|
};
|
|
664
817
|
if ui_schema::find_attribute(aggregate, &ruby_to_s(source)).is_none() {
|
|
665
|
-
return Err(format!(
|
|
818
|
+
return Err(format!(
|
|
819
|
+
"{agg_name} has no attribute {} to slug from",
|
|
820
|
+
inspect(source)
|
|
821
|
+
));
|
|
666
822
|
}
|
|
667
823
|
Ok(())
|
|
668
824
|
}
|
|
669
825
|
"sequence" => {
|
|
670
826
|
if present(rule.get("prefix")).is_none() {
|
|
671
|
-
return Err(format!(
|
|
827
|
+
return Err(format!(
|
|
828
|
+
"{agg_name}'s identity sequence strategy needs a prefix"
|
|
829
|
+
));
|
|
672
830
|
}
|
|
673
831
|
Ok(())
|
|
674
832
|
}
|
|
@@ -683,9 +841,16 @@ fn validate_identity(agg_name: &str, aggregate: &Value, rule: &Value) -> Refusal
|
|
|
683
841
|
}
|
|
684
842
|
}
|
|
685
843
|
|
|
686
|
-
fn validate_detail_fields(
|
|
844
|
+
fn validate_detail_fields(
|
|
845
|
+
agg_name: &str,
|
|
846
|
+
aggregate: &Value,
|
|
847
|
+
entries_value: Option<&Value>,
|
|
848
|
+
) -> Refusal {
|
|
687
849
|
for field in list(entries_value) {
|
|
688
|
-
let key = field
|
|
850
|
+
let key = field
|
|
851
|
+
.get("field")
|
|
852
|
+
.filter(|_| field.is_object())
|
|
853
|
+
.unwrap_or(field);
|
|
689
854
|
let name = ruby_to_s(key);
|
|
690
855
|
let Some(attribute) = ui_schema::find_attribute(aggregate, &name) else {
|
|
691
856
|
return Err(format!(
|
|
@@ -695,7 +860,9 @@ fn validate_detail_fields(agg_name: &str, aggregate: &Value, entries_value: Opti
|
|
|
695
860
|
));
|
|
696
861
|
};
|
|
697
862
|
|
|
698
|
-
let Some(object) = field.as_object() else {
|
|
863
|
+
let Some(object) = field.as_object() else {
|
|
864
|
+
continue;
|
|
865
|
+
};
|
|
699
866
|
|
|
700
867
|
if let Some(display) = present(object.get("display")) {
|
|
701
868
|
if !VALID_DISPLAYS.contains(&ruby_to_s(display).as_str()) {
|
|
@@ -746,7 +913,13 @@ fn validate_row_state_filter(
|
|
|
746
913
|
|
|
747
914
|
// A `columns:` on a field that is neither a value object nor an entity is a config mistake
|
|
748
915
|
// this catches by refusing rather than silently doing nothing.
|
|
749
|
-
fn validate_row_columns(
|
|
916
|
+
fn validate_row_columns(
|
|
917
|
+
agg_name: &str,
|
|
918
|
+
aggregate: &Value,
|
|
919
|
+
key: &str,
|
|
920
|
+
attribute: &Value,
|
|
921
|
+
columns: &Value,
|
|
922
|
+
) -> Refusal {
|
|
750
923
|
let type_name = ui_schema::type_of(attribute).to_string();
|
|
751
924
|
let rows_source = ui_schema::find_value_object(aggregate, &type_name)
|
|
752
925
|
.or_else(|| ui_schema::find_entity(aggregate, &type_name))
|
|
@@ -758,7 +931,10 @@ fn validate_row_columns(agg_name: &str, aggregate: &Value, key: &str, attribute:
|
|
|
758
931
|
));
|
|
759
932
|
};
|
|
760
933
|
|
|
761
|
-
let known: Vec<&str> = ui_schema::array(rows_source, "attributes")
|
|
934
|
+
let known: Vec<&str> = ui_schema::array(rows_source, "attributes")
|
|
935
|
+
.iter()
|
|
936
|
+
.map(ui_schema::attr_name)
|
|
937
|
+
.collect();
|
|
762
938
|
for column in list(Some(columns)) {
|
|
763
939
|
if !known.contains(&ruby_to_s(column).as_str()) {
|
|
764
940
|
return Err(format!(
|
|
@@ -777,7 +953,10 @@ fn validate_row_columns(agg_name: &str, aggregate: &Value, key: &str, attribute:
|
|
|
777
953
|
fn validate_list_query(agg_name: &str, aggregate: &Value, query_name: &Value) -> Refusal {
|
|
778
954
|
let name = ruby_to_s(query_name);
|
|
779
955
|
let Some(query) = find_query(aggregate, &name) else {
|
|
780
|
-
return Err(format!(
|
|
956
|
+
return Err(format!(
|
|
957
|
+
"{agg_name} has no query {} to use as its list_query",
|
|
958
|
+
inspect(query_name)
|
|
959
|
+
));
|
|
781
960
|
};
|
|
782
961
|
let arguments = ui_schema::array(query, "attributes");
|
|
783
962
|
if arguments.is_empty() {
|
|
@@ -791,17 +970,25 @@ fn validate_list_query(agg_name: &str, aggregate: &Value, query_name: &Value) ->
|
|
|
791
970
|
}
|
|
792
971
|
|
|
793
972
|
fn find_query<'a>(owner: &'a Value, name: &str) -> Option<&'a Value> {
|
|
794
|
-
ui_schema::array(owner, "queries")
|
|
973
|
+
ui_schema::array(owner, "queries")
|
|
974
|
+
.iter()
|
|
975
|
+
.find(|q| q.get("name").and_then(|v| v.as_str()) == Some(name))
|
|
795
976
|
}
|
|
796
977
|
|
|
797
978
|
// Cannot check whether a `$field` exists on the source aggregate, since every one of its own
|
|
798
979
|
// attributes is fair game — a real gap, and the same one Ruby has.
|
|
799
|
-
fn validate_after_create(
|
|
980
|
+
fn validate_after_create(
|
|
981
|
+
agg_name: &str,
|
|
982
|
+
after_create: &Value,
|
|
983
|
+
aggregates: &[&Value],
|
|
984
|
+
domain_ir: &Value,
|
|
985
|
+
) -> Refusal {
|
|
800
986
|
let Some(dispatch_to) = present(after_create.get("dispatch")) else {
|
|
801
987
|
return Err(format!("{agg_name}'s after_create names no dispatch"));
|
|
802
988
|
};
|
|
803
989
|
let dotted = ruby_to_s(dispatch_to);
|
|
804
|
-
let (target_agg_name, target_command_name) =
|
|
990
|
+
let (target_agg_name, target_command_name) =
|
|
991
|
+
dotted.split_once('.').unwrap_or((dotted.as_str(), ""));
|
|
805
992
|
|
|
806
993
|
let Some(target_agg) = find_aggregate(aggregates, target_agg_name) else {
|
|
807
994
|
return Err(format!(
|
|
@@ -818,7 +1005,9 @@ fn validate_after_create(agg_name: &str, after_create: &Value, aggregates: &[&Va
|
|
|
818
1005
|
));
|
|
819
1006
|
};
|
|
820
1007
|
|
|
821
|
-
let Some(args) = after_create.get("args").and_then(|v| v.as_object()) else {
|
|
1008
|
+
let Some(args) = after_create.get("args").and_then(|v| v.as_object()) else {
|
|
1009
|
+
return Ok(());
|
|
1010
|
+
};
|
|
822
1011
|
for (key, spec) in args {
|
|
823
1012
|
if ui_schema::find_attribute(target_command, key).is_none() {
|
|
824
1013
|
return Err(format!(
|
|
@@ -833,7 +1022,9 @@ fn validate_after_create(agg_name: &str, after_create: &Value, aggregates: &[&Va
|
|
|
833
1022
|
}
|
|
834
1023
|
|
|
835
1024
|
fn find_command<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
|
|
836
|
-
ui_schema::commands(aggregate)
|
|
1025
|
+
ui_schema::commands(aggregate)
|
|
1026
|
+
.into_iter()
|
|
1027
|
+
.find(|c| c.get("name").and_then(|v| v.as_str()) == Some(name))
|
|
837
1028
|
}
|
|
838
1029
|
|
|
839
1030
|
fn validate_after_create_arg_spec(
|
|
@@ -843,7 +1034,11 @@ fn validate_after_create_arg_spec(
|
|
|
843
1034
|
aggregates: &[&Value],
|
|
844
1035
|
_domain_ir: &Value,
|
|
845
1036
|
) -> Refusal {
|
|
846
|
-
let Some(count_query) = spec
|
|
1037
|
+
let Some(count_query) = spec
|
|
1038
|
+
.get("count_query")
|
|
1039
|
+
.filter(|_| spec.is_object())
|
|
1040
|
+
.and_then(|v| present(Some(v)))
|
|
1041
|
+
else {
|
|
847
1042
|
return Ok(());
|
|
848
1043
|
};
|
|
849
1044
|
let dotted = ruby_to_s(count_query);
|
|
@@ -901,8 +1096,10 @@ fn validate_after_create_arg_spec(
|
|
|
901
1096
|
// `collection` names a collection key, the one `key:` config can override, since a stat sits
|
|
902
1097
|
// above any one aggregate the same way a table does.
|
|
903
1098
|
fn validate_overview(overview: &Value, aggregates: &[&Value], config: &Value) -> Refusal {
|
|
904
|
-
let by_key: Vec<(String, &Value)> =
|
|
905
|
-
|
|
1099
|
+
let by_key: Vec<(String, &Value)> = aggregates
|
|
1100
|
+
.iter()
|
|
1101
|
+
.map(|a| (ui_schema::collection_key(a, config), *a))
|
|
1102
|
+
.collect();
|
|
906
1103
|
|
|
907
1104
|
for stat in list(overview.get("stats")) {
|
|
908
1105
|
validate_stat(stat, &by_key)?;
|
|
@@ -916,11 +1113,14 @@ fn validate_stat(stat: &Value, by_key: &[(String, &Value)]) -> Refusal {
|
|
|
916
1113
|
};
|
|
917
1114
|
let key = ruby_to_s(collection);
|
|
918
1115
|
let Some((_, aggregate)) = by_key.iter().find(|(k, _)| *k == key) else {
|
|
919
|
-
return Err(format!(
|
|
1116
|
+
return Err(format!(
|
|
1117
|
+
"overview stat names collection {}, which isn't a real collection key",
|
|
1118
|
+
inspect(collection)
|
|
1119
|
+
));
|
|
920
1120
|
};
|
|
921
1121
|
|
|
922
1122
|
let label = stat.get("label").cloned().unwrap_or(Value::Null);
|
|
923
|
-
let fold = present(stat.get("as")).
|
|
1123
|
+
let fold = present(stat.get("as")).map_or_else(|| "count".to_string(), ruby_to_s);
|
|
924
1124
|
if !VALID_FOLDS.contains(&fold.as_str()) {
|
|
925
1125
|
return Err(format!(
|
|
926
1126
|
"overview stat {}'s as: {} isn't one of {}",
|
|
@@ -930,16 +1130,31 @@ fn validate_stat(stat: &Value, by_key: &[(String, &Value)]) -> Refusal {
|
|
|
930
1130
|
));
|
|
931
1131
|
}
|
|
932
1132
|
if fold == "money_sum" && present(stat.get("field")).is_none() {
|
|
933
|
-
return Err(format!(
|
|
1133
|
+
return Err(format!(
|
|
1134
|
+
"overview stat {} folds as money_sum but names no field",
|
|
1135
|
+
inspect(&label)
|
|
1136
|
+
));
|
|
934
1137
|
}
|
|
935
1138
|
|
|
936
|
-
validate_where(
|
|
1139
|
+
validate_where(
|
|
1140
|
+
stat.get("where").unwrap_or(&Value::Null),
|
|
1141
|
+
aggregate,
|
|
1142
|
+
by_key,
|
|
1143
|
+
&label,
|
|
1144
|
+
)
|
|
937
1145
|
}
|
|
938
1146
|
|
|
939
1147
|
// Recurses through the same shape index.html's `matchesWhere` reads at evaluation time: `not:`
|
|
940
1148
|
// wraps another whole clause, and `has_related`/`not_has_related` is checked below.
|
|
941
|
-
fn validate_where(
|
|
942
|
-
|
|
1149
|
+
fn validate_where(
|
|
1150
|
+
clause: &Value,
|
|
1151
|
+
aggregate: &Value,
|
|
1152
|
+
by_key: &[(String, &Value)],
|
|
1153
|
+
label: &Value,
|
|
1154
|
+
) -> Refusal {
|
|
1155
|
+
let Some(object) = clause.as_object() else {
|
|
1156
|
+
return Ok(());
|
|
1157
|
+
};
|
|
943
1158
|
if let Some(negated) = object.get("not") {
|
|
944
1159
|
return validate_where(negated, aggregate, by_key, label);
|
|
945
1160
|
}
|
|
@@ -958,7 +1173,11 @@ fn validate_where(clause: &Value, aggregate: &Value, by_key: &[(String, &Value)]
|
|
|
958
1173
|
Value::Object(map) => map
|
|
959
1174
|
.get("eq")
|
|
960
1175
|
.or_else(|| map.get("ne"))
|
|
961
|
-
.or_else(||
|
|
1176
|
+
.or_else(|| {
|
|
1177
|
+
map.get("in")
|
|
1178
|
+
.and_then(|v| v.as_array())
|
|
1179
|
+
.and_then(|a| a.first())
|
|
1180
|
+
})
|
|
962
1181
|
.cloned()
|
|
963
1182
|
.unwrap_or(Value::Null),
|
|
964
1183
|
other => other.clone(),
|
|
@@ -978,9 +1197,17 @@ fn validate_where(clause: &Value, aggregate: &Value, by_key: &[(String, &Value)]
|
|
|
978
1197
|
}
|
|
979
1198
|
|
|
980
1199
|
// Spelled `field:`, not `on:` — YAML 1.1 reads a bare `on:` as the boolean `true`.
|
|
981
|
-
fn validate_has_related(
|
|
1200
|
+
fn validate_has_related(
|
|
1201
|
+
spec: &Value,
|
|
1202
|
+
aggregate: &Value,
|
|
1203
|
+
by_key: &[(String, &Value)],
|
|
1204
|
+
label: &Value,
|
|
1205
|
+
) -> Refusal {
|
|
982
1206
|
let Some(target_key) = present(spec.get("collection")) else {
|
|
983
|
-
return Err(format!(
|
|
1207
|
+
return Err(format!(
|
|
1208
|
+
"overview stat {}'s has_related names no collection",
|
|
1209
|
+
inspect(label)
|
|
1210
|
+
));
|
|
984
1211
|
};
|
|
985
1212
|
let Some((_, target)) = by_key.iter().find(|(k, _)| *k == ruby_to_s(target_key)) else {
|
|
986
1213
|
return Err(format!(
|
|
@@ -991,7 +1218,10 @@ fn validate_has_related(spec: &Value, aggregate: &Value, by_key: &[(String, &Val
|
|
|
991
1218
|
};
|
|
992
1219
|
|
|
993
1220
|
let Some(field) = present(spec.get("field")) else {
|
|
994
|
-
return Err(format!(
|
|
1221
|
+
return Err(format!(
|
|
1222
|
+
"overview stat {}'s has_related names no field:",
|
|
1223
|
+
inspect(label)
|
|
1224
|
+
));
|
|
995
1225
|
};
|
|
996
1226
|
if ui_schema::find_attribute(target, &ruby_to_s(field)).is_none() {
|
|
997
1227
|
return Err(format!(
|
|
@@ -1002,7 +1232,7 @@ fn validate_has_related(spec: &Value, aggregate: &Value, by_key: &[(String, &Val
|
|
|
1002
1232
|
));
|
|
1003
1233
|
}
|
|
1004
1234
|
|
|
1005
|
-
let equals = present(spec.get("equals")).
|
|
1235
|
+
let equals = present(spec.get("equals")).map_or_else(|| "id".to_string(), ruby_to_s);
|
|
1006
1236
|
if equals == "id" || ui_schema::find_attribute(aggregate, &equals).is_some() {
|
|
1007
1237
|
return Ok(());
|
|
1008
1238
|
}
|
|
@@ -1025,10 +1255,16 @@ fn inspect(value: &Value) -> String {
|
|
|
1025
1255
|
// same distinction, so the number's own JSON spelling is right.
|
|
1026
1256
|
Value::Number(number) => number.to_string(),
|
|
1027
1257
|
Value::String(text) => format!("{text:?}"),
|
|
1028
|
-
Value::Array(items) => format!(
|
|
1258
|
+
Value::Array(items) => format!(
|
|
1259
|
+
"[{}]",
|
|
1260
|
+
items.iter().map(inspect).collect::<Vec<_>>().join(", ")
|
|
1261
|
+
),
|
|
1029
1262
|
Value::Object(map) => format!(
|
|
1030
1263
|
"{{{}}}",
|
|
1031
|
-
map.iter()
|
|
1264
|
+
map.iter()
|
|
1265
|
+
.map(|(key, value)| format!("{key:?} => {}", inspect(value)))
|
|
1266
|
+
.collect::<Vec<_>>()
|
|
1267
|
+
.join(", ")
|
|
1032
1268
|
),
|
|
1033
1269
|
}
|
|
1034
1270
|
}
|
|
@@ -1086,8 +1322,8 @@ mod tests {
|
|
|
1086
1322
|
json!({"states": {"Event": {"open": {}, "closed": {}}}})
|
|
1087
1323
|
}
|
|
1088
1324
|
|
|
1089
|
-
fn refusal(config: Value) -> String {
|
|
1090
|
-
validate(
|
|
1325
|
+
fn refusal(config: &Value) -> String {
|
|
1326
|
+
validate(config, &domain()).expect_err("this config should have been refused")
|
|
1091
1327
|
}
|
|
1092
1328
|
|
|
1093
1329
|
#[test]
|
|
@@ -1098,7 +1334,7 @@ mod tests {
|
|
|
1098
1334
|
#[test]
|
|
1099
1335
|
fn every_real_state_needs_an_entry_even_a_bare_one() {
|
|
1100
1336
|
assert_eq!(
|
|
1101
|
-
refusal(json!({"states": {"Event": {"open": {}}}})),
|
|
1337
|
+
refusal(&json!({"states": {"Event": {"open": {}}}})),
|
|
1102
1338
|
"no presentation entry for Event.closed — every real state needs one, even a bare one"
|
|
1103
1339
|
);
|
|
1104
1340
|
}
|
|
@@ -1114,7 +1350,7 @@ mod tests {
|
|
|
1114
1350
|
#[test]
|
|
1115
1351
|
fn a_state_section_naming_an_aggregate_this_domain_does_not_declare_refuses() {
|
|
1116
1352
|
assert_eq!(
|
|
1117
|
-
refusal(json!({"states": {"Ghost": {"gone": {}}}})),
|
|
1353
|
+
refusal(&json!({"states": {"Ghost": {"gone": {}}}})),
|
|
1118
1354
|
"Ghost is not a real aggregate this domain declares"
|
|
1119
1355
|
);
|
|
1120
1356
|
}
|
|
@@ -1123,7 +1359,10 @@ mod tests {
|
|
|
1123
1359
|
fn a_state_this_aggregate_does_not_declare_refuses_and_names_the_ones_it_does() {
|
|
1124
1360
|
let mut config = complete();
|
|
1125
1361
|
config["states"]["Event"]["cancelled"] = json!({});
|
|
1126
|
-
assert_eq!(
|
|
1362
|
+
assert_eq!(
|
|
1363
|
+
refusal(&config),
|
|
1364
|
+
"Event has no state \"cancelled\" — it declares open, closed"
|
|
1365
|
+
);
|
|
1127
1366
|
}
|
|
1128
1367
|
|
|
1129
1368
|
#[test]
|
|
@@ -1131,7 +1370,7 @@ mod tests {
|
|
|
1131
1370
|
let mut config = complete();
|
|
1132
1371
|
config["states"]["Event"]["open"] = json!({"tone": "chartreuse"});
|
|
1133
1372
|
assert_eq!(
|
|
1134
|
-
refusal(config),
|
|
1373
|
+
refusal(&config),
|
|
1135
1374
|
"Event.open's tone \"chartreuse\" isn't one of good, warn, danger, muted, accent"
|
|
1136
1375
|
);
|
|
1137
1376
|
}
|
|
@@ -1140,7 +1379,10 @@ mod tests {
|
|
|
1140
1379
|
fn attention_is_a_real_boolean_not_a_string() {
|
|
1141
1380
|
let mut config = complete();
|
|
1142
1381
|
config["states"]["Event"]["open"] = json!({"attention": "yes"});
|
|
1143
|
-
assert_eq!(
|
|
1382
|
+
assert_eq!(
|
|
1383
|
+
refusal(&config),
|
|
1384
|
+
"Event.open's attention must be true or false, not \"yes\""
|
|
1385
|
+
);
|
|
1144
1386
|
}
|
|
1145
1387
|
|
|
1146
1388
|
#[test]
|
|
@@ -1148,7 +1390,7 @@ mod tests {
|
|
|
1148
1390
|
let mut config = complete();
|
|
1149
1391
|
config["collections"] = json!({"Event": {"columns": [{"field": "nope"}]}});
|
|
1150
1392
|
assert_eq!(
|
|
1151
|
-
refusal(config),
|
|
1393
|
+
refusal(&config),
|
|
1152
1394
|
"Event has no attribute \"nope\" to put in its columns — it declares slug, name, price, seats, organizer"
|
|
1153
1395
|
);
|
|
1154
1396
|
}
|
|
@@ -1163,44 +1405,69 @@ mod tests {
|
|
|
1163
1405
|
#[test]
|
|
1164
1406
|
fn a_columns_sortable_is_a_boolean_and_its_sort_default_is_one_of_two_words() {
|
|
1165
1407
|
let mut config = complete();
|
|
1166
|
-
config["collections"] =
|
|
1167
|
-
|
|
1408
|
+
config["collections"] =
|
|
1409
|
+
json!({"Event": {"columns": [{"field": "slug", "sortable": "true"}]}});
|
|
1410
|
+
assert_eq!(
|
|
1411
|
+
refusal(&config),
|
|
1412
|
+
"Event's column \"slug\" has sortable \"true\", not true or false"
|
|
1413
|
+
);
|
|
1168
1414
|
|
|
1169
1415
|
let mut config = complete();
|
|
1170
|
-
config["collections"] =
|
|
1171
|
-
|
|
1416
|
+
config["collections"] =
|
|
1417
|
+
json!({"Event": {"columns": [{"field": "slug", "sort_default": "sideways"}]}});
|
|
1418
|
+
assert_eq!(
|
|
1419
|
+
refusal(&config),
|
|
1420
|
+
"Event's column \"slug\" has sort_default \"sideways\", not asc or desc"
|
|
1421
|
+
);
|
|
1172
1422
|
}
|
|
1173
1423
|
|
|
1174
1424
|
#[test]
|
|
1175
1425
|
fn nav_order_is_a_number() {
|
|
1176
1426
|
let mut config = complete();
|
|
1177
1427
|
config["collections"] = json!({"Event": {"nav_order": "3"}});
|
|
1178
|
-
assert_eq!(
|
|
1428
|
+
assert_eq!(
|
|
1429
|
+
refusal(&config),
|
|
1430
|
+
"Event's nav_order must be a number, not \"3\""
|
|
1431
|
+
);
|
|
1179
1432
|
}
|
|
1180
1433
|
|
|
1181
1434
|
#[test]
|
|
1182
1435
|
fn a_precondition_only_makes_sense_against_a_reference() {
|
|
1183
1436
|
let mut config = complete();
|
|
1184
|
-
config["collections"] =
|
|
1185
|
-
|
|
1437
|
+
config["collections"] =
|
|
1438
|
+
json!({"Event": {"preconditions": [{"field": "slug", "state": "open"}]}});
|
|
1439
|
+
assert_eq!(
|
|
1440
|
+
refusal(&config),
|
|
1441
|
+
"Event.slug isn't a reference — a precondition only makes sense against one"
|
|
1442
|
+
);
|
|
1186
1443
|
}
|
|
1187
1444
|
|
|
1188
1445
|
#[test]
|
|
1189
1446
|
fn a_precondition_against_a_lifecycle_less_target_names_the_states_it_has() {
|
|
1190
1447
|
let mut config = complete();
|
|
1191
|
-
config["collections"] =
|
|
1192
|
-
|
|
1448
|
+
config["collections"] =
|
|
1449
|
+
json!({"Event": {"preconditions": [{"field": "organizer", "state": "active"}]}});
|
|
1450
|
+
assert_eq!(
|
|
1451
|
+
refusal(&config),
|
|
1452
|
+
"Event.organizer points at Person, which has no state \"active\" — it declares "
|
|
1453
|
+
);
|
|
1193
1454
|
}
|
|
1194
1455
|
|
|
1195
1456
|
#[test]
|
|
1196
1457
|
fn a_format_names_a_real_attribute_and_one_of_two_formats() {
|
|
1197
1458
|
let mut config = complete();
|
|
1198
1459
|
config["collections"] = json!({"Event": {"field_formats": {"nope": "date"}}});
|
|
1199
|
-
assert_eq!(
|
|
1460
|
+
assert_eq!(
|
|
1461
|
+
refusal(&config),
|
|
1462
|
+
"Event has no attribute \"nope\" to format"
|
|
1463
|
+
);
|
|
1200
1464
|
|
|
1201
1465
|
let mut config = complete();
|
|
1202
1466
|
config["collections"] = json!({"Event": {"field_formats": {"slug": "shouty"}}});
|
|
1203
|
-
assert_eq!(
|
|
1467
|
+
assert_eq!(
|
|
1468
|
+
refusal(&config),
|
|
1469
|
+
"Event.slug's format \"shouty\" isn't one of date, percent"
|
|
1470
|
+
);
|
|
1204
1471
|
}
|
|
1205
1472
|
|
|
1206
1473
|
#[test]
|
|
@@ -1213,20 +1480,36 @@ mod tests {
|
|
|
1213
1480
|
#[test]
|
|
1214
1481
|
fn an_identity_rule_names_a_plain_field_and_one_of_three_strategies() {
|
|
1215
1482
|
let mut config = complete();
|
|
1216
|
-
config["collections"] =
|
|
1217
|
-
|
|
1483
|
+
config["collections"] =
|
|
1484
|
+
json!({"Event": {"identity": {"field": "organizer", "strategy": "slug"}}});
|
|
1485
|
+
assert_eq!(
|
|
1486
|
+
refusal(&config),
|
|
1487
|
+
"Event.organizer is a reference — identity can only derive a plain field"
|
|
1488
|
+
);
|
|
1218
1489
|
|
|
1219
1490
|
let mut config = complete();
|
|
1220
|
-
config["collections"] =
|
|
1221
|
-
|
|
1491
|
+
config["collections"] =
|
|
1492
|
+
json!({"Event": {"identity": {"field": "slug", "strategy": "guesswork"}}});
|
|
1493
|
+
assert_eq!(
|
|
1494
|
+
refusal(&config),
|
|
1495
|
+
"Event's identity strategy \"guesswork\" isn't 'slug', 'sequence', or 'port'"
|
|
1496
|
+
);
|
|
1222
1497
|
|
|
1223
1498
|
let mut config = complete();
|
|
1224
|
-
config["collections"] =
|
|
1225
|
-
|
|
1499
|
+
config["collections"] =
|
|
1500
|
+
json!({"Event": {"identity": {"field": "slug", "strategy": "slug"}}});
|
|
1501
|
+
assert_eq!(
|
|
1502
|
+
refusal(&config),
|
|
1503
|
+
"Event's identity slug strategy needs a source field"
|
|
1504
|
+
);
|
|
1226
1505
|
|
|
1227
1506
|
let mut config = complete();
|
|
1228
|
-
config["collections"] =
|
|
1229
|
-
|
|
1507
|
+
config["collections"] =
|
|
1508
|
+
json!({"Event": {"identity": {"field": "slug", "strategy": "sequence"}}});
|
|
1509
|
+
assert_eq!(
|
|
1510
|
+
refusal(&config),
|
|
1511
|
+
"Event's identity sequence strategy needs a prefix"
|
|
1512
|
+
);
|
|
1230
1513
|
|
|
1231
1514
|
let mut config = complete();
|
|
1232
1515
|
config["collections"] =
|
|
@@ -1237,9 +1520,10 @@ mod tests {
|
|
|
1237
1520
|
#[test]
|
|
1238
1521
|
fn a_detail_fields_display_is_one_of_five_placements() {
|
|
1239
1522
|
let mut config = complete();
|
|
1240
|
-
config["collections"] =
|
|
1523
|
+
config["collections"] =
|
|
1524
|
+
json!({"Event": {"detail_fields": [{"field": "name", "display": "interpretive"}]}});
|
|
1241
1525
|
assert_eq!(
|
|
1242
|
-
refusal(config),
|
|
1526
|
+
refusal(&config),
|
|
1243
1527
|
"Event's detail field \"name\" has display \"interpretive\", not one of simple, prose, bullet, mono, rows"
|
|
1244
1528
|
);
|
|
1245
1529
|
}
|
|
@@ -1247,24 +1531,34 @@ mod tests {
|
|
|
1247
1531
|
#[test]
|
|
1248
1532
|
fn row_columns_are_checked_against_the_rows_shape_the_field_actually_holds() {
|
|
1249
1533
|
let mut config = complete();
|
|
1250
|
-
config["collections"] =
|
|
1534
|
+
config["collections"] =
|
|
1535
|
+
json!({"Event": {"detail_fields": [{"field": "seats", "columns": ["row"]}]}});
|
|
1251
1536
|
assert!(validate(&config, &domain()).is_ok());
|
|
1252
1537
|
|
|
1253
1538
|
let mut config = complete();
|
|
1254
|
-
config["collections"] =
|
|
1255
|
-
|
|
1539
|
+
config["collections"] =
|
|
1540
|
+
json!({"Event": {"detail_fields": [{"field": "seats", "columns": ["aisle"]}]}});
|
|
1541
|
+
assert_eq!(
|
|
1542
|
+
refusal(&config),
|
|
1543
|
+
"Event's detail field \"seats\" has no row column \"aisle\" — it has row, number"
|
|
1544
|
+
);
|
|
1256
1545
|
|
|
1257
1546
|
let mut config = complete();
|
|
1258
|
-
config["collections"] =
|
|
1259
|
-
|
|
1547
|
+
config["collections"] =
|
|
1548
|
+
json!({"Event": {"detail_fields": [{"field": "name", "columns": ["row"]}]}});
|
|
1549
|
+
assert_eq!(
|
|
1550
|
+
refusal(&config),
|
|
1551
|
+
"Event's detail field \"name\" has columns: but isn't a rows-shaped field"
|
|
1552
|
+
);
|
|
1260
1553
|
}
|
|
1261
1554
|
|
|
1262
1555
|
#[test]
|
|
1263
1556
|
fn a_state_filter_needs_an_entity_backed_rows_field() {
|
|
1264
1557
|
let mut config = complete();
|
|
1265
|
-
config["collections"] =
|
|
1558
|
+
config["collections"] =
|
|
1559
|
+
json!({"Event": {"detail_fields": [{"field": "seats", "state_filter": "taken"}]}});
|
|
1266
1560
|
assert_eq!(
|
|
1267
|
-
refusal(config),
|
|
1561
|
+
refusal(&config),
|
|
1268
1562
|
"Event's detail field \"seats\" has state_filter: but isn't an entity-backed rows field"
|
|
1269
1563
|
);
|
|
1270
1564
|
}
|
|
@@ -1277,12 +1571,15 @@ mod tests {
|
|
|
1277
1571
|
|
|
1278
1572
|
let mut config = complete();
|
|
1279
1573
|
config["collections"] = json!({"Event": {"list_query": "Nowhere"}});
|
|
1280
|
-
assert_eq!(
|
|
1574
|
+
assert_eq!(
|
|
1575
|
+
refusal(&config),
|
|
1576
|
+
"Event has no query \"Nowhere\" to use as its list_query"
|
|
1577
|
+
);
|
|
1281
1578
|
|
|
1282
1579
|
let mut config = complete();
|
|
1283
1580
|
config["collections"] = json!({"Event": {"list_query": "ByOrganizer"}});
|
|
1284
1581
|
assert_eq!(
|
|
1285
|
-
refusal(config),
|
|
1582
|
+
refusal(&config),
|
|
1286
1583
|
"Event's list_query \"ByOrganizer\" takes its own arguments (organizer) — \
|
|
1287
1584
|
a generic table view has nowhere to get them from"
|
|
1288
1585
|
);
|
|
@@ -1293,7 +1590,7 @@ mod tests {
|
|
|
1293
1590
|
let mut config = complete();
|
|
1294
1591
|
config["collections"] = json!({"Event": {"after_create": {"dispatch": "Ghost.Add"}}});
|
|
1295
1592
|
assert_eq!(
|
|
1296
|
-
refusal(config),
|
|
1593
|
+
refusal(&config),
|
|
1297
1594
|
"Event's after_create dispatches \"Ghost.Add\", but \"Ghost\" is not a real aggregate this domain declares"
|
|
1298
1595
|
);
|
|
1299
1596
|
|
|
@@ -1302,7 +1599,7 @@ mod tests {
|
|
|
1302
1599
|
let mut config = complete();
|
|
1303
1600
|
config["collections"] = json!({"Event": {"after_create": {"dispatch": "Person.Nope"}}});
|
|
1304
1601
|
assert_eq!(
|
|
1305
|
-
refusal(config),
|
|
1602
|
+
refusal(&config),
|
|
1306
1603
|
"Event's after_create dispatches \"Person.Nope\", but Person declares no command \"Nope\""
|
|
1307
1604
|
);
|
|
1308
1605
|
}
|
|
@@ -1310,7 +1607,8 @@ mod tests {
|
|
|
1310
1607
|
#[test]
|
|
1311
1608
|
fn a_storable_after_create_still_refuses_because_nothing_stores_it_yet() {
|
|
1312
1609
|
let mut ir = domain();
|
|
1313
|
-
ir["aggregates"][1]["commands"] =
|
|
1610
|
+
ir["aggregates"][1]["commands"] =
|
|
1611
|
+
json!([{"name": "Note", "references": "Person", "attributes": []}]);
|
|
1314
1612
|
let mut config = complete();
|
|
1315
1613
|
config["collections"] = json!({"Event": {"after_create": {"dispatch": "Person.Note"}}});
|
|
1316
1614
|
assert_eq!(
|
|
@@ -1324,38 +1622,46 @@ mod tests {
|
|
|
1324
1622
|
fn an_overview_stat_names_a_real_collection_key_and_a_real_fold() {
|
|
1325
1623
|
let mut config = complete();
|
|
1326
1624
|
config["overview"] = json!({"stats": [{"label": "Open", "collection": "nowhere"}]});
|
|
1327
|
-
assert_eq!(
|
|
1625
|
+
assert_eq!(
|
|
1626
|
+
refusal(&config),
|
|
1627
|
+
"overview stat names collection \"nowhere\", which isn't a real collection key"
|
|
1628
|
+
);
|
|
1328
1629
|
|
|
1329
1630
|
let mut config = complete();
|
|
1330
|
-
config["overview"] =
|
|
1331
|
-
|
|
1631
|
+
config["overview"] =
|
|
1632
|
+
json!({"stats": [{"label": "Open", "collection": "events", "as": "vibes"}]});
|
|
1633
|
+
assert_eq!(
|
|
1634
|
+
refusal(&config),
|
|
1635
|
+
"overview stat \"Open\"'s as: \"vibes\" isn't one of count, money_sum"
|
|
1636
|
+
);
|
|
1332
1637
|
|
|
1333
1638
|
let mut config = complete();
|
|
1334
|
-
config["overview"] =
|
|
1335
|
-
|
|
1639
|
+
config["overview"] =
|
|
1640
|
+
json!({"stats": [{"label": "Take", "collection": "events", "as": "money_sum"}]});
|
|
1641
|
+
assert_eq!(
|
|
1642
|
+
refusal(&config),
|
|
1643
|
+
"overview stat \"Take\" folds as money_sum but names no field"
|
|
1644
|
+
);
|
|
1336
1645
|
}
|
|
1337
1646
|
|
|
1338
1647
|
#[test]
|
|
1339
1648
|
fn a_stats_where_clause_is_walked_including_through_a_negation() {
|
|
1340
1649
|
let mut config = complete();
|
|
1341
|
-
config["overview"] =
|
|
1342
|
-
json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": "ajar"}}]});
|
|
1650
|
+
config["overview"] = json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": "ajar"}}]});
|
|
1343
1651
|
assert_eq!(
|
|
1344
|
-
refusal(config),
|
|
1652
|
+
refusal(&config),
|
|
1345
1653
|
"overview stat \"Live\"'s where: state \"ajar\" isn't one of Event's own states (open, closed)"
|
|
1346
1654
|
);
|
|
1347
1655
|
|
|
1348
1656
|
let mut config = complete();
|
|
1349
|
-
config["overview"] =
|
|
1350
|
-
json!({"stats": [{"label": "Live", "collection": "events", "where": {"not": {"state": {"eq": "ajar"}}}}]});
|
|
1657
|
+
config["overview"] = json!({"stats": [{"label": "Live", "collection": "events", "where": {"not": {"state": {"eq": "ajar"}}}}]});
|
|
1351
1658
|
assert_eq!(
|
|
1352
|
-
refusal(config),
|
|
1659
|
+
refusal(&config),
|
|
1353
1660
|
"overview stat \"Live\"'s where: state \"ajar\" isn't one of Event's own states (open, closed)"
|
|
1354
1661
|
);
|
|
1355
1662
|
|
|
1356
1663
|
let mut config = complete();
|
|
1357
|
-
config["overview"] =
|
|
1358
|
-
json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": {"in": ["open"]}}}]});
|
|
1664
|
+
config["overview"] = json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": {"in": ["open"]}}}]});
|
|
1359
1665
|
assert!(validate(&config, &domain()).is_ok());
|
|
1360
1666
|
}
|
|
1361
1667
|
|
|
@@ -1364,13 +1670,16 @@ mod tests {
|
|
|
1364
1670
|
let mut config = complete();
|
|
1365
1671
|
config["overview"] = json!({"stats": [{"label": "Hosted", "collection": "events",
|
|
1366
1672
|
"where": {"has_related": {"collection": "persons", "field": "nope"}}}]});
|
|
1367
|
-
assert_eq!(
|
|
1673
|
+
assert_eq!(
|
|
1674
|
+
refusal(&config),
|
|
1675
|
+
"overview stat \"Hosted\"'s has_related.field \"nope\" isn't a real field on Person"
|
|
1676
|
+
);
|
|
1368
1677
|
|
|
1369
1678
|
let mut config = complete();
|
|
1370
1679
|
config["overview"] = json!({"stats": [{"label": "Hosted", "collection": "events",
|
|
1371
1680
|
"where": {"has_related": {"collection": "persons", "field": "email", "equals": "nope"}}}]});
|
|
1372
1681
|
assert_eq!(
|
|
1373
|
-
refusal(config),
|
|
1682
|
+
refusal(&config),
|
|
1374
1683
|
"overview stat \"Hosted\"'s has_related.equals \"nope\" isn't a real field on Event"
|
|
1375
1684
|
);
|
|
1376
1685
|
|
|
@@ -1383,7 +1692,9 @@ mod tests {
|
|
|
1383
1692
|
#[test]
|
|
1384
1693
|
fn a_bare_column_name_and_a_descriptor_both_dispatch_as_a_row() {
|
|
1385
1694
|
assert_eq!(
|
|
1386
|
-
columns_for_dispatch(Some(
|
|
1695
|
+
columns_for_dispatch(Some(
|
|
1696
|
+
&json!(["slug", {"field": "name", "sortable": true, "head": "Title"}])
|
|
1697
|
+
)),
|
|
1387
1698
|
vec![
|
|
1388
1699
|
json!({"field": "slug"}),
|
|
1389
1700
|
json!({"field": "name", "sortable": "true", "extra_json": "{\"head\":\"Title\"}"})
|
|
@@ -1418,7 +1729,11 @@ mod tests {
|
|
|
1418
1729
|
let stats = stats_for_dispatch(Some(&json!([{"label": "Live", "collection": "events",
|
|
1419
1730
|
"where": {"state": "open"}}])));
|
|
1420
1731
|
assert_eq!(stats[0]["where_json"], json!("{\"state\":\"open\"}"));
|
|
1421
|
-
assert!(
|
|
1732
|
+
assert!(
|
|
1733
|
+
stats[0].get("as").is_none(),
|
|
1734
|
+
"an absent fold is absent, not null: {}",
|
|
1735
|
+
stats[0]
|
|
1736
|
+
);
|
|
1422
1737
|
}
|
|
1423
1738
|
|
|
1424
1739
|
#[test]
|