hecks 3.14.0 → 5.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/exe/hecks +1 -1
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +9 -16
- 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/cli/project_cli/launcher_source.rb +1 -1
- 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/app/responses.rb +3 -1
- 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/first_admin.rb +3 -3
- 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/site/cms_editor/templates/src/host.ts.tmpl +7 -2
- 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/unwrapping.rb +60 -0
- data/lib/hecks/runtime/value.rb +10 -21
- 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/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
- data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.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 +306 -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 +151 -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/presenters.rs +335 -0
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +574 -229
- 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 +1037 -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 +555 -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 +244 -114
- 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 +244 -148
- data/rust/src/kernel/mod.rs +47 -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/present.rs +261 -0
- 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/rust/host/src/approval.rs +0 -652
data/rust/host/src/api.rs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
use crate::auth::Session;
|
|
5
5
|
use crate::dispatch;
|
|
6
|
+
use crate::host::Host;
|
|
6
7
|
use crate::journal::LineageConfig;
|
|
7
|
-
use crate::lambda_client::LambdaInvoker;
|
|
8
8
|
use crate::presentation;
|
|
9
9
|
use crate::presentation_write;
|
|
10
10
|
use crate::ui_schema;
|
|
@@ -19,7 +19,7 @@ use tokio_postgres::Client;
|
|
|
19
19
|
/// Every `/api/...` request, answered. `session` is `None` only in tests —
|
|
20
20
|
/// `web::route` runs `auth_gate` first in production — but it stays an
|
|
21
21
|
/// `Option` because `/api/me` itself has a real empty-session answer.
|
|
22
|
-
#[allow(clippy::
|
|
22
|
+
#[allow(clippy::implicit_hasher)] // reason: the query map always comes from this crate's own default-hashed parser
|
|
23
23
|
pub async fn route(
|
|
24
24
|
domain_ir: &Value,
|
|
25
25
|
method: &str,
|
|
@@ -27,11 +27,14 @@ pub async fn route(
|
|
|
27
27
|
query: &HashMap<String, String>,
|
|
28
28
|
raw_body: &str,
|
|
29
29
|
session: Option<&Session>,
|
|
30
|
-
|
|
31
|
-
wasm_path: &Path,
|
|
32
|
-
config: &LineageConfig,
|
|
33
|
-
invoker: &dyn LambdaInvoker,
|
|
30
|
+
host: Host<'_>,
|
|
34
31
|
) -> Value {
|
|
32
|
+
let Host {
|
|
33
|
+
client,
|
|
34
|
+
wasm_path,
|
|
35
|
+
config,
|
|
36
|
+
..
|
|
37
|
+
} = host;
|
|
35
38
|
match (method, path) {
|
|
36
39
|
("GET", "/api/me") => ok(&me(session)),
|
|
37
40
|
|
|
@@ -56,9 +59,7 @@ pub async fn route(
|
|
|
56
59
|
Err(e) => internal_error(&e.to_string()),
|
|
57
60
|
},
|
|
58
61
|
|
|
59
|
-
("PUT", "/api/presentation") =>
|
|
60
|
-
presentation_save(domain_ir, raw_body, client, wasm_path, config, invoker).await
|
|
61
|
-
}
|
|
62
|
+
("PUT", "/api/presentation") => presentation_save(domain_ir, raw_body, host).await,
|
|
62
63
|
|
|
63
64
|
// Collection routes (`/api/:coll[/:id]`), matched last so `:coll`
|
|
64
65
|
// doesn't swallow `me`/`schema`. Segments are percent-decoded: a
|
|
@@ -76,12 +77,14 @@ pub async fn route(
|
|
|
76
77
|
("GET", [collection]) => {
|
|
77
78
|
collection_index(domain_ir, collection, query, client, wasm_path, config).await
|
|
78
79
|
}
|
|
79
|
-
("GET", [collection, id]) =>
|
|
80
|
+
("GET", [collection, id]) => {
|
|
81
|
+
record_show(domain_ir, collection, id, client, wasm_path, config).await
|
|
82
|
+
}
|
|
80
83
|
("POST", [collection]) => {
|
|
81
|
-
collection_create(domain_ir, collection, raw_body,
|
|
84
|
+
collection_create(domain_ir, collection, raw_body, host).await
|
|
82
85
|
}
|
|
83
86
|
("POST", [collection, id, command]) => {
|
|
84
|
-
command_route(domain_ir, collection, id, command, raw_body,
|
|
87
|
+
command_route(domain_ir, collection, id, command, raw_body, host).await
|
|
85
88
|
}
|
|
86
89
|
_ => not_found(&format!("no API route for {method} {path}")),
|
|
87
90
|
}
|
|
@@ -109,7 +112,10 @@ async fn collection_index(
|
|
|
109
112
|
Err(refusal) => return refusal,
|
|
110
113
|
};
|
|
111
114
|
|
|
112
|
-
let wanted = params
|
|
115
|
+
let wanted = params
|
|
116
|
+
.get("query")
|
|
117
|
+
.map(String::as_str)
|
|
118
|
+
.filter(|name| !name.is_empty());
|
|
113
119
|
let named = wanted.and_then(|name| find_query(aggregate, name));
|
|
114
120
|
if let Some(named) = named {
|
|
115
121
|
if let Some(args) = query_args_from_params(aggregate, named, params) {
|
|
@@ -121,8 +127,16 @@ async fn collection_index(
|
|
|
121
127
|
Ok(records) => records,
|
|
122
128
|
Err(refusal) => return refusal,
|
|
123
129
|
};
|
|
124
|
-
sort_records(
|
|
125
|
-
|
|
130
|
+
sort_records(
|
|
131
|
+
&mut records,
|
|
132
|
+
resolve_sort(domain_ir, &config, collection, aggregate, params).as_ref(),
|
|
133
|
+
);
|
|
134
|
+
ok(&Value::Array(
|
|
135
|
+
records
|
|
136
|
+
.into_iter()
|
|
137
|
+
.map(|(id, state)| with_id(&id, &state))
|
|
138
|
+
.collect(),
|
|
139
|
+
))
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
/// `GET /api/:coll/:id` — one record's full state, or the Ruby
|
|
@@ -149,14 +163,22 @@ async fn record_show(
|
|
|
149
163
|
};
|
|
150
164
|
match records.into_iter().find(|(record_id, _)| record_id == id) {
|
|
151
165
|
Some((record_id, state)) => ok(&with_id(&record_id, &state)),
|
|
152
|
-
None => not_found(&format!(
|
|
166
|
+
None => not_found(&format!(
|
|
167
|
+
"no {} found for id {:?}",
|
|
168
|
+
ui_schema::agg_name(aggregate),
|
|
169
|
+
id
|
|
170
|
+
)),
|
|
153
171
|
}
|
|
154
172
|
}
|
|
155
173
|
|
|
156
174
|
/// A collection key is presentation, not domain (`collections.<Name>.key`
|
|
157
175
|
/// can rename it), so this resolves through the same `collection_key`
|
|
158
176
|
/// `/api/ui-schema` hands the client, regardless of lifecycle.
|
|
159
|
-
fn resolve_collection<'a>(
|
|
177
|
+
fn resolve_collection<'a>(
|
|
178
|
+
domain_ir: &'a Value,
|
|
179
|
+
config: &Value,
|
|
180
|
+
collection: &str,
|
|
181
|
+
) -> Result<&'a Value, Value> {
|
|
160
182
|
ui_schema::aggregates(domain_ir)
|
|
161
183
|
.into_iter()
|
|
162
184
|
.find(|aggregate| ui_schema::collection_key(aggregate, config) == collection)
|
|
@@ -179,7 +201,9 @@ async fn read_records(
|
|
|
179
201
|
client: &Mutex<Client>,
|
|
180
202
|
wasm_path: &Path,
|
|
181
203
|
) -> Result<Vec<(String, Value)>, Value> {
|
|
182
|
-
let result = dispatch::read(client, wasm_path)
|
|
204
|
+
let result = dispatch::read(client, wasm_path)
|
|
205
|
+
.await
|
|
206
|
+
.map_err(|e| internal_error(&format!("{e:#}")))?;
|
|
183
207
|
let prefix = format!(
|
|
184
208
|
"{}::{}#",
|
|
185
209
|
domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or(""),
|
|
@@ -191,7 +215,10 @@ async fn read_records(
|
|
|
191
215
|
.map(|instances| {
|
|
192
216
|
instances
|
|
193
217
|
.iter()
|
|
194
|
-
.filter_map(|(key, state)|
|
|
218
|
+
.filter_map(|(key, state)| {
|
|
219
|
+
key.strip_prefix(&prefix)
|
|
220
|
+
.map(|id| (id.to_string(), state.clone()))
|
|
221
|
+
})
|
|
195
222
|
.collect()
|
|
196
223
|
})
|
|
197
224
|
.unwrap_or_default())
|
|
@@ -225,28 +252,40 @@ async fn named_query_rows(
|
|
|
225
252
|
Ok(result) => result,
|
|
226
253
|
Err(e) => return internal_error(&format!("{e:#}")),
|
|
227
254
|
};
|
|
228
|
-
let answered = result
|
|
255
|
+
let answered = result
|
|
256
|
+
.get("queries")
|
|
257
|
+
.and_then(|v| v.as_array())
|
|
258
|
+
.and_then(|queries| queries.first())
|
|
259
|
+
.cloned();
|
|
229
260
|
let Some(answered) = answered else {
|
|
230
261
|
return internal_error(&format!("the kernel answered no query step for {question}"));
|
|
231
262
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
263
|
+
// No rows is a refused query, not a missing one — the refusal lands in
|
|
264
|
+
// the kernel's own top-level `refusals` array, read the same way a
|
|
265
|
+
// refused command is.
|
|
266
|
+
answered
|
|
267
|
+
.get("rows")
|
|
268
|
+
.filter(|rows| !rows.is_null())
|
|
269
|
+
.map_or_else(|| domain_refusal(&result), ok)
|
|
239
270
|
}
|
|
240
271
|
|
|
241
272
|
/// Same-named params, shaped per argument (bare for a reference/primitive,
|
|
242
273
|
/// wrapped for a value object). `None` the moment a required one is missing.
|
|
243
|
-
fn query_args_from_params(
|
|
274
|
+
fn query_args_from_params(
|
|
275
|
+
aggregate: &Value,
|
|
276
|
+
named: &Value,
|
|
277
|
+
params: &HashMap<String, String>,
|
|
278
|
+
) -> Option<Value> {
|
|
244
279
|
let mut args = Map::new();
|
|
245
280
|
for attribute in named.get("attributes").and_then(|v| v.as_array())? {
|
|
246
281
|
let name = attribute.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
247
282
|
let raw = params.get(name).filter(|value| !value.is_empty());
|
|
248
283
|
let Some(raw) = raw else {
|
|
249
|
-
if attribute
|
|
284
|
+
if attribute
|
|
285
|
+
.get("optional")
|
|
286
|
+
.and_then(serde_json::Value::as_bool)
|
|
287
|
+
.unwrap_or(false)
|
|
288
|
+
{
|
|
250
289
|
continue;
|
|
251
290
|
}
|
|
252
291
|
return None;
|
|
@@ -272,7 +311,10 @@ fn value_object_field(aggregate: &Value, type_name: &str) -> Option<String> {
|
|
|
272
311
|
.and_then(|v| v.as_array())?
|
|
273
312
|
.iter()
|
|
274
313
|
.find(|v| v.get("name").and_then(|n| n.as_str()) == Some(type_name))?;
|
|
275
|
-
let first = value_object
|
|
314
|
+
let first = value_object
|
|
315
|
+
.get("attributes")
|
|
316
|
+
.and_then(|v| v.as_array())?
|
|
317
|
+
.first()?;
|
|
276
318
|
first.get("name").and_then(|v| v.as_str()).map(String::from)
|
|
277
319
|
}
|
|
278
320
|
|
|
@@ -282,16 +324,18 @@ fn value_object_field(aggregate: &Value, type_name: &str) -> Option<String> {
|
|
|
282
324
|
/// console does around it: minting an identity (`apply_identity`) and
|
|
283
325
|
/// checking a cross-aggregate precondition a `given` can't express
|
|
284
326
|
/// (`check_preconditions`), both driven by `collections.<Name>` config.
|
|
285
|
-
#[allow(clippy::too_many_arguments)]
|
|
286
327
|
async fn collection_create(
|
|
287
328
|
domain_ir: &Value,
|
|
288
329
|
collection: &str,
|
|
289
330
|
raw_body: &str,
|
|
290
|
-
|
|
291
|
-
wasm_path: &Path,
|
|
292
|
-
config: &LineageConfig,
|
|
293
|
-
invoker: &dyn LambdaInvoker,
|
|
331
|
+
host: Host<'_>,
|
|
294
332
|
) -> Value {
|
|
333
|
+
let Host {
|
|
334
|
+
client,
|
|
335
|
+
wasm_path,
|
|
336
|
+
config,
|
|
337
|
+
..
|
|
338
|
+
} = host;
|
|
295
339
|
let presentation = match presentation::load(client, wasm_path, config).await {
|
|
296
340
|
Ok(presentation) => presentation,
|
|
297
341
|
Err(e) => return internal_error(&e.to_string()),
|
|
@@ -305,7 +349,10 @@ async fn collection_create(
|
|
|
305
349
|
Err(refusal) => return refusal,
|
|
306
350
|
};
|
|
307
351
|
let name = ui_schema::agg_name(aggregate);
|
|
308
|
-
let Some(creating) = ui_schema::commands(aggregate)
|
|
352
|
+
let Some(creating) = ui_schema::commands(aggregate)
|
|
353
|
+
.into_iter()
|
|
354
|
+
.find(|c| ui_schema::creates(c))
|
|
355
|
+
else {
|
|
309
356
|
return not_found(&format!("{name} declares no creating command"));
|
|
310
357
|
};
|
|
311
358
|
|
|
@@ -316,12 +363,25 @@ async fn collection_create(
|
|
|
316
363
|
if let Err(refusal) = apply_identity(&presentation, aggregate, &mut args, records.len()) {
|
|
317
364
|
return refusal;
|
|
318
365
|
}
|
|
319
|
-
if let Err(refusal) = check_preconditions(
|
|
366
|
+
if let Err(refusal) = check_preconditions(
|
|
367
|
+
domain_ir,
|
|
368
|
+
&presentation,
|
|
369
|
+
aggregate,
|
|
370
|
+
&args,
|
|
371
|
+
client,
|
|
372
|
+
wasm_path,
|
|
373
|
+
)
|
|
374
|
+
.await
|
|
375
|
+
{
|
|
320
376
|
return refusal;
|
|
321
377
|
}
|
|
322
378
|
|
|
323
|
-
let verb = format!(
|
|
324
|
-
|
|
379
|
+
let verb = format!(
|
|
380
|
+
"{}::{name}.{}",
|
|
381
|
+
domain_name(domain_ir),
|
|
382
|
+
command_name(creating)
|
|
383
|
+
);
|
|
384
|
+
let outcome = match dispatch::handle_facts(host, &verb, args, None).await {
|
|
325
385
|
Ok(outcome) => outcome,
|
|
326
386
|
Err(e) => return internal_error(&format!("{e:#}")),
|
|
327
387
|
};
|
|
@@ -338,18 +398,20 @@ async fn collection_create(
|
|
|
338
398
|
/// `POST /api/:coll/:id/:command` — a command against an existing record.
|
|
339
399
|
/// Order matters: find the record first (404), then the command (404),
|
|
340
400
|
/// then dispatch — the Ruby engine's own order.
|
|
341
|
-
#[allow(clippy::too_many_arguments)]
|
|
342
401
|
async fn command_route(
|
|
343
402
|
domain_ir: &Value,
|
|
344
403
|
collection: &str,
|
|
345
404
|
id: &str,
|
|
346
405
|
command: &str,
|
|
347
406
|
raw_body: &str,
|
|
348
|
-
|
|
349
|
-
wasm_path: &Path,
|
|
350
|
-
config: &LineageConfig,
|
|
351
|
-
invoker: &dyn LambdaInvoker,
|
|
407
|
+
host: Host<'_>,
|
|
352
408
|
) -> Value {
|
|
409
|
+
let Host {
|
|
410
|
+
client,
|
|
411
|
+
wasm_path,
|
|
412
|
+
config,
|
|
413
|
+
..
|
|
414
|
+
} = host;
|
|
353
415
|
let presentation = match presentation::load(client, wasm_path, config).await {
|
|
354
416
|
Ok(presentation) => presentation,
|
|
355
417
|
Err(e) => return internal_error(&e.to_string()),
|
|
@@ -374,8 +436,12 @@ async fn command_route(
|
|
|
374
436
|
Err(refusal) => return refusal,
|
|
375
437
|
};
|
|
376
438
|
|
|
377
|
-
let verb = format!(
|
|
378
|
-
|
|
439
|
+
let verb = format!(
|
|
440
|
+
"{}::{name}.{}",
|
|
441
|
+
domain_name(domain_ir),
|
|
442
|
+
command_name(declared)
|
|
443
|
+
);
|
|
444
|
+
let outcome = match dispatch::handle_routed(host, &verb, json!(id), args, None).await {
|
|
379
445
|
Ok(outcome) => outcome,
|
|
380
446
|
Err(e) => return internal_error(&format!("{e:#}")),
|
|
381
447
|
};
|
|
@@ -392,7 +458,7 @@ fn dispatchable_command<'a>(aggregate: &'a Value, wanted: &str) -> Option<&'a Va
|
|
|
392
458
|
ui_schema::commands(aggregate)
|
|
393
459
|
.into_iter()
|
|
394
460
|
.filter(|command| !ui_schema::creates(command))
|
|
395
|
-
.find(|command|
|
|
461
|
+
.find(|command| crate::hecks_naming::snake(command_name(command)) == wanted)
|
|
396
462
|
}
|
|
397
463
|
|
|
398
464
|
fn command_name(command: &Value) -> &str {
|
|
@@ -412,7 +478,11 @@ fn parsed_body(raw: &str) -> Result<Value, Value> {
|
|
|
412
478
|
}
|
|
413
479
|
match serde_json::from_str::<Value>(raw) {
|
|
414
480
|
Ok(Value::Object(body)) => Ok(Value::Object(body)),
|
|
415
|
-
_ => Err(refusal(
|
|
481
|
+
_ => Err(refusal(
|
|
482
|
+
400,
|
|
483
|
+
"MalformedBody",
|
|
484
|
+
"request body is not valid JSON",
|
|
485
|
+
)),
|
|
416
486
|
}
|
|
417
487
|
}
|
|
418
488
|
|
|
@@ -432,13 +502,24 @@ fn created_id(result: &Value) -> Option<String> {
|
|
|
432
502
|
/// The record as it stands after the command, read straight out of the
|
|
433
503
|
/// kernel's post-dispatch `instances` rather than re-reading the journal.
|
|
434
504
|
fn created_record(domain_ir: &Value, aggregate: &Value, result: &Value, id: &str) -> Value {
|
|
435
|
-
let key = format!(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
505
|
+
let key = format!(
|
|
506
|
+
"{}::{}#{id}",
|
|
507
|
+
domain_name(domain_ir),
|
|
508
|
+
ui_schema::agg_name(aggregate)
|
|
509
|
+
);
|
|
510
|
+
// A missing record was accepted, but this host cannot see what it just
|
|
511
|
+
// wrote — a real inconsistency, never a 200 with an empty body.
|
|
512
|
+
result
|
|
513
|
+
.get("instances")
|
|
514
|
+
.and_then(|instances| instances.get(&key))
|
|
515
|
+
.map_or_else(
|
|
516
|
+
|| {
|
|
517
|
+
internal_error(&format!(
|
|
518
|
+
"{key} was accepted but is not in the resulting state"
|
|
519
|
+
))
|
|
520
|
+
},
|
|
521
|
+
|state| ok(&with_id(id, state)),
|
|
522
|
+
)
|
|
442
523
|
}
|
|
443
524
|
|
|
444
525
|
/// A refused command, in the Ruby engine's own envelope (422, refusal
|
|
@@ -446,12 +527,21 @@ fn created_record(domain_ir: &Value, aggregate: &Value, result: &Value, id: &str
|
|
|
446
527
|
/// replays the whole rehydrated history, and every earlier step already
|
|
447
528
|
/// succeeded once.
|
|
448
529
|
fn domain_refusal(result: &Value) -> Value {
|
|
449
|
-
let last = result
|
|
450
|
-
|
|
530
|
+
let last = result
|
|
531
|
+
.get("refusals")
|
|
532
|
+
.and_then(|r| r.as_array())
|
|
533
|
+
.and_then(|refusals| refusals.last());
|
|
534
|
+
let Some(last) = last else {
|
|
535
|
+
return refusal(422, "Refused", "the command was refused");
|
|
536
|
+
};
|
|
451
537
|
refusal(
|
|
452
538
|
422,
|
|
453
|
-
last.get("kind")
|
|
454
|
-
|
|
539
|
+
last.get("kind")
|
|
540
|
+
.and_then(|v| v.as_str())
|
|
541
|
+
.unwrap_or("Refused"),
|
|
542
|
+
last.get("error")
|
|
543
|
+
.and_then(|v| v.as_str())
|
|
544
|
+
.unwrap_or("the command was refused"),
|
|
455
545
|
)
|
|
456
546
|
}
|
|
457
547
|
|
|
@@ -461,16 +551,29 @@ fn domain_refusal(result: &Value) -> Value {
|
|
|
461
551
|
/// derives it from another submitted field, `sequence` pads the next
|
|
462
552
|
/// count. Only fires when the field is genuinely missing; a caller that
|
|
463
553
|
/// supplies one is left alone.
|
|
464
|
-
fn apply_identity(
|
|
554
|
+
fn apply_identity(
|
|
555
|
+
presentation: &Value,
|
|
556
|
+
aggregate: &Value,
|
|
557
|
+
args: &mut Value,
|
|
558
|
+
existing: usize,
|
|
559
|
+
) -> Result<(), Value> {
|
|
465
560
|
let name = ui_schema::agg_name(aggregate);
|
|
466
|
-
let Some(rule) = presentation
|
|
561
|
+
let Some(rule) = presentation
|
|
562
|
+
.get("collections")
|
|
563
|
+
.and_then(|c| c.get(name))
|
|
564
|
+
.and_then(|c| c.get("identity"))
|
|
565
|
+
else {
|
|
566
|
+
return Ok(());
|
|
567
|
+
};
|
|
568
|
+
let Some(field) = rule.get("field").and_then(|v| v.as_str()) else {
|
|
467
569
|
return Ok(());
|
|
468
570
|
};
|
|
469
|
-
let Some(field) = rule.get("field").and_then(|v| v.as_str()) else { return Ok(()) };
|
|
470
571
|
if args.get(field).is_some() {
|
|
471
572
|
return Ok(());
|
|
472
573
|
}
|
|
473
|
-
let Some(wire_key) = identity_wire_key(aggregate, field) else {
|
|
574
|
+
let Some(wire_key) = identity_wire_key(aggregate, field) else {
|
|
575
|
+
return Ok(());
|
|
576
|
+
};
|
|
474
577
|
|
|
475
578
|
let minted = match rule.get("strategy").and_then(|v| v.as_str()) {
|
|
476
579
|
Some("slug") => {
|
|
@@ -479,7 +582,7 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
|
|
|
479
582
|
}
|
|
480
583
|
Some("sequence") => {
|
|
481
584
|
let prefix = rule.get("prefix").and_then(|v| v.as_str()).unwrap_or("");
|
|
482
|
-
let pad = rule.get("pad").and_then(
|
|
585
|
+
let pad = rule.get("pad").and_then(serde_json::Value::as_u64).unwrap_or(3);
|
|
483
586
|
// `pad` rides in as free-form config JSON, and a width like 10^15 would have the
|
|
484
587
|
// formatter allocate that many bytes: refuse it rather than abort the process.
|
|
485
588
|
if pad > MAX_IDENTITY_PAD {
|
|
@@ -497,7 +600,9 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
|
|
|
497
600
|
}
|
|
498
601
|
_ => None,
|
|
499
602
|
};
|
|
500
|
-
let Some(minted) = minted.filter(|value| !value.is_empty()) else {
|
|
603
|
+
let Some(minted) = minted.filter(|value| !value.is_empty()) else {
|
|
604
|
+
return Ok(());
|
|
605
|
+
};
|
|
501
606
|
if let Some(object) = args.as_object_mut() {
|
|
502
607
|
object.insert(field.to_string(), json!({ wire_key: minted }));
|
|
503
608
|
}
|
|
@@ -521,7 +626,11 @@ fn identity_wire_key(aggregate: &Value, field: &str) -> Option<String> {
|
|
|
521
626
|
|
|
522
627
|
fn dig_source(args: &Value, source: &str) -> String {
|
|
523
628
|
match args.get(source) {
|
|
524
|
-
Some(Value::Object(fields)) => fields
|
|
629
|
+
Some(Value::Object(fields)) => fields
|
|
630
|
+
.values()
|
|
631
|
+
.next()
|
|
632
|
+
.map(scalar_to_string)
|
|
633
|
+
.unwrap_or_default(),
|
|
525
634
|
Some(value) => scalar_to_string(value),
|
|
526
635
|
None => String::new(),
|
|
527
636
|
}
|
|
@@ -580,8 +689,13 @@ async fn check_preconditions(
|
|
|
580
689
|
return Ok(());
|
|
581
690
|
}
|
|
582
691
|
|
|
583
|
-
let instances = dispatch::read(client, wasm_path)
|
|
584
|
-
|
|
692
|
+
let instances = dispatch::read(client, wasm_path)
|
|
693
|
+
.await
|
|
694
|
+
.map_err(|e| internal_error(&format!("{e:#}")))?;
|
|
695
|
+
let instances = instances
|
|
696
|
+
.get("instances")
|
|
697
|
+
.cloned()
|
|
698
|
+
.unwrap_or_else(|| json!({}));
|
|
585
699
|
|
|
586
700
|
for rule in &rules {
|
|
587
701
|
check_precondition(domain_ir, aggregate, rule, args, &instances)?;
|
|
@@ -589,11 +703,21 @@ async fn check_preconditions(
|
|
|
589
703
|
Ok(())
|
|
590
704
|
}
|
|
591
705
|
|
|
592
|
-
fn check_precondition(
|
|
593
|
-
|
|
706
|
+
fn check_precondition(
|
|
707
|
+
domain_ir: &Value,
|
|
708
|
+
aggregate: &Value,
|
|
709
|
+
rule: &Value,
|
|
710
|
+
args: &Value,
|
|
711
|
+
instances: &Value,
|
|
712
|
+
) -> Result<(), Value> {
|
|
713
|
+
let Some(field) = rule.get("field").and_then(|v| v.as_str()) else {
|
|
714
|
+
return Ok(());
|
|
715
|
+
};
|
|
594
716
|
// A missing required reference is the command's own validation to
|
|
595
717
|
// refuse, not this one's.
|
|
596
|
-
let Some(target_id) = args.get(field).and_then(|v| v.as_str()) else {
|
|
718
|
+
let Some(target_id) = args.get(field).and_then(|v| v.as_str()) else {
|
|
719
|
+
return Ok(());
|
|
720
|
+
};
|
|
597
721
|
// Config named a field that isn't a reference at all — caught at
|
|
598
722
|
// save time by the console's own validation, not at dispatch time.
|
|
599
723
|
let Some(target_name) = ui_schema::find_attribute(aggregate, field)
|
|
@@ -612,8 +736,12 @@ fn check_precondition(domain_ir: &Value, aggregate: &Value, rule: &Value, args:
|
|
|
612
736
|
));
|
|
613
737
|
};
|
|
614
738
|
|
|
615
|
-
let Some(expected) = rule.get("state").and_then(|v| v.as_str()) else {
|
|
616
|
-
|
|
739
|
+
let Some(expected) = rule.get("state").and_then(|v| v.as_str()) else {
|
|
740
|
+
return Ok(());
|
|
741
|
+
};
|
|
742
|
+
let target_aggregate = ui_schema::aggregates(domain_ir)
|
|
743
|
+
.into_iter()
|
|
744
|
+
.find(|a| ui_schema::agg_name(a) == target_name);
|
|
617
745
|
// Config named a state precondition against an aggregate with no
|
|
618
746
|
// lifecycle — again a save-time concern, not a dispatch-time one.
|
|
619
747
|
let Some(state_field) = target_aggregate
|
|
@@ -624,22 +752,42 @@ fn check_precondition(domain_ir: &Value, aggregate: &Value, rule: &Value, args:
|
|
|
624
752
|
return Ok(());
|
|
625
753
|
};
|
|
626
754
|
|
|
627
|
-
let actual = target
|
|
755
|
+
let actual = target
|
|
756
|
+
.get(&state_field)
|
|
757
|
+
.and_then(|v| v.as_str())
|
|
758
|
+
.unwrap_or("");
|
|
628
759
|
if actual == expected {
|
|
629
760
|
return Ok(());
|
|
630
761
|
}
|
|
631
|
-
Err(precondition_failed(
|
|
762
|
+
Err(precondition_failed(
|
|
763
|
+
rule,
|
|
764
|
+
&format!("{target_name} is at {actual}, not {expected}"),
|
|
765
|
+
))
|
|
632
766
|
}
|
|
633
767
|
|
|
634
768
|
fn precondition_failed(rule: &Value, derived: &str) -> Value {
|
|
635
|
-
refusal(
|
|
769
|
+
refusal(
|
|
770
|
+
422,
|
|
771
|
+
"PreconditionFailed",
|
|
772
|
+
rule.get("message")
|
|
773
|
+
.and_then(|v| v.as_str())
|
|
774
|
+
.unwrap_or(derived),
|
|
775
|
+
)
|
|
636
776
|
}
|
|
637
777
|
|
|
638
778
|
// ---- sorting ---------------------------------------------------------
|
|
639
779
|
|
|
640
780
|
/// Column shapes a JSONB order path resolves correctly. A fold over a
|
|
641
781
|
/// list or a cross-aggregate join is excluded and stays client-side.
|
|
642
|
-
const SQL_SORTABLE_SHAPES: &[&str] = &[
|
|
782
|
+
const SQL_SORTABLE_SHAPES: &[&str] = &[
|
|
783
|
+
"text_value",
|
|
784
|
+
"text",
|
|
785
|
+
"address",
|
|
786
|
+
"number",
|
|
787
|
+
"money",
|
|
788
|
+
"enum",
|
|
789
|
+
"pill",
|
|
790
|
+
];
|
|
643
791
|
|
|
644
792
|
struct SortSpec {
|
|
645
793
|
field: String,
|
|
@@ -676,14 +824,19 @@ fn resolve_sort(
|
|
|
676
824
|
let field = if key == "__state__" {
|
|
677
825
|
collection_schema.get("stateField")?.as_str()?.to_string()
|
|
678
826
|
} else {
|
|
679
|
-
key.
|
|
827
|
+
key.clone()
|
|
680
828
|
};
|
|
681
829
|
// Config could still name a field the aggregate doesn't have.
|
|
682
|
-
if ui_schema::find_attribute(aggregate, &field).is_none()
|
|
830
|
+
if ui_schema::find_attribute(aggregate, &field).is_none()
|
|
831
|
+
&& lifecycle_field(aggregate) != Some(field.as_str())
|
|
832
|
+
{
|
|
683
833
|
return None;
|
|
684
834
|
}
|
|
685
835
|
|
|
686
|
-
Some(SortSpec {
|
|
836
|
+
Some(SortSpec {
|
|
837
|
+
field,
|
|
838
|
+
descending: params.get("direction").map(String::as_str) == Some("desc"),
|
|
839
|
+
})
|
|
687
840
|
}
|
|
688
841
|
|
|
689
842
|
fn lifecycle_field(aggregate: &Value) -> Option<&str> {
|
|
@@ -692,11 +845,15 @@ fn lifecycle_field(aggregate: &Value) -> Option<&str> {
|
|
|
692
845
|
|
|
693
846
|
/// In memory, not SQL: there's no ORDER BY to push into, so this follows
|
|
694
847
|
/// Postgres's own null ordering, with a stable id fallback for ties.
|
|
695
|
-
fn sort_records(records: &mut [(String, Value)], spec:
|
|
848
|
+
fn sort_records(records: &mut [(String, Value)], spec: Option<&SortSpec>) {
|
|
696
849
|
match spec {
|
|
697
850
|
None => records.sort_by(|(a, _), (b, _)| a.cmp(b)),
|
|
698
851
|
Some(spec) => records.sort_by(|(a_id, a), (b_id, b)| {
|
|
699
|
-
let ordering = compare_sort_keys(
|
|
852
|
+
let ordering = compare_sort_keys(
|
|
853
|
+
sort_key(a, &spec.field),
|
|
854
|
+
sort_key(b, &spec.field),
|
|
855
|
+
spec.descending,
|
|
856
|
+
);
|
|
700
857
|
ordering.then_with(|| a_id.cmp(b_id))
|
|
701
858
|
}),
|
|
702
859
|
}
|
|
@@ -706,7 +863,9 @@ fn sort_records(records: &mut [(String, Value)], spec: &Option<SortSpec>) {
|
|
|
706
863
|
/// a money value object, or a single-attribute value object's member.
|
|
707
864
|
fn sort_key<'a>(state: &'a Value, field: &str) -> Option<&'a Value> {
|
|
708
865
|
let value = state.get(field).filter(|v| !v.is_null())?;
|
|
709
|
-
let Some(object) = value.as_object() else {
|
|
866
|
+
let Some(object) = value.as_object() else {
|
|
867
|
+
return Some(value);
|
|
868
|
+
};
|
|
710
869
|
if let Some(cents) = object.get("cents") {
|
|
711
870
|
return Some(cents);
|
|
712
871
|
}
|
|
@@ -739,7 +898,7 @@ fn compare_sort_keys(left: Option<&Value>, right: Option<&Value>, descending: bo
|
|
|
739
898
|
/// hold mixed types, and `sort_by` panics on a comparison that is not a total order: comparing a
|
|
740
899
|
/// number with a string as `""` made `5`, `""` and `3` each equal to a neighbour but not to each other.
|
|
741
900
|
fn compare_values(left: &Value, right: &Value) -> Ordering {
|
|
742
|
-
fn rank(value: &Value) -> u8 {
|
|
901
|
+
const fn rank(value: &Value) -> u8 {
|
|
743
902
|
match value {
|
|
744
903
|
Value::Number(_) => 0,
|
|
745
904
|
Value::String(_) => 1,
|
|
@@ -747,7 +906,10 @@ fn compare_values(left: &Value, right: &Value) -> Ordering {
|
|
|
747
906
|
}
|
|
748
907
|
}
|
|
749
908
|
match (left, right) {
|
|
750
|
-
(Value::Number(a), Value::Number(b)) => a
|
|
909
|
+
(Value::Number(a), Value::Number(b)) => a
|
|
910
|
+
.as_f64()
|
|
911
|
+
.unwrap_or(0.0)
|
|
912
|
+
.total_cmp(&b.as_f64().unwrap_or(0.0)),
|
|
751
913
|
(Value::String(a), Value::String(b)) => a.cmp(b),
|
|
752
914
|
_ => rank(left).cmp(&rank(right)),
|
|
753
915
|
}
|
|
@@ -757,7 +919,9 @@ fn compare_values(left: &Value, right: &Value) -> Ordering {
|
|
|
757
919
|
/// `session`, or `{}` with no session, which index.html treats as
|
|
758
920
|
/// "don't render the signed-in banner".
|
|
759
921
|
fn me(session: Option<&Session>) -> Value {
|
|
760
|
-
let Some(session) = session else {
|
|
922
|
+
let Some(session) = session else {
|
|
923
|
+
return json!({});
|
|
924
|
+
};
|
|
761
925
|
json!({
|
|
762
926
|
"email": session.email,
|
|
763
927
|
"name": session.name,
|
|
@@ -770,24 +934,20 @@ fn me(session: Option<&Session>) -> Value {
|
|
|
770
934
|
/// against this domain's live shape, answered with the reloaded config.
|
|
771
935
|
/// 200 on save; 400 `MalformedBody`; 422 `Malformed` or a domain refusal;
|
|
772
936
|
/// 501 when this host's kernel carries no ConsoleSettings chapter.
|
|
773
|
-
|
|
774
|
-
async fn presentation_save(
|
|
775
|
-
domain_ir: &Value,
|
|
776
|
-
raw_body: &str,
|
|
777
|
-
client: &Mutex<Client>,
|
|
778
|
-
wasm_path: &Path,
|
|
779
|
-
config: &LineageConfig,
|
|
780
|
-
invoker: &dyn LambdaInvoker,
|
|
781
|
-
) -> Value {
|
|
937
|
+
async fn presentation_save(domain_ir: &Value, raw_body: &str, host: Host<'_>) -> Value {
|
|
782
938
|
let submitted = match parsed_body(raw_body) {
|
|
783
939
|
Ok(submitted) => submitted,
|
|
784
940
|
Err(refusal) => return refusal,
|
|
785
941
|
};
|
|
786
942
|
|
|
787
|
-
match presentation_write::save(domain_ir, &submitted,
|
|
943
|
+
match presentation_write::save(domain_ir, &submitted, host).await {
|
|
788
944
|
Ok(saved) => ok(&saved),
|
|
789
|
-
Err(presentation_write::SaveRefusal::NoKernel) =>
|
|
790
|
-
|
|
945
|
+
Err(presentation_write::SaveRefusal::NoKernel) => {
|
|
946
|
+
not_implemented(PRESENTATION_WRITE_REFUSAL)
|
|
947
|
+
}
|
|
948
|
+
Err(presentation_write::SaveRefusal::Malformed(message)) => {
|
|
949
|
+
refusal(422, "Malformed", &message)
|
|
950
|
+
}
|
|
791
951
|
Err(presentation_write::SaveRefusal::Domain(result)) => domain_refusal(&result),
|
|
792
952
|
Err(presentation_write::SaveRefusal::Internal(message)) => internal_error(&message),
|
|
793
953
|
}
|
|
@@ -821,7 +981,11 @@ fn not_implemented(message: &str) -> Value {
|
|
|
821
981
|
}
|
|
822
982
|
|
|
823
983
|
pub(crate) fn refusal(status: u16, error: &str, message: &str) -> Value {
|
|
824
|
-
respond(
|
|
984
|
+
respond(
|
|
985
|
+
status,
|
|
986
|
+
"application/json",
|
|
987
|
+
&json!({"error": error, "message": message}).to_string(),
|
|
988
|
+
)
|
|
825
989
|
}
|
|
826
990
|
|
|
827
991
|
#[cfg(test)]
|
|
@@ -885,7 +1049,11 @@ mod tests {
|
|
|
885
1049
|
// pinned against app.rb's own `json({ error:, message: })`.
|
|
886
1050
|
#[test]
|
|
887
1051
|
fn a_refusal_carries_exactly_error_and_message() {
|
|
888
|
-
let response = refusal(
|
|
1052
|
+
let response = refusal(
|
|
1053
|
+
422,
|
|
1054
|
+
"PreconditionFailed",
|
|
1055
|
+
"the proposal hasn't been accepted yet",
|
|
1056
|
+
);
|
|
889
1057
|
assert_eq!(response["statusCode"], 422);
|
|
890
1058
|
assert_eq!(
|
|
891
1059
|
response["body"],
|
|
@@ -933,7 +1101,10 @@ mod tests {
|
|
|
933
1101
|
}
|
|
934
1102
|
|
|
935
1103
|
fn params(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
|
936
|
-
pairs
|
|
1104
|
+
pairs
|
|
1105
|
+
.iter()
|
|
1106
|
+
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
1107
|
+
.collect()
|
|
937
1108
|
}
|
|
938
1109
|
|
|
939
1110
|
#[test]
|
|
@@ -950,10 +1121,14 @@ mod tests {
|
|
|
950
1121
|
|
|
951
1122
|
#[test]
|
|
952
1123
|
fn an_unknown_collection_is_the_ruby_engines_own_not_found_message() {
|
|
953
|
-
let refusal = resolve_collection(&domain(), &json!({}), "nope")
|
|
1124
|
+
let refusal = resolve_collection(&domain(), &json!({}), "nope")
|
|
1125
|
+
.expect_err("unknown collections refuse");
|
|
954
1126
|
|
|
955
1127
|
assert_eq!(refusal["statusCode"], 404);
|
|
956
|
-
assert_eq!(
|
|
1128
|
+
assert_eq!(
|
|
1129
|
+
body(&refusal),
|
|
1130
|
+
json!({"error": "NotFound", "message": "no such collection: nope"})
|
|
1131
|
+
);
|
|
957
1132
|
}
|
|
958
1133
|
|
|
959
1134
|
#[test]
|
|
@@ -973,7 +1148,11 @@ mod tests {
|
|
|
973
1148
|
let by_name = find_query(&aggregate, "ByName").expect("declared");
|
|
974
1149
|
|
|
975
1150
|
assert_eq!(
|
|
976
|
-
query_args_from_params(
|
|
1151
|
+
query_args_from_params(
|
|
1152
|
+
&aggregate,
|
|
1153
|
+
by_owner,
|
|
1154
|
+
¶ms(&[("owner", "chris@example.com")])
|
|
1155
|
+
),
|
|
977
1156
|
Some(json!({"owner": "chris@example.com"}))
|
|
978
1157
|
);
|
|
979
1158
|
assert_eq!(
|
|
@@ -987,10 +1166,16 @@ mod tests {
|
|
|
987
1166
|
let aggregate = client_aggregate();
|
|
988
1167
|
let by_owner = find_query(&aggregate, "ByOwner").expect("declared");
|
|
989
1168
|
|
|
990
|
-
assert_eq!(
|
|
1169
|
+
assert_eq!(
|
|
1170
|
+
query_args_from_params(&aggregate, by_owner, ¶ms(&[])),
|
|
1171
|
+
None
|
|
1172
|
+
);
|
|
991
1173
|
// An empty param is the same as an absent one, matching Ruby's
|
|
992
1174
|
// own `raw.nil? || raw.empty?`.
|
|
993
|
-
assert_eq!(
|
|
1175
|
+
assert_eq!(
|
|
1176
|
+
query_args_from_params(&aggregate, by_owner, ¶ms(&[("owner", "")])),
|
|
1177
|
+
None
|
|
1178
|
+
);
|
|
994
1179
|
}
|
|
995
1180
|
|
|
996
1181
|
#[test]
|
|
@@ -998,8 +1183,14 @@ mod tests {
|
|
|
998
1183
|
let aggregate = client_aggregate();
|
|
999
1184
|
let since = find_query(&aggregate, "Since").expect("declared");
|
|
1000
1185
|
|
|
1001
|
-
assert_eq!(
|
|
1002
|
-
|
|
1186
|
+
assert_eq!(
|
|
1187
|
+
query_args_from_params(&aggregate, since, ¶ms(&[])),
|
|
1188
|
+
Some(json!({}))
|
|
1189
|
+
);
|
|
1190
|
+
assert_eq!(
|
|
1191
|
+
query_args_from_params(&aggregate, since, ¶ms(&[("year", "2026")])),
|
|
1192
|
+
Some(json!({"year": "2026"}))
|
|
1193
|
+
);
|
|
1003
1194
|
}
|
|
1004
1195
|
|
|
1005
1196
|
#[test]
|
|
@@ -1007,7 +1198,10 @@ mod tests {
|
|
|
1007
1198
|
let aggregate = client_aggregate();
|
|
1008
1199
|
let active = find_query(&aggregate, "Active").expect("declared");
|
|
1009
1200
|
|
|
1010
|
-
assert_eq!(
|
|
1201
|
+
assert_eq!(
|
|
1202
|
+
query_args_from_params(&aggregate, active, ¶ms(&[])),
|
|
1203
|
+
Some(json!({}))
|
|
1204
|
+
);
|
|
1011
1205
|
}
|
|
1012
1206
|
|
|
1013
1207
|
#[test]
|
|
@@ -1021,27 +1215,61 @@ mod tests {
|
|
|
1021
1215
|
fn a_sortable_column_of_a_pushable_shape_sorts_and_anything_else_falls_back() {
|
|
1022
1216
|
let domain = domain();
|
|
1023
1217
|
let aggregate = client_aggregate();
|
|
1024
|
-
let spec = resolve_sort(
|
|
1218
|
+
let spec = resolve_sort(
|
|
1219
|
+
&domain,
|
|
1220
|
+
&json!({}),
|
|
1221
|
+
"clients",
|
|
1222
|
+
&aggregate,
|
|
1223
|
+
¶ms(&[("sort", "name")]),
|
|
1224
|
+
);
|
|
1025
1225
|
let spec = spec.expect("a text_value column is sortable");
|
|
1026
1226
|
assert_eq!(spec.field, "name");
|
|
1027
1227
|
assert!(!spec.descending);
|
|
1028
1228
|
|
|
1029
1229
|
// A reference column is deliberately excluded — it would need a
|
|
1030
1230
|
// cross-aggregate join no order clause wires.
|
|
1031
|
-
assert!(resolve_sort(
|
|
1231
|
+
assert!(resolve_sort(
|
|
1232
|
+
&domain,
|
|
1233
|
+
&json!({}),
|
|
1234
|
+
"clients",
|
|
1235
|
+
&aggregate,
|
|
1236
|
+
¶ms(&[("sort", "owner")])
|
|
1237
|
+
)
|
|
1238
|
+
.is_none());
|
|
1032
1239
|
// …as is a column config explicitly opts out of.
|
|
1033
|
-
let opted_out =
|
|
1034
|
-
|
|
1240
|
+
let opted_out =
|
|
1241
|
+
json!({"collections": {"Client": {"columns": [{"field": "name", "sortable": false}]}}});
|
|
1242
|
+
assert!(resolve_sort(
|
|
1243
|
+
&domain,
|
|
1244
|
+
&opted_out,
|
|
1245
|
+
"clients",
|
|
1246
|
+
&aggregate,
|
|
1247
|
+
¶ms(&[("sort", "name")])
|
|
1248
|
+
)
|
|
1249
|
+
.is_none());
|
|
1035
1250
|
// …and a column key that isn't in this collection at all.
|
|
1036
|
-
assert!(resolve_sort(
|
|
1251
|
+
assert!(resolve_sort(
|
|
1252
|
+
&domain,
|
|
1253
|
+
&json!({}),
|
|
1254
|
+
"clients",
|
|
1255
|
+
&aggregate,
|
|
1256
|
+
¶ms(&[("sort", "nope")])
|
|
1257
|
+
)
|
|
1258
|
+
.is_none());
|
|
1037
1259
|
}
|
|
1038
1260
|
|
|
1039
1261
|
#[test]
|
|
1040
1262
|
fn sorting_by_the_state_column_sorts_by_whatever_the_lifecycle_field_is_really_called() {
|
|
1041
1263
|
let domain = domain();
|
|
1042
1264
|
let aggregate = client_aggregate();
|
|
1043
|
-
let spec = resolve_sort(
|
|
1044
|
-
|
|
1265
|
+
let spec = resolve_sort(
|
|
1266
|
+
&domain,
|
|
1267
|
+
&json!({}),
|
|
1268
|
+
"clients",
|
|
1269
|
+
&aggregate,
|
|
1270
|
+
¶ms(&[("sort", "__state__"), ("direction", "desc")]),
|
|
1271
|
+
)
|
|
1272
|
+
.expect("the derived state pill is sortable");
|
|
1045
1273
|
|
|
1046
1274
|
assert_eq!(spec.field, "status");
|
|
1047
1275
|
assert!(spec.descending);
|
|
@@ -1053,25 +1281,55 @@ mod tests {
|
|
|
1053
1281
|
("b".to_string(), json!({"name": {"value": "Zed"}})),
|
|
1054
1282
|
("a".to_string(), json!({"name": {"value": "Ann"}})),
|
|
1055
1283
|
];
|
|
1056
|
-
sort_records(&mut records,
|
|
1284
|
+
sort_records(&mut records, None);
|
|
1057
1285
|
|
|
1058
|
-
assert_eq!(
|
|
1286
|
+
assert_eq!(
|
|
1287
|
+
records
|
|
1288
|
+
.iter()
|
|
1289
|
+
.map(|(id, _)| id.as_str())
|
|
1290
|
+
.collect::<Vec<&str>>(),
|
|
1291
|
+
vec!["a", "b"]
|
|
1292
|
+
);
|
|
1059
1293
|
}
|
|
1060
1294
|
|
|
1061
1295
|
#[test]
|
|
1062
1296
|
fn sorting_drills_through_a_single_attribute_value_object_and_through_money_to_its_cents() {
|
|
1063
1297
|
let mut records = vec![
|
|
1064
|
-
(
|
|
1065
|
-
|
|
1298
|
+
(
|
|
1299
|
+
"c".to_string(),
|
|
1300
|
+
json!({"name": {"value": "Cara"}, "fee": {"cents": 300, "currency": "USD"}}),
|
|
1301
|
+
),
|
|
1302
|
+
(
|
|
1303
|
+
"a".to_string(),
|
|
1304
|
+
json!({"name": {"value": "Ann"}, "fee": {"cents": 1000, "currency": "USD"}}),
|
|
1305
|
+
),
|
|
1066
1306
|
];
|
|
1067
1307
|
|
|
1068
|
-
sort_records(
|
|
1308
|
+
sort_records(
|
|
1309
|
+
&mut records,
|
|
1310
|
+
Some(&SortSpec {
|
|
1311
|
+
field: "name".to_string(),
|
|
1312
|
+
descending: false,
|
|
1313
|
+
}),
|
|
1314
|
+
);
|
|
1069
1315
|
assert_eq!(records[0].0, "a");
|
|
1070
1316
|
|
|
1071
1317
|
// Money sorts numerically by cents — 300 before 1000, which a
|
|
1072
1318
|
// string comparison would get backwards.
|
|
1073
|
-
sort_records(
|
|
1074
|
-
|
|
1319
|
+
sort_records(
|
|
1320
|
+
&mut records,
|
|
1321
|
+
Some(&SortSpec {
|
|
1322
|
+
field: "fee".to_string(),
|
|
1323
|
+
descending: false,
|
|
1324
|
+
}),
|
|
1325
|
+
);
|
|
1326
|
+
assert_eq!(
|
|
1327
|
+
records
|
|
1328
|
+
.iter()
|
|
1329
|
+
.map(|(id, _)| id.as_str())
|
|
1330
|
+
.collect::<Vec<&str>>(),
|
|
1331
|
+
vec!["c", "a"]
|
|
1332
|
+
);
|
|
1075
1333
|
}
|
|
1076
1334
|
|
|
1077
1335
|
#[test]
|
|
@@ -1081,11 +1339,35 @@ mod tests {
|
|
|
1081
1339
|
("ann".to_string(), json!({"name": {"value": "Ann"}})),
|
|
1082
1340
|
];
|
|
1083
1341
|
|
|
1084
|
-
sort_records(
|
|
1085
|
-
|
|
1342
|
+
sort_records(
|
|
1343
|
+
&mut records,
|
|
1344
|
+
Some(&SortSpec {
|
|
1345
|
+
field: "name".to_string(),
|
|
1346
|
+
descending: false,
|
|
1347
|
+
}),
|
|
1348
|
+
);
|
|
1349
|
+
assert_eq!(
|
|
1350
|
+
records
|
|
1351
|
+
.iter()
|
|
1352
|
+
.map(|(id, _)| id.as_str())
|
|
1353
|
+
.collect::<Vec<&str>>(),
|
|
1354
|
+
vec!["ann", "missing"]
|
|
1355
|
+
);
|
|
1086
1356
|
|
|
1087
|
-
sort_records(
|
|
1088
|
-
|
|
1357
|
+
sort_records(
|
|
1358
|
+
&mut records,
|
|
1359
|
+
Some(&SortSpec {
|
|
1360
|
+
field: "name".to_string(),
|
|
1361
|
+
descending: true,
|
|
1362
|
+
}),
|
|
1363
|
+
);
|
|
1364
|
+
assert_eq!(
|
|
1365
|
+
records
|
|
1366
|
+
.iter()
|
|
1367
|
+
.map(|(id, _)| id.as_str())
|
|
1368
|
+
.collect::<Vec<&str>>(),
|
|
1369
|
+
vec!["missing", "ann"]
|
|
1370
|
+
);
|
|
1089
1371
|
}
|
|
1090
1372
|
|
|
1091
1373
|
#[test]
|
|
@@ -1093,7 +1375,10 @@ mod tests {
|
|
|
1093
1375
|
// `JSON_ADAPTER.find!`'s own message, `id` inspected the way Ruby
|
|
1094
1376
|
// inspects it.
|
|
1095
1377
|
let refusal = not_found(&format!("no {} found for id {:?}", "Client", "acme"));
|
|
1096
|
-
assert_eq!(
|
|
1378
|
+
assert_eq!(
|
|
1379
|
+
body(&refusal),
|
|
1380
|
+
json!({"error": "NotFound", "message": "no Client found for id \"acme\""})
|
|
1381
|
+
);
|
|
1097
1382
|
}
|
|
1098
1383
|
|
|
1099
1384
|
// ---- write routes ------------------------------------------------
|
|
@@ -1102,11 +1387,17 @@ mod tests {
|
|
|
1102
1387
|
fn an_empty_body_is_an_empty_argument_hash_and_anything_unparseable_refuses() {
|
|
1103
1388
|
assert_eq!(parsed_body(""), Ok(json!({})));
|
|
1104
1389
|
assert_eq!(parsed_body(" "), Ok(json!({})));
|
|
1105
|
-
assert_eq!(
|
|
1390
|
+
assert_eq!(
|
|
1391
|
+
parsed_body(r#"{"name":{"value":"Acme"}}"#),
|
|
1392
|
+
Ok(json!({"name": {"value": "Acme"}}))
|
|
1393
|
+
);
|
|
1106
1394
|
|
|
1107
1395
|
let refusal = parsed_body("not json").expect_err("a malformed body refuses");
|
|
1108
1396
|
assert_eq!(refusal["statusCode"], 400);
|
|
1109
|
-
assert_eq!(
|
|
1397
|
+
assert_eq!(
|
|
1398
|
+
body(&refusal),
|
|
1399
|
+
json!({"error": "MalformedBody", "message": "request body is not valid JSON"})
|
|
1400
|
+
);
|
|
1110
1401
|
// A body that parses but isn't an object has no arguments to
|
|
1111
1402
|
// splat — refused the same way rather than dispatched.
|
|
1112
1403
|
assert!(parsed_body("[1,2,3]").is_err());
|
|
@@ -1122,7 +1413,10 @@ mod tests {
|
|
|
1122
1413
|
]
|
|
1123
1414
|
});
|
|
1124
1415
|
|
|
1125
|
-
assert_eq!(
|
|
1416
|
+
assert_eq!(
|
|
1417
|
+
command_name(dispatchable_command(&aggregate, "advance_cycle").expect("declared")),
|
|
1418
|
+
"AdvanceCycle"
|
|
1419
|
+
);
|
|
1126
1420
|
// The creating command is reachable through POST /api/:coll,
|
|
1127
1421
|
// never here — accepting it would pass this gate and then fail
|
|
1128
1422
|
// as something far less legible.
|
|
@@ -1160,7 +1454,7 @@ mod tests {
|
|
|
1160
1454
|
|
|
1161
1455
|
// ---- identity minting --------------------------------------------
|
|
1162
1456
|
|
|
1163
|
-
fn client_with_identity(identity: Value) -> (Value, Value) {
|
|
1457
|
+
fn client_with_identity(identity: &Value) -> (Value, Value) {
|
|
1164
1458
|
let aggregate = json!({
|
|
1165
1459
|
"name": "Client",
|
|
1166
1460
|
"identified_by": ["reference.value"],
|
|
@@ -1181,7 +1475,9 @@ mod tests {
|
|
|
1181
1475
|
|
|
1182
1476
|
#[test]
|
|
1183
1477
|
fn a_slug_identity_is_minted_from_another_submitted_field() {
|
|
1184
|
-
let (aggregate, presentation) = client_with_identity(
|
|
1478
|
+
let (aggregate, presentation) = client_with_identity(
|
|
1479
|
+
&json!({"field": "reference", "strategy": "slug", "source": "name"}),
|
|
1480
|
+
);
|
|
1185
1481
|
let mut args = json!({"name": {"value": "Acme Corp."}});
|
|
1186
1482
|
|
|
1187
1483
|
apply_identity(&presentation, &aggregate, &mut args, 0).expect("slugging never refuses");
|
|
@@ -1191,8 +1487,9 @@ mod tests {
|
|
|
1191
1487
|
|
|
1192
1488
|
#[test]
|
|
1193
1489
|
fn a_sequence_identity_counts_the_records_already_there_and_pads_to_the_configured_width() {
|
|
1194
|
-
let (aggregate, presentation) =
|
|
1195
|
-
|
|
1490
|
+
let (aggregate, presentation) = client_with_identity(
|
|
1491
|
+
&json!({"field": "reference", "strategy": "sequence", "prefix": "C-", "pad": 4}),
|
|
1492
|
+
);
|
|
1196
1493
|
let mut args = json!({"name": {"value": "Acme"}});
|
|
1197
1494
|
|
|
1198
1495
|
apply_identity(&presentation, &aggregate, &mut args, 41).expect("sequencing never refuses");
|
|
@@ -1202,17 +1499,21 @@ mod tests {
|
|
|
1202
1499
|
|
|
1203
1500
|
#[test]
|
|
1204
1501
|
fn an_identity_the_caller_supplied_is_left_exactly_as_it_came() {
|
|
1205
|
-
let (aggregate, presentation) = client_with_identity(
|
|
1502
|
+
let (aggregate, presentation) = client_with_identity(
|
|
1503
|
+
&json!({"field": "reference", "strategy": "slug", "source": "name"}),
|
|
1504
|
+
);
|
|
1206
1505
|
let mut args = json!({"reference": {"value": "chosen-by-hand"}, "name": {"value": "Acme"}});
|
|
1207
1506
|
|
|
1208
|
-
apply_identity(&presentation, &aggregate, &mut args, 0)
|
|
1507
|
+
apply_identity(&presentation, &aggregate, &mut args, 0)
|
|
1508
|
+
.expect("supplied identities never refuse");
|
|
1209
1509
|
|
|
1210
1510
|
assert_eq!(args["reference"], json!({"value": "chosen-by-hand"}));
|
|
1211
1511
|
}
|
|
1212
1512
|
|
|
1213
1513
|
#[test]
|
|
1214
1514
|
fn an_identity_rule_naming_a_reference_field_mints_nothing_rather_than_guessing_a_wire_shape() {
|
|
1215
|
-
let (aggregate, presentation) =
|
|
1515
|
+
let (aggregate, presentation) =
|
|
1516
|
+
client_with_identity(&json!({"field": "owner", "strategy": "slug", "source": "name"}));
|
|
1216
1517
|
let mut args = json!({"name": {"value": "Acme"}});
|
|
1217
1518
|
|
|
1218
1519
|
apply_identity(&presentation, &aggregate, &mut args, 0).expect("no refusal");
|
|
@@ -1224,14 +1525,19 @@ mod tests {
|
|
|
1224
1525
|
// surfacing a confusing "absent argument" from the kernel.
|
|
1225
1526
|
#[test]
|
|
1226
1527
|
fn a_port_identity_strategy_refuses_in_its_own_words_rather_than_dispatching_without_one() {
|
|
1227
|
-
let (aggregate, presentation) =
|
|
1528
|
+
let (aggregate, presentation) =
|
|
1529
|
+
client_with_identity(&json!({"field": "reference", "strategy": "port"}));
|
|
1228
1530
|
let mut args = json!({"name": {"value": "Acme"}});
|
|
1229
1531
|
|
|
1230
|
-
let refusal = apply_identity(&presentation, &aggregate, &mut args, 0)
|
|
1532
|
+
let refusal = apply_identity(&presentation, &aggregate, &mut args, 0)
|
|
1533
|
+
.expect_err("the port strategy refuses");
|
|
1231
1534
|
|
|
1232
1535
|
assert_eq!(refusal["statusCode"], 501);
|
|
1233
1536
|
assert!(
|
|
1234
|
-
body(&refusal)["message"]
|
|
1537
|
+
body(&refusal)["message"]
|
|
1538
|
+
.as_str()
|
|
1539
|
+
.expect("a message")
|
|
1540
|
+
.contains("identity_assignment"),
|
|
1235
1541
|
"{refusal}"
|
|
1236
1542
|
);
|
|
1237
1543
|
}
|
|
@@ -1280,7 +1586,14 @@ mod tests {
|
|
|
1280
1586
|
let instances = json!({"SampleApp::Proposal#P-001": {"status": "accepted"}});
|
|
1281
1587
|
let rule = json!({"field": "proposal_id", "state": "accepted"});
|
|
1282
1588
|
|
|
1283
|
-
assert!(check_precondition(
|
|
1589
|
+
assert!(check_precondition(
|
|
1590
|
+
&precondition_domain(),
|
|
1591
|
+
&contract(),
|
|
1592
|
+
&rule,
|
|
1593
|
+
&json!({"proposal_id": "P-001"}),
|
|
1594
|
+
&instances
|
|
1595
|
+
)
|
|
1596
|
+
.is_ok());
|
|
1284
1597
|
}
|
|
1285
1598
|
|
|
1286
1599
|
#[test]
|
|
@@ -1288,8 +1601,14 @@ mod tests {
|
|
|
1288
1601
|
let instances = json!({"SampleApp::Proposal#P-001": {"status": "sent"}});
|
|
1289
1602
|
let rule = json!({"field": "proposal_id", "state": "accepted"});
|
|
1290
1603
|
|
|
1291
|
-
let refusal = check_precondition(
|
|
1292
|
-
|
|
1604
|
+
let refusal = check_precondition(
|
|
1605
|
+
&precondition_domain(),
|
|
1606
|
+
&contract(),
|
|
1607
|
+
&rule,
|
|
1608
|
+
&json!({"proposal_id": "P-001"}),
|
|
1609
|
+
&instances,
|
|
1610
|
+
)
|
|
1611
|
+
.expect_err("a proposal that isn't accepted refuses");
|
|
1293
1612
|
|
|
1294
1613
|
assert_eq!(refusal["statusCode"], 422);
|
|
1295
1614
|
assert_eq!(
|
|
@@ -1303,20 +1622,38 @@ mod tests {
|
|
|
1303
1622
|
let rule = json!({"field": "proposal_id", "state": "accepted", "message": "the proposal hasn't been accepted yet"});
|
|
1304
1623
|
let instances = json!({"SampleApp::Proposal#P-001": {"status": "sent"}});
|
|
1305
1624
|
|
|
1306
|
-
let refusal = check_precondition(
|
|
1307
|
-
|
|
1625
|
+
let refusal = check_precondition(
|
|
1626
|
+
&precondition_domain(),
|
|
1627
|
+
&contract(),
|
|
1628
|
+
&rule,
|
|
1629
|
+
&json!({"proposal_id": "P-001"}),
|
|
1630
|
+
&instances,
|
|
1631
|
+
)
|
|
1632
|
+
.expect_err("refuses");
|
|
1308
1633
|
|
|
1309
|
-
assert_eq!(
|
|
1634
|
+
assert_eq!(
|
|
1635
|
+
body(&refusal)["message"],
|
|
1636
|
+
"the proposal hasn't been accepted yet"
|
|
1637
|
+
);
|
|
1310
1638
|
}
|
|
1311
1639
|
|
|
1312
1640
|
#[test]
|
|
1313
1641
|
fn a_precondition_refuses_a_reference_to_a_record_that_does_not_exist_at_all() {
|
|
1314
1642
|
let rule = json!({"field": "proposal_id", "state": "accepted"});
|
|
1315
1643
|
|
|
1316
|
-
let refusal = check_precondition(
|
|
1317
|
-
|
|
1644
|
+
let refusal = check_precondition(
|
|
1645
|
+
&precondition_domain(),
|
|
1646
|
+
&contract(),
|
|
1647
|
+
&rule,
|
|
1648
|
+
&json!({"proposal_id": "ghost"}),
|
|
1649
|
+
&json!({}),
|
|
1650
|
+
)
|
|
1651
|
+
.expect_err("refuses");
|
|
1318
1652
|
|
|
1319
|
-
assert_eq!(
|
|
1653
|
+
assert_eq!(
|
|
1654
|
+
body(&refusal),
|
|
1655
|
+
json!({"error": "PreconditionFailed", "message": "no proposal with that id"})
|
|
1656
|
+
);
|
|
1320
1657
|
}
|
|
1321
1658
|
|
|
1322
1659
|
#[test]
|
|
@@ -1325,14 +1662,28 @@ mod tests {
|
|
|
1325
1662
|
// to refuse, in the domain's own words — not this check's.
|
|
1326
1663
|
let rule = json!({"field": "proposal_id", "state": "accepted"});
|
|
1327
1664
|
|
|
1328
|
-
assert!(check_precondition(
|
|
1665
|
+
assert!(check_precondition(
|
|
1666
|
+
&precondition_domain(),
|
|
1667
|
+
&contract(),
|
|
1668
|
+
&rule,
|
|
1669
|
+
&json!({}),
|
|
1670
|
+
&json!({})
|
|
1671
|
+
)
|
|
1672
|
+
.is_ok());
|
|
1329
1673
|
}
|
|
1330
1674
|
|
|
1331
1675
|
#[test]
|
|
1332
1676
|
fn a_precondition_naming_a_field_that_is_not_a_reference_simply_does_not_fire() {
|
|
1333
1677
|
let rule = json!({"field": "number", "state": "accepted"});
|
|
1334
1678
|
|
|
1335
|
-
assert!(check_precondition(
|
|
1679
|
+
assert!(check_precondition(
|
|
1680
|
+
&precondition_domain(),
|
|
1681
|
+
&contract(),
|
|
1682
|
+
&rule,
|
|
1683
|
+
&json!({"number": "C-001"}),
|
|
1684
|
+
&json!({})
|
|
1685
|
+
)
|
|
1686
|
+
.is_ok());
|
|
1336
1687
|
}
|
|
1337
1688
|
|
|
1338
1689
|
// ---- both write routes, end to end --------------------------------
|
|
@@ -1343,9 +1694,13 @@ mod tests {
|
|
|
1343
1694
|
// is the derived one ("customers").
|
|
1344
1695
|
|
|
1345
1696
|
fn banking_ir() -> Value {
|
|
1346
|
-
let path =
|
|
1347
|
-
|
|
1348
|
-
|
|
1697
|
+
let path =
|
|
1698
|
+
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/banking.ir.json");
|
|
1699
|
+
serde_json::from_str(
|
|
1700
|
+
&std::fs::read_to_string(path)
|
|
1701
|
+
.expect("hecks build_wasm writes banking.ir.json beside the wasm"),
|
|
1702
|
+
)
|
|
1703
|
+
.expect("valid IR")
|
|
1349
1704
|
}
|
|
1350
1705
|
|
|
1351
1706
|
/// A fixture whose `.wasm` carries a ConsoleSettings chapter, the
|
|
@@ -1377,20 +1732,36 @@ mod tests {
|
|
|
1377
1732
|
&*client.lock().await,
|
|
1378
1733
|
"CheckoutFixture",
|
|
1379
1734
|
1,
|
|
1380
|
-
&[
|
|
1735
|
+
&[
|
|
1736
|
+
"Event",
|
|
1737
|
+
"Registration",
|
|
1738
|
+
"StateStyle",
|
|
1739
|
+
"Collection",
|
|
1740
|
+
"Overview",
|
|
1741
|
+
],
|
|
1381
1742
|
)
|
|
1382
1743
|
.await;
|
|
1383
1744
|
let (wasm, ir) = console_fixture();
|
|
1384
|
-
let lineage = LineageConfig {
|
|
1745
|
+
let lineage = LineageConfig {
|
|
1746
|
+
domain: "CheckoutFixture".to_string(),
|
|
1747
|
+
era: Some(1),
|
|
1748
|
+
mirrored: None,
|
|
1749
|
+
};
|
|
1385
1750
|
let invoker = crate::lambda_client::NeverInvoker;
|
|
1386
1751
|
|
|
1387
1752
|
// Nothing saved yet — every section empty, the same answer the
|
|
1388
1753
|
// Ruby engine gives a console that has never saved.
|
|
1389
|
-
let before = presentation::load(&client, &wasm, &lineage)
|
|
1390
|
-
|
|
1754
|
+
let before = presentation::load(&client, &wasm, &lineage)
|
|
1755
|
+
.await
|
|
1756
|
+
.expect("a config");
|
|
1757
|
+
assert_eq!(
|
|
1758
|
+
before,
|
|
1759
|
+
json!({"states": {}, "collections": {}, "overview": {}})
|
|
1760
|
+
);
|
|
1391
1761
|
|
|
1392
1762
|
let mut submitted = every_state_styled();
|
|
1393
|
-
submitted["states"]["Event"]["open"] =
|
|
1763
|
+
submitted["states"]["Event"]["open"] =
|
|
1764
|
+
json!({"tone": "good", "attention": true, "label": "Open for signups"});
|
|
1394
1765
|
submitted["collections"] = json!({
|
|
1395
1766
|
"Event": {
|
|
1396
1767
|
"label": "Sessions",
|
|
@@ -1405,7 +1776,12 @@ mod tests {
|
|
|
1405
1776
|
submitted["overview"] = json!({"stats": [{"label": "Open", "collection": "events",
|
|
1406
1777
|
"where": {"state": "open"}}]});
|
|
1407
1778
|
|
|
1408
|
-
let saved = presentation_save(
|
|
1779
|
+
let saved = presentation_save(
|
|
1780
|
+
&ir,
|
|
1781
|
+
&submitted.to_string(),
|
|
1782
|
+
Host::new(&client, &wasm, &lineage, &invoker),
|
|
1783
|
+
)
|
|
1784
|
+
.await;
|
|
1409
1785
|
assert_eq!(saved["statusCode"], 200, "{saved}");
|
|
1410
1786
|
let stored = body(&saved);
|
|
1411
1787
|
|
|
@@ -1415,46 +1791,90 @@ mod tests {
|
|
|
1415
1791
|
assert_eq!(stored["states"]["Event"]["open"]["attention"], json!(true));
|
|
1416
1792
|
// A field this chapter does not model individually still
|
|
1417
1793
|
// round-trips, through `extra_json`.
|
|
1418
|
-
assert_eq!(
|
|
1794
|
+
assert_eq!(
|
|
1795
|
+
stored["states"]["Event"]["open"]["label"],
|
|
1796
|
+
"Open for signups"
|
|
1797
|
+
);
|
|
1419
1798
|
assert_eq!(stored["states"]["Event"]["closed"]["tone"], "muted");
|
|
1420
|
-
assert_eq!(
|
|
1799
|
+
assert_eq!(
|
|
1800
|
+
stored["states"]["Registration"]["archived"]["tone"],
|
|
1801
|
+
"muted"
|
|
1802
|
+
);
|
|
1421
1803
|
assert_eq!(stored["collections"]["Event"]["label"], "Sessions");
|
|
1422
1804
|
assert_eq!(stored["collections"]["Event"]["nav_order"], json!(1));
|
|
1423
1805
|
assert_eq!(stored["collections"]["Event"]["noun_sing"], "session");
|
|
1424
|
-
assert_eq!(
|
|
1425
|
-
|
|
1806
|
+
assert_eq!(
|
|
1807
|
+
stored["collections"]["Event"]["columns"][1]["sortable"],
|
|
1808
|
+
json!(true)
|
|
1809
|
+
);
|
|
1810
|
+
assert_eq!(
|
|
1811
|
+
stored["collections"]["Event"]["columns"][1]["head"],
|
|
1812
|
+
"Title"
|
|
1813
|
+
);
|
|
1426
1814
|
assert_eq!(stored["collections"]["Event"]["identity"]["pad"], json!(3));
|
|
1427
|
-
assert_eq!(
|
|
1428
|
-
|
|
1815
|
+
assert_eq!(
|
|
1816
|
+
stored["collections"]["Event"]["field_formats"],
|
|
1817
|
+
json!({"capacity": "percent"})
|
|
1818
|
+
);
|
|
1819
|
+
assert_eq!(
|
|
1820
|
+
stored["overview"]["stats"][0]["where"],
|
|
1821
|
+
json!({"state": "open"})
|
|
1822
|
+
);
|
|
1429
1823
|
|
|
1430
1824
|
// A fresh read agrees, proving the rows are in the kernel's store.
|
|
1431
|
-
assert_eq!(
|
|
1825
|
+
assert_eq!(
|
|
1826
|
+
presentation::load(&client, &wasm, &lineage)
|
|
1827
|
+
.await
|
|
1828
|
+
.expect("a config"),
|
|
1829
|
+
stored
|
|
1830
|
+
);
|
|
1432
1831
|
|
|
1433
1832
|
// Saving again is safe: an existing row is a `Set*`, never a
|
|
1434
1833
|
// second `Declare` (which would refuse `AlreadyExists`).
|
|
1435
|
-
let again = presentation_save(
|
|
1834
|
+
let again = presentation_save(
|
|
1835
|
+
&ir,
|
|
1836
|
+
&submitted.to_string(),
|
|
1837
|
+
Host::new(&client, &wasm, &lineage, &invoker),
|
|
1838
|
+
)
|
|
1839
|
+
.await;
|
|
1436
1840
|
assert_eq!(again["statusCode"], 200, "{again}");
|
|
1437
1841
|
assert_eq!(body(&again), stored);
|
|
1438
1842
|
}
|
|
1439
1843
|
|
|
1440
1844
|
#[tokio::test]
|
|
1441
1845
|
async fn a_config_that_breaks_a_rule_refuses_422_malformed_and_writes_nothing() {
|
|
1442
|
-
let client =
|
|
1846
|
+
let client =
|
|
1847
|
+
crate::dispatch::tests::scratch_db("rust_host_api_presentation_malformed").await;
|
|
1443
1848
|
crate::dispatch::tests::provision_lineage(
|
|
1444
1849
|
&*client.lock().await,
|
|
1445
1850
|
"CheckoutFixture",
|
|
1446
1851
|
1,
|
|
1447
|
-
&[
|
|
1852
|
+
&[
|
|
1853
|
+
"Event",
|
|
1854
|
+
"Registration",
|
|
1855
|
+
"StateStyle",
|
|
1856
|
+
"Collection",
|
|
1857
|
+
"Overview",
|
|
1858
|
+
],
|
|
1448
1859
|
)
|
|
1449
1860
|
.await;
|
|
1450
1861
|
let (wasm, ir) = console_fixture();
|
|
1451
|
-
let lineage = LineageConfig {
|
|
1862
|
+
let lineage = LineageConfig {
|
|
1863
|
+
domain: "CheckoutFixture".to_string(),
|
|
1864
|
+
era: Some(1),
|
|
1865
|
+
mirrored: None,
|
|
1866
|
+
};
|
|
1452
1867
|
let invoker = crate::lambda_client::NeverInvoker;
|
|
1453
1868
|
|
|
1454
1869
|
let mut bad = every_state_styled();
|
|
1455
1870
|
bad["states"]["Event"]["open"] = json!({"tone": "chartreuse"});
|
|
1456
1871
|
|
|
1457
|
-
let refused = presentation_save(
|
|
1872
|
+
let refused = presentation_save(
|
|
1873
|
+
&ir,
|
|
1874
|
+
&bad.to_string(),
|
|
1875
|
+
Host::new(&client, &wasm, &lineage, &invoker),
|
|
1876
|
+
)
|
|
1877
|
+
.await;
|
|
1458
1878
|
|
|
1459
1879
|
assert_eq!(refused["statusCode"], 422, "{refused}");
|
|
1460
1880
|
assert_eq!(body(&refused)["error"], "Malformed");
|
|
@@ -1465,25 +1885,40 @@ mod tests {
|
|
|
1465
1885
|
|
|
1466
1886
|
// Nothing written: `validate` runs before the first dispatch.
|
|
1467
1887
|
assert_eq!(
|
|
1468
|
-
presentation::load(&client, &wasm, &lineage)
|
|
1888
|
+
presentation::load(&client, &wasm, &lineage)
|
|
1889
|
+
.await
|
|
1890
|
+
.expect("a config"),
|
|
1469
1891
|
json!({"states": {}, "collections": {}, "overview": {}})
|
|
1470
1892
|
);
|
|
1471
1893
|
}
|
|
1472
1894
|
|
|
1473
1895
|
#[tokio::test]
|
|
1474
1896
|
async fn a_body_that_is_not_json_at_all_is_a_400_before_anything_is_validated() {
|
|
1475
|
-
let client =
|
|
1476
|
-
|
|
1897
|
+
let client =
|
|
1898
|
+
crate::dispatch::tests::scratch_db("rust_host_api_presentation_bad_body").await;
|
|
1899
|
+
crate::dispatch::tests::provision_lineage(
|
|
1900
|
+
&*client.lock().await,
|
|
1901
|
+
"CheckoutFixture",
|
|
1902
|
+
1,
|
|
1903
|
+
&["Event"],
|
|
1904
|
+
)
|
|
1905
|
+
.await;
|
|
1477
1906
|
let (wasm, ir) = console_fixture();
|
|
1478
|
-
let lineage = LineageConfig {
|
|
1907
|
+
let lineage = LineageConfig {
|
|
1908
|
+
domain: "CheckoutFixture".to_string(),
|
|
1909
|
+
era: Some(1),
|
|
1910
|
+
mirrored: None,
|
|
1911
|
+
};
|
|
1479
1912
|
|
|
1480
1913
|
let refused = presentation_save(
|
|
1481
1914
|
&ir,
|
|
1482
1915
|
"not json",
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1916
|
+
Host::new(
|
|
1917
|
+
&client,
|
|
1918
|
+
&wasm,
|
|
1919
|
+
&lineage,
|
|
1920
|
+
&crate::lambda_client::NeverInvoker,
|
|
1921
|
+
),
|
|
1487
1922
|
)
|
|
1488
1923
|
.await;
|
|
1489
1924
|
|
|
@@ -1495,26 +1930,42 @@ mod tests {
|
|
|
1495
1930
|
/// asks the kernel rather than inferring it from anything else.
|
|
1496
1931
|
#[tokio::test]
|
|
1497
1932
|
async fn a_host_whose_kernel_has_no_console_settings_chapter_still_refuses_501() {
|
|
1498
|
-
let client =
|
|
1499
|
-
|
|
1933
|
+
let client =
|
|
1934
|
+
crate::dispatch::tests::scratch_db("rust_host_api_presentation_no_chapter").await;
|
|
1935
|
+
crate::dispatch::tests::provision_lineage(
|
|
1936
|
+
&*client.lock().await,
|
|
1937
|
+
"Banking",
|
|
1938
|
+
1,
|
|
1939
|
+
&["Customer"],
|
|
1940
|
+
)
|
|
1941
|
+
.await;
|
|
1500
1942
|
let wasm = crate::dispatch::tests::wasm_path();
|
|
1501
1943
|
let ir = banking_ir();
|
|
1502
|
-
let lineage = LineageConfig {
|
|
1944
|
+
let lineage = LineageConfig {
|
|
1945
|
+
domain: "Banking".to_string(),
|
|
1946
|
+
era: Some(1),
|
|
1947
|
+
mirrored: None,
|
|
1948
|
+
};
|
|
1503
1949
|
|
|
1504
1950
|
let refused = presentation_save(
|
|
1505
1951
|
&ir,
|
|
1506
1952
|
r#"{"states":{}}"#,
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1953
|
+
Host::new(
|
|
1954
|
+
&client,
|
|
1955
|
+
&wasm,
|
|
1956
|
+
&lineage,
|
|
1957
|
+
&crate::lambda_client::NeverInvoker,
|
|
1958
|
+
),
|
|
1511
1959
|
)
|
|
1512
1960
|
.await;
|
|
1513
1961
|
|
|
1514
1962
|
assert_eq!(refused["statusCode"], 501, "{refused}");
|
|
1515
1963
|
assert_eq!(body(&refused)["error"], "NotImplemented");
|
|
1516
1964
|
assert!(
|
|
1517
|
-
body(&refused)["message"]
|
|
1965
|
+
body(&refused)["message"]
|
|
1966
|
+
.as_str()
|
|
1967
|
+
.expect("a message")
|
|
1968
|
+
.contains("ConsoleSettings"),
|
|
1518
1969
|
"the refusal has to name what is missing: {refused}"
|
|
1519
1970
|
);
|
|
1520
1971
|
}
|
|
@@ -1522,21 +1973,23 @@ mod tests {
|
|
|
1522
1973
|
#[tokio::test]
|
|
1523
1974
|
async fn a_record_is_created_then_commanded_then_read_back_through_the_api_routes() {
|
|
1524
1975
|
let client = crate::dispatch::tests::scratch_db("rust_host_api_write_routes").await;
|
|
1525
|
-
crate::dispatch::tests::provision_lineage(
|
|
1976
|
+
crate::dispatch::tests::provision_lineage(
|
|
1977
|
+
&*client.lock().await,
|
|
1978
|
+
"Banking",
|
|
1979
|
+
1,
|
|
1980
|
+
&["Customer"],
|
|
1981
|
+
)
|
|
1982
|
+
.await;
|
|
1526
1983
|
let wasm = crate::dispatch::tests::wasm_path();
|
|
1527
1984
|
let ir = banking_ir();
|
|
1528
|
-
let lineage = LineageConfig {
|
|
1985
|
+
let lineage = LineageConfig {
|
|
1986
|
+
domain: "Banking".to_string(),
|
|
1987
|
+
era: Some(1),
|
|
1988
|
+
mirrored: None,
|
|
1989
|
+
};
|
|
1529
1990
|
let invoker = crate::lambda_client::NeverInvoker;
|
|
1530
1991
|
|
|
1531
|
-
let created = collection_create(
|
|
1532
|
-
&ir,
|
|
1533
|
-
"customers",
|
|
1534
|
-
r#"{"reference":{"value":"CUST-9001"},"name":{"given":"Ada","family":"Lovelace"},"email":{"address":"ada@example.com"}}"#,
|
|
1535
|
-
&client,
|
|
1536
|
-
&wasm,
|
|
1537
|
-
&lineage,
|
|
1538
|
-
&invoker,
|
|
1539
|
-
)
|
|
1992
|
+
let created = collection_create(&ir, "customers", r#"{"reference":{"value":"CUST-9001"},"name":{"given":"Ada","family":"Lovelace"},"email":{"address":"ada@example.com"}}"#, Host::new(&client, &wasm, &lineage, &invoker))
|
|
1540
1993
|
.await;
|
|
1541
1994
|
|
|
1542
1995
|
assert_eq!(created["statusCode"], 200, "{created}");
|
|
@@ -1545,7 +1998,8 @@ mod tests {
|
|
|
1545
1998
|
assert_eq!(record["reference"]["value"], "CUST-9001");
|
|
1546
1999
|
|
|
1547
2000
|
// …and it is there to read, both ways.
|
|
1548
|
-
let index =
|
|
2001
|
+
let index =
|
|
2002
|
+
collection_index(&ir, "customers", ¶ms(&[]), &client, &wasm, &lineage).await;
|
|
1549
2003
|
assert_eq!(body(&index).as_array().expect("an array").len(), 1);
|
|
1550
2004
|
let shown = record_show(&ir, "customers", "CUST-9001", &client, &wasm, &lineage).await;
|
|
1551
2005
|
assert_eq!(body(&shown)["id"], "CUST-9001");
|
|
@@ -1558,10 +2012,7 @@ mod tests {
|
|
|
1558
2012
|
"CUST-9001",
|
|
1559
2013
|
"suspend",
|
|
1560
2014
|
r#"{"standing":{"value":"watch"}}"#,
|
|
1561
|
-
&client,
|
|
1562
|
-
&wasm,
|
|
1563
|
-
&lineage,
|
|
1564
|
-
&invoker,
|
|
2015
|
+
Host::new(&client, &wasm, &lineage, &invoker),
|
|
1565
2016
|
)
|
|
1566
2017
|
.await;
|
|
1567
2018
|
|
|
@@ -1577,29 +2028,55 @@ mod tests {
|
|
|
1577
2028
|
"CUST-9001",
|
|
1578
2029
|
"suspend",
|
|
1579
2030
|
r#"{"standing":{"value":"watch"}}"#,
|
|
1580
|
-
&client,
|
|
1581
|
-
&wasm,
|
|
1582
|
-
&lineage,
|
|
1583
|
-
&invoker,
|
|
2031
|
+
Host::new(&client, &wasm, &lineage, &invoker),
|
|
1584
2032
|
)
|
|
1585
2033
|
.await;
|
|
1586
2034
|
|
|
1587
2035
|
assert_eq!(again["statusCode"], 422, "{again}");
|
|
1588
2036
|
let refused = body(&again);
|
|
1589
|
-
assert!(
|
|
1590
|
-
|
|
2037
|
+
assert!(
|
|
2038
|
+
!refused["error"]
|
|
2039
|
+
.as_str()
|
|
2040
|
+
.expect("a refusal class")
|
|
2041
|
+
.is_empty(),
|
|
2042
|
+
"{refused}"
|
|
2043
|
+
);
|
|
2044
|
+
assert!(
|
|
2045
|
+
!refused["message"].as_str().expect("a message").is_empty(),
|
|
2046
|
+
"{refused}"
|
|
2047
|
+
);
|
|
1591
2048
|
|
|
1592
2049
|
// An unknown command, and an unknown record, both refuse the
|
|
1593
2050
|
// way Json refuses them.
|
|
1594
|
-
let unknown_command =
|
|
1595
|
-
|
|
2051
|
+
let unknown_command = command_route(
|
|
2052
|
+
&ir,
|
|
2053
|
+
"customers",
|
|
2054
|
+
"CUST-9001",
|
|
2055
|
+
"nope",
|
|
2056
|
+
"",
|
|
2057
|
+
Host::new(&client, &wasm, &lineage, &invoker),
|
|
2058
|
+
)
|
|
2059
|
+
.await;
|
|
1596
2060
|
assert_eq!(unknown_command["statusCode"], 404);
|
|
1597
|
-
assert_eq!(
|
|
2061
|
+
assert_eq!(
|
|
2062
|
+
body(&unknown_command)["message"],
|
|
2063
|
+
"Customer declares no command named \"nope\""
|
|
2064
|
+
);
|
|
1598
2065
|
|
|
1599
|
-
let unknown_record =
|
|
1600
|
-
|
|
2066
|
+
let unknown_record = command_route(
|
|
2067
|
+
&ir,
|
|
2068
|
+
"customers",
|
|
2069
|
+
"ghost",
|
|
2070
|
+
"suspend",
|
|
2071
|
+
"",
|
|
2072
|
+
Host::new(&client, &wasm, &lineage, &invoker),
|
|
2073
|
+
)
|
|
2074
|
+
.await;
|
|
1601
2075
|
assert_eq!(unknown_record["statusCode"], 404);
|
|
1602
|
-
assert_eq!(
|
|
2076
|
+
assert_eq!(
|
|
2077
|
+
body(&unknown_record)["message"],
|
|
2078
|
+
"no Customer found for id \"ghost\""
|
|
2079
|
+
);
|
|
1603
2080
|
}
|
|
1604
2081
|
|
|
1605
2082
|
#[test]
|
|
@@ -1608,7 +2085,10 @@ mod tests {
|
|
|
1608
2085
|
assert_eq!(response["statusCode"], 501);
|
|
1609
2086
|
assert_eq!(body(&response)["error"], "NotImplemented");
|
|
1610
2087
|
assert!(
|
|
1611
|
-
body(&response)["message"]
|
|
2088
|
+
body(&response)["message"]
|
|
2089
|
+
.as_str()
|
|
2090
|
+
.expect("a message")
|
|
2091
|
+
.contains("ConsoleSettings"),
|
|
1612
2092
|
"the refusal has to name what is missing: {response}"
|
|
1613
2093
|
);
|
|
1614
2094
|
}
|