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
|
@@ -12,8 +12,17 @@ use std::collections::HashMap;
|
|
|
12
12
|
///
|
|
13
13
|
/// Local entities are checked first: value objects are merged domain-wide, so a same-named
|
|
14
14
|
/// value object (Command's `Argument`) must not shadow Syntax's own `Argument` entity.
|
|
15
|
-
pub fn append_element<'a>(
|
|
16
|
-
|
|
15
|
+
pub fn append_element<'a>(
|
|
16
|
+
aggregate: &'a Json,
|
|
17
|
+
target_type: &str,
|
|
18
|
+
value_objects_by_name: &HashMap<String, &'a Json>,
|
|
19
|
+
) -> Option<&'a Json> {
|
|
20
|
+
if let Some(local) = aggregate
|
|
21
|
+
.get("entities")
|
|
22
|
+
.map_or_default(Json::each)
|
|
23
|
+
.iter()
|
|
24
|
+
.find(|e| e.get("name").and_then(Json::as_str) == Some(target_type))
|
|
25
|
+
{
|
|
17
26
|
return Some(local);
|
|
18
27
|
}
|
|
19
28
|
value_objects_by_name.get(target_type).copied()
|
|
@@ -21,8 +30,11 @@ pub fn append_element<'a>(aggregate: &'a Json, target_type: &str, value_objects_
|
|
|
21
30
|
|
|
22
31
|
/// Returns an entity's identity attribute and its single-field value object, when it can be
|
|
23
32
|
/// auto-minted at append time.
|
|
24
|
-
pub fn entity_identity_mint<'a>(
|
|
25
|
-
|
|
33
|
+
pub fn entity_identity_mint<'a>(
|
|
34
|
+
entity: &'a Json,
|
|
35
|
+
value_objects_by_name: &HashMap<String, &'a Json>,
|
|
36
|
+
) -> Option<(&'a Json, &'a Json)> {
|
|
37
|
+
let identified_by = entity.get("identified_by").map_or_default(Json::each);
|
|
26
38
|
let id_path = identified_by.first()?.as_str()?;
|
|
27
39
|
let mut parts = id_path.split('.');
|
|
28
40
|
let head = parts.next()?;
|
|
@@ -31,13 +43,15 @@ pub fn entity_identity_mint<'a>(entity: &'a Json, value_objects_by_name: &HashMa
|
|
|
31
43
|
return None;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
let attrs = entity.get("attributes").
|
|
46
|
+
let attrs = entity.get("attributes").map_or_default(Json::each);
|
|
35
47
|
let attr = attrs.iter().find(|a| crate::attr::name(a) == head)?;
|
|
36
|
-
let vo = value_objects_by_name
|
|
37
|
-
|
|
48
|
+
let vo = value_objects_by_name
|
|
49
|
+
.get(crate::attr::type_name(attr))
|
|
50
|
+
.copied()?;
|
|
51
|
+
if vo.get("closed_set").is_some_and(Json::as_bool) {
|
|
38
52
|
return None;
|
|
39
53
|
}
|
|
40
|
-
let vo_attrs = vo.get("attributes").
|
|
54
|
+
let vo_attrs = vo.get("attributes").map_or_default(Json::each);
|
|
41
55
|
if vo_attrs.len() != 1 || crate::attr::name(&vo_attrs[0]) != rest[0] {
|
|
42
56
|
return None;
|
|
43
57
|
}
|
|
@@ -50,26 +64,39 @@ pub fn entity_identity_mint<'a>(entity: &'a Json, value_objects_by_name: &HashMa
|
|
|
50
64
|
/// Every `identified_by` head is compared at once (not just the first), so a composite identity
|
|
51
65
|
/// is guarded the same as a single-field one — see `check_entity_collision` in
|
|
52
66
|
/// `entity_element.rb` (Ruby's `heads.all?`).
|
|
53
|
-
pub fn entity_list_replace_guard
|
|
67
|
+
pub fn entity_list_replace_guard(
|
|
68
|
+
aggregate: &Json,
|
|
69
|
+
target_attr: &Json,
|
|
70
|
+
target_field: &str,
|
|
71
|
+
rhs: &str,
|
|
72
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
73
|
+
) -> (String, String) {
|
|
54
74
|
let target_type = crate::attr::type_name(target_attr);
|
|
55
|
-
let entity = aggregate
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
let entity = aggregate
|
|
76
|
+
.get("entities")
|
|
77
|
+
.map_or_default(Json::each)
|
|
78
|
+
.iter()
|
|
79
|
+
.find(|e| e.get("name").and_then(Json::as_str) == Some(target_type));
|
|
80
|
+
let Some(entity) = entity else {
|
|
81
|
+
return (String::new(), rhs.to_string());
|
|
59
82
|
};
|
|
60
|
-
let identified_by = entity.get("identified_by").
|
|
83
|
+
let identified_by = entity.get("identified_by").map_or_default(Json::each);
|
|
61
84
|
if identified_by.is_empty() {
|
|
62
85
|
return (String::new(), rhs.to_string());
|
|
63
86
|
}
|
|
64
87
|
|
|
65
|
-
let attrs = entity.get("attributes").
|
|
88
|
+
let attrs = entity.get("attributes").map_or_default(Json::each);
|
|
66
89
|
let mut checks = Vec::new();
|
|
67
90
|
let mut offered_exprs = Vec::new();
|
|
68
91
|
for path in identified_by {
|
|
69
|
-
let id_head = path
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
let id_head = path
|
|
93
|
+
.to_s()
|
|
94
|
+
.split('.')
|
|
95
|
+
.next()
|
|
96
|
+
.unwrap_or_default()
|
|
97
|
+
.to_string();
|
|
98
|
+
let Some(id_attr) = attrs.iter().find(|a| crate::attr::name(a) == id_head) else {
|
|
99
|
+
return (String::new(), rhs.to_string());
|
|
73
100
|
};
|
|
74
101
|
let id_field = naming::rust_ident_field(crate::attr::name(id_attr));
|
|
75
102
|
checks.push(format!("prior.{id_field} == e.{id_field}"));
|
|
@@ -77,10 +104,17 @@ pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, targe
|
|
|
77
104
|
// Unwrap a single-field identity so `offered` prints the bare scalar, as
|
|
78
105
|
// `Rendering.describe` does; Debug-printing the value object would give
|
|
79
106
|
// `EntrySequence { value: 1 }`.
|
|
80
|
-
let id_vo = value_objects_by_name
|
|
107
|
+
let id_vo = value_objects_by_name
|
|
108
|
+
.get(crate::attr::type_name(id_attr))
|
|
109
|
+
.copied();
|
|
81
110
|
let offered_expr = match id_vo {
|
|
82
|
-
Some(vo)
|
|
83
|
-
|
|
111
|
+
Some(vo)
|
|
112
|
+
if !vo.get("closed_set").is_some_and(Json::as_bool)
|
|
113
|
+
&& vo.get("attributes").map_or_default(Json::each).len() == 1 =>
|
|
114
|
+
{
|
|
115
|
+
let inner = naming::rust_ident_field(crate::attr::name(
|
|
116
|
+
&vo.get("attributes").map_or_default(Json::each)[0],
|
|
117
|
+
));
|
|
84
118
|
format!("e.{id_field}.{inner}")
|
|
85
119
|
}
|
|
86
120
|
_ => format!("e.{id_field}"),
|
|
@@ -89,15 +123,31 @@ pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, targe
|
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
let local_var = format!("replaced_{target_field}");
|
|
92
|
-
let entity_name = entity
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
let entity_name = entity
|
|
127
|
+
.get("name")
|
|
128
|
+
.and_then(Json::as_str)
|
|
129
|
+
.unwrap_or_default();
|
|
130
|
+
let aggregate_name = aggregate
|
|
131
|
+
.get("name")
|
|
132
|
+
.and_then(Json::as_str)
|
|
133
|
+
.unwrap_or_default();
|
|
134
|
+
let identity_reading = identified_by
|
|
135
|
+
.iter()
|
|
136
|
+
.map(Json::to_s)
|
|
137
|
+
.collect::<Vec<_>>()
|
|
138
|
+
.join(", ");
|
|
95
139
|
let entity_lit = naming::ruby_inspect_string(entity_name);
|
|
96
140
|
let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
|
|
97
141
|
let identity_lit = naming::ruby_inspect_string(&identity_reading);
|
|
98
142
|
let condition = checks.join(" && ");
|
|
99
|
-
let offered_lets: Vec<String> = offered_exprs
|
|
100
|
-
|
|
143
|
+
let offered_lets: Vec<String> = offered_exprs
|
|
144
|
+
.iter()
|
|
145
|
+
.enumerate()
|
|
146
|
+
.map(|(i, expr)| format!("let offered_{i} = format!(\"{{:?}}\", {expr});"))
|
|
147
|
+
.collect();
|
|
148
|
+
let offered_refs: Vec<String> = (0..offered_exprs.len())
|
|
149
|
+
.map(|i| format!("offered_{i}.as_str()"))
|
|
150
|
+
.collect();
|
|
101
151
|
let guard = format!(
|
|
102
152
|
"let {local_var} = {rhs};\n for (i, e) in {local_var}.iter().enumerate() {{ if {local_var}[..i].iter().any(|prior| {condition}) {{ {} let offered = [{}]; return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &offered }}.render_args())); }} }}\n ",
|
|
103
153
|
offered_lets.join(" "), offered_refs.join(", ")
|
|
@@ -111,6 +161,10 @@ pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, targe
|
|
|
111
161
|
/// `EntityElement.check_entity_collision`'s `heads.all?` on the Ruby side. Called only once every
|
|
112
162
|
/// head is confirmed present in the append's own `fields` map (composite identities never mint).
|
|
113
163
|
#[allow(clippy::too_many_arguments)]
|
|
164
|
+
#[allow(
|
|
165
|
+
clippy::expect_used,
|
|
166
|
+
reason = "the message names an invariant an earlier check in this generator established"
|
|
167
|
+
)]
|
|
114
168
|
fn composite_append_collision_guard(
|
|
115
169
|
heads: &[String],
|
|
116
170
|
fields: &[(String, String)],
|
|
@@ -140,10 +194,17 @@ fn composite_append_collision_guard(
|
|
|
140
194
|
// Unwrap a single-field identity component so `offered` prints the bare scalar, as
|
|
141
195
|
// `Rendering.describe` does; Debug-printing the value object would give
|
|
142
196
|
// `LineSequence { value: 654 }`.
|
|
143
|
-
let vo = value_objects_by_name
|
|
197
|
+
let vo = value_objects_by_name
|
|
198
|
+
.get(crate::attr::type_name(field_attr))
|
|
199
|
+
.copied();
|
|
144
200
|
let offered_expr = match vo {
|
|
145
|
-
Some(vo)
|
|
146
|
-
|
|
201
|
+
Some(vo)
|
|
202
|
+
if !vo.get("closed_set").is_some_and(Json::as_bool)
|
|
203
|
+
&& vo.get("attributes").map_or_default(Json::each).len() == 1 =>
|
|
204
|
+
{
|
|
205
|
+
let inner = naming::rust_ident_field(crate::attr::name(
|
|
206
|
+
&vo.get("attributes").map_or_default(Json::each)[0],
|
|
207
|
+
));
|
|
147
208
|
format!("{rhs}.{inner}")
|
|
148
209
|
}
|
|
149
210
|
_ => rhs,
|
|
@@ -151,9 +212,19 @@ fn composite_append_collision_guard(
|
|
|
151
212
|
offered_exprs.push(offered_expr);
|
|
152
213
|
}
|
|
153
214
|
|
|
154
|
-
let entity_name = entity
|
|
155
|
-
|
|
156
|
-
|
|
215
|
+
let entity_name = entity
|
|
216
|
+
.get("name")
|
|
217
|
+
.and_then(Json::as_str)
|
|
218
|
+
.unwrap_or_default();
|
|
219
|
+
let aggregate_name = aggregate
|
|
220
|
+
.get("name")
|
|
221
|
+
.and_then(Json::as_str)
|
|
222
|
+
.unwrap_or_default();
|
|
223
|
+
let identity_reading = identified_by
|
|
224
|
+
.iter()
|
|
225
|
+
.map(Json::to_s)
|
|
226
|
+
.collect::<Vec<_>>()
|
|
227
|
+
.join(", ");
|
|
157
228
|
let entity_lit = naming::ruby_inspect_string(entity_name);
|
|
158
229
|
let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
|
|
159
230
|
let identity_lit = naming::ruby_inspect_string(&identity_reading);
|
|
@@ -163,7 +234,9 @@ fn composite_append_collision_guard(
|
|
|
163
234
|
.enumerate()
|
|
164
235
|
.map(|(i, expr)| format!("let offered_{i} = format!(\"{{:?}}\", {expr});"))
|
|
165
236
|
.collect();
|
|
166
|
-
let offered_refs: Vec<String> = (0..offered_exprs.len())
|
|
237
|
+
let offered_refs: Vec<String> = (0..offered_exprs.len())
|
|
238
|
+
.map(|i| format!("offered_{i}.as_str()"))
|
|
239
|
+
.collect();
|
|
167
240
|
|
|
168
241
|
format!(
|
|
169
242
|
"if record.{target_field}.iter().any(|e| {condition}) {{ {} let offered = [{}]; return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &offered }}.render_args())); }}\n ",
|
|
@@ -179,21 +252,97 @@ pub fn append_field_source(source: &str) -> Literal {
|
|
|
179
252
|
/// Whether any effect reads the record's own state (a `state(:field)` source, or an expression that
|
|
180
253
|
/// may name a field), so the closure must bind `let pre`.
|
|
181
254
|
pub fn reads_pre_state(mutations: &[Json]) -> bool {
|
|
182
|
-
mutations.iter().any(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
255
|
+
mutations.iter().any(
|
|
256
|
+
|m| match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
|
|
257
|
+
"set" => matches!(
|
|
258
|
+
m.get("source")
|
|
259
|
+
.and_then(|s| s.get("kind"))
|
|
260
|
+
.map(Json::to_s)
|
|
261
|
+
.unwrap_or_default()
|
|
262
|
+
.as_str(),
|
|
263
|
+
"state" | "expression"
|
|
264
|
+
),
|
|
265
|
+
"append" => match m.get("fields") {
|
|
266
|
+
Some(Json::Object(pairs)) => pairs
|
|
267
|
+
.iter()
|
|
268
|
+
.any(|(_, source)| source.to_s().starts_with("state(:")),
|
|
269
|
+
_ => false,
|
|
270
|
+
},
|
|
186
271
|
_ => false,
|
|
187
272
|
},
|
|
188
|
-
|
|
189
|
-
})
|
|
273
|
+
)
|
|
190
274
|
}
|
|
191
275
|
|
|
192
276
|
pub fn pre_state_line() -> String {
|
|
193
277
|
" let pre = record.clone();".to_string()
|
|
194
278
|
}
|
|
195
279
|
|
|
280
|
+
/// The value an arithmetic op (`increment`/`decrement`/`multiply`/`clamp`) reads from its target.
|
|
281
|
+
///
|
|
282
|
+
/// An absent optional target reads as 0 (C4.4), the integer field zeroed and every other field at
|
|
283
|
+
/// its declared default. A value object whose other fields lack defaults has no sensible zero, so
|
|
284
|
+
/// that case keeps the unwrap.
|
|
285
|
+
fn numeric_current(
|
|
286
|
+
optional: bool,
|
|
287
|
+
target_field: &str,
|
|
288
|
+
target_attr: &Json,
|
|
289
|
+
numeric_field: &str,
|
|
290
|
+
kind: crate::bridging::NumericKind,
|
|
291
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
292
|
+
) -> String {
|
|
293
|
+
if !optional {
|
|
294
|
+
return format!("record.{target_field}.clone()");
|
|
295
|
+
}
|
|
296
|
+
let vo_type = crate::attr::type_name(target_attr);
|
|
297
|
+
let attrs = value_objects_by_name
|
|
298
|
+
.get(vo_type)
|
|
299
|
+
.and_then(|vo| vo.get("attributes"))
|
|
300
|
+
.map_or_default(Json::each);
|
|
301
|
+
let zero_fields: Option<Vec<String>> = attrs
|
|
302
|
+
.iter()
|
|
303
|
+
.map(|f| {
|
|
304
|
+
let name = crate::attr::name(f);
|
|
305
|
+
let rhs = if name == numeric_field {
|
|
306
|
+
Some(
|
|
307
|
+
match kind {
|
|
308
|
+
crate::bridging::NumericKind::Integer => "0",
|
|
309
|
+
crate::bridging::NumericKind::Float => "0.0",
|
|
310
|
+
}
|
|
311
|
+
.to_string(),
|
|
312
|
+
)
|
|
313
|
+
} else {
|
|
314
|
+
crate::attr::default(f).map(naming::literal_rhs)
|
|
315
|
+
};
|
|
316
|
+
rhs.map(|rhs| format!("{}: {rhs}", naming::rust_ident_field(name)))
|
|
317
|
+
})
|
|
318
|
+
.collect();
|
|
319
|
+
zero_fields.map_or_else(
|
|
320
|
+
|| format!("record.{target_field}.clone().unwrap()"),
|
|
321
|
+
|fields| {
|
|
322
|
+
format!(
|
|
323
|
+
"record.{target_field}.clone().unwrap_or({} {{ {} }})",
|
|
324
|
+
naming::rust_ident(vo_type),
|
|
325
|
+
fields.join(", ")
|
|
326
|
+
)
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// The mutation for a text operand: refuses `TypeMismatch` as Ruby's rewrap of the amount does.
|
|
332
|
+
///
|
|
333
|
+
/// The `?` on an `Err` keeps the closure's `Ok(())` reachable, and the offered text renders
|
|
334
|
+
/// through `Json::describe`, as every refusal quotes a value.
|
|
335
|
+
fn text_operand_refusal(target_type: &str, integer_field: &str, text_expr: &str) -> String {
|
|
336
|
+
format!(
|
|
337
|
+
"Err::<(), _>(crate::kernel::Refusal::TypeMismatch(format!(\"{target_type}.{integer_field} expects Integer, got {{}}\", crate::kernel::Json::Str({text_expr}.clone()).describe())))?;"
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
196
341
|
// Integer arithmetic that leaves signed 64 bits is a `Refusal::Fault`, never a wrap or a panic.
|
|
342
|
+
#[allow(
|
|
343
|
+
clippy::panic,
|
|
344
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
345
|
+
)]
|
|
197
346
|
fn checked_arithmetic(op: &str, field_ident: &str, symbol: &str, amount_expr: &str) -> String {
|
|
198
347
|
let checked = match symbol {
|
|
199
348
|
"+" => "checked_add",
|
|
@@ -206,19 +355,61 @@ fn checked_arithmetic(op: &str, field_ident: &str, symbol: &str, amount_expr: &s
|
|
|
206
355
|
)
|
|
207
356
|
}
|
|
208
357
|
|
|
209
|
-
|
|
210
|
-
|
|
358
|
+
// A Float result must stay finite (C3.4); a NaN or infinity is a `Refusal::Fault`, worded as Ruby words it.
|
|
359
|
+
fn float_arithmetic(op: &str, field_ident: &str, symbol: &str, amount_expr: &str) -> String {
|
|
360
|
+
format!(
|
|
361
|
+
"{{ let amount = {amount_expr}; crate::kernel::ruby_float::finite_or_fault(\"{op}\", \"{symbol}\", current.{field_ident}, amount, current.{field_ident} {symbol} amount)? }}"
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// The new numeric field value: `checked_*` for an Integer target, `finite_or_fault` for a Float one.
|
|
366
|
+
fn arithmetic_field_value(
|
|
367
|
+
kind: crate::bridging::NumericKind,
|
|
368
|
+
op: &str,
|
|
369
|
+
field_ident: &str,
|
|
370
|
+
symbol: &str,
|
|
371
|
+
amount_expr: &str,
|
|
372
|
+
) -> String {
|
|
373
|
+
match kind {
|
|
374
|
+
crate::bridging::NumericKind::Integer => {
|
|
375
|
+
checked_arithmetic(op, field_ident, symbol, amount_expr)
|
|
376
|
+
}
|
|
377
|
+
crate::bridging::NumericKind::Float => {
|
|
378
|
+
float_arithmetic(op, field_ident, symbol, amount_expr)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
pub fn literal_problem(
|
|
384
|
+
mutation: &Json,
|
|
385
|
+
field_name: &str,
|
|
386
|
+
lit: &Literal,
|
|
387
|
+
field_attr: &Json,
|
|
388
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
389
|
+
) -> Option<String> {
|
|
390
|
+
if crate::bridging::literal_set_bridgeable(
|
|
391
|
+
lit,
|
|
392
|
+
Some(crate::attr::type_name(field_attr)),
|
|
393
|
+
value_objects_by_name,
|
|
394
|
+
) {
|
|
211
395
|
return None;
|
|
212
396
|
}
|
|
213
397
|
let target = mutation.get("target").map(Json::to_s).unwrap_or_default();
|
|
214
|
-
Some(format!(
|
|
398
|
+
Some(format!(
|
|
399
|
+
"{target}.{field_name}: literal doesn't bridge to {}",
|
|
400
|
+
crate::attr::type_name(field_attr)
|
|
401
|
+
))
|
|
215
402
|
}
|
|
216
403
|
|
|
217
404
|
/// Problems with `set`/`append` fields sourced from `state(:field)`: an undeclared state field,
|
|
218
405
|
/// or a type or cardinality that differs from the target's.
|
|
219
|
-
pub fn state_source_problems(
|
|
220
|
-
|
|
221
|
-
|
|
406
|
+
pub fn state_source_problems(
|
|
407
|
+
command: &Json,
|
|
408
|
+
aggregate: &Json,
|
|
409
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
410
|
+
) -> Vec<String> {
|
|
411
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
412
|
+
let mutations = command.get("mutations").map_or_default(Json::each);
|
|
222
413
|
mutations
|
|
223
414
|
.iter()
|
|
224
415
|
.flat_map(|m| {
|
|
@@ -226,23 +417,56 @@ pub fn state_source_problems(command: &Json, aggregate: &Json, value_objects_by_
|
|
|
226
417
|
match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
|
|
227
418
|
"set" => {
|
|
228
419
|
let source = m.get("source");
|
|
229
|
-
if source
|
|
420
|
+
if source
|
|
421
|
+
.and_then(|s| s.get("kind"))
|
|
422
|
+
.map(Json::to_s)
|
|
423
|
+
.unwrap_or_default()
|
|
424
|
+
!= "state"
|
|
425
|
+
{
|
|
230
426
|
return Vec::new();
|
|
231
427
|
}
|
|
232
|
-
let state_name = source
|
|
233
|
-
|
|
428
|
+
let state_name = source
|
|
429
|
+
.and_then(|s| s.get("name"))
|
|
430
|
+
.map(Json::to_s)
|
|
431
|
+
.unwrap_or_default();
|
|
432
|
+
state_source_problem(
|
|
433
|
+
&target,
|
|
434
|
+
&state_name,
|
|
435
|
+
aggregate,
|
|
436
|
+
attrs.iter().find(|a| crate::attr::name(a) == target),
|
|
437
|
+
)
|
|
438
|
+
.into_iter()
|
|
439
|
+
.collect()
|
|
234
440
|
}
|
|
235
441
|
"append" => {
|
|
236
|
-
let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
442
|
+
let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target)
|
|
443
|
+
else {
|
|
444
|
+
return Vec::new();
|
|
445
|
+
};
|
|
446
|
+
let Some(element) = append_element(
|
|
447
|
+
aggregate,
|
|
448
|
+
crate::attr::type_name(target_attr),
|
|
449
|
+
value_objects_by_name,
|
|
450
|
+
) else {
|
|
451
|
+
return Vec::new();
|
|
452
|
+
};
|
|
453
|
+
let element_attrs = element.get("attributes").map_or_default(Json::each);
|
|
454
|
+
let Some(Json::Object(fields)) = m.get("fields") else {
|
|
455
|
+
return Vec::new();
|
|
456
|
+
};
|
|
240
457
|
fields
|
|
241
458
|
.iter()
|
|
242
459
|
.filter_map(|(field_name, source)| {
|
|
243
460
|
let text = source.to_s();
|
|
244
461
|
let state_name = text.strip_prefix("state(:")?.strip_suffix(')')?;
|
|
245
|
-
state_source_problem(
|
|
462
|
+
state_source_problem(
|
|
463
|
+
&format!("{target}.{field_name}"),
|
|
464
|
+
state_name,
|
|
465
|
+
aggregate,
|
|
466
|
+
element_attrs
|
|
467
|
+
.iter()
|
|
468
|
+
.find(|a| crate::attr::name(a) == field_name),
|
|
469
|
+
)
|
|
246
470
|
})
|
|
247
471
|
.collect()
|
|
248
472
|
}
|
|
@@ -255,12 +479,15 @@ pub fn state_source_problems(command: &Json, aggregate: &Json, value_objects_by_
|
|
|
255
479
|
/// Problems with `set` mutations sourced from arithmetic: the target must be a scalar
|
|
256
480
|
/// (non-list) attribute typed by a single-field Integer or Float value object, the one shape the
|
|
257
481
|
/// generated code can rebuild a computed number into.
|
|
258
|
-
pub fn expression_source_problems(
|
|
259
|
-
|
|
482
|
+
pub fn expression_source_problems(
|
|
483
|
+
command: &Json,
|
|
484
|
+
aggregate: &Json,
|
|
485
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
486
|
+
) -> Vec<String> {
|
|
487
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
260
488
|
command
|
|
261
489
|
.get("mutations")
|
|
262
|
-
.
|
|
263
|
-
.unwrap_or(&[])
|
|
490
|
+
.map_or_default(Json::each)
|
|
264
491
|
.iter()
|
|
265
492
|
.filter(|m| m.get("op").map(Json::to_s).as_deref() == Some("set"))
|
|
266
493
|
.filter(|m| m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).as_deref() == Some("expression"))
|
|
@@ -276,13 +503,21 @@ pub fn expression_source_problems(command: &Json, aggregate: &Json, value_object
|
|
|
276
503
|
}
|
|
277
504
|
|
|
278
505
|
// The scalar type (`Integer` or `Float`) of the one field of a non-list, single-field value object.
|
|
279
|
-
fn computed_field_type<'a>(
|
|
506
|
+
fn computed_field_type<'a>(
|
|
507
|
+
attr: &Json,
|
|
508
|
+
value_objects_by_name: &HashMap<String, &'a Json>,
|
|
509
|
+
) -> Option<(&'a Json, &'static str)> {
|
|
280
510
|
if crate::attr::list(attr) {
|
|
281
511
|
return None;
|
|
282
512
|
}
|
|
283
|
-
let vo = value_objects_by_name
|
|
284
|
-
|
|
285
|
-
|
|
513
|
+
let vo = value_objects_by_name
|
|
514
|
+
.get(crate::attr::type_name(attr))
|
|
515
|
+
.copied()?;
|
|
516
|
+
let fields = vo.get("attributes").map_or_default(Json::each);
|
|
517
|
+
if vo.get("closed_set").is_some_and(Json::as_bool)
|
|
518
|
+
|| fields.len() != 1
|
|
519
|
+
|| crate::attr::list(&fields[0])
|
|
520
|
+
{
|
|
286
521
|
return None;
|
|
287
522
|
}
|
|
288
523
|
match crate::attr::type_name(&fields[0]) {
|
|
@@ -295,10 +530,23 @@ fn computed_field_type<'a>(attr: &Json, value_objects_by_name: &HashMap<String,
|
|
|
295
530
|
/// Right-hand side for a `set` sourced from arithmetic: evaluates the expression through the
|
|
296
531
|
/// kernel over the command's arguments and then the pre-dispatch record, and rebuilds the number
|
|
297
532
|
/// into the target's single-field value object. A fault returns from the mutation closure.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
533
|
+
#[allow(
|
|
534
|
+
clippy::expect_used,
|
|
535
|
+
reason = "the message names an invariant an earlier check in this generator established"
|
|
536
|
+
)]
|
|
537
|
+
pub fn expression_set_rhs(
|
|
538
|
+
source: &Json,
|
|
539
|
+
target_attr: &Json,
|
|
540
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
541
|
+
) -> String {
|
|
542
|
+
let (vo, scalar) = computed_field_type(target_attr, value_objects_by_name)
|
|
543
|
+
.expect("expression_source_problems already confirmed the target");
|
|
544
|
+
let ast = source
|
|
545
|
+
.get("ast")
|
|
546
|
+
.expect("an expression source carries its ast");
|
|
547
|
+
let field = naming::rust_ident_field(crate::attr::name(
|
|
548
|
+
&vo.get("attributes").map_or_default(Json::each)[0],
|
|
549
|
+
));
|
|
302
550
|
let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
|
|
303
551
|
let arms = if scalar == "Integer" {
|
|
304
552
|
"crate::kernel::Value::Int(number) => number,".to_string()
|
|
@@ -312,20 +560,34 @@ pub fn expression_set_rhs(source: &Json, target_attr: &Json, value_objects_by_na
|
|
|
312
560
|
)
|
|
313
561
|
}
|
|
314
562
|
|
|
315
|
-
fn state_source_problem(
|
|
316
|
-
|
|
563
|
+
fn state_source_problem(
|
|
564
|
+
label: &str,
|
|
565
|
+
state_name: &str,
|
|
566
|
+
aggregate: &Json,
|
|
567
|
+
target_attr: Option<&Json>,
|
|
568
|
+
) -> Option<String> {
|
|
569
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
317
570
|
let Some(state_attr) = attrs.iter().find(|a| crate::attr::name(a) == state_name) else {
|
|
318
571
|
let aggregate_name = aggregate.get("name").map(Json::to_s).unwrap_or_default();
|
|
319
|
-
return Some(format!(
|
|
572
|
+
return Some(format!(
|
|
573
|
+
"{label}: sources state(:{state_name}), which {aggregate_name} does not declare"
|
|
574
|
+
));
|
|
320
575
|
};
|
|
321
576
|
let Some(target_attr) = target_attr else {
|
|
322
577
|
return Some(format!("{label}: no such target field"));
|
|
323
578
|
};
|
|
324
|
-
let same = crate::attr::type_name(state_attr) == crate::attr::type_name(target_attr)
|
|
579
|
+
let same = crate::attr::type_name(state_attr) == crate::attr::type_name(target_attr)
|
|
580
|
+
&& crate::attr::list(state_attr) == crate::attr::list(target_attr);
|
|
325
581
|
if same {
|
|
326
582
|
return None;
|
|
327
583
|
}
|
|
328
|
-
let list_of = |a: &Json|
|
|
584
|
+
let list_of = |a: &Json| {
|
|
585
|
+
if crate::attr::list(a) {
|
|
586
|
+
"a list of "
|
|
587
|
+
} else {
|
|
588
|
+
""
|
|
589
|
+
}
|
|
590
|
+
};
|
|
329
591
|
Some(format!(
|
|
330
592
|
"{label}: state(:{state_name}) is {}{}, the target wants {}{} — not generated yet",
|
|
331
593
|
list_of(state_attr),
|
|
@@ -336,55 +598,109 @@ fn state_source_problem(label: &str, state_name: &str, aggregate: &Json, target_
|
|
|
336
598
|
}
|
|
337
599
|
|
|
338
600
|
/// Problems with each `append` mutation's fields, checked against the element they build.
|
|
339
|
-
|
|
340
|
-
|
|
601
|
+
#[allow(
|
|
602
|
+
clippy::unwrap_used,
|
|
603
|
+
reason = "the value was checked just before; None would mean invalid IR"
|
|
604
|
+
)]
|
|
605
|
+
pub fn append_field_problems(
|
|
606
|
+
command: &Json,
|
|
607
|
+
aggregate: &Json,
|
|
608
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
609
|
+
) -> Vec<String> {
|
|
610
|
+
let mutations = command.get("mutations").map_or_default(Json::each);
|
|
341
611
|
mutations
|
|
342
612
|
.iter()
|
|
343
613
|
.filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "append")
|
|
344
614
|
.flat_map(|m| {
|
|
345
615
|
let target_name = m.get("target").map(Json::to_s).unwrap_or_default();
|
|
346
|
-
let attrs = aggregate.get("attributes").
|
|
616
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
347
617
|
let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name);
|
|
348
|
-
let element = target_attr.and_then(|ta|
|
|
618
|
+
let element = target_attr.and_then(|ta| {
|
|
619
|
+
append_element(aggregate, crate::attr::type_name(ta), value_objects_by_name)
|
|
620
|
+
});
|
|
349
621
|
let Some(element) = element else {
|
|
350
|
-
let type_desc = target_attr.
|
|
351
|
-
return vec![format!(
|
|
622
|
+
let type_desc = target_attr.map_or("", crate::attr::type_name);
|
|
623
|
+
return vec![format!(
|
|
624
|
+
"{target_name}: element type {} not resolvable",
|
|
625
|
+
naming::ruby_inspect_string(type_desc)
|
|
626
|
+
)];
|
|
352
627
|
};
|
|
353
628
|
let target_attr = target_attr.unwrap();
|
|
354
629
|
|
|
355
630
|
let fields = m.get("fields").map(fields_pairs).unwrap_or_default();
|
|
356
|
-
let element_attrs = element.get("attributes").
|
|
631
|
+
let element_attrs = element.get("attributes").map_or_default(Json::each);
|
|
357
632
|
let mut problems: Vec<String> = fields
|
|
358
633
|
.iter()
|
|
359
634
|
.filter_map(|(field_name, source)| {
|
|
360
|
-
let field_attr = element_attrs
|
|
635
|
+
let field_attr = element_attrs
|
|
636
|
+
.iter()
|
|
637
|
+
.find(|a| crate::attr::name(a) == field_name.as_str());
|
|
361
638
|
let Some(field_attr) = field_attr else {
|
|
362
639
|
return Some(format!("{target_name}.{field_name}: not a declared field"));
|
|
363
640
|
};
|
|
364
641
|
|
|
365
642
|
let parsed = append_field_source(source);
|
|
366
643
|
let Literal::Symbol(arg_name) = &parsed else {
|
|
367
|
-
return literal_problem(
|
|
644
|
+
return literal_problem(
|
|
645
|
+
m,
|
|
646
|
+
field_name,
|
|
647
|
+
&parsed,
|
|
648
|
+
field_attr,
|
|
649
|
+
value_objects_by_name,
|
|
650
|
+
);
|
|
368
651
|
};
|
|
369
652
|
|
|
370
|
-
let cmd_attrs = command.get("attributes").
|
|
371
|
-
match cmd_attrs
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
653
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
654
|
+
match cmd_attrs
|
|
655
|
+
.iter()
|
|
656
|
+
.find(|a| crate::attr::name(a) == arg_name.as_str())
|
|
657
|
+
{
|
|
658
|
+
None => Some(format!(
|
|
659
|
+
"{target_name}.{field_name}: sources undeclared argument {arg_name}"
|
|
660
|
+
)),
|
|
661
|
+
Some(arg_attr)
|
|
662
|
+
if !crate::bridging::bridgeable_value_types(
|
|
663
|
+
crate::attr::type_name(arg_attr),
|
|
664
|
+
crate::attr::type_name(field_attr),
|
|
665
|
+
value_objects_by_name,
|
|
666
|
+
) =>
|
|
667
|
+
{
|
|
668
|
+
Some(format!(
|
|
669
|
+
"{target_name}.{field_name}: {} doesn't bridge to {}",
|
|
670
|
+
crate::attr::type_name(arg_attr),
|
|
671
|
+
crate::attr::type_name(field_attr)
|
|
672
|
+
))
|
|
375
673
|
}
|
|
376
674
|
_ => None,
|
|
377
675
|
}
|
|
378
676
|
})
|
|
379
677
|
.collect();
|
|
380
678
|
|
|
381
|
-
let entity = aggregate
|
|
679
|
+
let entity = aggregate
|
|
680
|
+
.get("entities")
|
|
681
|
+
.map_or_default(Json::each)
|
|
682
|
+
.iter()
|
|
683
|
+
.find(|e| {
|
|
684
|
+
e.get("name").and_then(Json::as_str)
|
|
685
|
+
== Some(crate::attr::type_name(target_attr))
|
|
686
|
+
});
|
|
382
687
|
if let Some(entity) = entity {
|
|
383
|
-
let
|
|
384
|
-
let
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
688
|
+
let identified_by = entity.get("identified_by").map_or_default(Json::each);
|
|
689
|
+
let id_head = identified_by
|
|
690
|
+
.first()
|
|
691
|
+
.and_then(Json::as_str)
|
|
692
|
+
.unwrap_or("")
|
|
693
|
+
.split('.')
|
|
694
|
+
.next()
|
|
695
|
+
.unwrap_or("")
|
|
696
|
+
.to_string();
|
|
697
|
+
if !fields.iter().map(|(k, _)| k.clone()).any(|x| x == id_head)
|
|
698
|
+
&& entity_identity_mint(entity, value_objects_by_name).is_none()
|
|
699
|
+
{
|
|
700
|
+
problems.push(format!(
|
|
701
|
+
"{target_name}: {}'s identity doesn't auto-mint",
|
|
702
|
+
entity.get("name").and_then(Json::as_str).unwrap_or("")
|
|
703
|
+
));
|
|
388
704
|
}
|
|
389
705
|
}
|
|
390
706
|
problems
|
|
@@ -392,27 +708,75 @@ pub fn append_field_problems(command: &Json, aggregate: &Json, value_objects_by_
|
|
|
392
708
|
.collect()
|
|
393
709
|
}
|
|
394
710
|
|
|
395
|
-
///
|
|
396
|
-
///
|
|
397
|
-
|
|
398
|
-
|
|
711
|
+
/// Why a `remove:` on a list of value objects can't be generated, or `None` when it can.
|
|
712
|
+
///
|
|
713
|
+
/// The argument must be a declared command attribute of the element's own value-object type, so
|
|
714
|
+
/// the generated `retain` compares whole values and nothing needs bridging.
|
|
715
|
+
fn value_object_remove_problem(
|
|
716
|
+
mutation: &Json,
|
|
717
|
+
command: &Json,
|
|
718
|
+
target_attr: &Json,
|
|
719
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
720
|
+
) -> Option<String> {
|
|
721
|
+
let target_name = mutation.get("target").map(Json::to_s).unwrap_or_default();
|
|
722
|
+
let element_type = crate::attr::type_name(target_attr);
|
|
723
|
+
if !value_objects_by_name.contains_key(element_type) {
|
|
724
|
+
return Some(format!(
|
|
725
|
+
"{target_name}: remove on a list of {element_type}, which is neither an entity nor a value object, is not generated yet"
|
|
726
|
+
));
|
|
727
|
+
}
|
|
728
|
+
let source = mutation.get("source").filter(|s| {
|
|
729
|
+
matches!(s, Json::Object(_))
|
|
730
|
+
&& s.get("kind").map(Json::to_s).unwrap_or_default() == "argument"
|
|
731
|
+
});
|
|
732
|
+
let Some(source) = source else {
|
|
733
|
+
return Some(format!(
|
|
734
|
+
"{target_name}: remove sources a literal or record state, not an argument — not generated yet"
|
|
735
|
+
));
|
|
736
|
+
};
|
|
737
|
+
let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
|
|
738
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
739
|
+
let Some(source_attr) = cmd_attrs
|
|
740
|
+
.iter()
|
|
741
|
+
.find(|a| crate::attr::name(a) == source_name)
|
|
742
|
+
else {
|
|
743
|
+
return Some(format!(
|
|
744
|
+
"{target_name}: remove sources undeclared argument {source_name}"
|
|
745
|
+
));
|
|
746
|
+
};
|
|
747
|
+
if crate::attr::list(source_attr) || crate::attr::type_name(source_attr) != element_type {
|
|
748
|
+
return Some(format!(
|
|
749
|
+
"{target_name}: remove on a list of {element_type} needs a single {element_type} argument, not {source_name}"
|
|
750
|
+
));
|
|
751
|
+
}
|
|
752
|
+
None
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/// Problems with `remove:` mutations: an entity list whose single-head identity mints, or a
|
|
756
|
+
/// value-object list, each sourced from a declared argument of a matching type.
|
|
757
|
+
pub fn remove_field_problems(
|
|
758
|
+
command: &Json,
|
|
759
|
+
aggregate: &Json,
|
|
760
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
761
|
+
) -> Vec<String> {
|
|
762
|
+
let mutations = command.get("mutations").map_or_default(Json::each);
|
|
399
763
|
mutations
|
|
400
764
|
.iter()
|
|
401
765
|
.filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "remove")
|
|
402
766
|
.filter_map(|m| {
|
|
403
767
|
let target_name = m.get("target").map(Json::to_s).unwrap_or_default();
|
|
404
|
-
let attrs = aggregate.get("attributes").
|
|
768
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
405
769
|
let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target_name).filter(|a| crate::attr::list(a)) else {
|
|
406
770
|
return Some(format!("{target_name}: not a declared list attribute"));
|
|
407
771
|
};
|
|
408
772
|
|
|
409
|
-
let entities = aggregate.get("entities").
|
|
773
|
+
let entities = aggregate.get("entities").map_or_default(Json::each);
|
|
410
774
|
let Some(entity) = entities.iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr))) else {
|
|
411
|
-
return
|
|
775
|
+
return value_object_remove_problem(m, command, target_attr, value_objects_by_name);
|
|
412
776
|
};
|
|
413
777
|
let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("");
|
|
414
778
|
|
|
415
|
-
if entity.get("identified_by").
|
|
779
|
+
if entity.get("identified_by").map_or_default(Json::each).len() != 1 {
|
|
416
780
|
return Some(format!("{target_name}: {entity_name}'s identity is composite — remove not generated yet"));
|
|
417
781
|
}
|
|
418
782
|
|
|
@@ -426,7 +790,7 @@ pub fn remove_field_problems(command: &Json, aggregate: &Json, value_objects_by_
|
|
|
426
790
|
};
|
|
427
791
|
|
|
428
792
|
let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
|
|
429
|
-
let cmd_attrs = command.get("attributes").
|
|
793
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
430
794
|
let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name) else {
|
|
431
795
|
return Some(format!("{target_name}: remove sources undeclared argument {source_name}"));
|
|
432
796
|
};
|
|
@@ -466,7 +830,11 @@ pub fn transition_check_arg(transition: Option<&Transition>) -> String {
|
|
|
466
830
|
Some(t) if !t.unconstrained => format!(
|
|
467
831
|
"Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
|
|
468
832
|
naming::ruby_inspect_string(&t.field),
|
|
469
|
-
t.from_states
|
|
833
|
+
t.from_states
|
|
834
|
+
.iter()
|
|
835
|
+
.map(|s| naming::ruby_inspect_string(s))
|
|
836
|
+
.collect::<Vec<_>>()
|
|
837
|
+
.join(", ")
|
|
470
838
|
),
|
|
471
839
|
_ => "None".to_string(),
|
|
472
840
|
}
|
|
@@ -476,8 +844,11 @@ pub fn transition_check_arg(transition: Option<&Transition>) -> String {
|
|
|
476
844
|
pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Transition> {
|
|
477
845
|
let lifecycle = aggregate.get("lifecycle")?;
|
|
478
846
|
let command_name = command.get("name").map(Json::to_s).unwrap_or_default();
|
|
479
|
-
let transitions = lifecycle.get("transitions").
|
|
480
|
-
let rows: Vec<&Json> = transitions
|
|
847
|
+
let transitions = lifecycle.get("transitions").map_or_default(Json::each);
|
|
848
|
+
let rows: Vec<&Json> = transitions
|
|
849
|
+
.iter()
|
|
850
|
+
.filter(|t| t.get("command").map(Json::to_s).unwrap_or_default() == command_name)
|
|
851
|
+
.collect();
|
|
481
852
|
if rows.is_empty() {
|
|
482
853
|
// `from:` without a transition guards the current state and moves nothing.
|
|
483
854
|
let froms: Vec<String> = match command.get("from") {
|
|
@@ -494,11 +865,24 @@ pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Tran
|
|
|
494
865
|
from_states.push(from);
|
|
495
866
|
}
|
|
496
867
|
}
|
|
497
|
-
let field = lifecycle
|
|
498
|
-
|
|
868
|
+
let field = lifecycle
|
|
869
|
+
.get("field")
|
|
870
|
+
.and_then(Json::as_str)
|
|
871
|
+
.unwrap_or("")
|
|
872
|
+
.to_string();
|
|
873
|
+
return Some(Transition {
|
|
874
|
+
field,
|
|
875
|
+
to_state: String::new(),
|
|
876
|
+
from_states,
|
|
877
|
+
unconstrained: false,
|
|
878
|
+
});
|
|
499
879
|
}
|
|
500
880
|
|
|
501
|
-
let field = lifecycle
|
|
881
|
+
let field = lifecycle
|
|
882
|
+
.get("field")
|
|
883
|
+
.and_then(Json::as_str)
|
|
884
|
+
.unwrap_or("")
|
|
885
|
+
.to_string();
|
|
502
886
|
let to_state = rows[0].get("to_state").map(Json::to_s).unwrap_or_default();
|
|
503
887
|
let mut from_states: Vec<String> = Vec::new();
|
|
504
888
|
let mut unconstrained = false;
|
|
@@ -513,26 +897,52 @@ pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Tran
|
|
|
513
897
|
}
|
|
514
898
|
}
|
|
515
899
|
}
|
|
516
|
-
Some(Transition {
|
|
900
|
+
Some(Transition {
|
|
901
|
+
field,
|
|
902
|
+
to_state,
|
|
903
|
+
from_states,
|
|
904
|
+
unconstrained,
|
|
905
|
+
})
|
|
517
906
|
}
|
|
518
907
|
|
|
519
908
|
/// Whether a `state(:field)` source reads an `Option` out of the pre-dispatch record: the field
|
|
520
909
|
/// is declared optional, or the record holds every field as an `Option` (`record_optional`).
|
|
521
910
|
/// Such a source already has the target's wrapping, so it must not be wrapped in `Some` again.
|
|
522
911
|
fn state_source_is_option(source: Option<&Json>, aggregate: &Json, record_optional: bool) -> bool {
|
|
523
|
-
let Some(source) =
|
|
912
|
+
let Some(source) =
|
|
913
|
+
source.filter(|s| s.get("kind").map(Json::to_s).unwrap_or_default() == "state")
|
|
914
|
+
else {
|
|
915
|
+
return false;
|
|
916
|
+
};
|
|
524
917
|
let name = source.get("name").map(Json::to_s).unwrap_or_default();
|
|
525
|
-
let attrs = aggregate.get("attributes").
|
|
526
|
-
record_optional
|
|
918
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
919
|
+
record_optional
|
|
920
|
+
|| attrs
|
|
921
|
+
.iter()
|
|
922
|
+
.any(|a| crate::attr::name(a) == name && crate::attr::optional(a))
|
|
527
923
|
}
|
|
528
924
|
|
|
529
925
|
/// Right-hand side for a `set`, coercing the source into the target attribute's declared type.
|
|
530
926
|
///
|
|
531
927
|
/// `target_list` routes list-to-list sets through `list_value_rhs`; `value_rhs` is scalar-only.
|
|
532
|
-
|
|
928
|
+
#[allow(
|
|
929
|
+
clippy::expect_used,
|
|
930
|
+
reason = "the message names an invariant an earlier check in this generator established"
|
|
931
|
+
)]
|
|
932
|
+
pub fn mutation_set_rhs(
|
|
933
|
+
source: &Json,
|
|
934
|
+
target_type: &str,
|
|
935
|
+
command: &Json,
|
|
936
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
937
|
+
target_list: bool,
|
|
938
|
+
) -> String {
|
|
533
939
|
if source.get("kind").map(Json::to_s).unwrap_or_default() == "literal" {
|
|
534
940
|
let value = source.get("value").unwrap_or(&Json::Null);
|
|
535
|
-
return crate::bridging::literal_rhs_for(
|
|
941
|
+
return crate::bridging::literal_rhs_for(
|
|
942
|
+
&Literal::from_json(value),
|
|
943
|
+
Some(target_type),
|
|
944
|
+
value_objects_by_name,
|
|
945
|
+
);
|
|
536
946
|
}
|
|
537
947
|
|
|
538
948
|
let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
|
|
@@ -540,13 +950,32 @@ pub fn mutation_set_rhs(source: &Json, target_type: &str, command: &Json, value_
|
|
|
540
950
|
if source.get("kind").map(Json::to_s).unwrap_or_default() == "state" {
|
|
541
951
|
return format!("pre.{}.clone()", naming::rust_ident_field(&source_name));
|
|
542
952
|
}
|
|
543
|
-
let cmd_attrs = command.get("attributes").
|
|
544
|
-
let source_attr = cmd_attrs
|
|
953
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
954
|
+
let source_attr = cmd_attrs
|
|
955
|
+
.iter()
|
|
956
|
+
.find(|a| crate::attr::name(a) == source_name)
|
|
957
|
+
.expect("mutation source argument must be a declared command attribute");
|
|
545
958
|
let source_expr = format!("args.{}", naming::rust_ident_field(&source_name));
|
|
546
|
-
if target_list
|
|
547
|
-
|
|
959
|
+
if target_list
|
|
960
|
+
&& crate::attr::list(source_attr)
|
|
961
|
+
&& crate::bridging::list_bridge_requires_element_mapping(
|
|
962
|
+
crate::attr::type_name(source_attr),
|
|
963
|
+
target_type,
|
|
964
|
+
)
|
|
965
|
+
{
|
|
966
|
+
return crate::bridging::list_value_rhs(
|
|
967
|
+
&source_expr,
|
|
968
|
+
crate::attr::type_name(source_attr),
|
|
969
|
+
target_type,
|
|
970
|
+
value_objects_by_name,
|
|
971
|
+
);
|
|
548
972
|
}
|
|
549
|
-
crate::bridging::value_rhs(
|
|
973
|
+
crate::bridging::value_rhs(
|
|
974
|
+
&source_expr,
|
|
975
|
+
crate::attr::type_name(source_attr),
|
|
976
|
+
target_type,
|
|
977
|
+
value_objects_by_name,
|
|
978
|
+
)
|
|
550
979
|
}
|
|
551
980
|
|
|
552
981
|
pub struct IdentityComponent {
|
|
@@ -558,13 +987,12 @@ pub struct IdentityComponent {
|
|
|
558
987
|
/// Builds one component per identity part: a `set` target or same-named command argument reads
|
|
559
988
|
/// `args.<head>`, and anything else becomes a parameter.
|
|
560
989
|
pub fn identity_components(aggregate: &Json, command: &Json) -> Vec<IdentityComponent> {
|
|
561
|
-
let identified_by = aggregate.get("identified_by").
|
|
562
|
-
let cmd_mutations = command.get("mutations").
|
|
990
|
+
let identified_by = aggregate.get("identified_by").map_or_default(Json::each);
|
|
991
|
+
let cmd_mutations = command.get("mutations").map_or_default(Json::each);
|
|
563
992
|
let append_claimed = crate::shared::append_claimed_names(command);
|
|
564
993
|
let declared_names: std::collections::HashSet<String> = command
|
|
565
994
|
.get("attributes")
|
|
566
|
-
.
|
|
567
|
-
.unwrap_or(&[])
|
|
995
|
+
.map_or_default(Json::each)
|
|
568
996
|
.iter()
|
|
569
997
|
.map(|a| crate::attr::name(a).to_string())
|
|
570
998
|
.filter(|name| !append_claimed.contains(name))
|
|
@@ -579,19 +1007,40 @@ pub fn identity_components(aggregate: &Json, command: &Json) -> Vec<IdentityComp
|
|
|
579
1007
|
let rest: Vec<&str> = parts.collect();
|
|
580
1008
|
|
|
581
1009
|
let set_target = cmd_mutations.iter().any(|m| {
|
|
582
|
-
m.get("op").map(Json::to_s).as_deref() == Some("set")
|
|
1010
|
+
m.get("op").map(Json::to_s).as_deref() == Some("set")
|
|
1011
|
+
&& m.get("target").map(Json::to_s).as_deref() == Some(head)
|
|
583
1012
|
});
|
|
584
1013
|
|
|
585
1014
|
if set_target || declared_names.contains(head) {
|
|
586
|
-
if
|
|
587
|
-
|
|
588
|
-
|
|
1015
|
+
if rest.is_empty() {
|
|
1016
|
+
IdentityComponent {
|
|
1017
|
+
expr: format!("args.{}.to_string()", naming::rust_ident_field(head)),
|
|
1018
|
+
param: None,
|
|
1019
|
+
head: None,
|
|
1020
|
+
}
|
|
589
1021
|
} else {
|
|
590
|
-
|
|
1022
|
+
let rest_path = rest
|
|
1023
|
+
.iter()
|
|
1024
|
+
.map(|seg| naming::rust_ident_field(seg))
|
|
1025
|
+
.collect::<Vec<_>>()
|
|
1026
|
+
.join(".");
|
|
1027
|
+
IdentityComponent {
|
|
1028
|
+
expr: format!(
|
|
1029
|
+
"args.{}.{}.to_string()",
|
|
1030
|
+
naming::rust_ident_field(head),
|
|
1031
|
+
rest_path
|
|
1032
|
+
),
|
|
1033
|
+
param: None,
|
|
1034
|
+
head: None,
|
|
1035
|
+
}
|
|
591
1036
|
}
|
|
592
1037
|
} else {
|
|
593
1038
|
let param = naming::rust_ident_field(head);
|
|
594
|
-
IdentityComponent {
|
|
1039
|
+
IdentityComponent {
|
|
1040
|
+
expr: format!("{param}.to_string()"),
|
|
1041
|
+
param: Some(format!("{param}: &str")),
|
|
1042
|
+
head: Some(head.to_string()),
|
|
1043
|
+
}
|
|
595
1044
|
}
|
|
596
1045
|
})
|
|
597
1046
|
.collect()
|
|
@@ -601,34 +1050,80 @@ pub fn build_identity_expr(components: &[IdentityComponent]) -> String {
|
|
|
601
1050
|
if components.len() == 1 {
|
|
602
1051
|
return components[0].expr.clone();
|
|
603
1052
|
}
|
|
604
|
-
let placeholders = components
|
|
605
|
-
|
|
1053
|
+
let placeholders = components
|
|
1054
|
+
.iter()
|
|
1055
|
+
.map(|_| "{}")
|
|
1056
|
+
.collect::<Vec<_>>()
|
|
1057
|
+
.join(":");
|
|
1058
|
+
format!(
|
|
1059
|
+
"format!({}, {})",
|
|
1060
|
+
naming::ruby_inspect_string(&placeholders),
|
|
1061
|
+
components
|
|
1062
|
+
.iter()
|
|
1063
|
+
// A format argument is displayed already, so the `.to_string()` an id part carries is dropped.
|
|
1064
|
+
.map(|c| c.expr.strip_suffix(".to_string()").unwrap_or(&c.expr))
|
|
1065
|
+
.collect::<Vec<_>>()
|
|
1066
|
+
.join(", ")
|
|
1067
|
+
)
|
|
606
1068
|
}
|
|
607
1069
|
|
|
608
1070
|
/// The value expression for one `append` field.
|
|
609
|
-
|
|
1071
|
+
#[allow(
|
|
1072
|
+
clippy::expect_used,
|
|
1073
|
+
reason = "the message names an invariant an earlier check in this generator established"
|
|
1074
|
+
)]
|
|
1075
|
+
pub fn append_field_rhs(
|
|
1076
|
+
source: &str,
|
|
1077
|
+
field_attr: &Json,
|
|
1078
|
+
command: &Json,
|
|
1079
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1080
|
+
) -> String {
|
|
610
1081
|
let parsed = append_field_source(source);
|
|
611
1082
|
let Literal::Symbol(arg_name) = &parsed else {
|
|
612
|
-
return crate::bridging::literal_rhs_for(
|
|
1083
|
+
return crate::bridging::literal_rhs_for(
|
|
1084
|
+
&parsed,
|
|
1085
|
+
Some(crate::attr::type_name(field_attr)),
|
|
1086
|
+
value_objects_by_name,
|
|
1087
|
+
);
|
|
613
1088
|
};
|
|
614
1089
|
|
|
615
|
-
let cmd_attrs = command.get("attributes").
|
|
616
|
-
let arg_attr = cmd_attrs
|
|
617
|
-
|
|
1090
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
1091
|
+
let arg_attr = cmd_attrs
|
|
1092
|
+
.iter()
|
|
1093
|
+
.find(|a| crate::attr::name(a) == arg_name.as_str())
|
|
1094
|
+
.expect("append field argument must be a declared command attribute");
|
|
1095
|
+
let arg_expr = format!(
|
|
1096
|
+
"args.{}",
|
|
1097
|
+
naming::rust_ident_field(crate::attr::name(arg_attr))
|
|
1098
|
+
);
|
|
618
1099
|
|
|
619
1100
|
if crate::attr::optional(arg_attr) {
|
|
620
|
-
let same_representation = crate::attr::type_name(arg_attr)
|
|
621
|
-
|
|
1101
|
+
let same_representation = crate::attr::type_name(arg_attr)
|
|
1102
|
+
== crate::attr::type_name(field_attr)
|
|
1103
|
+
|| match (
|
|
1104
|
+
naming::effective_scalar_type(crate::attr::type_name(arg_attr)),
|
|
1105
|
+
naming::effective_scalar_type(crate::attr::type_name(field_attr)),
|
|
1106
|
+
) {
|
|
622
1107
|
(Some(a), Some(b)) => a == b,
|
|
623
1108
|
_ => false,
|
|
624
1109
|
};
|
|
625
1110
|
if same_representation {
|
|
626
1111
|
return format!("{arg_expr}.clone()");
|
|
627
1112
|
}
|
|
628
|
-
return optional_value_rhs(
|
|
1113
|
+
return optional_value_rhs(
|
|
1114
|
+
&arg_expr,
|
|
1115
|
+
crate::attr::type_name(arg_attr),
|
|
1116
|
+
crate::attr::type_name(field_attr),
|
|
1117
|
+
value_objects_by_name,
|
|
1118
|
+
);
|
|
629
1119
|
}
|
|
630
1120
|
|
|
631
|
-
let rhs = crate::bridging::value_rhs(
|
|
1121
|
+
let rhs = crate::bridging::value_rhs(
|
|
1122
|
+
&arg_expr,
|
|
1123
|
+
crate::attr::type_name(arg_attr),
|
|
1124
|
+
crate::attr::type_name(field_attr),
|
|
1125
|
+
value_objects_by_name,
|
|
1126
|
+
);
|
|
632
1127
|
if crate::attr::optional(field_attr) {
|
|
633
1128
|
format!("Some({rhs})")
|
|
634
1129
|
} else {
|
|
@@ -637,17 +1132,55 @@ pub fn append_field_rhs(source: &str, field_attr: &Json, command: &Json, value_o
|
|
|
637
1132
|
}
|
|
638
1133
|
|
|
639
1134
|
/// The optional-source variant of `value_rhs`.
|
|
640
|
-
pub fn optional_value_rhs(
|
|
641
|
-
|
|
1135
|
+
pub fn optional_value_rhs(
|
|
1136
|
+
source_expr: &str,
|
|
1137
|
+
source_type: &str,
|
|
1138
|
+
target_type: &str,
|
|
1139
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1140
|
+
) -> String {
|
|
1141
|
+
format!(
|
|
1142
|
+
"{source_expr}.clone().map(|v| {})",
|
|
1143
|
+
crate::bridging::value_rhs("v", source_type, target_type, value_objects_by_name)
|
|
1144
|
+
)
|
|
642
1145
|
}
|
|
643
1146
|
|
|
644
1147
|
/// Emits the closure line for one mutation.
|
|
645
|
-
pub fn emit_mutation_line(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1148
|
+
pub fn emit_mutation_line(
|
|
1149
|
+
exemplar: &Exemplar,
|
|
1150
|
+
mutation: &Json,
|
|
1151
|
+
aggregate: &Json,
|
|
1152
|
+
command: &Json,
|
|
1153
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1154
|
+
optional: bool,
|
|
1155
|
+
) -> String {
|
|
1156
|
+
let target_field =
|
|
1157
|
+
naming::rust_ident_field(&mutation.get("target").map(Json::to_s).unwrap_or_default());
|
|
1158
|
+
let lifecycle_field = aggregate
|
|
1159
|
+
.get("lifecycle")
|
|
1160
|
+
.and_then(|l| l.get("field"))
|
|
1161
|
+
.map(Json::to_s);
|
|
1162
|
+
format!(
|
|
1163
|
+
" {}",
|
|
1164
|
+
emit_mutation_line_body(
|
|
1165
|
+
exemplar,
|
|
1166
|
+
mutation,
|
|
1167
|
+
aggregate,
|
|
1168
|
+
command,
|
|
1169
|
+
value_objects_by_name,
|
|
1170
|
+
&target_field,
|
|
1171
|
+
lifecycle_field.as_deref(),
|
|
1172
|
+
optional
|
|
1173
|
+
)
|
|
1174
|
+
)
|
|
649
1175
|
}
|
|
650
1176
|
|
|
1177
|
+
#[allow(clippy::too_many_arguments)] // established emitter signature
|
|
1178
|
+
#[allow(
|
|
1179
|
+
clippy::expect_used,
|
|
1180
|
+
clippy::panic,
|
|
1181
|
+
clippy::unwrap_used,
|
|
1182
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose; the message names an invariant an earlier check in this generator established; the value was checked just before; None would mean invalid IR"
|
|
1183
|
+
)]
|
|
651
1184
|
fn emit_mutation_line_body(
|
|
652
1185
|
exemplar: &Exemplar,
|
|
653
1186
|
mutation: &Json,
|
|
@@ -663,26 +1196,49 @@ fn emit_mutation_line_body(
|
|
|
663
1196
|
|
|
664
1197
|
match op.as_str() {
|
|
665
1198
|
"append" => {
|
|
666
|
-
let attrs = aggregate.get("attributes").
|
|
667
|
-
let target_attr = attrs
|
|
1199
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
1200
|
+
let target_attr = attrs
|
|
1201
|
+
.iter()
|
|
1202
|
+
.find(|a| crate::attr::name(a) == target_name)
|
|
1203
|
+
.expect("append target must be a declared aggregate attribute");
|
|
668
1204
|
let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
|
|
669
|
-
let entity = aggregate
|
|
670
|
-
|
|
1205
|
+
let entity = aggregate
|
|
1206
|
+
.get("entities")
|
|
1207
|
+
.map_or_default(Json::each)
|
|
1208
|
+
.iter()
|
|
1209
|
+
.find(|e| {
|
|
1210
|
+
e.get("name").and_then(Json::as_str)
|
|
1211
|
+
== Some(crate::attr::type_name(target_attr))
|
|
1212
|
+
});
|
|
1213
|
+
let element = entity
|
|
1214
|
+
.or_else(|| {
|
|
1215
|
+
value_objects_by_name
|
|
1216
|
+
.get(crate::attr::type_name(target_attr))
|
|
1217
|
+
.copied()
|
|
1218
|
+
})
|
|
1219
|
+
.expect("append target's element type must resolve");
|
|
671
1220
|
|
|
672
1221
|
let fields = mutation.get("fields").map(fields_pairs).unwrap_or_default();
|
|
673
|
-
let element_attrs = element.get("attributes").
|
|
1222
|
+
let element_attrs = element.get("attributes").map_or_default(Json::each);
|
|
674
1223
|
let mut fields_assignment: Vec<String> = fields
|
|
675
1224
|
.iter()
|
|
676
1225
|
.map(|(field_name, source)| {
|
|
677
|
-
let field_attr = element_attrs
|
|
678
|
-
|
|
1226
|
+
let field_attr = element_attrs
|
|
1227
|
+
.iter()
|
|
1228
|
+
.find(|a| crate::attr::name(a) == field_name.as_str())
|
|
1229
|
+
.expect("append field must be a declared element attribute");
|
|
1230
|
+
format!(
|
|
1231
|
+
"{}: {}",
|
|
1232
|
+
naming::rust_ident_field(field_name),
|
|
1233
|
+
append_field_rhs(source, field_attr, command, value_objects_by_name)
|
|
1234
|
+
)
|
|
679
1235
|
})
|
|
680
1236
|
.collect();
|
|
681
1237
|
|
|
682
1238
|
let mut collision_guard = String::new();
|
|
683
1239
|
if let Some(entity) = entity {
|
|
684
1240
|
let mut present: Vec<String> = fields.iter().map(|(k, _)| k.clone()).collect();
|
|
685
|
-
let identified_by = entity.get("identified_by").
|
|
1241
|
+
let identified_by = entity.get("identified_by").map_or_default(Json::each);
|
|
686
1242
|
if identified_by.len() > 1 {
|
|
687
1243
|
// Composite identity: minting a partial key makes no sense, so every head must
|
|
688
1244
|
// already be caller-supplied (as `Venue.AddLine`'s `append: { batch: :batch,
|
|
@@ -691,24 +1247,28 @@ fn emit_mutation_line_body(
|
|
|
691
1247
|
// used to be skipped outright for any composite identity (its comment read
|
|
692
1248
|
// "Composite identities are excluded"), so two elements sharing every identity
|
|
693
1249
|
// component went through unrefused. Root cause of this bug.
|
|
694
|
-
let heads: Vec<String> = identified_by
|
|
1250
|
+
let heads: Vec<String> = identified_by
|
|
1251
|
+
.iter()
|
|
1252
|
+
.map(|p| p.to_s().split('.').next().unwrap_or_default().to_string())
|
|
1253
|
+
.collect();
|
|
695
1254
|
if heads.iter().all(|h| present.iter().any(|p| p == h)) {
|
|
696
|
-
collision_guard = composite_append_collision_guard(
|
|
1255
|
+
collision_guard = composite_append_collision_guard(
|
|
1256
|
+
&heads,
|
|
1257
|
+
&fields,
|
|
1258
|
+
element_attrs,
|
|
1259
|
+
command,
|
|
1260
|
+
value_objects_by_name,
|
|
1261
|
+
target_field,
|
|
1262
|
+
entity,
|
|
1263
|
+
aggregate,
|
|
1264
|
+
identified_by,
|
|
1265
|
+
);
|
|
697
1266
|
}
|
|
698
|
-
} else if let Some((id_attr, id_vo)) =
|
|
1267
|
+
} else if let Some((id_attr, id_vo)) =
|
|
1268
|
+
entity_identity_mint(entity, value_objects_by_name)
|
|
1269
|
+
{
|
|
699
1270
|
let id_name = crate::attr::name(id_attr);
|
|
700
|
-
if
|
|
701
|
-
let id_vo_attrs = id_vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
702
|
-
// Mint one past the highest identity held.
|
|
703
|
-
let id_field = naming::rust_ident_field(id_name);
|
|
704
|
-
let vo_field = naming::rust_ident_field(crate::attr::name(&id_vo_attrs[0]));
|
|
705
|
-
let mint = format!(
|
|
706
|
-
"{} {{ {vo_field}: record.{target_field}.iter().map(|e| e.{id_field}.{vo_field}).max().unwrap_or(0) + 1 }}",
|
|
707
|
-
naming::rust_ident(crate::attr::type_name(id_attr)),
|
|
708
|
-
);
|
|
709
|
-
fields_assignment.push(format!("{}: {mint}", naming::rust_ident_field(id_name)));
|
|
710
|
-
present.push(id_name.to_string());
|
|
711
|
-
} else {
|
|
1271
|
+
if present.iter().any(|p| p == id_name) {
|
|
712
1272
|
// Caller-supplied single-field identity: refuse a duplicate, which would
|
|
713
1273
|
// be unaddressable later.
|
|
714
1274
|
let id_field = naming::rust_ident_field(id_name);
|
|
@@ -723,26 +1283,60 @@ fn emit_mutation_line_body(
|
|
|
723
1283
|
append_field_rhs(source, field_attr, command, value_objects_by_name)
|
|
724
1284
|
})
|
|
725
1285
|
.expect("caller-supplied identity field must be in the append's own field map");
|
|
726
|
-
let entity_name = entity
|
|
727
|
-
|
|
728
|
-
|
|
1286
|
+
let entity_name = entity
|
|
1287
|
+
.get("name")
|
|
1288
|
+
.and_then(Json::as_str)
|
|
1289
|
+
.unwrap_or_default();
|
|
1290
|
+
let aggregate_name = aggregate
|
|
1291
|
+
.get("name")
|
|
1292
|
+
.and_then(Json::as_str)
|
|
1293
|
+
.unwrap_or_default();
|
|
1294
|
+
let identity_reading = identified_by
|
|
1295
|
+
.iter()
|
|
1296
|
+
.map(Json::to_s)
|
|
1297
|
+
.collect::<Vec<_>>()
|
|
1298
|
+
.join(", ");
|
|
729
1299
|
let entity_lit = naming::ruby_inspect_string(entity_name);
|
|
730
1300
|
let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
|
|
731
1301
|
let identity_lit = naming::ruby_inspect_string(&identity_reading);
|
|
732
1302
|
// Unwrap to the bare scalar, as `entity_list_replace_guard` does; the mint
|
|
733
1303
|
// branch above guarantees exactly one attribute.
|
|
734
|
-
let offered_field = naming::rust_ident_field(crate::attr::name(
|
|
1304
|
+
let offered_field = naming::rust_ident_field(crate::attr::name(
|
|
1305
|
+
&id_vo.get("attributes").map_or_default(Json::each)[0],
|
|
1306
|
+
));
|
|
735
1307
|
let offered_expr = format!("{id_rhs}.{offered_field}");
|
|
736
1308
|
collision_guard = format!(
|
|
737
1309
|
"if record.{target_field}.iter().any(|e| e.{id_field} == {id_rhs}) {{ let offered = format!(\"{{:?}}\", {offered_expr}); return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &[offered.as_str()] }}.render_args())); }}\n "
|
|
738
1310
|
);
|
|
1311
|
+
} else {
|
|
1312
|
+
let id_vo_attrs = id_vo.get("attributes").map_or_default(Json::each);
|
|
1313
|
+
// Mint one past the highest identity held.
|
|
1314
|
+
let id_field = naming::rust_ident_field(id_name);
|
|
1315
|
+
let vo_field = naming::rust_ident_field(crate::attr::name(&id_vo_attrs[0]));
|
|
1316
|
+
let mint = format!(
|
|
1317
|
+
"{} {{ {vo_field}: record.{target_field}.iter().map(|e| e.{id_field}.{vo_field}).max().unwrap_or(0) + 1 }}",
|
|
1318
|
+
naming::rust_ident(crate::attr::type_name(id_attr)),
|
|
1319
|
+
);
|
|
1320
|
+
fields_assignment
|
|
1321
|
+
.push(format!("{}: {mint}", naming::rust_ident_field(id_name)));
|
|
1322
|
+
present.push(id_name.to_string());
|
|
739
1323
|
}
|
|
740
1324
|
}
|
|
741
1325
|
if let Some(entity_lifecycle) = entity.get("lifecycle") {
|
|
742
|
-
let lc_field = entity_lifecycle
|
|
1326
|
+
let lc_field = entity_lifecycle
|
|
1327
|
+
.get("field")
|
|
1328
|
+
.map(Json::to_s)
|
|
1329
|
+
.unwrap_or_default();
|
|
743
1330
|
if !present.iter().any(|p| p == &lc_field) {
|
|
744
|
-
let default = entity_lifecycle
|
|
745
|
-
|
|
1331
|
+
let default = entity_lifecycle
|
|
1332
|
+
.get("default")
|
|
1333
|
+
.map(Json::to_s)
|
|
1334
|
+
.unwrap_or_default();
|
|
1335
|
+
fields_assignment.push(format!(
|
|
1336
|
+
"{}: {}.to_string()",
|
|
1337
|
+
naming::rust_ident_field(&lc_field),
|
|
1338
|
+
naming::ruby_inspect_string(&default)
|
|
1339
|
+
));
|
|
746
1340
|
present.push(lc_field);
|
|
747
1341
|
}
|
|
748
1342
|
}
|
|
@@ -756,7 +1350,10 @@ fn emit_mutation_line_body(
|
|
|
756
1350
|
if present.iter().any(|p| p == attr_name) {
|
|
757
1351
|
continue;
|
|
758
1352
|
}
|
|
759
|
-
fields_assignment.push(format!(
|
|
1353
|
+
fields_assignment.push(format!(
|
|
1354
|
+
"{}: Vec::new()",
|
|
1355
|
+
naming::rust_ident_field(attr_name)
|
|
1356
|
+
));
|
|
760
1357
|
present.push(attr_name.to_string());
|
|
761
1358
|
}
|
|
762
1359
|
|
|
@@ -769,116 +1366,372 @@ fn emit_mutation_line_body(
|
|
|
769
1366
|
if present.iter().any(|p| p == attr_name) {
|
|
770
1367
|
continue;
|
|
771
1368
|
}
|
|
772
|
-
fields_assignment
|
|
1369
|
+
fields_assignment
|
|
1370
|
+
.push(format!("{}: None", naming::rust_ident_field(attr_name)));
|
|
773
1371
|
present.push(attr_name.to_string());
|
|
774
1372
|
}
|
|
775
1373
|
}
|
|
776
1374
|
|
|
777
|
-
format!(
|
|
1375
|
+
format!(
|
|
1376
|
+
"{collision_guard}{}",
|
|
1377
|
+
exemplar.render(
|
|
1378
|
+
"mutation_append",
|
|
1379
|
+
&[
|
|
1380
|
+
("tmpl_field", target_field.to_string()),
|
|
1381
|
+
(
|
|
1382
|
+
"tmpl_fields_placeholder()",
|
|
1383
|
+
format!("{vo_type} {{ {} }}", fields_assignment.join(", "))
|
|
1384
|
+
)
|
|
1385
|
+
]
|
|
1386
|
+
)
|
|
1387
|
+
)
|
|
778
1388
|
}
|
|
779
1389
|
"set" => {
|
|
780
|
-
if lifecycle_field.
|
|
781
|
-
let rhs = mutation_set_rhs(
|
|
782
|
-
|
|
1390
|
+
if lifecycle_field.is_some_and(|f| f == target_name) {
|
|
1391
|
+
let rhs = mutation_set_rhs(
|
|
1392
|
+
mutation.get("source").unwrap_or(&Json::Null),
|
|
1393
|
+
"String",
|
|
1394
|
+
command,
|
|
1395
|
+
value_objects_by_name,
|
|
1396
|
+
false,
|
|
1397
|
+
);
|
|
1398
|
+
exemplar.render(
|
|
1399
|
+
"mutation_set_plain",
|
|
1400
|
+
&[
|
|
1401
|
+
("tmpl_field", target_field.to_string()),
|
|
1402
|
+
("tmpl_rhs_placeholder2()", rhs),
|
|
1403
|
+
],
|
|
1404
|
+
)
|
|
783
1405
|
} else {
|
|
784
|
-
let attrs = aggregate.get("attributes").
|
|
785
|
-
let target_attr = attrs
|
|
1406
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
1407
|
+
let target_attr = attrs
|
|
1408
|
+
.iter()
|
|
1409
|
+
.find(|a| crate::attr::name(a) == target_name)
|
|
1410
|
+
.expect("set target must be a declared aggregate attribute");
|
|
786
1411
|
let source = mutation.get("source");
|
|
787
|
-
let rhs = if source
|
|
1412
|
+
let rhs = if source
|
|
1413
|
+
.and_then(|s| s.get("kind"))
|
|
1414
|
+
.map(Json::to_s)
|
|
1415
|
+
.as_deref()
|
|
1416
|
+
== Some("expression")
|
|
1417
|
+
{
|
|
788
1418
|
expression_set_rhs(source.unwrap(), target_attr, value_objects_by_name)
|
|
789
1419
|
} else {
|
|
790
|
-
mutation_set_rhs(
|
|
1420
|
+
mutation_set_rhs(
|
|
1421
|
+
mutation.get("source").unwrap_or(&Json::Null),
|
|
1422
|
+
crate::attr::type_name(target_attr),
|
|
1423
|
+
command,
|
|
1424
|
+
value_objects_by_name,
|
|
1425
|
+
crate::attr::list(target_attr),
|
|
1426
|
+
)
|
|
791
1427
|
};
|
|
792
|
-
let source_attr = if source
|
|
793
|
-
|
|
794
|
-
|
|
1428
|
+
let source_attr = if source
|
|
1429
|
+
.map(|s| s.get("kind").map(Json::to_s).unwrap_or_default())
|
|
1430
|
+
== Some("argument".to_string())
|
|
1431
|
+
{
|
|
1432
|
+
let name = source
|
|
1433
|
+
.unwrap()
|
|
1434
|
+
.get("name")
|
|
1435
|
+
.map(Json::to_s)
|
|
1436
|
+
.unwrap_or_default();
|
|
1437
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
795
1438
|
cmd_attrs.iter().find(|a| crate::attr::name(a) == name)
|
|
796
1439
|
} else {
|
|
797
1440
|
None
|
|
798
1441
|
};
|
|
799
1442
|
|
|
800
|
-
if crate::attr::list(target_attr)
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1443
|
+
if crate::attr::list(target_attr)
|
|
1444
|
+
&& optional
|
|
1445
|
+
&& crate::shared::list_attr_creation_optional(
|
|
1446
|
+
aggregate,
|
|
1447
|
+
crate::attr::name(target_attr),
|
|
1448
|
+
)
|
|
1449
|
+
{
|
|
1450
|
+
exemplar.render(
|
|
1451
|
+
"mutation_set_plain",
|
|
1452
|
+
&[
|
|
1453
|
+
("tmpl_field", target_field.to_string()),
|
|
1454
|
+
("tmpl_rhs_placeholder2()", rhs),
|
|
1455
|
+
],
|
|
1456
|
+
)
|
|
1457
|
+
} else if crate::attr::list(target_attr)
|
|
1458
|
+
&& source_attr.is_some_and(crate::attr::optional)
|
|
1459
|
+
{
|
|
1460
|
+
exemplar.render(
|
|
1461
|
+
"mutation_set_unwrap_or_default",
|
|
1462
|
+
&[
|
|
1463
|
+
("tmpl_field", target_field.to_string()),
|
|
1464
|
+
("tmpl_optional_rhs_placeholder()", rhs),
|
|
1465
|
+
],
|
|
1466
|
+
)
|
|
804
1467
|
} else if crate::attr::list(target_attr) {
|
|
805
1468
|
// Only whole-list replaces carry the duplicate-identity guard.
|
|
806
|
-
let (guard, effective_rhs) = entity_list_replace_guard(
|
|
807
|
-
|
|
1469
|
+
let (guard, effective_rhs) = entity_list_replace_guard(
|
|
1470
|
+
aggregate,
|
|
1471
|
+
target_attr,
|
|
1472
|
+
target_field,
|
|
1473
|
+
&rhs,
|
|
1474
|
+
value_objects_by_name,
|
|
1475
|
+
);
|
|
1476
|
+
format!(
|
|
1477
|
+
"{guard}{}",
|
|
1478
|
+
exemplar.render(
|
|
1479
|
+
"mutation_set_plain",
|
|
1480
|
+
&[
|
|
1481
|
+
("tmpl_field", target_field.to_string()),
|
|
1482
|
+
("tmpl_rhs_placeholder2()", effective_rhs)
|
|
1483
|
+
]
|
|
1484
|
+
)
|
|
1485
|
+
)
|
|
808
1486
|
} else {
|
|
809
|
-
let source_is_option = source_attr.
|
|
810
|
-
|
|
1487
|
+
let source_is_option = source_attr.is_some_and(crate::attr::optional)
|
|
1488
|
+
|| state_source_is_option(source, aggregate, optional);
|
|
1489
|
+
let wrap =
|
|
1490
|
+
(optional || crate::attr::optional(target_attr)) && !source_is_option;
|
|
811
1491
|
if wrap {
|
|
812
|
-
exemplar.render(
|
|
1492
|
+
exemplar.render(
|
|
1493
|
+
"mutation_set_wrapped",
|
|
1494
|
+
&[
|
|
1495
|
+
("tmpl_field", target_field.to_string()),
|
|
1496
|
+
("tmpl_rhs_placeholder2()", rhs),
|
|
1497
|
+
],
|
|
1498
|
+
)
|
|
813
1499
|
} else {
|
|
814
|
-
exemplar.render(
|
|
1500
|
+
exemplar.render(
|
|
1501
|
+
"mutation_set_plain",
|
|
1502
|
+
&[
|
|
1503
|
+
("tmpl_field", target_field.to_string()),
|
|
1504
|
+
("tmpl_rhs_placeholder2()", rhs),
|
|
1505
|
+
],
|
|
1506
|
+
)
|
|
815
1507
|
}
|
|
816
1508
|
}
|
|
817
1509
|
}
|
|
818
1510
|
}
|
|
819
1511
|
"increment" | "decrement" => {
|
|
820
|
-
let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(
|
|
1512
|
+
let (target_attr, integer_field, kind) = crate::bridging::arithmetic_target_field(
|
|
1513
|
+
mutation,
|
|
1514
|
+
aggregate,
|
|
1515
|
+
value_objects_by_name,
|
|
1516
|
+
)
|
|
1517
|
+
.expect("arithmetic target must resolve");
|
|
821
1518
|
let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
|
|
822
1519
|
let field_ident = naming::rust_ident_field(&integer_field);
|
|
823
|
-
let
|
|
1520
|
+
if let Some(text) = crate::bridging::text_operand_expr(
|
|
1521
|
+
mutation,
|
|
1522
|
+
aggregate,
|
|
1523
|
+
command,
|
|
1524
|
+
value_objects_by_name,
|
|
1525
|
+
&integer_field,
|
|
1526
|
+
) {
|
|
1527
|
+
return text_operand_refusal(
|
|
1528
|
+
crate::attr::type_name(target_attr),
|
|
1529
|
+
&integer_field,
|
|
1530
|
+
&text,
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
let amount_expr = crate::bridging::arithmetic_amount_expr(
|
|
1534
|
+
mutation.get("source").unwrap_or(&Json::Null),
|
|
1535
|
+
command,
|
|
1536
|
+
value_objects_by_name,
|
|
1537
|
+
&integer_field,
|
|
1538
|
+
kind,
|
|
1539
|
+
)
|
|
1540
|
+
.expect("arithmetic amount must resolve");
|
|
824
1541
|
// Use the IR's `sign`, not the op name.
|
|
825
|
-
let sign = if mutation.get("sign").map(Json::to_s).unwrap_or_default() == "1" {
|
|
826
|
-
|
|
1542
|
+
let sign = if mutation.get("sign").map(Json::to_s).unwrap_or_default() == "1" {
|
|
1543
|
+
"+"
|
|
1544
|
+
} else {
|
|
1545
|
+
"-"
|
|
1546
|
+
};
|
|
1547
|
+
let current = numeric_current(
|
|
1548
|
+
optional,
|
|
1549
|
+
target_field,
|
|
1550
|
+
target_attr,
|
|
1551
|
+
&integer_field,
|
|
1552
|
+
kind,
|
|
1553
|
+
value_objects_by_name,
|
|
1554
|
+
);
|
|
827
1555
|
let op = mutation.get("op").map(Json::to_s).unwrap_or_default();
|
|
828
|
-
let updated = format!(
|
|
1556
|
+
let updated = format!(
|
|
1557
|
+
"{vo_type} {{ {field_ident}: {}, ..current }}",
|
|
1558
|
+
arithmetic_field_value(kind, &op, &field_ident, sign, &amount_expr)
|
|
1559
|
+
);
|
|
829
1560
|
exemplar.render(
|
|
830
1561
|
"mutation_arithmetic",
|
|
831
|
-
&[
|
|
1562
|
+
&[
|
|
1563
|
+
("tmpl_field", target_field.to_string()),
|
|
1564
|
+
("tmpl_current_placeholder()", current),
|
|
1565
|
+
(
|
|
1566
|
+
"tmpl_updated_placeholder()",
|
|
1567
|
+
if optional {
|
|
1568
|
+
format!("Some({updated})")
|
|
1569
|
+
} else {
|
|
1570
|
+
updated
|
|
1571
|
+
},
|
|
1572
|
+
),
|
|
1573
|
+
],
|
|
832
1574
|
)
|
|
833
1575
|
}
|
|
834
1576
|
"multiply" => {
|
|
835
|
-
// Same
|
|
836
|
-
let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(
|
|
1577
|
+
// Same numeric-field subset as `increment`/`decrement`, with `*`.
|
|
1578
|
+
let (target_attr, integer_field, kind) = crate::bridging::arithmetic_target_field(
|
|
1579
|
+
mutation,
|
|
1580
|
+
aggregate,
|
|
1581
|
+
value_objects_by_name,
|
|
1582
|
+
)
|
|
1583
|
+
.expect("arithmetic target must resolve");
|
|
837
1584
|
let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
|
|
838
1585
|
let field_ident = naming::rust_ident_field(&integer_field);
|
|
839
|
-
let
|
|
840
|
-
|
|
841
|
-
|
|
1586
|
+
if let Some(text) = crate::bridging::text_operand_expr(
|
|
1587
|
+
mutation,
|
|
1588
|
+
aggregate,
|
|
1589
|
+
command,
|
|
1590
|
+
value_objects_by_name,
|
|
1591
|
+
&integer_field,
|
|
1592
|
+
) {
|
|
1593
|
+
return text_operand_refusal(
|
|
1594
|
+
crate::attr::type_name(target_attr),
|
|
1595
|
+
&integer_field,
|
|
1596
|
+
&text,
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
let amount_expr = crate::bridging::arithmetic_amount_expr(
|
|
1600
|
+
mutation.get("source").unwrap_or(&Json::Null),
|
|
1601
|
+
command,
|
|
1602
|
+
value_objects_by_name,
|
|
1603
|
+
&integer_field,
|
|
1604
|
+
kind,
|
|
1605
|
+
)
|
|
1606
|
+
.expect("arithmetic amount must resolve");
|
|
1607
|
+
let current = numeric_current(
|
|
1608
|
+
optional,
|
|
1609
|
+
target_field,
|
|
1610
|
+
target_attr,
|
|
1611
|
+
&integer_field,
|
|
1612
|
+
kind,
|
|
1613
|
+
value_objects_by_name,
|
|
1614
|
+
);
|
|
1615
|
+
let updated = format!(
|
|
1616
|
+
"{vo_type} {{ {field_ident}: {}, ..current }}",
|
|
1617
|
+
arithmetic_field_value(kind, "multiply", &field_ident, "*", &amount_expr)
|
|
1618
|
+
);
|
|
842
1619
|
exemplar.render(
|
|
843
1620
|
"mutation_arithmetic",
|
|
844
|
-
&[
|
|
1621
|
+
&[
|
|
1622
|
+
("tmpl_field", target_field.to_string()),
|
|
1623
|
+
("tmpl_current_placeholder()", current),
|
|
1624
|
+
(
|
|
1625
|
+
"tmpl_updated_placeholder()",
|
|
1626
|
+
if optional {
|
|
1627
|
+
format!("Some({updated})")
|
|
1628
|
+
} else {
|
|
1629
|
+
updated
|
|
1630
|
+
},
|
|
1631
|
+
),
|
|
1632
|
+
],
|
|
845
1633
|
)
|
|
846
1634
|
}
|
|
847
1635
|
"clamp" => {
|
|
848
1636
|
// The source is always a literal `[min, max]` pair, so there is no amount to resolve.
|
|
849
|
-
let (target_attr, integer_field) =
|
|
1637
|
+
let (target_attr, integer_field) =
|
|
1638
|
+
crate::bridging::integer_target_field(mutation, aggregate, value_objects_by_name)
|
|
1639
|
+
.expect("clamp target must resolve");
|
|
850
1640
|
let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
|
|
851
1641
|
let field_ident = naming::rust_ident_field(&integer_field);
|
|
852
|
-
let (min, max) = mutation
|
|
853
|
-
|
|
1642
|
+
let (min, max) = mutation
|
|
1643
|
+
.get("source")
|
|
1644
|
+
.and_then(crate::bridging::clamp_bounds_ints)
|
|
1645
|
+
.expect("clamp bounds must resolve");
|
|
1646
|
+
let current = numeric_current(
|
|
1647
|
+
optional,
|
|
1648
|
+
target_field,
|
|
1649
|
+
target_attr,
|
|
1650
|
+
&integer_field,
|
|
1651
|
+
crate::bridging::NumericKind::Integer,
|
|
1652
|
+
value_objects_by_name,
|
|
1653
|
+
);
|
|
854
1654
|
let updated = format!("{vo_type} {{ {field_ident}: current.{field_ident}.clamp({min}, {max}), ..current }}");
|
|
855
1655
|
exemplar.render(
|
|
856
1656
|
"mutation_arithmetic",
|
|
857
|
-
&[
|
|
1657
|
+
&[
|
|
1658
|
+
("tmpl_field", target_field.to_string()),
|
|
1659
|
+
("tmpl_current_placeholder()", current),
|
|
1660
|
+
(
|
|
1661
|
+
"tmpl_updated_placeholder()",
|
|
1662
|
+
if optional {
|
|
1663
|
+
format!("Some({updated})")
|
|
1664
|
+
} else {
|
|
1665
|
+
updated
|
|
1666
|
+
},
|
|
1667
|
+
),
|
|
1668
|
+
],
|
|
858
1669
|
)
|
|
859
1670
|
}
|
|
860
1671
|
"remove" => {
|
|
861
1672
|
// `remove_field_problems` already confirmed every lookup below resolves.
|
|
862
|
-
let attrs = aggregate.get("attributes").
|
|
863
|
-
let target_attr = attrs
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1673
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
1674
|
+
let target_attr = attrs
|
|
1675
|
+
.iter()
|
|
1676
|
+
.find(|a| crate::attr::name(a) == target_name)
|
|
1677
|
+
.expect("remove target must be a declared aggregate attribute");
|
|
1678
|
+
let entities = aggregate.get("entities").map_or_default(Json::each);
|
|
1679
|
+
let Some(entity) = entities.iter().find(|e| {
|
|
1680
|
+
e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr))
|
|
1681
|
+
}) else {
|
|
1682
|
+
// A value-object list: drop every element equal by value to the argument.
|
|
1683
|
+
let source_name = mutation
|
|
1684
|
+
.get("source")
|
|
1685
|
+
.and_then(|s| s.get("name"))
|
|
1686
|
+
.map(Json::to_s)
|
|
1687
|
+
.unwrap_or_default();
|
|
1688
|
+
return exemplar.render(
|
|
1689
|
+
"mutation_remove_value",
|
|
1690
|
+
&[
|
|
1691
|
+
("tmpl_field", target_field.to_string()),
|
|
1692
|
+
(
|
|
1693
|
+
"tmpl_remove_value_placeholder()",
|
|
1694
|
+
format!("args.{}", naming::rust_ident_field(&source_name)),
|
|
1695
|
+
),
|
|
1696
|
+
],
|
|
1697
|
+
);
|
|
1698
|
+
};
|
|
1699
|
+
let (id_attr, _) = entity_identity_mint(entity, value_objects_by_name)
|
|
1700
|
+
.expect("remove target entity's identity must mint");
|
|
867
1701
|
let id_field = naming::rust_ident_field(crate::attr::name(id_attr));
|
|
868
|
-
let source_name = mutation
|
|
869
|
-
|
|
870
|
-
|
|
1702
|
+
let source_name = mutation
|
|
1703
|
+
.get("source")
|
|
1704
|
+
.and_then(|s| s.get("name"))
|
|
1705
|
+
.map(Json::to_s)
|
|
1706
|
+
.unwrap_or_default();
|
|
1707
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
1708
|
+
let source_attr = cmd_attrs
|
|
1709
|
+
.iter()
|
|
1710
|
+
.find(|a| crate::attr::name(a) == source_name)
|
|
1711
|
+
.expect("remove source must be a declared argument");
|
|
871
1712
|
let match_expr = crate::bridging::value_rhs(
|
|
872
|
-
&format!(
|
|
1713
|
+
&format!(
|
|
1714
|
+
"args.{}",
|
|
1715
|
+
naming::rust_ident_field(crate::attr::name(source_attr))
|
|
1716
|
+
),
|
|
873
1717
|
crate::attr::type_name(source_attr),
|
|
874
1718
|
crate::attr::type_name(id_attr),
|
|
875
1719
|
value_objects_by_name,
|
|
876
1720
|
);
|
|
877
|
-
exemplar.render(
|
|
1721
|
+
exemplar.render(
|
|
1722
|
+
"mutation_remove",
|
|
1723
|
+
&[
|
|
1724
|
+
("tmpl_field", target_field.to_string()),
|
|
1725
|
+
("tmpl_id_field", id_field),
|
|
1726
|
+
("tmpl_remove_match_placeholder()", match_expr),
|
|
1727
|
+
],
|
|
1728
|
+
)
|
|
878
1729
|
}
|
|
879
1730
|
// `corrects` targets no field; entity-level commands do not filter it out first.
|
|
880
1731
|
"corrects" => String::new(),
|
|
881
|
-
other => panic!(
|
|
1732
|
+
other => panic!(
|
|
1733
|
+
"unsupported mutation op {other:?} — command_skip_reason should have caught this"
|
|
1734
|
+
),
|
|
882
1735
|
}
|
|
883
1736
|
}
|
|
884
1737
|
|
|
@@ -942,17 +1795,29 @@ mod state_source_tests {
|
|
|
942
1795
|
|
|
943
1796
|
#[test]
|
|
944
1797
|
fn a_state_source_in_an_all_optional_record_is_already_an_option() {
|
|
945
|
-
assert!(state_source_is_option(
|
|
1798
|
+
assert!(state_source_is_option(
|
|
1799
|
+
Some(&state("body")),
|
|
1800
|
+
&aggregate(),
|
|
1801
|
+
true
|
|
1802
|
+
));
|
|
946
1803
|
}
|
|
947
1804
|
|
|
948
1805
|
#[test]
|
|
949
1806
|
fn a_state_source_reads_an_option_where_it_is_declared_optional() {
|
|
950
|
-
assert!(state_source_is_option(
|
|
1807
|
+
assert!(state_source_is_option(
|
|
1808
|
+
Some(&state("draft_body")),
|
|
1809
|
+
&aggregate(),
|
|
1810
|
+
false
|
|
1811
|
+
));
|
|
951
1812
|
}
|
|
952
1813
|
|
|
953
1814
|
#[test]
|
|
954
1815
|
fn a_required_state_source_in_a_plain_record_is_not_an_option() {
|
|
955
|
-
assert!(!state_source_is_option(
|
|
1816
|
+
assert!(!state_source_is_option(
|
|
1817
|
+
Some(&state("body")),
|
|
1818
|
+
&aggregate(),
|
|
1819
|
+
false
|
|
1820
|
+
));
|
|
956
1821
|
}
|
|
957
1822
|
|
|
958
1823
|
#[test]
|
|
@@ -967,57 +1832,165 @@ mod expression_source_tests {
|
|
|
967
1832
|
use super::*;
|
|
968
1833
|
|
|
969
1834
|
fn aggregate() -> Json {
|
|
970
|
-
Json::parse(
|
|
1835
|
+
Json::parse(
|
|
1836
|
+
r#"{"name":"Settlement","attributes":[
|
|
971
1837
|
{"name":"paid","type":"Cents","list":false},
|
|
972
1838
|
{"name":"refund","type":"Cents","list":false},
|
|
973
|
-
{"name":"note","type":"Note","list":false}]}"
|
|
1839
|
+
{"name":"note","type":"Note","list":false}]}"#,
|
|
1840
|
+
)
|
|
974
1841
|
.unwrap()
|
|
975
1842
|
}
|
|
976
1843
|
|
|
977
1844
|
fn vos() -> Vec<Json> {
|
|
978
1845
|
vec![
|
|
979
|
-
Json::parse(
|
|
980
|
-
|
|
1846
|
+
Json::parse(
|
|
1847
|
+
r#"{"name":"Cents","attributes":[{"name":"value","type":"Integer","list":false}]}"#,
|
|
1848
|
+
)
|
|
1849
|
+
.unwrap(),
|
|
1850
|
+
Json::parse(
|
|
1851
|
+
r#"{"name":"Note","attributes":[{"name":"value","type":"String","list":false}]}"#,
|
|
1852
|
+
)
|
|
1853
|
+
.unwrap(),
|
|
981
1854
|
]
|
|
982
1855
|
}
|
|
983
1856
|
|
|
984
1857
|
fn command(target: &str) -> Json {
|
|
985
1858
|
let source = r#"{"kind":"expression","text":"paid * 2","ast":{"op":"mul","left":{"op":"lookup","path":["paid"]},"right":{"op":"int","value":2}}}"#;
|
|
986
|
-
Json::parse(&format!(
|
|
1859
|
+
Json::parse(&format!(
|
|
1860
|
+
r#"{{"mutations":[{{"target":"{target}","op":"set","source":{source}}}]}}"#
|
|
1861
|
+
))
|
|
1862
|
+
.unwrap()
|
|
987
1863
|
}
|
|
988
1864
|
|
|
989
1865
|
fn by_name(vos: &[Json]) -> HashMap<String, &Json> {
|
|
990
|
-
vos.iter()
|
|
1866
|
+
vos.iter()
|
|
1867
|
+
.map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), vo))
|
|
1868
|
+
.collect()
|
|
991
1869
|
}
|
|
992
1870
|
|
|
993
1871
|
#[test]
|
|
994
1872
|
fn an_expression_source_binds_the_pre_dispatch_record() {
|
|
995
|
-
assert!(reads_pre_state(
|
|
1873
|
+
assert!(reads_pre_state(
|
|
1874
|
+
command("refund")
|
|
1875
|
+
.get("mutations")
|
|
1876
|
+
.map_or_default(Json::each)
|
|
1877
|
+
));
|
|
996
1878
|
}
|
|
997
1879
|
|
|
998
1880
|
#[test]
|
|
999
1881
|
fn a_target_typed_by_an_integer_value_object_is_generated() {
|
|
1000
1882
|
let vos = vos();
|
|
1001
1883
|
|
|
1002
|
-
assert!(
|
|
1884
|
+
assert!(
|
|
1885
|
+
expression_source_problems(&command("refund"), &aggregate(), &by_name(&vos)).is_empty()
|
|
1886
|
+
);
|
|
1003
1887
|
}
|
|
1004
1888
|
|
|
1005
1889
|
#[test]
|
|
1006
1890
|
fn a_target_typed_by_a_text_value_object_is_not_generated_yet() {
|
|
1007
1891
|
let vos = vos();
|
|
1008
1892
|
|
|
1009
|
-
assert_eq!(
|
|
1893
|
+
assert_eq!(
|
|
1894
|
+
expression_source_problems(&command("note"), &aggregate(), &by_name(&vos)).len(),
|
|
1895
|
+
1
|
|
1896
|
+
);
|
|
1010
1897
|
}
|
|
1011
1898
|
|
|
1012
1899
|
#[test]
|
|
1013
1900
|
fn the_right_hand_side_evaluates_through_the_kernel_and_rebuilds_the_value_object() {
|
|
1014
1901
|
let vos = vos();
|
|
1015
1902
|
let aggregate = aggregate();
|
|
1016
|
-
let attrs = aggregate.get("attributes").
|
|
1017
|
-
let source = command("refund")
|
|
1903
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
1904
|
+
let source = command("refund")
|
|
1905
|
+
.get("mutations")
|
|
1906
|
+
.map_or_default(Json::each)[0]
|
|
1907
|
+
.get("source")
|
|
1908
|
+
.cloned()
|
|
1909
|
+
.unwrap();
|
|
1018
1910
|
|
|
1019
1911
|
let rhs = expression_set_rhs(&source, &attrs[1], &by_name(&vos));
|
|
1020
1912
|
|
|
1021
|
-
assert!(
|
|
1913
|
+
assert!(
|
|
1914
|
+
rhs.contains("crate::kernel::interpret(&Expr::Mul(")
|
|
1915
|
+
&& rhs.contains("Cents { value: number }")
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
#[cfg(test)]
|
|
1921
|
+
mod value_object_remove_tests {
|
|
1922
|
+
use super::*;
|
|
1923
|
+
|
|
1924
|
+
fn aggregate() -> Json {
|
|
1925
|
+
Json::parse(r#"{"name":"Tally","attributes":[{"name":"tags","type":"Label","list":true}]}"#)
|
|
1926
|
+
.unwrap()
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
fn label() -> Json {
|
|
1930
|
+
Json::parse(
|
|
1931
|
+
r#"{"name":"Label","attributes":[{"name":"value","type":"String","list":false}]}"#,
|
|
1932
|
+
)
|
|
1933
|
+
.unwrap()
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
fn command(argument_type: &str, source: &str) -> Json {
|
|
1937
|
+
Json::parse(&format!(
|
|
1938
|
+
r#"{{"name":"Untag","attributes":[{{"name":"label","type":"{argument_type}","list":false}}],
|
|
1939
|
+
"mutations":[{{"target":"tags","op":"remove","source":{source}}}]}}"#
|
|
1940
|
+
))
|
|
1941
|
+
.unwrap()
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
fn problems(command: &Json) -> Vec<String> {
|
|
1945
|
+
let label = label();
|
|
1946
|
+
let by_name: HashMap<String, &Json> = HashMap::from([("Label".to_string(), &label)]);
|
|
1947
|
+
remove_field_problems(command, &aggregate(), &by_name)
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
#[test]
|
|
1951
|
+
fn a_value_object_argument_of_the_element_type_is_generatable() {
|
|
1952
|
+
let command = command("Label", r#"{"kind":"argument","name":"label"}"#);
|
|
1953
|
+
assert!(problems(&command).is_empty());
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
#[test]
|
|
1957
|
+
fn an_argument_of_another_type_is_refused() {
|
|
1958
|
+
let command = command("Other", r#"{"kind":"argument","name":"label"}"#);
|
|
1959
|
+
assert!(problems(&command)[0].contains("needs a single Label argument"));
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
#[test]
|
|
1963
|
+
fn a_literal_source_is_refused() {
|
|
1964
|
+
let command = command("Label", r#"{"kind":"literal","value":"x"}"#);
|
|
1965
|
+
assert!(problems(&command)[0].contains("not an argument"));
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
#[cfg(test)]
|
|
1970
|
+
mod float_arithmetic_tests {
|
|
1971
|
+
use super::*;
|
|
1972
|
+
use crate::bridging::NumericKind;
|
|
1973
|
+
|
|
1974
|
+
#[test]
|
|
1975
|
+
fn a_float_increment_faults_unless_the_sum_is_finite() {
|
|
1976
|
+
assert_eq!(
|
|
1977
|
+
float_arithmetic("increment", "value", "+", "args.more.value"),
|
|
1978
|
+
"{ let amount = args.more.value; crate::kernel::ruby_float::finite_or_fault(\"increment\", \"+\", current.value, amount, current.value + amount)? }"
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
#[test]
|
|
1983
|
+
fn a_float_multiply_uses_the_product() {
|
|
1984
|
+
assert!(
|
|
1985
|
+
float_arithmetic("multiply", "value", "*", "2.0").contains("current.value * amount)?")
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
#[test]
|
|
1990
|
+
fn the_kind_picks_checked_integer_or_finite_float_arithmetic() {
|
|
1991
|
+
let integer = arithmetic_field_value(NumericKind::Integer, "increment", "value", "+", "1");
|
|
1992
|
+
let float = arithmetic_field_value(NumericKind::Float, "increment", "value", "+", "1.0");
|
|
1993
|
+
assert!(integer.contains("checked_add") && !integer.contains("ruby_float"));
|
|
1994
|
+
assert!(float.contains("finite_or_fault") && !float.contains("checked_add"));
|
|
1022
1995
|
}
|
|
1023
1996
|
}
|