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
data/rust/host/src/ui_schema.rs
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Turns a domain IR plus config into the exact JSON `GET /api/ui-schema` serves (nav, columns,
|
|
2
|
+
// forms). There is no Ruby ui_schema: the Ruby HTML renderer (lib/hecks/forms/field_shape.rb and
|
|
3
|
+
// value_object_shape.rb) shares its field-shape rules, but this module is the only builder.
|
|
3
4
|
|
|
4
5
|
use serde_json::{json, Map, Value};
|
|
5
6
|
use std::collections::HashMap;
|
|
6
|
-
use std::sync::LazyLock;
|
|
7
7
|
|
|
8
|
-
use
|
|
8
|
+
use crate::hecks_naming::{plural, snake};
|
|
9
9
|
|
|
10
10
|
/// `Hecks::Bluebook::IR::Attribute::PRIMITIVES`.
|
|
11
11
|
const PRIMITIVES: &[&str] = &["String", "Integer", "Float", "TrueClass", "FalseClass"];
|
|
12
12
|
|
|
13
13
|
/// `UiSchema::DISPLAY_BY_SHAPE` — a detail field's default placement,
|
|
14
14
|
/// taken from its own shape; `display:` config overrides it.
|
|
15
|
-
const DISPLAY_BY_SHAPE: &[(&str, &str)] = &[
|
|
15
|
+
const DISPLAY_BY_SHAPE: &[(&str, &str)] = &[
|
|
16
|
+
("text", "prose"),
|
|
17
|
+
("lines", "bullet"),
|
|
18
|
+
("rows", "rows"),
|
|
19
|
+
("compound", "mono"),
|
|
20
|
+
];
|
|
16
21
|
|
|
17
22
|
/// Shapes a table column can't carry, so they're left out of the
|
|
18
23
|
/// default (unconfigured) column list.
|
|
@@ -28,7 +33,10 @@ pub fn build(domain_ir: &Value, config: &Value) -> Value {
|
|
|
28
33
|
|
|
29
34
|
let mut collections = Map::new();
|
|
30
35
|
for aggregate in &aggregates {
|
|
31
|
-
let key = key_for
|
|
36
|
+
let key = key_for
|
|
37
|
+
.get(agg_name(aggregate))
|
|
38
|
+
.cloned()
|
|
39
|
+
.unwrap_or_default();
|
|
32
40
|
let scoped = with_shared_value_objects(aggregate, &aggregates);
|
|
33
41
|
collections.insert(key, build_collection(&scoped, config, &key_for, &by_name));
|
|
34
42
|
}
|
|
@@ -71,16 +79,18 @@ pub fn schema(domain_ir: &Value, config: &Value) -> Value {
|
|
|
71
79
|
/// `UiSchema.collection_key` — config's own `key:` if it names one,
|
|
72
80
|
/// else the pluralized snake_case of the aggregate's own name.
|
|
73
81
|
pub fn collection_key(aggregate: &Value, config: &Value) -> String {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
82
|
+
dig(config, &["collections", agg_name(aggregate), "key"])
|
|
83
|
+
.and_then(|v| v.as_str())
|
|
84
|
+
.map_or_else(|| plural(&snake(agg_name(aggregate))), ToString::to_string)
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
/// Aggregate name -> collection key, for every aggregate in the
|
|
81
88
|
/// domain: what a reference field's own `targetKey` resolves through.
|
|
82
89
|
pub fn key_for(aggregates: &[&Value], config: &Value) -> KeyFor {
|
|
83
|
-
aggregates
|
|
90
|
+
aggregates
|
|
91
|
+
.iter()
|
|
92
|
+
.map(|a| (agg_name(a).to_string(), collection_key(a, config)))
|
|
93
|
+
.collect()
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
/// The aggregate with every value object the rest of the domain
|
|
@@ -108,35 +118,62 @@ fn with_shared_value_objects(aggregate: &Value, aggregates: &[&Value]) -> Value
|
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
pub fn aggregates(domain_ir: &Value) -> Vec<&Value> {
|
|
111
|
-
domain_ir
|
|
121
|
+
domain_ir
|
|
122
|
+
.get("aggregates")
|
|
123
|
+
.and_then(|v| v.as_array())
|
|
124
|
+
.map(|a| a.iter().collect())
|
|
125
|
+
.unwrap_or_default()
|
|
112
126
|
}
|
|
113
127
|
|
|
114
|
-
fn build_collection(
|
|
128
|
+
fn build_collection(
|
|
129
|
+
aggregate: &Value,
|
|
130
|
+
config: &Value,
|
|
131
|
+
key_for: &KeyFor,
|
|
132
|
+
by_name: &HashMap<&str, &Value>,
|
|
133
|
+
) -> Value {
|
|
115
134
|
let name = agg_name(aggregate);
|
|
116
|
-
let agg_cfg = dig(config, &["collections", name])
|
|
117
|
-
|
|
135
|
+
let agg_cfg = dig(config, &["collections", name])
|
|
136
|
+
.cloned()
|
|
137
|
+
.unwrap_or_else(|| json!({}));
|
|
138
|
+
let state_cfg = dig(config, &["states", name])
|
|
139
|
+
.cloned()
|
|
140
|
+
.unwrap_or_else(|| json!({}));
|
|
118
141
|
let formats = agg_cfg.get("field_formats").cloned().unwrap_or(Value::Null);
|
|
119
142
|
let identity = agg_cfg.get("identity").filter(|v| !v.is_null());
|
|
120
143
|
let creating = commands(aggregate).into_iter().find(|c| creates(c));
|
|
121
|
-
let others: Vec<&Value> = commands(aggregate)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
let others: Vec<&Value> = commands(aggregate)
|
|
145
|
+
.into_iter()
|
|
146
|
+
.filter(|c| !creates(c))
|
|
147
|
+
.collect();
|
|
148
|
+
let noun_sing = agg_cfg
|
|
149
|
+
.get("noun_sing")
|
|
150
|
+
.and_then(|v| v.as_str())
|
|
151
|
+
.map_or_else(|| snake(name).replace('_', " "), ToString::to_string);
|
|
126
152
|
|
|
127
|
-
let fields: Vec<Value> = array(aggregate, "attributes")
|
|
153
|
+
let fields: Vec<Value> = array(aggregate, "attributes")
|
|
154
|
+
.iter()
|
|
155
|
+
.map(|a| field(a, aggregate, key_for, &formats))
|
|
156
|
+
.collect();
|
|
128
157
|
|
|
129
158
|
let mut collection = Map::new();
|
|
130
159
|
collection.insert("aggregate".to_string(), json!(name));
|
|
131
|
-
collection.insert(
|
|
160
|
+
collection.insert(
|
|
161
|
+
"label".to_string(),
|
|
162
|
+
json!(string_or(&agg_cfg, "label", &humanize_words(name))),
|
|
163
|
+
);
|
|
132
164
|
collection.insert("nounSing".to_string(), json!(noun_sing));
|
|
133
165
|
collection.insert("navGroup".to_string(), optional(&agg_cfg, "nav_group"));
|
|
134
|
-
collection.insert(
|
|
166
|
+
collection.insert(
|
|
167
|
+
"primaryField".to_string(),
|
|
168
|
+
optional(&agg_cfg, "primary_field"),
|
|
169
|
+
);
|
|
135
170
|
collection.insert("listQuery".to_string(), optional(&agg_cfg, "list_query"));
|
|
136
|
-
collection.insert(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
collection.insert(
|
|
172
|
+
"stateField".to_string(),
|
|
173
|
+
lifecycle(aggregate).map_or(Value::Null, |l| {
|
|
174
|
+
l.get("field").cloned().unwrap_or(Value::Null)
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
140
177
|
collection.insert("statusLabels".to_string(), state_facet(&state_cfg, "label"));
|
|
141
178
|
collection.insert("statusTone".to_string(), state_facet(&state_cfg, "tone"));
|
|
142
179
|
collection.insert("statusNotes".to_string(), state_facet(&state_cfg, "note"));
|
|
@@ -148,16 +185,29 @@ fn build_collection(aggregate: &Value, config: &Value, key_for: &KeyFor, by_name
|
|
|
148
185
|
);
|
|
149
186
|
collection.insert(
|
|
150
187
|
"detailFields".to_string(),
|
|
151
|
-
build_detail_fields(
|
|
188
|
+
build_detail_fields(
|
|
189
|
+
agg_cfg.get("detail_fields"),
|
|
190
|
+
&fields,
|
|
191
|
+
aggregate,
|
|
192
|
+
by_name,
|
|
193
|
+
key_for,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
collection.insert(
|
|
197
|
+
"transitions".to_string(),
|
|
198
|
+
build_transitions(aggregate, &others, key_for, &formats),
|
|
199
|
+
);
|
|
200
|
+
collection.insert(
|
|
201
|
+
"alwaysAvailable".to_string(),
|
|
202
|
+
build_always_available(aggregate, &others, key_for, &formats),
|
|
152
203
|
);
|
|
153
|
-
collection.insert("transitions".to_string(), build_transitions(aggregate, &others, key_for, &formats));
|
|
154
|
-
collection.insert("alwaysAvailable".to_string(), build_always_available(aggregate, &others, key_for, &formats));
|
|
155
204
|
collection.insert(
|
|
156
205
|
"create".to_string(),
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
206
|
+
creating.map_or(Value::Null, |command| {
|
|
207
|
+
build_create(
|
|
208
|
+
command, aggregate, &agg_cfg, identity, key_for, &formats, &noun_sing,
|
|
209
|
+
)
|
|
210
|
+
}),
|
|
161
211
|
);
|
|
162
212
|
Value::Object(collection)
|
|
163
213
|
}
|
|
@@ -175,11 +225,17 @@ fn build_create(
|
|
|
175
225
|
// An identity-derived field is computed server-side at dispatch
|
|
176
226
|
// (`apply_identity!`); asking someone to type "P-004" by hand would
|
|
177
227
|
// only invite a collision with what the server already computes.
|
|
178
|
-
let identity_field = identity
|
|
228
|
+
let identity_field = identity
|
|
229
|
+
.and_then(|i| i.get("field"))
|
|
230
|
+
.and_then(|v| v.as_str());
|
|
179
231
|
let fields: Vec<Value> = array(command, "attributes")
|
|
180
232
|
.iter()
|
|
181
|
-
.map(|attribute|
|
|
182
|
-
|
|
233
|
+
.map(|attribute| {
|
|
234
|
+
field_with_precondition(attribute, aggregate, key_for, preconditions, formats)
|
|
235
|
+
})
|
|
236
|
+
.filter(|f| {
|
|
237
|
+
identity_field.is_none_or(|name| f.get("key").and_then(|v| v.as_str()) != Some(name))
|
|
238
|
+
})
|
|
183
239
|
.collect();
|
|
184
240
|
|
|
185
241
|
json!({
|
|
@@ -217,44 +273,69 @@ fn status_attention(state_cfg: &Value) -> Value {
|
|
|
217
273
|
Value::Array(out)
|
|
218
274
|
}
|
|
219
275
|
|
|
220
|
-
fn build_columns(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
276
|
+
fn build_columns(
|
|
277
|
+
configured: Option<&Value>,
|
|
278
|
+
fields: &[Value],
|
|
279
|
+
aggregate: &Value,
|
|
280
|
+
by_name: &HashMap<&str, &Value>,
|
|
281
|
+
key_for: &KeyFor,
|
|
282
|
+
) -> Value {
|
|
283
|
+
// A configured empty list is a real answer, not "unconfigured"
|
|
284
|
+
// — Ruby's `configured || default` only falls through on nil,
|
|
285
|
+
// and `[]` is truthy there.
|
|
286
|
+
let entries = configured.filter(|v| !v.is_null()).map_or_else(
|
|
287
|
+
|| {
|
|
227
288
|
let mut default: Vec<Value> = fields
|
|
228
289
|
.iter()
|
|
229
|
-
.filter(|f|
|
|
290
|
+
.filter(|f| {
|
|
291
|
+
!UNCOLUMNABLE.contains(&f.get("shape").and_then(|v| v.as_str()).unwrap_or(""))
|
|
292
|
+
})
|
|
230
293
|
.map(|f| f.get("key").cloned().unwrap_or(Value::Null))
|
|
231
294
|
.collect();
|
|
232
295
|
if lifecycle(aggregate).is_some() {
|
|
233
296
|
default.push(json!("__state__"));
|
|
234
297
|
}
|
|
235
298
|
default
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
299
|
+
},
|
|
300
|
+
|list| list.as_array().cloned().unwrap_or_default(),
|
|
301
|
+
);
|
|
302
|
+
Value::Array(
|
|
303
|
+
entries
|
|
304
|
+
.iter()
|
|
305
|
+
.map(|entry| column_for(entry, fields, aggregate, by_name, key_for))
|
|
306
|
+
.collect(),
|
|
307
|
+
)
|
|
239
308
|
}
|
|
240
309
|
|
|
241
310
|
/// `UiSchema.column_for` — a bare string names an attribute as-is; a
|
|
242
311
|
/// hash asks for something the shape alone can't say (`as:`/`head:`/`hop:`).
|
|
243
|
-
fn column_for(
|
|
312
|
+
fn column_for(
|
|
313
|
+
entry: &Value,
|
|
314
|
+
fields: &[Value],
|
|
315
|
+
aggregate: &Value,
|
|
316
|
+
by_name: &HashMap<&str, &Value>,
|
|
317
|
+
key_for: &KeyFor,
|
|
318
|
+
) -> Value {
|
|
244
319
|
let sort = sort_config(entry);
|
|
245
320
|
|
|
246
321
|
if entry.as_str() == Some("__state__") {
|
|
247
322
|
// `|| "state"` guards a hand-edited `columns:` naming
|
|
248
323
|
// `__state__` on an aggregate with no lifecycle at all: a
|
|
249
324
|
// config mistake degrades to a generic label, never a 500.
|
|
250
|
-
let field_name = lifecycle(aggregate)
|
|
251
|
-
|
|
325
|
+
let field_name = lifecycle(aggregate)
|
|
326
|
+
.and_then(|l| l.get("field"))
|
|
327
|
+
.and_then(|v| v.as_str())
|
|
328
|
+
.unwrap_or("state");
|
|
329
|
+
return merge(
|
|
330
|
+
json!({"key": "__state__", "label": humanize_words(field_name), "shape": "pill"}),
|
|
331
|
+
sort,
|
|
332
|
+
);
|
|
252
333
|
}
|
|
253
334
|
|
|
254
335
|
if let Some(name) = entry.as_str() {
|
|
255
|
-
let base = find_field(fields, name)
|
|
256
|
-
|
|
257
|
-
|
|
336
|
+
let base = find_field(fields, name).cloned().unwrap_or_else(
|
|
337
|
+
|| json!({"key": name, "label": humanize(name), "shape": "text_value"}),
|
|
338
|
+
);
|
|
258
339
|
return merge(base, sort);
|
|
259
340
|
}
|
|
260
341
|
|
|
@@ -264,15 +345,17 @@ fn column_for(entry: &Value, fields: &[Value], aggregate: &Value, by_name: &Hash
|
|
|
264
345
|
|
|
265
346
|
let key = entry.get("field").cloned().unwrap_or(Value::Null);
|
|
266
347
|
let key_name = key.as_str().unwrap_or("");
|
|
267
|
-
let base = find_field(fields, key_name)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
348
|
+
let base = find_field(fields, key_name)
|
|
349
|
+
.cloned()
|
|
350
|
+
.unwrap_or_else(|| json!({"key": key, "label": humanize(key_name)}));
|
|
351
|
+
let shape = entry.get("as").filter(|v| !v.is_null()).map_or_else(
|
|
352
|
+
|| base.get("shape").cloned().unwrap_or(Value::Null),
|
|
353
|
+
Value::clone,
|
|
354
|
+
);
|
|
355
|
+
let label = entry.get("head").filter(|v| !v.is_null()).map_or_else(
|
|
356
|
+
|| base.get("label").cloned().unwrap_or(Value::Null),
|
|
357
|
+
Value::clone,
|
|
358
|
+
);
|
|
276
359
|
let overridden = merge(base, json!({"key": key, "shape": shape, "label": label}));
|
|
277
360
|
merge(overridden, sort)
|
|
278
361
|
}
|
|
@@ -287,44 +370,80 @@ fn sort_config(entry: &Value) -> Value {
|
|
|
287
370
|
Some(value) if !value.is_null() => value.clone(),
|
|
288
371
|
_ => json!(true),
|
|
289
372
|
};
|
|
290
|
-
let sort_default =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
373
|
+
let sort_default = entry
|
|
374
|
+
.get("sort_default")
|
|
375
|
+
.filter(|v| !v.is_null())
|
|
376
|
+
.map_or_else(|| json!("asc"), Value::clone);
|
|
294
377
|
json!({"sortable": sortable, "sortDefault": sort_default})
|
|
295
378
|
}
|
|
296
379
|
|
|
297
|
-
fn build_detail_fields(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
380
|
+
fn build_detail_fields(
|
|
381
|
+
configured: Option<&Value>,
|
|
382
|
+
fields: &[Value],
|
|
383
|
+
aggregate: &Value,
|
|
384
|
+
by_name: &HashMap<&str, &Value>,
|
|
385
|
+
key_for: &KeyFor,
|
|
386
|
+
) -> Value {
|
|
387
|
+
let entries = configured.filter(|v| !v.is_null()).map_or_else(
|
|
388
|
+
|| {
|
|
389
|
+
fields
|
|
390
|
+
.iter()
|
|
391
|
+
.map(|f| f.get("key").cloned().unwrap_or(Value::Null))
|
|
392
|
+
.collect()
|
|
393
|
+
},
|
|
394
|
+
|list| list.as_array().cloned().unwrap_or_default(),
|
|
395
|
+
);
|
|
396
|
+
Value::Array(
|
|
397
|
+
entries
|
|
398
|
+
.iter()
|
|
399
|
+
.map(|entry| detail_field_for(entry, fields, aggregate, by_name, key_for))
|
|
400
|
+
.collect(),
|
|
401
|
+
)
|
|
303
402
|
}
|
|
304
403
|
|
|
305
|
-
fn detail_field_for(
|
|
404
|
+
fn detail_field_for(
|
|
405
|
+
entry: &Value,
|
|
406
|
+
fields: &[Value],
|
|
407
|
+
aggregate: &Value,
|
|
408
|
+
by_name: &HashMap<&str, &Value>,
|
|
409
|
+
key_for: &KeyFor,
|
|
410
|
+
) -> Value {
|
|
306
411
|
let mut base = column_for(entry, fields, aggregate, by_name, key_for);
|
|
307
412
|
if base.get("shape").and_then(|v| v.as_str()) == Some("rows") {
|
|
308
413
|
base = apply_row_columns_filter(base, entry);
|
|
309
414
|
base = apply_row_state_filter(base, entry, aggregate);
|
|
310
415
|
}
|
|
311
416
|
let shape = base.get("shape").and_then(|v| v.as_str()).unwrap_or("");
|
|
312
|
-
let display =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
417
|
+
let display = entry.get("display").filter(|v| !v.is_null()).map_or_else(
|
|
418
|
+
|| {
|
|
419
|
+
json!(DISPLAY_BY_SHAPE
|
|
420
|
+
.iter()
|
|
421
|
+
.find(|(s, _)| *s == shape)
|
|
422
|
+
.map_or("simple", |(_, d)| *d))
|
|
423
|
+
},
|
|
424
|
+
Value::clone,
|
|
425
|
+
);
|
|
316
426
|
merge(base, json!({"display": display}))
|
|
317
427
|
}
|
|
318
428
|
|
|
319
429
|
/// Which of a rows field's own sub-columns to show, and in what order.
|
|
320
430
|
fn apply_row_columns_filter(base: Value, entry: &Value) -> Value {
|
|
321
|
-
let Some(wanted) = entry.get("columns").and_then(|v| v.as_array()) else {
|
|
322
|
-
|
|
431
|
+
let Some(wanted) = entry.get("columns").and_then(|v| v.as_array()) else {
|
|
432
|
+
return base;
|
|
433
|
+
};
|
|
434
|
+
let existing = base
|
|
435
|
+
.get("columns")
|
|
436
|
+
.and_then(|v| v.as_array())
|
|
437
|
+
.cloned()
|
|
438
|
+
.unwrap_or_default();
|
|
323
439
|
let kept: Vec<Value> = wanted
|
|
324
440
|
.iter()
|
|
325
441
|
.filter_map(|want| {
|
|
326
442
|
let want = want.as_str()?;
|
|
327
|
-
existing
|
|
443
|
+
existing
|
|
444
|
+
.iter()
|
|
445
|
+
.find(|c| c.get("key").and_then(|v| v.as_str()) == Some(want))
|
|
446
|
+
.cloned()
|
|
328
447
|
})
|
|
329
448
|
.collect();
|
|
330
449
|
merge(base, json!({"columns": kept}))
|
|
@@ -333,35 +452,63 @@ fn apply_row_columns_filter(base: Value, entry: &Value) -> Value {
|
|
|
333
452
|
/// Which rows count as "current" — `list_of` never drops a removed
|
|
334
453
|
/// element, so the frontend filters using what this stamps per row.
|
|
335
454
|
fn apply_row_state_filter(base: Value, entry: &Value, aggregate: &Value) -> Value {
|
|
336
|
-
let Some(state_filter) = entry.get("state_filter").filter(|v| !v.is_null()) else {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
455
|
+
let Some(state_filter) = entry.get("state_filter").filter(|v| !v.is_null()) else {
|
|
456
|
+
return base;
|
|
457
|
+
};
|
|
458
|
+
let key = entry
|
|
459
|
+
.get("field")
|
|
460
|
+
.and_then(|v| v.as_str())
|
|
461
|
+
.map_or_else(|| entry.as_str().unwrap_or(""), |field| field);
|
|
462
|
+
let Some(attribute) = find_attribute(aggregate, key) else {
|
|
463
|
+
return base;
|
|
464
|
+
};
|
|
465
|
+
let Some(entity) = find_entity(aggregate, type_of(attribute)) else {
|
|
466
|
+
return base;
|
|
467
|
+
};
|
|
468
|
+
let Some(field_name) = lifecycle(entity)
|
|
469
|
+
.and_then(|l| l.get("field"))
|
|
470
|
+
.filter(|v| !v.is_null())
|
|
471
|
+
else {
|
|
472
|
+
return base;
|
|
340
473
|
};
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
474
|
+
merge(
|
|
475
|
+
base,
|
|
476
|
+
json!({"stateFilter": {"field": field_name, "value": state_filter}}),
|
|
477
|
+
)
|
|
345
478
|
}
|
|
346
479
|
|
|
347
480
|
/// A second hop, for display only — resolves this reference, then one
|
|
348
481
|
/// of its references, falling back to the plain column if it can't.
|
|
349
|
-
fn hop_column(
|
|
482
|
+
fn hop_column(
|
|
483
|
+
entry: &Value,
|
|
484
|
+
fields: &[Value],
|
|
485
|
+
by_name: &HashMap<&str, &Value>,
|
|
486
|
+
key_for: &KeyFor,
|
|
487
|
+
) -> Value {
|
|
350
488
|
let key = entry.get("field").cloned().unwrap_or(Value::Null);
|
|
351
489
|
let key_name = key.as_str().unwrap_or("");
|
|
352
490
|
let hop = entry.get("hop").and_then(|v| v.as_str()).unwrap_or("");
|
|
353
491
|
let head = entry.get("head").filter(|v| !v.is_null());
|
|
354
492
|
|
|
355
|
-
let base_field = find_field(fields, key_name)
|
|
493
|
+
let base_field = find_field(fields, key_name)
|
|
494
|
+
.filter(|f| f.get("shape").and_then(|v| v.as_str()) == Some("reference"));
|
|
356
495
|
let Some(base_field) = base_field else {
|
|
357
496
|
let label = head.cloned().unwrap_or_else(|| json!(humanize(key_name)));
|
|
358
497
|
return json!({"key": key, "label": label, "shape": "text_value"});
|
|
359
498
|
};
|
|
360
499
|
|
|
361
|
-
let target = base_field
|
|
362
|
-
|
|
500
|
+
let target = base_field
|
|
501
|
+
.get("target")
|
|
502
|
+
.and_then(|v| v.as_str())
|
|
503
|
+
.unwrap_or("");
|
|
504
|
+
let hop_attribute = by_name
|
|
505
|
+
.get(target)
|
|
506
|
+
.and_then(|agg| find_attribute(agg, hop))
|
|
507
|
+
.filter(|a| is_reference(a));
|
|
363
508
|
let Some(hop_attribute) = hop_attribute else {
|
|
364
|
-
let label = head
|
|
509
|
+
let label = head
|
|
510
|
+
.cloned()
|
|
511
|
+
.unwrap_or_else(|| base_field.get("label").cloned().unwrap_or(Value::Null));
|
|
365
512
|
return merge(base_field.clone(), json!({"label": label}));
|
|
366
513
|
};
|
|
367
514
|
|
|
@@ -372,14 +519,16 @@ fn hop_column(entry: &Value, fields: &[Value], by_name: &HashMap<&str, &Value>,
|
|
|
372
519
|
"label": head.cloned().unwrap_or_else(|| json!(humanize(hop))),
|
|
373
520
|
"through": hop,
|
|
374
521
|
"targetKey": base_field.get("targetKey").cloned().unwrap_or(Value::Null),
|
|
375
|
-
"hopTargetKey": key_for.get(&hop_target).
|
|
522
|
+
"hopTargetKey": key_for.get(&hop_target).map_or(Value::Null, |k| json!(k)),
|
|
376
523
|
})
|
|
377
524
|
}
|
|
378
525
|
|
|
379
526
|
/// `Lifecycle#states` — the default state, then every state some
|
|
380
527
|
/// transition targets, deduplicated in that order.
|
|
381
528
|
pub(crate) fn lifecycle_states(aggregate: &Value) -> Vec<String> {
|
|
382
|
-
let Some(lifecycle) = lifecycle(aggregate) else {
|
|
529
|
+
let Some(lifecycle) = lifecycle(aggregate) else {
|
|
530
|
+
return Vec::new();
|
|
531
|
+
};
|
|
383
532
|
let mut states = Vec::new();
|
|
384
533
|
if let Some(default) = lifecycle.get("default").and_then(|v| v.as_str()) {
|
|
385
534
|
states.push(default.to_string());
|
|
@@ -396,8 +545,15 @@ pub(crate) fn lifecycle_states(aggregate: &Value) -> Vec<String> {
|
|
|
396
545
|
|
|
397
546
|
/// `from: nil` means no constraint at all — an unconstrained transition
|
|
398
547
|
/// is offered from every state rather than resolved to just one.
|
|
399
|
-
fn build_transitions(
|
|
400
|
-
|
|
548
|
+
fn build_transitions(
|
|
549
|
+
aggregate: &Value,
|
|
550
|
+
others: &[&Value],
|
|
551
|
+
key_for: &KeyFor,
|
|
552
|
+
formats: &Value,
|
|
553
|
+
) -> Value {
|
|
554
|
+
let Some(lifecycle) = lifecycle(aggregate) else {
|
|
555
|
+
return json!({});
|
|
556
|
+
};
|
|
401
557
|
let transitions = array(lifecycle, "transitions");
|
|
402
558
|
|
|
403
559
|
let mut by_state = Map::new();
|
|
@@ -405,8 +561,10 @@ fn build_transitions(aggregate: &Value, others: &[&Value], key_for: &KeyFor, for
|
|
|
405
561
|
let mut available = Vec::new();
|
|
406
562
|
for command in others {
|
|
407
563
|
let name = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
408
|
-
let candidates: Vec<&Value> =
|
|
409
|
-
|
|
564
|
+
let candidates: Vec<&Value> = transitions
|
|
565
|
+
.iter()
|
|
566
|
+
.filter(|t| t.get("command").and_then(|v| v.as_str()) == Some(name))
|
|
567
|
+
.collect();
|
|
410
568
|
if candidates.is_empty() {
|
|
411
569
|
// "Always available" — handled separately below.
|
|
412
570
|
continue;
|
|
@@ -430,13 +588,20 @@ fn build_transitions(aggregate: &Value, others: &[&Value], key_for: &KeyFor, for
|
|
|
430
588
|
|
|
431
589
|
/// A command with no lifecycle transition fires regardless of state
|
|
432
590
|
/// (Contract's `Revise`); an aggregate with none has every command here.
|
|
433
|
-
fn build_always_available(
|
|
434
|
-
|
|
591
|
+
fn build_always_available(
|
|
592
|
+
aggregate: &Value,
|
|
593
|
+
others: &[&Value],
|
|
594
|
+
key_for: &KeyFor,
|
|
595
|
+
formats: &Value,
|
|
596
|
+
) -> Value {
|
|
597
|
+
let transitions = lifecycle(aggregate).map_or(&[][..], |l| array(l, "transitions"));
|
|
435
598
|
let available: Vec<Value> = others
|
|
436
599
|
.iter()
|
|
437
600
|
.filter(|command| {
|
|
438
601
|
let name = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
439
|
-
!transitions
|
|
602
|
+
!transitions
|
|
603
|
+
.iter()
|
|
604
|
+
.any(|t| t.get("command").and_then(|v| v.as_str()) == Some(name))
|
|
440
605
|
})
|
|
441
606
|
.map(|command| {
|
|
442
607
|
let name = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
@@ -451,22 +616,37 @@ fn build_always_available(aggregate: &Value, others: &[&Value], key_for: &KeyFor
|
|
|
451
616
|
}
|
|
452
617
|
|
|
453
618
|
fn command_inputs(command: &Value, aggregate: &Value, key_for: &KeyFor, formats: &Value) -> Value {
|
|
454
|
-
Value::Array(
|
|
619
|
+
Value::Array(
|
|
620
|
+
array(command, "attributes")
|
|
621
|
+
.iter()
|
|
622
|
+
.map(|a| field(a, aggregate, key_for, formats))
|
|
623
|
+
.collect(),
|
|
624
|
+
)
|
|
455
625
|
}
|
|
456
626
|
|
|
457
627
|
/// Stamps a create-form reference field with the state config says it
|
|
458
628
|
/// must point at, so the client's picker can filter what the server
|
|
459
629
|
/// would refuse anyway.
|
|
460
|
-
fn field_with_precondition(
|
|
630
|
+
fn field_with_precondition(
|
|
631
|
+
attribute: &Value,
|
|
632
|
+
aggregate: &Value,
|
|
633
|
+
key_for: &KeyFor,
|
|
634
|
+
preconditions: Option<&Value>,
|
|
635
|
+
formats: &Value,
|
|
636
|
+
) -> Value {
|
|
461
637
|
let descriptor = field(attribute, aggregate, key_for, formats);
|
|
462
638
|
if descriptor.get("shape").and_then(|v| v.as_str()) != Some("reference") {
|
|
463
639
|
return descriptor;
|
|
464
640
|
}
|
|
465
641
|
let key = descriptor.get("key").and_then(|v| v.as_str()).unwrap_or("");
|
|
466
|
-
let rule = preconditions
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
642
|
+
let rule = preconditions.and_then(|v| v.as_array()).and_then(|rules| {
|
|
643
|
+
rules
|
|
644
|
+
.iter()
|
|
645
|
+
.find(|r| r.get("field").and_then(|v| v.as_str()) == Some(key))
|
|
646
|
+
});
|
|
647
|
+
let Some(state) = rule.and_then(|r| r.get("state")).filter(|v| !v.is_null()) else {
|
|
648
|
+
return descriptor;
|
|
649
|
+
};
|
|
470
650
|
merge(descriptor, json!({"filterState": state}))
|
|
471
651
|
}
|
|
472
652
|
|
|
@@ -485,18 +665,20 @@ fn build_nav(aggregates: &[&Value], config: &Value, key_for: &KeyFor) -> Value {
|
|
|
485
665
|
|
|
486
666
|
for (index, aggregate) in aggregates.iter().enumerate() {
|
|
487
667
|
let name = agg_name(aggregate);
|
|
488
|
-
let agg_cfg = dig(config, &["collections", name])
|
|
668
|
+
let agg_cfg = dig(config, &["collections", name])
|
|
669
|
+
.cloned()
|
|
670
|
+
.unwrap_or_else(|| json!({}));
|
|
489
671
|
let nav_group = agg_cfg.get("nav_group").and_then(|v| v.as_str());
|
|
490
672
|
let collection_key = key_for.get(name).cloned().unwrap_or_default();
|
|
491
|
-
let group_key = nav_group.
|
|
492
|
-
let nav_order = agg_cfg.get("nav_order").and_then(
|
|
673
|
+
let group_key = nav_group.map_or_else(|| collection_key.clone(), String::from);
|
|
674
|
+
let nav_order = agg_cfg.get("nav_order").and_then(serde_json::Value::as_f64);
|
|
493
675
|
|
|
494
676
|
if !groups.contains_key(&group_key) {
|
|
495
677
|
order.push(group_key.clone());
|
|
496
|
-
let label =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
678
|
+
let label = nav_group.map_or_else(
|
|
679
|
+
|| string_or(&agg_cfg, "label", &humanize_words(name)),
|
|
680
|
+
humanize_words,
|
|
681
|
+
);
|
|
500
682
|
groups.insert(
|
|
501
683
|
group_key.clone(),
|
|
502
684
|
Group {
|
|
@@ -508,17 +690,23 @@ fn build_nav(aggregates: &[&Value], config: &Value, key_for: &KeyFor) -> Value {
|
|
|
508
690
|
);
|
|
509
691
|
}
|
|
510
692
|
|
|
511
|
-
let group = groups.get_mut(&group_key)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
693
|
+
if let Some(group) = groups.get_mut(&group_key) {
|
|
694
|
+
group.collections.push(collection_key);
|
|
695
|
+
if let Some(nav_order) = nav_order {
|
|
696
|
+
if nav_order < group.sort.0 {
|
|
697
|
+
group.sort = (nav_order, index);
|
|
698
|
+
}
|
|
516
699
|
}
|
|
517
700
|
}
|
|
518
701
|
}
|
|
519
702
|
|
|
520
703
|
let mut ordered: Vec<&Group> = order.iter().filter_map(|k| groups.get(k)).collect();
|
|
521
|
-
|
|
704
|
+
// A NaN `nav_order` compares equal, keeping declaration order.
|
|
705
|
+
ordered.sort_by(|a, b| {
|
|
706
|
+
a.sort
|
|
707
|
+
.partial_cmp(&b.sort)
|
|
708
|
+
.unwrap_or(std::cmp::Ordering::Equal)
|
|
709
|
+
});
|
|
522
710
|
Value::Array(
|
|
523
711
|
ordered
|
|
524
712
|
.into_iter()
|
|
@@ -558,7 +746,7 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
|
|
|
558
746
|
let base = json!({
|
|
559
747
|
"key": name,
|
|
560
748
|
"label": humanize(name),
|
|
561
|
-
"optional": attribute.get("optional").and_then(
|
|
749
|
+
"optional": attribute.get("optional").and_then(serde_json::Value::as_bool).unwrap_or(false),
|
|
562
750
|
});
|
|
563
751
|
|
|
564
752
|
// `to_h` merges the reduced identity into `id`; an aggregate whose
|
|
@@ -574,7 +762,7 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
|
|
|
574
762
|
json!({
|
|
575
763
|
"shape": "reference",
|
|
576
764
|
"target": target,
|
|
577
|
-
"targetKey": key_for.get(&target).
|
|
765
|
+
"targetKey": key_for.get(&target).map_or(Value::Null, |k| json!(k)),
|
|
578
766
|
}),
|
|
579
767
|
);
|
|
580
768
|
}
|
|
@@ -593,7 +781,10 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
|
|
|
593
781
|
if is_list(attribute) {
|
|
594
782
|
return merge(base, list_shape(value_object, aggregate, key_for, formats));
|
|
595
783
|
}
|
|
596
|
-
return merge(
|
|
784
|
+
return merge(
|
|
785
|
+
base,
|
|
786
|
+
scalar_vo_shape(value_object, aggregate, key_for, formats, format),
|
|
787
|
+
);
|
|
597
788
|
}
|
|
598
789
|
|
|
599
790
|
// An entity, not a value object — a `list_of` of pieces rather than
|
|
@@ -601,7 +792,10 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
|
|
|
601
792
|
// this same recursion.
|
|
602
793
|
if is_list(attribute) {
|
|
603
794
|
if let Some(entity) = find_entity(aggregate, ty) {
|
|
604
|
-
return merge(
|
|
795
|
+
return merge(
|
|
796
|
+
base,
|
|
797
|
+
json!({"shape": "rows", "columns": row_columns(entity, aggregate, key_for, formats)}),
|
|
798
|
+
);
|
|
605
799
|
}
|
|
606
800
|
}
|
|
607
801
|
|
|
@@ -611,7 +805,10 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
|
|
|
611
805
|
|
|
612
806
|
fn primitive_shape(ty: &str, format: Option<Value>) -> Value {
|
|
613
807
|
let mut shape = Map::new();
|
|
614
|
-
shape.insert(
|
|
808
|
+
shape.insert(
|
|
809
|
+
"shape".to_string(),
|
|
810
|
+
json!(if numeric(ty) { "number" } else { "text_value" }),
|
|
811
|
+
);
|
|
615
812
|
if let Some(format) = format {
|
|
616
813
|
shape.insert("format".to_string(), format);
|
|
617
814
|
}
|
|
@@ -620,7 +817,7 @@ fn primitive_shape(ty: &str, format: Option<Value>) -> Value {
|
|
|
620
817
|
|
|
621
818
|
fn enum_shape(value_object: &Value) -> Value {
|
|
622
819
|
let attributes = array(value_object, "attributes");
|
|
623
|
-
let discriminant = attributes.first().
|
|
820
|
+
let discriminant = attributes.first().map_or("value", attr_name);
|
|
624
821
|
let options: Vec<Value> = array(value_object, "members")
|
|
625
822
|
.iter()
|
|
626
823
|
.filter_map(|member| {
|
|
@@ -633,31 +830,45 @@ fn enum_shape(value_object: &Value) -> Value {
|
|
|
633
830
|
json!({"shape": "enum", "field": discriminant, "options": options})
|
|
634
831
|
}
|
|
635
832
|
|
|
636
|
-
fn scalar_vo_shape(
|
|
833
|
+
fn scalar_vo_shape(
|
|
834
|
+
value_object: &Value,
|
|
835
|
+
aggregate: &Value,
|
|
836
|
+
key_for: &KeyFor,
|
|
837
|
+
formats: &Value,
|
|
838
|
+
format: Option<Value>,
|
|
839
|
+
) -> Value {
|
|
637
840
|
if money_shaped(value_object) {
|
|
638
841
|
return json!({"shape": "money"});
|
|
639
842
|
}
|
|
640
843
|
if let Some(sole) = sole_attribute(value_object) {
|
|
641
844
|
return single_attr_shape(sole, format);
|
|
642
845
|
}
|
|
643
|
-
let fields: Vec<Value> = array(value_object, "attributes")
|
|
846
|
+
let fields: Vec<Value> = array(value_object, "attributes")
|
|
847
|
+
.iter()
|
|
848
|
+
.map(|a| field(a, aggregate, key_for, formats))
|
|
849
|
+
.collect();
|
|
644
850
|
json!({"shape": "compound", "fields": fields})
|
|
645
851
|
}
|
|
646
852
|
|
|
647
853
|
fn list_shape(value_object: &Value, aggregate: &Value, key_for: &KeyFor, formats: &Value) -> Value {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
854
|
+
sole_attribute(value_object).map_or_else(
|
|
855
|
+
|| json!({"shape": "rows", "columns": row_columns(value_object, aggregate, key_for, formats)}),
|
|
856
|
+
|sole| json!({"shape": "lines", "field": attr_name(sole)}),
|
|
857
|
+
)
|
|
652
858
|
}
|
|
653
859
|
|
|
654
860
|
/// A row lays a value object's attributes across a table's width; a
|
|
655
861
|
/// cents+currency pair collapses into one "money" column, as at top level.
|
|
656
862
|
fn row_columns(shape: &Value, aggregate: &Value, key_for: &KeyFor, formats: &Value) -> Value {
|
|
657
863
|
let attributes = array(shape, "attributes");
|
|
658
|
-
let names: Vec<&str> = attributes.iter().map(
|
|
864
|
+
let names: Vec<&str> = attributes.iter().map(attr_name).collect();
|
|
659
865
|
if !(names.contains(&"cents") && names.contains(&"currency")) {
|
|
660
|
-
return Value::Array(
|
|
866
|
+
return Value::Array(
|
|
867
|
+
attributes
|
|
868
|
+
.iter()
|
|
869
|
+
.map(|a| field(a, aggregate, key_for, formats))
|
|
870
|
+
.collect(),
|
|
871
|
+
);
|
|
661
872
|
}
|
|
662
873
|
Value::Array(
|
|
663
874
|
attributes
|
|
@@ -709,7 +920,10 @@ pub(crate) fn type_of(attribute: &Value) -> &str {
|
|
|
709
920
|
}
|
|
710
921
|
|
|
711
922
|
pub(crate) fn array<'a>(value: &'a Value, field: &str) -> &'a [Value] {
|
|
712
|
-
value
|
|
923
|
+
value
|
|
924
|
+
.get(field)
|
|
925
|
+
.and_then(|v| v.as_array())
|
|
926
|
+
.map_or(&[][..], std::vec::Vec::as_slice)
|
|
713
927
|
}
|
|
714
928
|
|
|
715
929
|
pub fn commands(aggregate: &Value) -> Vec<&Value> {
|
|
@@ -719,7 +933,9 @@ pub fn commands(aggregate: &Value) -> Vec<&Value> {
|
|
|
719
933
|
/// `IR::Command#creates?` — true exactly when the command declares no
|
|
720
934
|
/// `references`.
|
|
721
935
|
pub fn creates(command: &Value) -> bool {
|
|
722
|
-
command
|
|
936
|
+
command
|
|
937
|
+
.get("references")
|
|
938
|
+
.is_none_or(serde_json::Value::is_null)
|
|
723
939
|
}
|
|
724
940
|
|
|
725
941
|
fn lifecycle(aggregate: &Value) -> Option<&Value> {
|
|
@@ -727,25 +943,35 @@ fn lifecycle(aggregate: &Value) -> Option<&Value> {
|
|
|
727
943
|
}
|
|
728
944
|
|
|
729
945
|
pub fn find_attribute<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
|
|
730
|
-
array(aggregate, "attributes")
|
|
946
|
+
array(aggregate, "attributes")
|
|
947
|
+
.iter()
|
|
948
|
+
.find(|a| attr_name(a) == name)
|
|
731
949
|
}
|
|
732
950
|
|
|
733
951
|
pub(crate) fn find_value_object<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
|
|
734
|
-
array(aggregate, "value_objects")
|
|
952
|
+
array(aggregate, "value_objects")
|
|
953
|
+
.iter()
|
|
954
|
+
.find(|v| v.get("name").and_then(|n| n.as_str()) == Some(name))
|
|
735
955
|
}
|
|
736
956
|
|
|
737
957
|
pub(crate) fn find_entity<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
|
|
738
|
-
array(aggregate, "entities")
|
|
958
|
+
array(aggregate, "entities")
|
|
959
|
+
.iter()
|
|
960
|
+
.find(|e| e.get("name").and_then(|n| n.as_str()) == Some(name))
|
|
739
961
|
}
|
|
740
962
|
|
|
741
963
|
fn find_field<'a>(fields: &'a [Value], key: &str) -> Option<&'a Value> {
|
|
742
|
-
fields
|
|
964
|
+
fields
|
|
965
|
+
.iter()
|
|
966
|
+
.find(|f| f.get("key").and_then(|v| v.as_str()) == Some(key))
|
|
743
967
|
}
|
|
744
968
|
|
|
745
969
|
/// `Reference<X>` -> `X` — the export's pinned spelling, the same
|
|
746
970
|
/// prefix convention `web.rs`'s own `reference_target` reads.
|
|
747
971
|
pub fn reference_target(ty: &str) -> Option<String> {
|
|
748
|
-
ty.strip_prefix("Reference<")
|
|
972
|
+
ty.strip_prefix("Reference<")?
|
|
973
|
+
.strip_suffix('>')
|
|
974
|
+
.map(String::from)
|
|
749
975
|
}
|
|
750
976
|
|
|
751
977
|
pub(crate) fn is_reference(attribute: &Value) -> bool {
|
|
@@ -753,7 +979,10 @@ pub(crate) fn is_reference(attribute: &Value) -> bool {
|
|
|
753
979
|
}
|
|
754
980
|
|
|
755
981
|
pub(crate) fn is_list(attribute: &Value) -> bool {
|
|
756
|
-
attribute
|
|
982
|
+
attribute
|
|
983
|
+
.get("list")
|
|
984
|
+
.and_then(serde_json::Value::as_bool)
|
|
985
|
+
.unwrap_or(false)
|
|
757
986
|
}
|
|
758
987
|
|
|
759
988
|
/// `identity_heads` — the first segment of each declared identity
|
|
@@ -769,7 +998,10 @@ fn identity_heads(aggregate: &Value) -> Vec<&str> {
|
|
|
769
998
|
/// `Presentation::ValueObjectShape.money?` — exactly `{cents,
|
|
770
999
|
/// currency}`, sorted, and nothing else.
|
|
771
1000
|
fn money_shaped(value_object: &Value) -> bool {
|
|
772
|
-
let mut names: Vec<&str> = array(value_object, "attributes")
|
|
1001
|
+
let mut names: Vec<&str> = array(value_object, "attributes")
|
|
1002
|
+
.iter()
|
|
1003
|
+
.map(attr_name)
|
|
1004
|
+
.collect();
|
|
773
1005
|
names.sort_unstable();
|
|
774
1006
|
names == ["cents", "currency"]
|
|
775
1007
|
}
|
|
@@ -795,7 +1027,7 @@ fn dig<'a>(config: &'a Value, path: &[&str]) -> Option<&'a Value> {
|
|
|
795
1027
|
(!node.is_null()).then_some(node)
|
|
796
1028
|
}
|
|
797
1029
|
|
|
798
|
-
fn truthy(value: Option<&Value>) -> bool {
|
|
1030
|
+
const fn truthy(value: Option<&Value>) -> bool {
|
|
799
1031
|
match value {
|
|
800
1032
|
None | Some(Value::Null) => false,
|
|
801
1033
|
Some(Value::Bool(b)) => *b,
|
|
@@ -808,7 +1040,10 @@ fn optional(config: &Value, key: &str) -> Value {
|
|
|
808
1040
|
}
|
|
809
1041
|
|
|
810
1042
|
fn string_or(config: &Value, key: &str, fallback: &str) -> String {
|
|
811
|
-
config
|
|
1043
|
+
config
|
|
1044
|
+
.get(key)
|
|
1045
|
+
.and_then(|v| v.as_str())
|
|
1046
|
+
.map_or_else(|| fallback.to_string(), String::from)
|
|
812
1047
|
}
|
|
813
1048
|
|
|
814
1049
|
/// Ruby's `Hash#merge` semantics: an existing key keeps its position,
|
|
@@ -826,33 +1061,6 @@ fn merge(base: Value, extra: Value) -> Value {
|
|
|
826
1061
|
Value::Object(out)
|
|
827
1062
|
}
|
|
828
1063
|
|
|
829
|
-
static SNAKE_ACRONYM: LazyLock<Regex> =
|
|
830
|
-
LazyLock::new(|| Regex::new(r"([A-Z]+)([A-Z][a-z])").expect("a literal pattern must compile"));
|
|
831
|
-
static SNAKE_BOUNDARY: LazyLock<Regex> =
|
|
832
|
-
LazyLock::new(|| Regex::new(r"([a-z\d])([A-Z])").expect("a literal pattern must compile"));
|
|
833
|
-
static PLURAL_Y: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^aeiou]y$").expect("a literal pattern must compile"));
|
|
834
|
-
static PLURAL_ES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(s|x|z|ch|sh)$").expect("a literal pattern must compile"));
|
|
835
|
-
|
|
836
|
-
/// `Naming.snake` — the same two word-boundary substitutions, then
|
|
837
|
-
/// downcase.
|
|
838
|
-
pub fn snake(text: &str) -> String {
|
|
839
|
-
let once = SNAKE_ACRONYM.replace_all(text, "${1}_${2}");
|
|
840
|
-
let twice = SNAKE_BOUNDARY.replace_all(&once, "${1}_${2}");
|
|
841
|
-
twice.to_lowercase()
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
/// `Naming.plural` — three suffix rules, exactly as declared; getting
|
|
845
|
-
/// a collection name wrong reads as a typo forever.
|
|
846
|
-
pub fn plural(word: &str) -> String {
|
|
847
|
-
if PLURAL_Y.is_match(word) {
|
|
848
|
-
return format!("{}ies", &word[..word.len() - 1]);
|
|
849
|
-
}
|
|
850
|
-
if PLURAL_ES.is_match(word) {
|
|
851
|
-
return format!("{word}es");
|
|
852
|
-
}
|
|
853
|
-
format!("{word}s")
|
|
854
|
-
}
|
|
855
|
-
|
|
856
1064
|
/// `UiSchema.humanize_words` — snake_case, then each word capitalized.
|
|
857
1065
|
/// Not `Naming.words`: "ATMCard" reads "Atm Card" here, as in Ruby.
|
|
858
1066
|
fn humanize_words(text: &str) -> String {
|
|
@@ -868,10 +1076,9 @@ fn humanize_words(text: &str) -> String {
|
|
|
868
1076
|
.into_iter()
|
|
869
1077
|
.map(|word| {
|
|
870
1078
|
let mut chars = word.chars();
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
1079
|
+
chars.next().map_or_else(String::new, |first| {
|
|
1080
|
+
first.to_uppercase().collect::<String>() + chars.as_str()
|
|
1081
|
+
})
|
|
875
1082
|
})
|
|
876
1083
|
.collect::<Vec<String>>()
|
|
877
1084
|
.join(" ")
|
|
@@ -999,7 +1206,9 @@ mod tests {
|
|
|
999
1206
|
}
|
|
1000
1207
|
|
|
1001
1208
|
fn field_named(aggregate: &Value, name: &str) -> Value {
|
|
1002
|
-
let attribute = find_attribute(aggregate, name)
|
|
1209
|
+
let attribute = find_attribute(aggregate, name)
|
|
1210
|
+
.expect("a declared attribute")
|
|
1211
|
+
.clone();
|
|
1003
1212
|
field(&attribute, aggregate, &keys(), &Value::Null)
|
|
1004
1213
|
}
|
|
1005
1214
|
|
|
@@ -1008,7 +1217,10 @@ mod tests {
|
|
|
1008
1217
|
assert_eq!(collection_key(&client(), &json!({})), "clients");
|
|
1009
1218
|
assert_eq!(collection_key(&proposal(), &json!({})), "proposals");
|
|
1010
1219
|
assert_eq!(
|
|
1011
|
-
collection_key(
|
|
1220
|
+
collection_key(
|
|
1221
|
+
&proposal(),
|
|
1222
|
+
&json!({"collections": {"Proposal": {"key": "pipeline"}}})
|
|
1223
|
+
),
|
|
1012
1224
|
"pipeline"
|
|
1013
1225
|
);
|
|
1014
1226
|
}
|
|
@@ -1068,7 +1280,10 @@ mod tests {
|
|
|
1068
1280
|
assert_eq!(field_named(&client(), "contact_email")["shape"], "address");
|
|
1069
1281
|
assert_eq!(field_named(&client(), "contact_email")["field"], "address");
|
|
1070
1282
|
// `notes` is a list of a sole-attribute VO — lines, not rows.
|
|
1071
|
-
assert_eq!(
|
|
1283
|
+
assert_eq!(
|
|
1284
|
+
field_named(&client(), "notes"),
|
|
1285
|
+
json!({"key": "notes", "label": "Notes", "optional": true, "shape": "lines", "field": "text"})
|
|
1286
|
+
);
|
|
1072
1287
|
}
|
|
1073
1288
|
|
|
1074
1289
|
#[test]
|
|
@@ -1104,7 +1319,9 @@ mod tests {
|
|
|
1104
1319
|
#[test]
|
|
1105
1320
|
fn a_field_format_rides_on_top_of_the_derived_shape_rather_than_replacing_it() {
|
|
1106
1321
|
let aggregate = client();
|
|
1107
|
-
let attribute = find_attribute(&aggregate, "name")
|
|
1322
|
+
let attribute = find_attribute(&aggregate, "name")
|
|
1323
|
+
.expect("declared")
|
|
1324
|
+
.clone();
|
|
1108
1325
|
let shaped = field(&attribute, &aggregate, &keys(), &json!({"name": "date"}));
|
|
1109
1326
|
|
|
1110
1327
|
assert_eq!(shaped["shape"], "text_value");
|
|
@@ -1123,7 +1340,10 @@ mod tests {
|
|
|
1123
1340
|
"entities": [], "commands": [], "queries": [], "lifecycle": null
|
|
1124
1341
|
});
|
|
1125
1342
|
|
|
1126
|
-
assert_eq!(
|
|
1343
|
+
assert_eq!(
|
|
1344
|
+
field_named(&aggregate, "id"),
|
|
1345
|
+
json!({"key": "id", "label": "Id", "optional": false, "shape": "text_value"})
|
|
1346
|
+
);
|
|
1127
1347
|
// …while an aggregate identified by any other attribute keeps
|
|
1128
1348
|
// its real wrapped shape, `field: "value"` and all.
|
|
1129
1349
|
assert_eq!(field_named(&client(), "reference")["field"], "value");
|
|
@@ -1138,10 +1358,23 @@ mod tests {
|
|
|
1138
1358
|
// column list; `__state__` is appended because Client has a
|
|
1139
1359
|
// lifecycle.
|
|
1140
1360
|
assert_eq!(
|
|
1141
|
-
columns
|
|
1142
|
-
|
|
1361
|
+
columns
|
|
1362
|
+
.as_array()
|
|
1363
|
+
.expect("columns")
|
|
1364
|
+
.iter()
|
|
1365
|
+
.map(|c| c["key"].clone())
|
|
1366
|
+
.collect::<Vec<Value>>(),
|
|
1367
|
+
vec![
|
|
1368
|
+
json!("reference"),
|
|
1369
|
+
json!("name"),
|
|
1370
|
+
json!("contact_email"),
|
|
1371
|
+
json!("__state__")
|
|
1372
|
+
]
|
|
1373
|
+
);
|
|
1374
|
+
assert_eq!(
|
|
1375
|
+
columns[3],
|
|
1376
|
+
json!({"key": "__state__", "label": "Status", "shape": "pill", "sortable": true, "sortDefault": "asc"})
|
|
1143
1377
|
);
|
|
1144
|
-
assert_eq!(columns[3], json!({"key": "__state__", "label": "Status", "shape": "pill", "sortable": true, "sortDefault": "asc"}));
|
|
1145
1378
|
assert_eq!(schema["collections"]["clients"]["stateField"], "status");
|
|
1146
1379
|
}
|
|
1147
1380
|
|
|
@@ -1153,7 +1386,11 @@ mod tests {
|
|
|
1153
1386
|
assert_eq!(invoices["stateField"], Value::Null);
|
|
1154
1387
|
assert_eq!(invoices["transitions"], json!({}));
|
|
1155
1388
|
assert!(
|
|
1156
|
-
!invoices["columns"]
|
|
1389
|
+
!invoices["columns"]
|
|
1390
|
+
.as_array()
|
|
1391
|
+
.expect("columns")
|
|
1392
|
+
.iter()
|
|
1393
|
+
.any(|c| c["key"] == "__state__"),
|
|
1157
1394
|
"{invoices}"
|
|
1158
1395
|
);
|
|
1159
1396
|
}
|
|
@@ -1201,7 +1438,12 @@ mod tests {
|
|
|
1201
1438
|
.as_array()
|
|
1202
1439
|
.expect("detail fields")
|
|
1203
1440
|
.iter()
|
|
1204
|
-
.map(|f|
|
|
1441
|
+
.map(|f| {
|
|
1442
|
+
(
|
|
1443
|
+
f["key"].as_str().unwrap_or("").to_string(),
|
|
1444
|
+
f["display"].clone(),
|
|
1445
|
+
)
|
|
1446
|
+
})
|
|
1205
1447
|
.collect();
|
|
1206
1448
|
|
|
1207
1449
|
assert_eq!(detail["line_items"], "rows");
|
|
@@ -1209,7 +1451,10 @@ mod tests {
|
|
|
1209
1451
|
|
|
1210
1452
|
let config = json!({"collections": {"Proposal": {"detail_fields": [{"field": "number", "display": "mono"}]}}});
|
|
1211
1453
|
let configured = build(&domain(), &config);
|
|
1212
|
-
assert_eq!(
|
|
1454
|
+
assert_eq!(
|
|
1455
|
+
configured["collections"]["proposals"]["detailFields"][0]["display"],
|
|
1456
|
+
"mono"
|
|
1457
|
+
);
|
|
1213
1458
|
}
|
|
1214
1459
|
|
|
1215
1460
|
#[test]
|
|
@@ -1217,8 +1462,14 @@ mod tests {
|
|
|
1217
1462
|
let schema = build(&domain(), &json!({}));
|
|
1218
1463
|
let transitions = &schema["collections"]["proposals"]["transitions"];
|
|
1219
1464
|
|
|
1220
|
-
assert_eq!(
|
|
1221
|
-
|
|
1465
|
+
assert_eq!(
|
|
1466
|
+
transitions["drafted"],
|
|
1467
|
+
json!([{"command": "send", "to": "sent", "label": "Send", "inputs": []}])
|
|
1468
|
+
);
|
|
1469
|
+
assert_eq!(
|
|
1470
|
+
transitions["sent"],
|
|
1471
|
+
json!([{"command": "accept", "to": "accepted", "label": "Accept", "inputs": []}])
|
|
1472
|
+
);
|
|
1222
1473
|
assert_eq!(transitions["accepted"], json!([]));
|
|
1223
1474
|
}
|
|
1224
1475
|
|
|
@@ -1246,7 +1497,10 @@ mod tests {
|
|
|
1246
1497
|
let schema = build(&domain, &json!({}));
|
|
1247
1498
|
|
|
1248
1499
|
assert_eq!(schema["collections"]["ledgers"]["transitions"], json!({}));
|
|
1249
|
-
assert_eq!(
|
|
1500
|
+
assert_eq!(
|
|
1501
|
+
schema["collections"]["ledgers"]["alwaysAvailable"][0]["command"],
|
|
1502
|
+
"post"
|
|
1503
|
+
);
|
|
1250
1504
|
}
|
|
1251
1505
|
|
|
1252
1506
|
#[test]
|
|
@@ -1261,7 +1515,12 @@ mod tests {
|
|
|
1261
1515
|
assert_eq!(create["command"], "register");
|
|
1262
1516
|
assert_eq!(create["label"], "Add a client");
|
|
1263
1517
|
assert_eq!(
|
|
1264
|
-
create["fields"]
|
|
1518
|
+
create["fields"]
|
|
1519
|
+
.as_array()
|
|
1520
|
+
.expect("fields")
|
|
1521
|
+
.iter()
|
|
1522
|
+
.map(|f| f["key"].clone())
|
|
1523
|
+
.collect::<Vec<Value>>(),
|
|
1265
1524
|
vec![json!("name")]
|
|
1266
1525
|
);
|
|
1267
1526
|
}
|
|
@@ -1289,14 +1548,25 @@ mod tests {
|
|
|
1289
1548
|
fn nav_keeps_declaration_order_until_nav_order_says_otherwise() {
|
|
1290
1549
|
let schema = build(&domain(), &json!({}));
|
|
1291
1550
|
assert_eq!(
|
|
1292
|
-
schema["nav"]
|
|
1551
|
+
schema["nav"]
|
|
1552
|
+
.as_array()
|
|
1553
|
+
.expect("nav")
|
|
1554
|
+
.iter()
|
|
1555
|
+
.map(|g| g["key"].clone())
|
|
1556
|
+
.collect::<Vec<Value>>(),
|
|
1293
1557
|
vec![json!("clients"), json!("proposals"), json!("invoices")]
|
|
1294
1558
|
);
|
|
1295
1559
|
|
|
1296
|
-
let config =
|
|
1560
|
+
let config =
|
|
1561
|
+
json!({"collections": {"Invoice": {"nav_order": 1}, "Client": {"nav_order": 2}}});
|
|
1297
1562
|
let ordered = build(&domain(), &config);
|
|
1298
1563
|
assert_eq!(
|
|
1299
|
-
ordered["nav"]
|
|
1564
|
+
ordered["nav"]
|
|
1565
|
+
.as_array()
|
|
1566
|
+
.expect("nav")
|
|
1567
|
+
.iter()
|
|
1568
|
+
.map(|g| g["key"].clone())
|
|
1569
|
+
.collect::<Vec<Value>>(),
|
|
1300
1570
|
vec![json!("invoices"), json!("clients"), json!("proposals")]
|
|
1301
1571
|
);
|
|
1302
1572
|
}
|
|
@@ -1311,7 +1581,10 @@ mod tests {
|
|
|
1311
1581
|
let nav = schema["nav"].as_array().expect("nav");
|
|
1312
1582
|
|
|
1313
1583
|
assert_eq!(nav.len(), 2);
|
|
1314
|
-
assert_eq!(
|
|
1584
|
+
assert_eq!(
|
|
1585
|
+
nav[0],
|
|
1586
|
+
json!({"key": "paperwork", "label": "Paperwork", "collections": ["proposals", "invoices"]})
|
|
1587
|
+
);
|
|
1315
1588
|
assert_eq!(nav[1]["key"], "clients");
|
|
1316
1589
|
}
|
|
1317
1590
|
|
|
@@ -1353,7 +1626,10 @@ mod tests {
|
|
|
1353
1626
|
let clients = &schema["collections"]["clients"];
|
|
1354
1627
|
|
|
1355
1628
|
assert_eq!(clients["statusLabels"], json!({"prospect": "Prospect"}));
|
|
1356
|
-
assert_eq!(
|
|
1629
|
+
assert_eq!(
|
|
1630
|
+
clients["statusTone"],
|
|
1631
|
+
json!({"prospect": "warn", "active": "good"})
|
|
1632
|
+
);
|
|
1357
1633
|
assert_eq!(clients["statusNotes"], json!({"active": "paying"}));
|
|
1358
1634
|
assert_eq!(clients["statusAttention"], json!(["active"]));
|
|
1359
1635
|
}
|
|
@@ -1377,7 +1653,7 @@ mod tests {
|
|
|
1377
1653
|
|
|
1378
1654
|
// Two aggregates: `Site` declares the value objects, `Deployment` types
|
|
1379
1655
|
// attributes by them without declaring any of its own.
|
|
1380
|
-
fn shared_domain(extra_deployment_vos: Value) -> Value {
|
|
1656
|
+
fn shared_domain(extra_deployment_vos: &Value) -> Value {
|
|
1381
1657
|
let vo = |name: &str, attrs: Value| json!({"name": name, "attributes": attrs, "closed_set": false, "members": []});
|
|
1382
1658
|
json!({
|
|
1383
1659
|
"name": "Fleet",
|
|
@@ -1419,7 +1695,7 @@ mod tests {
|
|
|
1419
1695
|
|
|
1420
1696
|
#[test]
|
|
1421
1697
|
fn a_one_attribute_value_object_another_aggregate_declares_keeps_its_wrapped_field() {
|
|
1422
|
-
let schema = build(&shared_domain(json!([])), &json!({}));
|
|
1698
|
+
let schema = build(&shared_domain(&json!([])), &json!({}));
|
|
1423
1699
|
assert_eq!(
|
|
1424
1700
|
deployment_field(&schema, "site_key"),
|
|
1425
1701
|
json!({"key": "site_key", "label": "Site Key", "optional": false, "shape": "text_value", "field": "value"})
|
|
@@ -1430,10 +1706,16 @@ mod tests {
|
|
|
1430
1706
|
}
|
|
1431
1707
|
|
|
1432
1708
|
#[test]
|
|
1433
|
-
fn compound_money_and_enum_value_objects_resolve_the_same_when_another_aggregate_declares_them()
|
|
1434
|
-
|
|
1709
|
+
fn compound_money_and_enum_value_objects_resolve_the_same_when_another_aggregate_declares_them()
|
|
1710
|
+
{
|
|
1711
|
+
let schema = build(&shared_domain(&json!([])), &json!({}));
|
|
1435
1712
|
assert_eq!(deployment_field(&schema, "window")["shape"], "compound");
|
|
1436
|
-
assert_eq!(
|
|
1713
|
+
assert_eq!(
|
|
1714
|
+
deployment_field(&schema, "window")["fields"]
|
|
1715
|
+
.as_array()
|
|
1716
|
+
.map(Vec::len),
|
|
1717
|
+
Some(2)
|
|
1718
|
+
);
|
|
1437
1719
|
assert_eq!(deployment_field(&schema, "price")["shape"], "money");
|
|
1438
1720
|
let tier = deployment_field(&schema, "tier");
|
|
1439
1721
|
assert_eq!(tier["shape"], "enum");
|
|
@@ -1453,11 +1735,11 @@ mod tests {
|
|
|
1453
1735
|
fn a_name_two_aggregates_declare_resolves_to_the_owner_then_the_first_declared() {
|
|
1454
1736
|
// The owner's own `SiteKey` (attribute `text`) shadows Site's.
|
|
1455
1737
|
let own = json!([{"name": "SiteKey", "attributes": [{"name": "text", "type": "String"}], "closed_set": false, "members": []}]);
|
|
1456
|
-
let schema = build(&shared_domain(own), &json!({}));
|
|
1738
|
+
let schema = build(&shared_domain(&own), &json!({}));
|
|
1457
1739
|
assert_eq!(deployment_field(&schema, "site_key")["field"], "text");
|
|
1458
1740
|
|
|
1459
1741
|
// Two other aggregates declaring one name: the earlier one wins.
|
|
1460
|
-
let mut domain = shared_domain(json!([]));
|
|
1742
|
+
let mut domain = shared_domain(&json!([]));
|
|
1461
1743
|
domain["aggregates"].as_array_mut().unwrap().push(json!({
|
|
1462
1744
|
"name": "Probe", "identified_by": ["id"],
|
|
1463
1745
|
"attributes": [{"name": "site_key", "type": "SiteKey", "list": false, "optional": false}],
|
|
@@ -1471,8 +1753,14 @@ mod tests {
|
|
|
1471
1753
|
}));
|
|
1472
1754
|
let schema = build(&domain, &json!({}));
|
|
1473
1755
|
// Site precedes Probe in declaration order, so Other reads Site's.
|
|
1474
|
-
assert_eq!(
|
|
1756
|
+
assert_eq!(
|
|
1757
|
+
schema["collections"]["others"]["fields"][0]["field"],
|
|
1758
|
+
"value"
|
|
1759
|
+
);
|
|
1475
1760
|
// Probe's own declaration still wins for Probe.
|
|
1476
|
-
assert_eq!(
|
|
1761
|
+
assert_eq!(
|
|
1762
|
+
schema["collections"]["probes"]["fields"][0]["field"],
|
|
1763
|
+
"text"
|
|
1764
|
+
);
|
|
1477
1765
|
}
|
|
1478
1766
|
}
|