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
|
@@ -10,16 +10,24 @@ use std::collections::HashMap;
|
|
|
10
10
|
|
|
11
11
|
/// The aggregate's invariant set, generated once per aggregate file and passed to every dispatch.
|
|
12
12
|
pub fn invariants_fn_name(aggregate: &Json) -> String {
|
|
13
|
-
format!(
|
|
13
|
+
format!(
|
|
14
|
+
"{}_invariants",
|
|
15
|
+
naming::rust_ident_field(aggregate.get("name").and_then(Json::as_str).unwrap_or(""))
|
|
16
|
+
.to_lowercase()
|
|
17
|
+
)
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
pub fn emit_invariants_fn(aggregate: &Json) -> String {
|
|
17
|
-
let aggregate_vec =
|
|
21
|
+
let aggregate_vec =
|
|
22
|
+
invariant_specs_vec(aggregate.get("invariants").map_or_default(Json::each), 8);
|
|
18
23
|
let entities_vec = entity_invariants_vec(aggregate, 8);
|
|
19
24
|
// `Expr` is imported only when an invariant literal was emitted; otherwise it is unused.
|
|
20
25
|
let needs_expr = aggregate_vec.contains("Expr::") || entities_vec.contains("Expr::");
|
|
21
26
|
[
|
|
22
|
-
Some(format!(
|
|
27
|
+
Some(format!(
|
|
28
|
+
"fn {}() -> crate::kernel::InvariantSet {{",
|
|
29
|
+
invariants_fn_name(aggregate)
|
|
30
|
+
)),
|
|
23
31
|
needs_expr.then(|| " use crate::kernel::Expr;".to_string()),
|
|
24
32
|
Some(" crate::kernel::InvariantSet {".to_string()),
|
|
25
33
|
Some(format!(" aggregate: {aggregate_vec},")),
|
|
@@ -33,6 +41,10 @@ pub fn emit_invariants_fn(aggregate: &Json) -> String {
|
|
|
33
41
|
.join("\n")
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
#[allow(
|
|
45
|
+
clippy::panic,
|
|
46
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
47
|
+
)]
|
|
36
48
|
fn invariant_specs_vec(rules: &[Json], indent: usize) -> String {
|
|
37
49
|
if rules.is_empty() {
|
|
38
50
|
return "vec![]".to_string();
|
|
@@ -42,7 +54,9 @@ fn invariant_specs_vec(rules: &[Json], indent: usize) -> String {
|
|
|
42
54
|
.iter()
|
|
43
55
|
.map(|rule| {
|
|
44
56
|
let description = rule.get("description").and_then(Json::as_str).unwrap_or("");
|
|
45
|
-
let ast = rule
|
|
57
|
+
let ast = rule
|
|
58
|
+
.get("ast")
|
|
59
|
+
.unwrap_or_else(|| panic!("invariant row has no ast: {rule:?}"));
|
|
46
60
|
format!(
|
|
47
61
|
"{pad}crate::kernel::InvariantSpec {{ description: {}, expr: {} }},",
|
|
48
62
|
naming::ruby_inspect_string(description),
|
|
@@ -54,18 +68,23 @@ fn invariant_specs_vec(rules: &[Json], indent: usize) -> String {
|
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
fn entity_invariants_vec(owner: &Json, indent: usize) -> String {
|
|
57
|
-
let entities = owner.get("entities").
|
|
58
|
-
let attributes = owner.get("attributes").
|
|
71
|
+
let entities = owner.get("entities").map_or_default(Json::each);
|
|
72
|
+
let attributes = owner.get("attributes").map_or_default(Json::each);
|
|
59
73
|
let pieces: Vec<(&Json, String)> = entities
|
|
60
74
|
.iter()
|
|
61
75
|
.filter_map(|entity| {
|
|
62
|
-
if entity
|
|
76
|
+
if entity
|
|
77
|
+
.get("invariants")
|
|
78
|
+
.map_or_default(Json::each)
|
|
79
|
+
.is_empty()
|
|
80
|
+
{
|
|
63
81
|
return None;
|
|
64
82
|
}
|
|
65
83
|
let name = entity.get("name").map(Json::to_s).unwrap_or_default();
|
|
66
|
-
let list = attributes
|
|
67
|
-
.
|
|
68
|
-
|
|
84
|
+
let list = attributes.iter().find(|a| {
|
|
85
|
+
a.get("list").is_some_and(Json::as_bool)
|
|
86
|
+
&& a.get("type").map(Json::to_s).unwrap_or_default() == name
|
|
87
|
+
})?;
|
|
69
88
|
Some((entity, list.get("name").map(Json::to_s).unwrap_or_default()))
|
|
70
89
|
})
|
|
71
90
|
.collect();
|
|
@@ -80,7 +99,7 @@ fn entity_invariants_vec(owner: &Json, indent: usize) -> String {
|
|
|
80
99
|
"{pad}crate::kernel::EntityInvariants {{ name: {}, list_field: {}, specs: {}, nested: {} }},",
|
|
81
100
|
naming::ruby_inspect_string(&entity.get("name").map(Json::to_s).unwrap_or_default()),
|
|
82
101
|
naming::ruby_inspect_string(list_field),
|
|
83
|
-
invariant_specs_vec(entity.get("invariants").
|
|
102
|
+
invariant_specs_vec(entity.get("invariants").map_or_default(Json::each), indent + 4),
|
|
84
103
|
entity_invariants_vec(entity, indent + 4)
|
|
85
104
|
)
|
|
86
105
|
})
|
|
@@ -89,14 +108,158 @@ fn entity_invariants_vec(owner: &Json, indent: usize) -> String {
|
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
/// Extra parameters of every generated `dispatch_*`/`dispatch_entity_*`, resolved by the router.
|
|
92
|
-
fn deref_params() -> [&'static str; 2] {
|
|
93
|
-
[
|
|
111
|
+
const fn deref_params() -> [&'static str; 2] {
|
|
112
|
+
[
|
|
113
|
+
"owner_deref: Vec<(&'static str, crate::kernel::DerefNode)>",
|
|
114
|
+
"command_deref: Vec<(&'static str, crate::kernel::DerefNode)>",
|
|
115
|
+
]
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
/// Write-side tenant boundary result, threaded through aggregate commands only.
|
|
97
119
|
/// The router always passes `Ok(())`: no domain generated from `ir.json` declares a boundary.
|
|
98
120
|
const TENANT_BOUNDARY_PARAM: &str = "tenant_boundary_check: Result<(), crate::kernel::Refusal>";
|
|
99
121
|
|
|
122
|
+
/// A node's `name`, or "" when it has none.
|
|
123
|
+
fn name_of(node: &Json) -> &str {
|
|
124
|
+
node.get("name").and_then(Json::as_str).unwrap_or("")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// The paths a node is identified by, joined the way a dispatch fn names them.
|
|
128
|
+
fn identity_reading(node: &Json) -> String {
|
|
129
|
+
node.get("identified_by")
|
|
130
|
+
.map_or_default(Json::each)
|
|
131
|
+
.iter()
|
|
132
|
+
.map(Json::to_s)
|
|
133
|
+
.collect::<Vec<_>>()
|
|
134
|
+
.join(", ")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// The first segment of each path a node is identified by.
|
|
138
|
+
fn identity_heads(node: &Json) -> Vec<String> {
|
|
139
|
+
node.get("identified_by")
|
|
140
|
+
.map_or_default(Json::each)
|
|
141
|
+
.iter()
|
|
142
|
+
.map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
|
|
143
|
+
.collect()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The Rust field of `owner` that holds `held` as a list.
|
|
147
|
+
#[allow(
|
|
148
|
+
clippy::panic,
|
|
149
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
150
|
+
)]
|
|
151
|
+
fn list_field_of(owner: &Json, held: &Json) -> String {
|
|
152
|
+
let held_name = name_of(held);
|
|
153
|
+
let list_attr = owner
|
|
154
|
+
.get("attributes")
|
|
155
|
+
.map_or_default(Json::each)
|
|
156
|
+
.iter()
|
|
157
|
+
.find(|a| crate::attr::list(a) && crate::attr::type_name(a) == held_name)
|
|
158
|
+
.unwrap_or_else(|| panic!("{held_name}: no list attribute on {} holds it — unsupported_attribute_types should have caught this", name_of(owner)));
|
|
159
|
+
naming::rust_ident_field(crate::attr::name(list_attr))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// The `pub struct <struct_name> { .. }` text of a command's argument struct.
|
|
163
|
+
fn args_struct_text(exemplar: &Exemplar, struct_name: &str, attrs: &[Json]) -> String {
|
|
164
|
+
let fields = attrs.iter().map(|attr| {
|
|
165
|
+
let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
|
|
166
|
+
if crate::attr::optional(attr) {
|
|
167
|
+
ty = format!("Option<{ty}>");
|
|
168
|
+
}
|
|
169
|
+
format!(
|
|
170
|
+
" {}",
|
|
171
|
+
exemplar.render(
|
|
172
|
+
"struct_field",
|
|
173
|
+
&[
|
|
174
|
+
("TmplFieldType", ty),
|
|
175
|
+
(
|
|
176
|
+
"tmpl_field",
|
|
177
|
+
naming::rust_ident_field(crate::attr::name(attr))
|
|
178
|
+
),
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
});
|
|
183
|
+
std::iter::once(format!("pub struct {struct_name} {{"))
|
|
184
|
+
.chain(fields)
|
|
185
|
+
.chain(std::iter::once("}".to_string()))
|
|
186
|
+
.collect::<Vec<_>>()
|
|
187
|
+
.join("\n")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// One `GivenSpec` line per `given` of `command`, each prefixed with `indent`.
|
|
191
|
+
#[allow(
|
|
192
|
+
clippy::panic,
|
|
193
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
194
|
+
)]
|
|
195
|
+
fn given_spec_lines(command: &Json, indent: &str) -> Vec<String> {
|
|
196
|
+
command
|
|
197
|
+
.get("givens")
|
|
198
|
+
.map_or_default(Json::each)
|
|
199
|
+
.iter()
|
|
200
|
+
.map(|g| {
|
|
201
|
+
let description = g.get("description").and_then(Json::as_str).unwrap_or("");
|
|
202
|
+
let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
|
|
203
|
+
format!("{indent}crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
|
|
204
|
+
})
|
|
205
|
+
.collect()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// One `EnsuresSpec` line per `ensures` of `command`, each prefixed with `indent`.
|
|
209
|
+
#[allow(
|
|
210
|
+
clippy::panic,
|
|
211
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
212
|
+
)]
|
|
213
|
+
fn ensures_spec_lines(command: &Json, indent: &str) -> Vec<String> {
|
|
214
|
+
command
|
|
215
|
+
.get("ensures")
|
|
216
|
+
.map_or_default(Json::each)
|
|
217
|
+
.iter()
|
|
218
|
+
.map(|e| {
|
|
219
|
+
let description = e.get("description").and_then(Json::as_str).unwrap_or("");
|
|
220
|
+
let ast = e
|
|
221
|
+
.get("ast")
|
|
222
|
+
.unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
|
|
223
|
+
format!(
|
|
224
|
+
"{indent}crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},",
|
|
225
|
+
naming::ruby_inspect_string(description),
|
|
226
|
+
crate::expr_emitter::emit_ast(ast)
|
|
227
|
+
)
|
|
228
|
+
})
|
|
229
|
+
.collect()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// The event names `command` emits, as a comma-separated list of Rust string literals.
|
|
233
|
+
fn emits_expr_of(command: &Json) -> String {
|
|
234
|
+
command
|
|
235
|
+
.get("emits")
|
|
236
|
+
.map_or_default(Json::each)
|
|
237
|
+
.iter()
|
|
238
|
+
.map(|e| naming::ruby_inspect_string(&e.to_s()))
|
|
239
|
+
.collect::<Vec<_>>()
|
|
240
|
+
.join(", ")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Ends a command's mutation lines: the lifecycle transition's new state, or `let _ = record;`
|
|
244
|
+
/// when there is nothing to write.
|
|
245
|
+
fn close_mutation_lines(
|
|
246
|
+
mut lines: Vec<String>,
|
|
247
|
+
transition: Option<&mutations::Transition>,
|
|
248
|
+
indent: &str,
|
|
249
|
+
) -> Vec<String> {
|
|
250
|
+
if let Some(t) = transition.filter(|t| !t.to_state.is_empty()) {
|
|
251
|
+
lines.push(format!(
|
|
252
|
+
"{indent}record.{} = {}.to_string();",
|
|
253
|
+
naming::rust_ident_field(&t.field),
|
|
254
|
+
naming::ruby_inspect_string(&t.to_state)
|
|
255
|
+
));
|
|
256
|
+
}
|
|
257
|
+
if lines.is_empty() {
|
|
258
|
+
lines.push(format!("{indent}let _ = record;"));
|
|
259
|
+
}
|
|
260
|
+
lines
|
|
261
|
+
}
|
|
262
|
+
|
|
100
263
|
fn with_references_binding() -> String {
|
|
101
264
|
"let with_references = crate::kernel::WithReferences { command_deref: &command_deref, args: &args, owner_deref: &owner_deref };".to_string()
|
|
102
265
|
}
|
|
@@ -107,12 +270,19 @@ fn seed_projections_binding(aggregate: &Json) -> String {
|
|
|
107
270
|
format!("let seed_projections = crate::kernel::seeded_projections(&with_references, {table}_PROJECTED_FIELDS);")
|
|
108
271
|
}
|
|
109
272
|
|
|
110
|
-
fn target_type_for<'a>(
|
|
273
|
+
fn target_type_for<'a>(
|
|
274
|
+
target: &str,
|
|
275
|
+
aggregate: &'a Json,
|
|
276
|
+
lifecycle_field: Option<&str>,
|
|
277
|
+
) -> Option<&'a str> {
|
|
111
278
|
if lifecycle_field == Some(target) {
|
|
112
279
|
return Some("String");
|
|
113
280
|
}
|
|
114
|
-
let attrs = aggregate.get("attributes").
|
|
115
|
-
attrs
|
|
281
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
282
|
+
attrs
|
|
283
|
+
.iter()
|
|
284
|
+
.find(|a| crate::attr::name(a) == target)
|
|
285
|
+
.map(crate::attr::type_name)
|
|
116
286
|
}
|
|
117
287
|
|
|
118
288
|
// The lifecycle field is never a list; `false` when the target is not found.
|
|
@@ -120,24 +290,49 @@ fn target_list_for(target: &str, aggregate: &Json, lifecycle_field: Option<&str>
|
|
|
120
290
|
if lifecycle_field == Some(target) {
|
|
121
291
|
return false;
|
|
122
292
|
}
|
|
123
|
-
let attrs = aggregate.get("attributes").
|
|
124
|
-
attrs
|
|
293
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
294
|
+
attrs
|
|
295
|
+
.iter()
|
|
296
|
+
.find(|a| crate::attr::name(a) == target)
|
|
297
|
+
.is_some_and(crate::attr::list)
|
|
125
298
|
}
|
|
126
299
|
|
|
127
|
-
pub fn command_skip_reason(
|
|
300
|
+
pub fn command_skip_reason(
|
|
301
|
+
command: &Json,
|
|
302
|
+
aggregate: &Json,
|
|
303
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
304
|
+
) -> Option<SkipReason> {
|
|
128
305
|
command_skip_reason_with(command, aggregate, value_objects_by_name, true)
|
|
129
306
|
}
|
|
130
307
|
|
|
131
308
|
/// `creating_possible` is false for an entity command: its optional identity argument is not an
|
|
132
309
|
/// identity source.
|
|
133
|
-
fn command_skip_reason_with(
|
|
134
|
-
|
|
310
|
+
fn command_skip_reason_with(
|
|
311
|
+
command: &Json,
|
|
312
|
+
aggregate: &Json,
|
|
313
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
314
|
+
creating_possible: bool,
|
|
315
|
+
) -> Option<SkipReason> {
|
|
316
|
+
let mutations_list = command.get("mutations").map_or_default(Json::each);
|
|
135
317
|
|
|
136
318
|
let mut unsupported_ops: Vec<String> = Vec::new();
|
|
137
319
|
for m in mutations_list {
|
|
138
320
|
let op = m.get("op").map(Json::to_s).unwrap_or_default();
|
|
139
321
|
// `remove` passes here; `mutations::remove_field_problems` narrows it to entity lists.
|
|
140
|
-
if ![
|
|
322
|
+
if ![
|
|
323
|
+
"append",
|
|
324
|
+
"set",
|
|
325
|
+
"increment",
|
|
326
|
+
"decrement",
|
|
327
|
+
"multiply",
|
|
328
|
+
"clamp",
|
|
329
|
+
"remove",
|
|
330
|
+
"delegate",
|
|
331
|
+
"corrects",
|
|
332
|
+
]
|
|
333
|
+
.contains(&op.as_str())
|
|
334
|
+
&& !unsupported_ops.contains(&op)
|
|
335
|
+
{
|
|
141
336
|
unsupported_ops.push(op);
|
|
142
337
|
}
|
|
143
338
|
}
|
|
@@ -148,7 +343,11 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
|
|
|
148
343
|
if let Some(corrects) = crate::bridging::corrects_of(command) {
|
|
149
344
|
// The IR is already derived, so only a command carrying nothing but its `corrects` is
|
|
150
345
|
// refused.
|
|
151
|
-
if crate::bridging::corrects_reverses(corrects)
|
|
346
|
+
if crate::bridging::corrects_reverses(corrects)
|
|
347
|
+
&& mutations_list
|
|
348
|
+
.iter()
|
|
349
|
+
.all(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "corrects")
|
|
350
|
+
{
|
|
152
351
|
let event = corrects.get("target").map(Json::to_s).unwrap_or_default();
|
|
153
352
|
return Some(skip("corrects_reverses", format!("corrects {event}, reverses: true — the derived append/remove-reversal shape is a real, separate gap Ruby's own authors haven't finished designing (AggregateBuilder#seal_correction_targets's own comment) — not generated yet")));
|
|
154
353
|
}
|
|
@@ -158,27 +357,49 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
|
|
|
158
357
|
return Some(skip("delegate", problem));
|
|
159
358
|
}
|
|
160
359
|
|
|
161
|
-
let append_problems =
|
|
360
|
+
let append_problems =
|
|
361
|
+
mutations::append_field_problems(command, aggregate, value_objects_by_name);
|
|
162
362
|
if !append_problems.is_empty() {
|
|
163
|
-
return Some(skip(
|
|
363
|
+
return Some(skip(
|
|
364
|
+
"append_field",
|
|
365
|
+
format!("sets append field(s): {}", append_problems.join("; ")),
|
|
366
|
+
));
|
|
164
367
|
}
|
|
165
368
|
|
|
166
|
-
let state_problems =
|
|
369
|
+
let state_problems =
|
|
370
|
+
mutations::state_source_problems(command, aggregate, value_objects_by_name);
|
|
167
371
|
if !state_problems.is_empty() {
|
|
168
|
-
return Some(skip(
|
|
372
|
+
return Some(skip(
|
|
373
|
+
"state_source",
|
|
374
|
+
format!("sets state source(s): {}", state_problems.join("; ")),
|
|
375
|
+
));
|
|
169
376
|
}
|
|
170
377
|
|
|
171
|
-
let expression_problems =
|
|
378
|
+
let expression_problems =
|
|
379
|
+
mutations::expression_source_problems(command, aggregate, value_objects_by_name);
|
|
172
380
|
if !expression_problems.is_empty() {
|
|
173
|
-
return Some(skip(
|
|
381
|
+
return Some(skip(
|
|
382
|
+
"expression_source",
|
|
383
|
+
format!(
|
|
384
|
+
"sets expression source(s): {}",
|
|
385
|
+
expression_problems.join("; ")
|
|
386
|
+
),
|
|
387
|
+
));
|
|
174
388
|
}
|
|
175
389
|
|
|
176
|
-
let remove_problems =
|
|
390
|
+
let remove_problems =
|
|
391
|
+
mutations::remove_field_problems(command, aggregate, value_objects_by_name);
|
|
177
392
|
if !remove_problems.is_empty() {
|
|
178
|
-
return Some(skip(
|
|
393
|
+
return Some(skip(
|
|
394
|
+
"remove_field",
|
|
395
|
+
format!("sets remove field(s): {}", remove_problems.join("; ")),
|
|
396
|
+
));
|
|
179
397
|
}
|
|
180
398
|
|
|
181
|
-
let lifecycle_field = aggregate
|
|
399
|
+
let lifecycle_field = aggregate
|
|
400
|
+
.get("lifecycle")
|
|
401
|
+
.and_then(|l| l.get("field"))
|
|
402
|
+
.map(Json::to_s);
|
|
182
403
|
|
|
183
404
|
let literal_set_targets: Vec<String> = mutations_list
|
|
184
405
|
.iter()
|
|
@@ -186,14 +407,20 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
|
|
|
186
407
|
if m.get("op").map(Json::to_s).unwrap_or_default() != "set" {
|
|
187
408
|
return false;
|
|
188
409
|
}
|
|
189
|
-
let Some(source) = m.get("source") else {
|
|
410
|
+
let Some(source) = m.get("source") else {
|
|
411
|
+
return false;
|
|
412
|
+
};
|
|
190
413
|
if source.get("kind").map(Json::to_s).unwrap_or_default() != "literal" {
|
|
191
414
|
return false;
|
|
192
415
|
}
|
|
193
416
|
let value = source.get("value").unwrap_or(&Json::Null);
|
|
194
417
|
let target = m.get("target").map(Json::to_s).unwrap_or_default();
|
|
195
418
|
let target_type = target_type_for(&target, aggregate, lifecycle_field.as_deref());
|
|
196
|
-
!crate::bridging::literal_set_bridgeable(
|
|
419
|
+
!crate::bridging::literal_set_bridgeable(
|
|
420
|
+
&Literal::from_json(value),
|
|
421
|
+
target_type,
|
|
422
|
+
value_objects_by_name,
|
|
423
|
+
)
|
|
197
424
|
})
|
|
198
425
|
.map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
|
|
199
426
|
.collect();
|
|
@@ -207,21 +434,26 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
|
|
|
207
434
|
if m.get("op").map(Json::to_s).unwrap_or_default() != "set" {
|
|
208
435
|
return false;
|
|
209
436
|
}
|
|
210
|
-
let Some(source) = m.get("source") else {
|
|
437
|
+
let Some(source) = m.get("source") else {
|
|
438
|
+
return false;
|
|
439
|
+
};
|
|
211
440
|
if source.get("kind").map(Json::to_s).unwrap_or_default() != "argument" {
|
|
212
441
|
return false;
|
|
213
442
|
}
|
|
214
443
|
let target = m.get("target").map(Json::to_s).unwrap_or_default();
|
|
215
444
|
let target_type = target_type_for(&target, aggregate, lifecycle_field.as_deref());
|
|
216
445
|
let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
|
|
217
|
-
let cmd_attrs = command.get("attributes").
|
|
218
|
-
let source_attr = cmd_attrs
|
|
446
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
447
|
+
let source_attr = cmd_attrs
|
|
448
|
+
.iter()
|
|
449
|
+
.find(|a| crate::attr::name(a) == source_name);
|
|
219
450
|
let source_type = source_attr.map(crate::attr::type_name);
|
|
220
451
|
// List-ness must match too: element-type bridging alone would collapse a `Vec` to a
|
|
221
452
|
// scalar.
|
|
222
453
|
match (target_type, source_type, source_attr) {
|
|
223
454
|
(Some(t), Some(s), Some(source_attr)) => {
|
|
224
|
-
target_list_for(&target, aggregate, lifecycle_field.as_deref())
|
|
455
|
+
target_list_for(&target, aggregate, lifecycle_field.as_deref())
|
|
456
|
+
!= crate::attr::list(source_attr)
|
|
225
457
|
|| !crate::bridging::bridgeable_value_types(s, t, value_objects_by_name)
|
|
226
458
|
}
|
|
227
459
|
_ => false,
|
|
@@ -233,13 +465,37 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
|
|
|
233
465
|
return Some(skip("set_argument_bridge", format!("sets :{} sources an argument no single-field rewrap can bridge to the target's type — not generated yet", mismatched_sets.join(", "))));
|
|
234
466
|
}
|
|
235
467
|
|
|
236
|
-
let arithmetic_targets: Vec<&Json> = mutations_list
|
|
468
|
+
let arithmetic_targets: Vec<&Json> = mutations_list
|
|
469
|
+
.iter()
|
|
470
|
+
.filter(|m| {
|
|
471
|
+
["increment", "decrement", "multiply"]
|
|
472
|
+
.contains(&m.get("op").map(Json::to_s).unwrap_or_default().as_str())
|
|
473
|
+
})
|
|
474
|
+
.collect();
|
|
237
475
|
let unsupported_arithmetic: Vec<String> = arithmetic_targets
|
|
238
476
|
.iter()
|
|
239
477
|
.filter(|m| {
|
|
240
|
-
let target =
|
|
478
|
+
let target =
|
|
479
|
+
crate::bridging::arithmetic_target_field(m, aggregate, value_objects_by_name);
|
|
241
480
|
match target {
|
|
242
|
-
Some((_, field)) =>
|
|
481
|
+
Some((_, field, kind)) => {
|
|
482
|
+
crate::bridging::arithmetic_amount_expr(
|
|
483
|
+
m.get("source").unwrap_or(&Json::Null),
|
|
484
|
+
command,
|
|
485
|
+
value_objects_by_name,
|
|
486
|
+
&field,
|
|
487
|
+
kind,
|
|
488
|
+
)
|
|
489
|
+
.is_none()
|
|
490
|
+
&& crate::bridging::text_operand_expr(
|
|
491
|
+
m,
|
|
492
|
+
aggregate,
|
|
493
|
+
command,
|
|
494
|
+
value_objects_by_name,
|
|
495
|
+
&field,
|
|
496
|
+
)
|
|
497
|
+
.is_none()
|
|
498
|
+
}
|
|
243
499
|
None => true,
|
|
244
500
|
}
|
|
245
501
|
})
|
|
@@ -250,104 +506,165 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
|
|
|
250
506
|
}
|
|
251
507
|
|
|
252
508
|
// `:clamp`: target as for increment/decrement/multiply; bounds via `clamp_bounds_ints`.
|
|
253
|
-
let clamp_targets: Vec<&Json> = mutations_list
|
|
509
|
+
let clamp_targets: Vec<&Json> = mutations_list
|
|
510
|
+
.iter()
|
|
511
|
+
.filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "clamp")
|
|
512
|
+
.collect();
|
|
254
513
|
let unsupported_clamp: Vec<String> = clamp_targets
|
|
255
514
|
.iter()
|
|
256
515
|
.filter(|m| {
|
|
257
|
-
let target = crate::bridging::
|
|
516
|
+
let target = crate::bridging::integer_target_field(m, aggregate, value_objects_by_name);
|
|
258
517
|
let bounds = m.get("source").and_then(crate::bridging::clamp_bounds_ints);
|
|
259
518
|
target.is_none() || bounds.is_none()
|
|
260
519
|
})
|
|
261
520
|
.map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
|
|
262
521
|
.collect();
|
|
263
522
|
if !unsupported_clamp.is_empty() {
|
|
264
|
-
return Some(skip(
|
|
523
|
+
return Some(skip(
|
|
524
|
+
"clamp",
|
|
525
|
+
format!(
|
|
526
|
+
"sets :{} clamp target field or bounds isn't bridgeable — not generated yet",
|
|
527
|
+
unsupported_clamp.join(", ")
|
|
528
|
+
),
|
|
529
|
+
));
|
|
265
530
|
}
|
|
266
531
|
|
|
267
|
-
let optional_problems = optional_source_mismatches_with(
|
|
532
|
+
let optional_problems = optional_source_mismatches_with(
|
|
533
|
+
command,
|
|
534
|
+
aggregate,
|
|
535
|
+
value_objects_by_name,
|
|
536
|
+
creating_possible,
|
|
537
|
+
);
|
|
268
538
|
if !optional_problems.is_empty() {
|
|
269
|
-
return Some(skip(
|
|
539
|
+
return Some(skip(
|
|
540
|
+
"optional_source",
|
|
541
|
+
format!(
|
|
542
|
+
"optional argument feeds a non-optional target: {} — not generated yet",
|
|
543
|
+
optional_problems.join("; ")
|
|
544
|
+
),
|
|
545
|
+
));
|
|
270
546
|
}
|
|
271
547
|
|
|
272
548
|
None
|
|
273
549
|
}
|
|
274
550
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
551
|
+
fn optional_source_mismatches_with(
|
|
552
|
+
command: &Json,
|
|
553
|
+
aggregate: &Json,
|
|
554
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
555
|
+
creating_possible: bool,
|
|
556
|
+
) -> Vec<String> {
|
|
557
|
+
let lifecycle_field = aggregate
|
|
558
|
+
.get("lifecycle")
|
|
559
|
+
.and_then(|l| l.get("field"))
|
|
560
|
+
.map(Json::to_s);
|
|
281
561
|
let mut problems: Vec<String> = Vec::new();
|
|
282
|
-
let cmd_attrs = command.get("attributes").
|
|
562
|
+
let cmd_attrs = command.get("attributes").map_or_default(Json::each);
|
|
283
563
|
|
|
284
|
-
if creating_possible && crate::shared::creates_owner(aggregate, command
|
|
285
|
-
let identified_by = aggregate.get("identified_by").
|
|
564
|
+
if creating_possible && crate::shared::creates_owner(aggregate, command) {
|
|
565
|
+
let identified_by = aggregate.get("identified_by").map_or_default(Json::each);
|
|
286
566
|
for path in identified_by {
|
|
287
567
|
let path = path.as_str().unwrap_or("");
|
|
288
568
|
let head = path.split('.').next().unwrap_or("");
|
|
289
569
|
if let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == head) {
|
|
290
570
|
if crate::attr::optional(source_attr) {
|
|
291
|
-
problems.push(format!(
|
|
571
|
+
problems.push(format!(
|
|
572
|
+
"identified_by :{path} sources optional argument {}",
|
|
573
|
+
crate::attr::name(source_attr)
|
|
574
|
+
));
|
|
292
575
|
}
|
|
293
576
|
}
|
|
294
577
|
}
|
|
295
578
|
}
|
|
296
579
|
|
|
297
|
-
let mutations_list = command.get("mutations").
|
|
580
|
+
let mutations_list = command.get("mutations").map_or_default(Json::each);
|
|
298
581
|
for m in mutations_list {
|
|
299
582
|
let op = m.get("op").map(Json::to_s).unwrap_or_default();
|
|
300
583
|
let target = m.get("target").map(Json::to_s).unwrap_or_default();
|
|
301
584
|
match op.as_str() {
|
|
302
585
|
"set" => {
|
|
303
|
-
let Some(source) = m.get("source") else {
|
|
586
|
+
let Some(source) = m.get("source") else {
|
|
587
|
+
continue;
|
|
588
|
+
};
|
|
304
589
|
if source.get("kind").map(Json::to_s).unwrap_or_default() != "argument" {
|
|
305
590
|
continue;
|
|
306
591
|
}
|
|
307
592
|
let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
|
|
308
|
-
let Some(source_attr) = cmd_attrs
|
|
593
|
+
let Some(source_attr) = cmd_attrs
|
|
594
|
+
.iter()
|
|
595
|
+
.find(|a| crate::attr::name(a) == source_name)
|
|
596
|
+
else {
|
|
597
|
+
continue;
|
|
598
|
+
};
|
|
309
599
|
if !crate::attr::optional(source_attr) {
|
|
310
600
|
continue;
|
|
311
601
|
}
|
|
312
602
|
|
|
313
603
|
if lifecycle_field.as_deref() == Some(target.as_str()) {
|
|
314
|
-
problems.push(format!(
|
|
604
|
+
problems.push(format!(
|
|
605
|
+
"sets :{target} sources optional argument {} into the lifecycle field",
|
|
606
|
+
crate::attr::name(source_attr)
|
|
607
|
+
));
|
|
315
608
|
continue;
|
|
316
609
|
}
|
|
317
610
|
|
|
318
|
-
let attrs = aggregate.get("attributes").
|
|
611
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
319
612
|
let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target);
|
|
320
|
-
if target_attr.
|
|
613
|
+
if target_attr.is_some_and(crate::attr::list) {
|
|
321
614
|
continue;
|
|
322
615
|
}
|
|
323
616
|
// An aggregate command's record fields are all `Option<T>`.
|
|
324
617
|
if creating_possible {
|
|
325
618
|
continue;
|
|
326
619
|
}
|
|
327
|
-
if !target_attr.
|
|
328
|
-
problems.push(format!(
|
|
620
|
+
if !target_attr.is_some_and(crate::attr::optional) {
|
|
621
|
+
problems.push(format!(
|
|
622
|
+
"sets :{target} sources optional argument {}",
|
|
623
|
+
crate::attr::name(source_attr)
|
|
624
|
+
));
|
|
329
625
|
}
|
|
330
626
|
}
|
|
331
627
|
"append" => {
|
|
332
|
-
let attrs = aggregate.get("attributes").
|
|
333
|
-
let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target)
|
|
334
|
-
|
|
628
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
629
|
+
let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target)
|
|
630
|
+
else {
|
|
631
|
+
continue;
|
|
632
|
+
};
|
|
633
|
+
let Some(element) = mutations::append_element(
|
|
634
|
+
aggregate,
|
|
635
|
+
crate::attr::type_name(target_attr),
|
|
636
|
+
value_objects_by_name,
|
|
637
|
+
) else {
|
|
638
|
+
continue;
|
|
639
|
+
};
|
|
335
640
|
|
|
336
641
|
let fields = match m.get("fields") {
|
|
337
642
|
Some(Json::Object(pairs)) => pairs.clone(),
|
|
338
643
|
_ => Vec::new(),
|
|
339
644
|
};
|
|
340
|
-
let element_attrs = element.get("attributes").
|
|
645
|
+
let element_attrs = element.get("attributes").map_or_default(Json::each);
|
|
341
646
|
for (field_name, source) in &fields {
|
|
342
647
|
let parsed = mutations::append_field_source(&source.to_s());
|
|
343
|
-
let Literal::Symbol(arg_name) = &parsed else {
|
|
344
|
-
|
|
648
|
+
let Literal::Symbol(arg_name) = &parsed else {
|
|
649
|
+
continue;
|
|
650
|
+
};
|
|
651
|
+
let Some(source_attr) = cmd_attrs
|
|
652
|
+
.iter()
|
|
653
|
+
.find(|a| crate::attr::name(a) == arg_name.as_str())
|
|
654
|
+
else {
|
|
655
|
+
continue;
|
|
656
|
+
};
|
|
345
657
|
if !crate::attr::optional(source_attr) {
|
|
346
658
|
continue;
|
|
347
659
|
}
|
|
348
|
-
let field_attr = element_attrs
|
|
349
|
-
|
|
350
|
-
|
|
660
|
+
let field_attr = element_attrs
|
|
661
|
+
.iter()
|
|
662
|
+
.find(|a| crate::attr::name(a) == field_name.as_str());
|
|
663
|
+
if !field_attr.is_some_and(crate::attr::optional) {
|
|
664
|
+
problems.push(format!(
|
|
665
|
+
"sets append {target}.{field_name} sources optional argument {}",
|
|
666
|
+
crate::attr::name(source_attr)
|
|
667
|
+
));
|
|
351
668
|
}
|
|
352
669
|
}
|
|
353
670
|
}
|
|
@@ -361,19 +678,31 @@ fn optional_source_mismatches_with(command: &Json, aggregate: &Json, value_objec
|
|
|
361
678
|
/// One attribute's `admits:` check and value-object invariant check against `value_expr`.
|
|
362
679
|
/// `pattern:` is not checked: Ruby never enforces it on a bare command attribute (ADR 0051).
|
|
363
680
|
/// List attributes get no `admits:` check either, matching Ruby.
|
|
364
|
-
pub fn argument_check_lines(
|
|
681
|
+
pub fn argument_check_lines(
|
|
682
|
+
exemplar: &Exemplar,
|
|
683
|
+
attr: &Json,
|
|
684
|
+
value_expr: &str,
|
|
685
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
686
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
687
|
+
) -> Vec<String> {
|
|
365
688
|
let mut lines: Vec<String> = Vec::new();
|
|
366
689
|
|
|
367
690
|
if !crate::attr::list(attr) {
|
|
368
691
|
// Raw field expression: `constraints.rs` handles optionals itself.
|
|
369
|
-
if let Some(c) = crate::constraints::emit_admits_check(
|
|
692
|
+
if let Some(c) = crate::constraints::emit_admits_check(
|
|
693
|
+
exemplar,
|
|
694
|
+
value_expr,
|
|
695
|
+
attr,
|
|
696
|
+
aggregates_by_name,
|
|
697
|
+
value_objects_by_name,
|
|
698
|
+
) {
|
|
370
699
|
lines.push(format!(" {c}"));
|
|
371
700
|
}
|
|
372
701
|
}
|
|
373
702
|
|
|
374
703
|
let vo = value_objects_by_name.get(crate::attr::type_name(attr));
|
|
375
704
|
if let Some(vo) = vo {
|
|
376
|
-
if !vo.get("closed_set").
|
|
705
|
+
if !vo.get("closed_set").is_some_and(Json::as_bool) {
|
|
377
706
|
let line = if crate::attr::optional(attr) {
|
|
378
707
|
if crate::attr::list(attr) {
|
|
379
708
|
format!(" if let Some(items) = &{value_expr} {{ for item in items {{ item.check_invariants()?; }} }}")
|
|
@@ -393,65 +722,68 @@ pub fn argument_check_lines(exemplar: &Exemplar, attr: &Json, value_expr: &str,
|
|
|
393
722
|
}
|
|
394
723
|
|
|
395
724
|
/// Invariant checks against the built `args.{field}`, redundant with `emit_from_json_flat`'s copy.
|
|
396
|
-
pub fn invariant_checks_for(
|
|
397
|
-
|
|
725
|
+
pub fn invariant_checks_for(
|
|
726
|
+
exemplar: &Exemplar,
|
|
727
|
+
command: &Json,
|
|
728
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
729
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
730
|
+
) -> Vec<String> {
|
|
731
|
+
let attrs = command.get("attributes").map_or_default(Json::each);
|
|
398
732
|
let mut lines: Vec<String> = Vec::new();
|
|
399
733
|
|
|
400
734
|
for attr in attrs {
|
|
401
735
|
let field = naming::rust_ident_field(crate::attr::name(attr));
|
|
402
736
|
let value_expr = format!("args.{field}");
|
|
403
|
-
lines.extend(argument_check_lines(
|
|
737
|
+
lines.extend(argument_check_lines(
|
|
738
|
+
exemplar,
|
|
739
|
+
attr,
|
|
740
|
+
&value_expr,
|
|
741
|
+
aggregates_by_name,
|
|
742
|
+
value_objects_by_name,
|
|
743
|
+
));
|
|
404
744
|
}
|
|
405
745
|
|
|
406
746
|
lines
|
|
407
747
|
}
|
|
408
748
|
|
|
409
749
|
/// One emitter for every command shape `kernel::dispatch` can run.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
750
|
+
#[allow(
|
|
751
|
+
clippy::option_if_let_else,
|
|
752
|
+
clippy::panic,
|
|
753
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose; a map_or_else closure would bury this long body"
|
|
754
|
+
)]
|
|
755
|
+
pub fn emit_command(
|
|
756
|
+
exemplar: &Exemplar,
|
|
757
|
+
command: &Json,
|
|
758
|
+
aggregate: &Json,
|
|
759
|
+
domain_name: &str,
|
|
760
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
761
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
762
|
+
) -> String {
|
|
763
|
+
let record = naming::rust_ident(name_of(aggregate));
|
|
764
|
+
let cmd = naming::rust_ident(name_of(command));
|
|
765
|
+
let creates = crate::shared::creates_owner(aggregate, command);
|
|
414
766
|
let identity = mutations::identity_components(aggregate, command);
|
|
415
|
-
let identity_extra_params: Vec<String> =
|
|
767
|
+
let identity_extra_params: Vec<String> =
|
|
768
|
+
identity.iter().filter_map(|c| c.param.clone()).collect();
|
|
416
769
|
|
|
417
|
-
let aggregate_name = aggregate
|
|
418
|
-
let identity_reading = aggregate
|
|
770
|
+
let aggregate_name = name_of(aggregate).to_string();
|
|
771
|
+
let identity_reading = identity_reading(aggregate);
|
|
419
772
|
|
|
420
|
-
let attrs = command.get("attributes").
|
|
421
|
-
let
|
|
422
|
-
for attr in attrs {
|
|
423
|
-
let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
|
|
424
|
-
if crate::attr::optional(attr) {
|
|
425
|
-
ty = format!("Option<{ty}>");
|
|
426
|
-
}
|
|
427
|
-
args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
|
|
428
|
-
}
|
|
429
|
-
args_struct.push("}".to_string());
|
|
773
|
+
let attrs = command.get("attributes").map_or_default(Json::each);
|
|
774
|
+
let args_struct = args_struct_text(exemplar, &format!("{cmd}Args"), attrs);
|
|
430
775
|
|
|
431
|
-
let invariant_checks =
|
|
776
|
+
let invariant_checks =
|
|
777
|
+
invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
|
|
432
778
|
|
|
433
|
-
let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
|
|
434
779
|
let mut given_specs: Vec<String> = crate::bridging::corrects_given_specs(command);
|
|
435
|
-
given_specs.extend(
|
|
436
|
-
|
|
437
|
-
let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
|
|
438
|
-
format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
|
|
439
|
-
}));
|
|
440
|
-
|
|
441
|
-
let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
|
|
442
|
-
let ensures_specs: Vec<String> = ensures
|
|
443
|
-
.iter()
|
|
444
|
-
.map(|e| {
|
|
445
|
-
let description = e.get("description").and_then(Json::as_str).unwrap_or("");
|
|
446
|
-
let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
|
|
447
|
-
format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
|
|
448
|
-
})
|
|
449
|
-
.collect();
|
|
780
|
+
given_specs.extend(given_spec_lines(command, " "));
|
|
781
|
+
let ensures_specs = ensures_spec_lines(command, " ");
|
|
450
782
|
|
|
451
783
|
let transition = mutations::lifecycle_transition_for(command, aggregate);
|
|
452
784
|
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
453
785
|
|
|
454
|
-
let mutations_list = command.get("mutations").
|
|
786
|
+
let mutations_list = command.get("mutations").map_or_default(Json::each);
|
|
455
787
|
// A `delegate` mutation is rendered by `delegation_of`, which overwrites `mutation_lines`;
|
|
456
788
|
// `mutations::emit_mutation_line` panics on the op, so it must be excluded here.
|
|
457
789
|
let mut mutation_lines: Vec<String> = mutations_list
|
|
@@ -460,7 +792,16 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
460
792
|
let op = m.get("op").map(Json::to_s).unwrap_or_default();
|
|
461
793
|
op != "corrects" && op != "delegate"
|
|
462
794
|
})
|
|
463
|
-
.map(|m|
|
|
795
|
+
.map(|m| {
|
|
796
|
+
mutations::emit_mutation_line(
|
|
797
|
+
exemplar,
|
|
798
|
+
m,
|
|
799
|
+
aggregate,
|
|
800
|
+
command,
|
|
801
|
+
value_objects_by_name,
|
|
802
|
+
true,
|
|
803
|
+
)
|
|
804
|
+
})
|
|
464
805
|
.collect();
|
|
465
806
|
if mutations::reads_pre_state(mutations_list) {
|
|
466
807
|
mutation_lines.insert(0, mutations::pre_state_line());
|
|
@@ -468,7 +809,12 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
468
809
|
// Stamp the fact forward for every command whose `emits` names an event a `corrects` targets.
|
|
469
810
|
let correctable = crate::bridging::correctable_event_names(aggregate);
|
|
470
811
|
let mut emitted_names: Vec<String> = Vec::new();
|
|
471
|
-
for name in command
|
|
812
|
+
for name in command
|
|
813
|
+
.get("emits")
|
|
814
|
+
.map_or_default(Json::each)
|
|
815
|
+
.iter()
|
|
816
|
+
.map(Json::to_s)
|
|
817
|
+
{
|
|
472
818
|
// `.uniq`: first-occurrence order, not sorted.
|
|
473
819
|
if !emitted_names.contains(&name) {
|
|
474
820
|
emitted_names.push(name);
|
|
@@ -476,30 +822,46 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
476
822
|
}
|
|
477
823
|
for name in &emitted_names {
|
|
478
824
|
if correctable.contains(name) {
|
|
479
|
-
mutation_lines.push(format!(
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if !t.to_state.is_empty() {
|
|
484
|
-
mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
|
|
825
|
+
mutation_lines.push(format!(
|
|
826
|
+
" record.{} = true;",
|
|
827
|
+
crate::bridging::corrects_flag_field(name)
|
|
828
|
+
));
|
|
485
829
|
}
|
|
486
830
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
831
|
+
let mut mutation_lines = close_mutation_lines(mutation_lines, transition.as_ref(), " ");
|
|
832
|
+
let delegation = delegation_of(
|
|
833
|
+
exemplar,
|
|
834
|
+
command,
|
|
835
|
+
aggregate,
|
|
836
|
+
value_objects_by_name,
|
|
837
|
+
domain_name,
|
|
838
|
+
);
|
|
491
839
|
if let Some(d) = &delegation {
|
|
492
|
-
assert!(
|
|
840
|
+
assert!(
|
|
841
|
+
!creates,
|
|
842
|
+
"{cmd}: a creating command cannot delegate — nothing exists to delegate to"
|
|
843
|
+
);
|
|
493
844
|
// `d.element` runs first, inside the closure `dispatch` calls after a successful hydrate.
|
|
494
845
|
mutation_lines = vec![d.element.clone(), d.apply.clone()];
|
|
495
846
|
}
|
|
496
|
-
let prelude = delegation
|
|
497
|
-
|
|
847
|
+
let prelude = delegation
|
|
848
|
+
.as_ref()
|
|
849
|
+
.map(|d| d.prelude.clone())
|
|
850
|
+
.unwrap_or_default();
|
|
851
|
+
let payload = if delegation.is_some() {
|
|
852
|
+
"delegate_facts.clone(),"
|
|
853
|
+
} else {
|
|
854
|
+
"args.to_json(),"
|
|
855
|
+
}
|
|
856
|
+
.to_string();
|
|
498
857
|
|
|
499
858
|
let (hydrate, fn_signature);
|
|
500
859
|
// A `projects` field is never a command argument; `seed_projections` populates it before save.
|
|
501
860
|
// Owned because the pseudo-attributes are freshly built.
|
|
502
|
-
let mut record_agg_attrs_owned: Vec<Json> = aggregate
|
|
861
|
+
let mut record_agg_attrs_owned: Vec<Json> = aggregate
|
|
862
|
+
.get("attributes")
|
|
863
|
+
.map_or_default(Json::each)
|
|
864
|
+
.to_vec();
|
|
503
865
|
record_agg_attrs_owned.extend(crate::types::projected_field_pseudo_attributes(aggregate));
|
|
504
866
|
let record_agg_attrs = record_agg_attrs_owned.as_slice();
|
|
505
867
|
|
|
@@ -507,20 +869,37 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
507
869
|
let record_fields: Vec<String> = record_agg_attrs
|
|
508
870
|
.iter()
|
|
509
871
|
.map(|attr| {
|
|
510
|
-
let matched = attrs
|
|
872
|
+
let matched = attrs
|
|
873
|
+
.iter()
|
|
874
|
+
.find(|a| crate::attr::name(a) == crate::attr::name(attr));
|
|
511
875
|
let field = naming::rust_ident_field(crate::attr::name(attr));
|
|
512
876
|
if let Some(matched) = matched {
|
|
513
877
|
let matched_type = crate::attr::type_name(matched);
|
|
514
878
|
let attr_type = crate::attr::type_name(attr);
|
|
515
879
|
if crate::attr::optional(matched) {
|
|
516
|
-
if crate::attr::list(attr)
|
|
517
|
-
|
|
880
|
+
if crate::attr::list(attr)
|
|
881
|
+
&& !crate::shared::list_attr_creation_optional(
|
|
882
|
+
aggregate,
|
|
883
|
+
crate::attr::name(attr),
|
|
884
|
+
)
|
|
885
|
+
{
|
|
886
|
+
format!(
|
|
887
|
+
" {field}: args.{field}.clone().unwrap_or_default(),"
|
|
888
|
+
)
|
|
518
889
|
} else if crate::attr::list(attr) || matched_type == attr_type {
|
|
519
890
|
format!(" {field}: args.{field}.clone(),")
|
|
520
891
|
} else {
|
|
521
892
|
// A cross-aggregate argument named like the owner's field but of
|
|
522
893
|
// another type needs a bridge.
|
|
523
|
-
format!(
|
|
894
|
+
format!(
|
|
895
|
+
" {field}: {},",
|
|
896
|
+
mutations::optional_value_rhs(
|
|
897
|
+
&format!("args.{field}"),
|
|
898
|
+
matched_type,
|
|
899
|
+
attr_type,
|
|
900
|
+
value_objects_by_name
|
|
901
|
+
)
|
|
902
|
+
)
|
|
524
903
|
}
|
|
525
904
|
} else if crate::attr::list(attr) || matched_type == attr_type {
|
|
526
905
|
if crate::attr::list(attr) {
|
|
@@ -529,15 +908,23 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
529
908
|
format!(" {field}: Some(args.{field}.clone()),")
|
|
530
909
|
}
|
|
531
910
|
} else {
|
|
532
|
-
format!(
|
|
911
|
+
format!(
|
|
912
|
+
" {field}: Some({}),",
|
|
913
|
+
crate::bridging::value_rhs(
|
|
914
|
+
&format!("args.{field}"),
|
|
915
|
+
matched_type,
|
|
916
|
+
attr_type,
|
|
917
|
+
value_objects_by_name
|
|
918
|
+
)
|
|
919
|
+
)
|
|
533
920
|
}
|
|
534
921
|
} else if crate::attr::list(attr) {
|
|
535
922
|
format!(" {field}: vec![],")
|
|
536
923
|
} else {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
924
|
+
crate::bridging::creation_default_rhs(attr, value_objects_by_name).map_or_else(
|
|
925
|
+
|| format!(" {field}: None,"),
|
|
926
|
+
|rhs| format!(" {field}: Some({rhs}),"),
|
|
927
|
+
)
|
|
541
928
|
}
|
|
542
929
|
})
|
|
543
930
|
.collect();
|
|
@@ -545,14 +932,25 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
545
932
|
if let Some(lifecycle) = aggregate.get("lifecycle") {
|
|
546
933
|
let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("");
|
|
547
934
|
let default = lifecycle.get("default").map(Json::to_s).unwrap_or_default();
|
|
548
|
-
record_fields.push(format!(
|
|
935
|
+
record_fields.push(format!(
|
|
936
|
+
" {}: {}.to_string(),",
|
|
937
|
+
naming::rust_ident_field(field),
|
|
938
|
+
naming::ruby_inspect_string(&default)
|
|
939
|
+
));
|
|
549
940
|
}
|
|
550
941
|
for ev in crate::bridging::correctable_event_names(aggregate) {
|
|
551
|
-
record_fields.push(format!(
|
|
942
|
+
record_fields.push(format!(
|
|
943
|
+
" {}: false,",
|
|
944
|
+
crate::bridging::corrects_flag_field(&ev)
|
|
945
|
+
));
|
|
552
946
|
}
|
|
553
947
|
|
|
554
948
|
// Mirrors `CommandInterpreter#step_hydrate`: `complete_state? && state_independent?`.
|
|
555
|
-
let state_independent = crate::dependency_planning::state_independent_creation(
|
|
949
|
+
let state_independent = crate::dependency_planning::state_independent_creation(
|
|
950
|
+
aggregate,
|
|
951
|
+
command,
|
|
952
|
+
value_objects_by_name,
|
|
953
|
+
);
|
|
556
954
|
let create_block = format!(
|
|
557
955
|
"crate::kernel::Hydrate::Create {{\n id: __hydrate_id,\n build: Box::new(|| {record} {{\n{}\n }}),\n state_independent: {state_independent},\n }}",
|
|
558
956
|
record_fields.join("\n")
|
|
@@ -560,7 +958,11 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
560
958
|
|
|
561
959
|
// A route on a creating command is checked against its derived identity (`TypeMismatch`)
|
|
562
960
|
// only when `complete_state_creation` holds; otherwise it forces find-or-`NotFound`.
|
|
563
|
-
let complete_state = crate::dependency_planning::complete_state_creation(
|
|
961
|
+
let complete_state = crate::dependency_planning::complete_state_creation(
|
|
962
|
+
aggregate,
|
|
963
|
+
command,
|
|
964
|
+
value_objects_by_name,
|
|
965
|
+
);
|
|
564
966
|
let derived_id_expr = mutations::build_identity_expr(&identity);
|
|
565
967
|
let route_mismatch_message = format!(
|
|
566
968
|
"{} routes to {{:?}}, but its identity facts name {{:?}}",
|
|
@@ -584,7 +986,7 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
584
986
|
sig_parts.extend(identity_extra_params.iter().cloned());
|
|
585
987
|
sig_parts.push(format!("args: {cmd}Args"));
|
|
586
988
|
sig_parts.push("mutations: &mut Vec<crate::kernel::MutationRecord>".to_string());
|
|
587
|
-
sig_parts.extend(deref_params().iter().map(
|
|
989
|
+
sig_parts.extend(deref_params().iter().map(std::string::ToString::to_string));
|
|
588
990
|
sig_parts.push(TENANT_BOUNDARY_PARAM.to_string());
|
|
589
991
|
fn_signature = sig_parts.join(", ");
|
|
590
992
|
} else {
|
|
@@ -594,15 +996,20 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
594
996
|
format!("args: {cmd}Args"),
|
|
595
997
|
"mutations: &mut Vec<crate::kernel::MutationRecord>".to_string(),
|
|
596
998
|
];
|
|
597
|
-
sig_parts.extend(deref_params().iter().map(
|
|
999
|
+
sig_parts.extend(deref_params().iter().map(std::string::ToString::to_string));
|
|
598
1000
|
sig_parts.push(TENANT_BOUNDARY_PARAM.to_string());
|
|
599
1001
|
hydrate = "crate::kernel::Hydrate::Act { id: id.to_string() }".to_string();
|
|
600
1002
|
fn_signature = sig_parts.join(", ");
|
|
601
1003
|
}
|
|
602
1004
|
|
|
603
1005
|
let emits_expr = match &delegation {
|
|
604
|
-
Some(d) => d
|
|
605
|
-
|
|
1006
|
+
Some(d) => d
|
|
1007
|
+
.emits
|
|
1008
|
+
.iter()
|
|
1009
|
+
.map(|e| naming::ruby_inspect_string(e))
|
|
1010
|
+
.collect::<Vec<_>>()
|
|
1011
|
+
.join(", "),
|
|
1012
|
+
None => emits_expr_of(command),
|
|
606
1013
|
};
|
|
607
1014
|
|
|
608
1015
|
let dispatch_fn = exemplar.render(
|
|
@@ -635,85 +1042,145 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
635
1042
|
],
|
|
636
1043
|
);
|
|
637
1044
|
|
|
638
|
-
format!(
|
|
1045
|
+
format!(
|
|
1046
|
+
"{}\n\n#[derive(Debug, Clone)]\n{}\n\n{dispatch_fn}",
|
|
1047
|
+
crate::fielded::emit_fielded_flat(
|
|
1048
|
+
exemplar,
|
|
1049
|
+
&format!("{cmd}Args"),
|
|
1050
|
+
attrs,
|
|
1051
|
+
value_objects_by_name,
|
|
1052
|
+
&[]
|
|
1053
|
+
),
|
|
1054
|
+
args_struct
|
|
1055
|
+
)
|
|
639
1056
|
}
|
|
640
1057
|
|
|
641
1058
|
/// The `:delegate` mutation (`delegates_to`).
|
|
642
1059
|
fn delegate_of(command: &Json) -> Option<&Json> {
|
|
643
|
-
command
|
|
1060
|
+
command
|
|
1061
|
+
.get("mutations")
|
|
1062
|
+
.map_or_default(Json::each)
|
|
1063
|
+
.iter()
|
|
1064
|
+
.find(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "delegate")
|
|
644
1065
|
}
|
|
645
1066
|
|
|
646
1067
|
fn delegate_mapping(delegation: &Json) -> Vec<(String, String)> {
|
|
647
1068
|
match delegation.get("fields") {
|
|
648
|
-
Some(Json::Object(pairs)) => pairs
|
|
1069
|
+
Some(Json::Object(pairs)) => pairs
|
|
1070
|
+
.iter()
|
|
1071
|
+
.map(|(k, v)| (k.clone(), v.to_s().trim_start_matches(':').to_string()))
|
|
1072
|
+
.collect(),
|
|
649
1073
|
_ => Vec::new(),
|
|
650
1074
|
}
|
|
651
1075
|
}
|
|
652
1076
|
|
|
653
|
-
fn delegate_target<'a>(
|
|
1077
|
+
fn delegate_target<'a>(
|
|
1078
|
+
delegation: &Json,
|
|
1079
|
+
aggregate: &'a Json,
|
|
1080
|
+
) -> (Option<&'a Json>, Option<&'a Json>) {
|
|
654
1081
|
let target = delegation.get("target").map(Json::to_s).unwrap_or_default();
|
|
655
|
-
let (entity_name, command_name) =
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1082
|
+
let (entity_name, command_name) = target.rfind('.').map_or(("", target.as_str()), |dot| {
|
|
1083
|
+
(&target[..dot], &target[dot + 1..])
|
|
1084
|
+
});
|
|
1085
|
+
let entity = aggregate
|
|
1086
|
+
.get("entities")
|
|
1087
|
+
.map_or_default(Json::each)
|
|
1088
|
+
.iter()
|
|
1089
|
+
.find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == entity_name);
|
|
1090
|
+
let command = entity.and_then(|e| {
|
|
1091
|
+
e.get("commands")
|
|
1092
|
+
.map_or_default(Json::each)
|
|
1093
|
+
.iter()
|
|
1094
|
+
.find(|c| c.get("name").map(Json::to_s).unwrap_or_default() == command_name)
|
|
1095
|
+
});
|
|
661
1096
|
(entity, command)
|
|
662
1097
|
}
|
|
663
1098
|
|
|
664
|
-
pub fn delegate_skip_reason(
|
|
1099
|
+
pub fn delegate_skip_reason(
|
|
1100
|
+
command: &Json,
|
|
1101
|
+
aggregate: &Json,
|
|
1102
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1103
|
+
) -> Option<String> {
|
|
665
1104
|
let delegation = delegate_of(command)?;
|
|
666
|
-
let label = format!(
|
|
667
|
-
|
|
1105
|
+
let label = format!(
|
|
1106
|
+
"delegates_to {}",
|
|
1107
|
+
delegation.get("target").map(Json::to_s).unwrap_or_default()
|
|
1108
|
+
);
|
|
1109
|
+
if command.get("mutations").map_or_default(Json::each).len() > 1 {
|
|
668
1110
|
return Some(format!("{label} alongside other sets — not generated yet"));
|
|
669
1111
|
}
|
|
670
1112
|
let (entity, target) = delegate_target(delegation, aggregate);
|
|
671
1113
|
let Some(entity) = entity else {
|
|
672
|
-
return Some(format!(
|
|
1114
|
+
return Some(format!(
|
|
1115
|
+
"{label}: {} has no such entity",
|
|
1116
|
+
aggregate.get("name").map(Json::to_s).unwrap_or_default()
|
|
1117
|
+
));
|
|
673
1118
|
};
|
|
674
1119
|
let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
|
|
675
1120
|
let Some(target) = target else {
|
|
676
1121
|
return Some(format!("{label}: {entity_name} declares no such command"));
|
|
677
1122
|
};
|
|
678
1123
|
if !crate::json_codec::extract_id_supported(entity) {
|
|
679
|
-
return Some(format!(
|
|
1124
|
+
return Some(format!(
|
|
1125
|
+
"{label}: {entity_name} cannot be addressed by identity"
|
|
1126
|
+
));
|
|
680
1127
|
}
|
|
681
1128
|
if let Some(problem) = entity_command_skip_reason(target, entity, value_objects_by_name) {
|
|
682
1129
|
return Some(format!("{label}: {problem}"));
|
|
683
1130
|
}
|
|
684
1131
|
let mapping = delegate_mapping(delegation);
|
|
685
|
-
let source_name_for = |name: &str|
|
|
686
|
-
|
|
687
|
-
|
|
1132
|
+
let source_name_for = |name: &str| {
|
|
1133
|
+
mapping
|
|
1134
|
+
.iter()
|
|
1135
|
+
.find(|(t, _)| t == name)
|
|
1136
|
+
.map_or_else(|| name.to_string(), |(_, s)| s.clone())
|
|
1137
|
+
};
|
|
1138
|
+
let argument_attrs = command.get("attributes").map_or_default(Json::each);
|
|
1139
|
+
for attr in target.get("attributes").map_or_default(Json::each) {
|
|
688
1140
|
let name = crate::attr::name(attr);
|
|
689
1141
|
let source_name = source_name_for(name);
|
|
690
|
-
let source = argument_attrs
|
|
1142
|
+
let source = argument_attrs
|
|
1143
|
+
.iter()
|
|
1144
|
+
.find(|a| crate::attr::name(a) == source_name);
|
|
691
1145
|
let Some(source) = source else {
|
|
692
1146
|
if crate::attr::optional(attr) {
|
|
693
1147
|
continue;
|
|
694
1148
|
}
|
|
695
|
-
return Some(format!(
|
|
1149
|
+
return Some(format!(
|
|
1150
|
+
"{label}: target argument {name} has no source on the entry point"
|
|
1151
|
+
));
|
|
696
1152
|
};
|
|
697
1153
|
if crate::attr::list(source) != crate::attr::list(attr) {
|
|
698
1154
|
return Some(format!("{label}: entry point argument {source_name} is a list, target wants a scalar (or vice versa) — not generated yet"));
|
|
699
1155
|
}
|
|
700
1156
|
// Bridgeable, not exact type-name equality: the alias-then-deserialize delegate never
|
|
701
1157
|
// compares declared type names (docs/decisions/0045).
|
|
702
|
-
if !crate::bridging::bridgeable_value_types(
|
|
1158
|
+
if !crate::bridging::bridgeable_value_types(
|
|
1159
|
+
crate::attr::type_name(source),
|
|
1160
|
+
crate::attr::type_name(attr),
|
|
1161
|
+
value_objects_by_name,
|
|
1162
|
+
) {
|
|
703
1163
|
return Some(format!("{label}: entry point argument {source_name} is {}, target wants {} — not generated yet", crate::attr::type_name(source), crate::attr::type_name(attr)));
|
|
704
1164
|
}
|
|
705
1165
|
if crate::attr::optional(source) && !crate::attr::optional(attr) {
|
|
706
|
-
return Some(format!(
|
|
1166
|
+
return Some(format!(
|
|
1167
|
+
"{label}: optional entry point argument {source_name} feeds required {name}"
|
|
1168
|
+
));
|
|
707
1169
|
}
|
|
708
1170
|
}
|
|
709
|
-
for path in entity.get("identified_by").
|
|
1171
|
+
for path in entity.get("identified_by").map_or_default(Json::each) {
|
|
710
1172
|
let path = path.to_s();
|
|
711
1173
|
let head = path.split('.').next().unwrap_or("").to_string();
|
|
712
1174
|
let source_name = source_name_for(&head);
|
|
713
|
-
if argument_attrs
|
|
1175
|
+
if argument_attrs
|
|
1176
|
+
.iter()
|
|
1177
|
+
.any(|a| crate::attr::name(a) == source_name && !crate::attr::optional(a))
|
|
1178
|
+
{
|
|
714
1179
|
continue;
|
|
715
1180
|
}
|
|
716
|
-
return Some(format!(
|
|
1181
|
+
return Some(format!(
|
|
1182
|
+
"{label}: the element's identity {head} has no source on the entry point"
|
|
1183
|
+
));
|
|
717
1184
|
}
|
|
718
1185
|
None
|
|
719
1186
|
}
|
|
@@ -727,30 +1194,59 @@ struct Delegation {
|
|
|
727
1194
|
}
|
|
728
1195
|
|
|
729
1196
|
fn indent_block(text: &str, indent: &str) -> String {
|
|
730
|
-
text.lines()
|
|
1197
|
+
text.lines()
|
|
1198
|
+
.map(|l| format!("{indent}{l}"))
|
|
1199
|
+
.collect::<Vec<_>>()
|
|
1200
|
+
.join("\n")
|
|
1201
|
+
.trim_end()
|
|
1202
|
+
.to_string()
|
|
731
1203
|
}
|
|
732
1204
|
|
|
733
|
-
|
|
1205
|
+
#[allow(
|
|
1206
|
+
clippy::expect_used,
|
|
1207
|
+
clippy::panic,
|
|
1208
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose; the message names an invariant an earlier check in this generator established"
|
|
1209
|
+
)]
|
|
1210
|
+
fn delegation_of(
|
|
1211
|
+
exemplar: &Exemplar,
|
|
1212
|
+
command: &Json,
|
|
1213
|
+
aggregate: &Json,
|
|
1214
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1215
|
+
domain_name: &str,
|
|
1216
|
+
) -> Option<Delegation> {
|
|
734
1217
|
let delegation = delegate_of(command)?;
|
|
735
1218
|
let (entity, target) = delegate_target(delegation, aggregate);
|
|
736
|
-
let (entity, target) = (
|
|
1219
|
+
let (entity, target) = (
|
|
1220
|
+
entity.expect("delegate_skip_reason admitted the entity"),
|
|
1221
|
+
target.expect("delegate_skip_reason admitted the target"),
|
|
1222
|
+
);
|
|
737
1223
|
let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
|
|
738
1224
|
let aggregate_name = aggregate.get("name").map(Json::to_s).unwrap_or_default();
|
|
739
1225
|
let list_attr = aggregate
|
|
740
1226
|
.get("attributes")
|
|
741
|
-
.
|
|
742
|
-
.unwrap_or(&[])
|
|
1227
|
+
.map_or_default(Json::each)
|
|
743
1228
|
.iter()
|
|
744
1229
|
.find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity_name)
|
|
745
1230
|
.unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it"));
|
|
746
1231
|
|
|
747
1232
|
let element_record = naming::rust_ident(&entity_name);
|
|
748
|
-
let target_args_name = format!(
|
|
749
|
-
|
|
1233
|
+
let target_args_name = format!(
|
|
1234
|
+
"{element_record}{}EntityArgs",
|
|
1235
|
+
naming::rust_ident(&target.get("name").map(Json::to_s).unwrap_or_default())
|
|
1236
|
+
);
|
|
1237
|
+
let aliases: Vec<String> = delegate_mapping(delegation)
|
|
1238
|
+
.iter()
|
|
1239
|
+
.map(|(t, s)| {
|
|
1240
|
+
format!(
|
|
1241
|
+
"({}, {})",
|
|
1242
|
+
naming::ruby_inspect_string(t),
|
|
1243
|
+
naming::ruby_inspect_string(s)
|
|
1244
|
+
)
|
|
1245
|
+
})
|
|
1246
|
+
.collect();
|
|
750
1247
|
let given_specs: Vec<String> = target
|
|
751
1248
|
.get("givens")
|
|
752
|
-
.
|
|
753
|
-
.unwrap_or(&[])
|
|
1249
|
+
.map_or_default(Json::each)
|
|
754
1250
|
.iter()
|
|
755
1251
|
.map(|g| {
|
|
756
1252
|
let description = g.get("description").and_then(Json::as_str).unwrap_or("");
|
|
@@ -760,55 +1256,103 @@ fn delegation_of(exemplar: &Exemplar, command: &Json, aggregate: &Json, value_ob
|
|
|
760
1256
|
.collect();
|
|
761
1257
|
let ensures_specs: Vec<String> = target
|
|
762
1258
|
.get("ensures")
|
|
763
|
-
.
|
|
764
|
-
.unwrap_or(&[])
|
|
1259
|
+
.map_or_default(Json::each)
|
|
765
1260
|
.iter()
|
|
766
1261
|
.map(|e| {
|
|
767
1262
|
let description = e.get("description").and_then(Json::as_str).unwrap_or("");
|
|
768
|
-
let ast = e
|
|
769
|
-
|
|
1263
|
+
let ast = e
|
|
1264
|
+
.get("ast")
|
|
1265
|
+
.unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
|
|
1266
|
+
format!(
|
|
1267
|
+
" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},",
|
|
1268
|
+
naming::ruby_inspect_string(description),
|
|
1269
|
+
crate::expr_emitter::emit_ast(ast)
|
|
1270
|
+
)
|
|
770
1271
|
})
|
|
771
1272
|
.collect();
|
|
772
1273
|
let transition = mutations::lifecycle_transition_for(target, entity);
|
|
773
1274
|
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
774
1275
|
let mut mutation_lines: Vec<String> = target
|
|
775
1276
|
.get("mutations")
|
|
776
|
-
.
|
|
777
|
-
.unwrap_or(&[])
|
|
1277
|
+
.map_or_default(Json::each)
|
|
778
1278
|
.iter()
|
|
779
|
-
.map(|m|
|
|
1279
|
+
.map(|m| {
|
|
1280
|
+
mutations::emit_mutation_line(exemplar, m, entity, target, value_objects_by_name, false)
|
|
1281
|
+
})
|
|
780
1282
|
.collect();
|
|
781
|
-
if mutations::reads_pre_state(target.get("mutations").
|
|
1283
|
+
if mutations::reads_pre_state(target.get("mutations").map_or_default(Json::each)) {
|
|
782
1284
|
mutation_lines.insert(0, mutations::pre_state_line());
|
|
783
1285
|
}
|
|
784
1286
|
if let Some(t) = &transition {
|
|
785
1287
|
if !t.to_state.is_empty() {
|
|
786
|
-
mutation_lines.push(format!(
|
|
1288
|
+
mutation_lines.push(format!(
|
|
1289
|
+
" record.{} = {}.to_string();",
|
|
1290
|
+
naming::rust_ident_field(&t.field),
|
|
1291
|
+
naming::ruby_inspect_string(&t.to_state)
|
|
1292
|
+
));
|
|
787
1293
|
}
|
|
788
1294
|
}
|
|
789
1295
|
if mutation_lines.is_empty() {
|
|
790
1296
|
mutation_lines = vec![" let _ = record;".to_string()];
|
|
791
1297
|
}
|
|
792
|
-
let identity_reading = entity
|
|
1298
|
+
let identity_reading = entity
|
|
1299
|
+
.get("identified_by")
|
|
1300
|
+
.map_or_default(Json::each)
|
|
1301
|
+
.iter()
|
|
1302
|
+
.map(Json::to_s)
|
|
1303
|
+
.collect::<Vec<_>>()
|
|
1304
|
+
.join(", ");
|
|
793
1305
|
|
|
794
|
-
let prelude = exemplar.render(
|
|
795
|
-
|
|
1306
|
+
let prelude = exemplar.render(
|
|
1307
|
+
"delegate_prelude",
|
|
1308
|
+
&[
|
|
1309
|
+
("tmpl_aliases_placeholder()", aliases.join(", ")),
|
|
1310
|
+
("TmplTargetArgs", target_args_name),
|
|
1311
|
+
],
|
|
1312
|
+
);
|
|
1313
|
+
let element = exemplar.render(
|
|
1314
|
+
"delegate_element",
|
|
1315
|
+
&[("TmplElement", element_record.clone())],
|
|
1316
|
+
);
|
|
796
1317
|
let apply = exemplar.render(
|
|
797
1318
|
"delegate_apply",
|
|
798
1319
|
&[
|
|
799
1320
|
("TmplRecord", naming::rust_ident(&aggregate_name)),
|
|
800
|
-
(
|
|
1321
|
+
(
|
|
1322
|
+
"tmpl_list_field",
|
|
1323
|
+
naming::rust_ident_field(crate::attr::name(list_attr)),
|
|
1324
|
+
),
|
|
801
1325
|
("TmplElement", element_record),
|
|
802
1326
|
// Bare, not entity-qualified: it feeds refusal-message text, as in Ruby.
|
|
803
|
-
(
|
|
1327
|
+
(
|
|
1328
|
+
"\"TmplQualifiedCommandName\"",
|
|
1329
|
+
naming::ruby_inspect_string(
|
|
1330
|
+
&target.get("name").map(Json::to_s).unwrap_or_default(),
|
|
1331
|
+
),
|
|
1332
|
+
),
|
|
804
1333
|
// Lets `apply_entity_command` (kernel/dispatch.rs) judge `corrects` admissibility.
|
|
805
|
-
(
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1334
|
+
(
|
|
1335
|
+
"\"TmplQualifiedName\"",
|
|
1336
|
+
naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}")),
|
|
1337
|
+
),
|
|
1338
|
+
(
|
|
1339
|
+
"\"TmplAggregateName\"",
|
|
1340
|
+
naming::ruby_inspect_string(&aggregate_name),
|
|
1341
|
+
),
|
|
1342
|
+
(
|
|
1343
|
+
"\"TmplEntityName\"",
|
|
1344
|
+
naming::ruby_inspect_string(&entity_name),
|
|
1345
|
+
),
|
|
1346
|
+
(
|
|
1347
|
+
"\"TmplEntityIdentityReading\"",
|
|
1348
|
+
naming::ruby_inspect_string(&identity_reading),
|
|
1349
|
+
),
|
|
809
1350
|
("tmpl_given_spec_placeholder(),", given_specs.join("\n")),
|
|
810
1351
|
("tmpl_transition_placeholder()", transition_arg),
|
|
811
|
-
(
|
|
1352
|
+
(
|
|
1353
|
+
"tmpl_entity_mutation_lines_placeholder(record);",
|
|
1354
|
+
mutation_lines.join("\n"),
|
|
1355
|
+
),
|
|
812
1356
|
("tmpl_ensures_spec_placeholder(),", ensures_specs.join("\n")),
|
|
813
1357
|
],
|
|
814
1358
|
);
|
|
@@ -816,16 +1360,30 @@ fn delegation_of(exemplar: &Exemplar, command: &Json, aggregate: &Json, value_ob
|
|
|
816
1360
|
prelude: indent_block(&prelude, " "),
|
|
817
1361
|
element: indent_block(&element, " "),
|
|
818
1362
|
apply: indent_block(&apply, " "),
|
|
819
|
-
emits: target
|
|
1363
|
+
emits: target
|
|
1364
|
+
.get("emits")
|
|
1365
|
+
.map_or_default(Json::each)
|
|
1366
|
+
.iter()
|
|
1367
|
+
.map(Json::to_s)
|
|
1368
|
+
.collect(),
|
|
820
1369
|
})
|
|
821
1370
|
}
|
|
822
1371
|
|
|
823
1372
|
/// `command_skip_reason` for an entity command; an entity has the same IR shape as an aggregate.
|
|
824
|
-
pub fn entity_command_skip_reason(
|
|
1373
|
+
pub fn entity_command_skip_reason(
|
|
1374
|
+
command: &Json,
|
|
1375
|
+
entity: &Json,
|
|
1376
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1377
|
+
) -> Option<SkipReason> {
|
|
825
1378
|
command_skip_reason_with(command, entity, value_objects_by_name, false)
|
|
826
1379
|
}
|
|
827
1380
|
|
|
828
1381
|
/// Emits the dispatch function for an entity command.
|
|
1382
|
+
#[allow(clippy::too_many_arguments)] // established emitter signature
|
|
1383
|
+
#[allow(
|
|
1384
|
+
clippy::panic,
|
|
1385
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
1386
|
+
)]
|
|
829
1387
|
pub fn emit_entity_command(
|
|
830
1388
|
exemplar: &Exemplar,
|
|
831
1389
|
command: &Json,
|
|
@@ -836,135 +1394,180 @@ pub fn emit_entity_command(
|
|
|
836
1394
|
aggregates_by_name: &HashMap<String, &Json>,
|
|
837
1395
|
process_managers: &[Json],
|
|
838
1396
|
) -> String {
|
|
839
|
-
let parent_record = naming::rust_ident(parent_aggregate
|
|
840
|
-
let element_record = naming::rust_ident(entity
|
|
841
|
-
let cmd = naming::rust_ident(command
|
|
1397
|
+
let parent_record = naming::rust_ident(name_of(parent_aggregate));
|
|
1398
|
+
let element_record = naming::rust_ident(name_of(entity));
|
|
1399
|
+
let cmd = naming::rust_ident(name_of(command));
|
|
842
1400
|
|
|
843
|
-
let aggregate_name = parent_aggregate
|
|
844
|
-
let parent_identity_reading = parent_aggregate
|
|
845
|
-
let entity_name = entity
|
|
846
|
-
let entity_identity_reading = entity
|
|
1401
|
+
let aggregate_name = name_of(parent_aggregate).to_string();
|
|
1402
|
+
let parent_identity_reading = identity_reading(parent_aggregate);
|
|
1403
|
+
let entity_name = name_of(entity).to_string();
|
|
1404
|
+
let entity_identity_reading = identity_reading(entity);
|
|
847
1405
|
|
|
848
|
-
let
|
|
849
|
-
let list_attr = parent_attrs
|
|
850
|
-
.iter()
|
|
851
|
-
.find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity.get("name").and_then(Json::as_str).unwrap_or(""))
|
|
852
|
-
.unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it — unsupported_attribute_types should have caught this"));
|
|
853
|
-
let list_field = naming::rust_ident_field(crate::attr::name(list_attr));
|
|
1406
|
+
let list_field = list_field_of(parent_aggregate, entity);
|
|
854
1407
|
let args_struct_name = format!("{element_record}{cmd}EntityArgs");
|
|
855
1408
|
|
|
856
|
-
let attrs = command.get("attributes").
|
|
857
|
-
let
|
|
858
|
-
for attr in attrs {
|
|
859
|
-
let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
|
|
860
|
-
if crate::attr::optional(attr) {
|
|
861
|
-
ty = format!("Option<{ty}>");
|
|
862
|
-
}
|
|
863
|
-
args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
|
|
864
|
-
}
|
|
865
|
-
args_struct.push("}".to_string());
|
|
1409
|
+
let attrs = command.get("attributes").map_or_default(Json::each);
|
|
1410
|
+
let args_struct = args_struct_text(exemplar, &args_struct_name, attrs);
|
|
866
1411
|
|
|
867
|
-
let invariant_checks =
|
|
1412
|
+
let invariant_checks =
|
|
1413
|
+
invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
|
|
868
1414
|
|
|
869
1415
|
// `corrects_given_specs` is prepended as in `emit_command`; `apply_entity_command` evaluates
|
|
870
1416
|
// it against the parent record, never the entity's element.
|
|
871
|
-
let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
|
|
872
1417
|
let mut given_specs: Vec<String> = crate::bridging::corrects_given_specs(command);
|
|
873
|
-
given_specs.extend(
|
|
874
|
-
|
|
875
|
-
let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
|
|
876
|
-
format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
|
|
877
|
-
}));
|
|
878
|
-
|
|
879
|
-
let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
|
|
880
|
-
let ensures_specs: Vec<String> = ensures
|
|
881
|
-
.iter()
|
|
882
|
-
.map(|e| {
|
|
883
|
-
let description = e.get("description").and_then(Json::as_str).unwrap_or("");
|
|
884
|
-
let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
|
|
885
|
-
format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
|
|
886
|
-
})
|
|
887
|
-
.collect();
|
|
1418
|
+
given_specs.extend(given_spec_lines(command, " "));
|
|
1419
|
+
let ensures_specs = ensures_spec_lines(command, " ");
|
|
888
1420
|
|
|
889
1421
|
let transition = mutations::lifecycle_transition_for(command, entity);
|
|
890
1422
|
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
891
1423
|
|
|
892
|
-
let mutations_list = command.get("mutations").
|
|
893
|
-
let mut mutation_lines: Vec<String> = mutations_list
|
|
1424
|
+
let mutations_list = command.get("mutations").map_or_default(Json::each);
|
|
1425
|
+
let mut mutation_lines: Vec<String> = mutations_list
|
|
1426
|
+
.iter()
|
|
1427
|
+
.map(|m| {
|
|
1428
|
+
mutations::emit_mutation_line(
|
|
1429
|
+
exemplar,
|
|
1430
|
+
m,
|
|
1431
|
+
entity,
|
|
1432
|
+
command,
|
|
1433
|
+
value_objects_by_name,
|
|
1434
|
+
false,
|
|
1435
|
+
)
|
|
1436
|
+
})
|
|
1437
|
+
.collect();
|
|
894
1438
|
if mutations::reads_pre_state(mutations_list) {
|
|
895
1439
|
mutation_lines.insert(0, mutations::pre_state_line());
|
|
896
1440
|
}
|
|
897
|
-
|
|
898
|
-
if !t.to_state.is_empty() {
|
|
899
|
-
mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
if mutation_lines.is_empty() {
|
|
903
|
-
mutation_lines = vec![" let _ = record;".to_string()];
|
|
904
|
-
}
|
|
1441
|
+
let mutation_lines = close_mutation_lines(mutation_lines, transition.as_ref(), " ");
|
|
905
1442
|
|
|
906
1443
|
// Bare, not entity-qualified, as in `delegate_apply`.
|
|
907
|
-
let qualified_command_name = command
|
|
908
|
-
let
|
|
909
|
-
let emits_expr = emits.iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", ");
|
|
1444
|
+
let qualified_command_name = name_of(command).to_string();
|
|
1445
|
+
let emits_expr = emits_expr_of(command);
|
|
910
1446
|
|
|
911
1447
|
let entity_dispatch_fn = exemplar.render(
|
|
912
1448
|
"entity_dispatch_fn",
|
|
913
1449
|
&[
|
|
914
|
-
(
|
|
1450
|
+
(
|
|
1451
|
+
"dispatch_entity_tmpl",
|
|
1452
|
+
format!(
|
|
1453
|
+
"dispatch_entity_{}_{}",
|
|
1454
|
+
entity
|
|
1455
|
+
.get("name")
|
|
1456
|
+
.and_then(Json::as_str)
|
|
1457
|
+
.unwrap_or("")
|
|
1458
|
+
.to_lowercase(),
|
|
1459
|
+
naming::dispatch_fn_name(&cmd)
|
|
1460
|
+
),
|
|
1461
|
+
),
|
|
915
1462
|
("TmplRecord", parent_record),
|
|
916
1463
|
("TmplArgs", args_struct_name.clone()),
|
|
917
|
-
(
|
|
918
|
-
|
|
919
|
-
|
|
1464
|
+
(
|
|
1465
|
+
"tmpl_deref_params_placeholder: ()",
|
|
1466
|
+
deref_params().join(", "),
|
|
1467
|
+
),
|
|
1468
|
+
(
|
|
1469
|
+
"tmpl_invariant_check_placeholder()?;",
|
|
1470
|
+
invariant_checks.join("\n"),
|
|
1471
|
+
),
|
|
1472
|
+
(
|
|
1473
|
+
"let tmpl_eval_fielded = tmpl_with_references_placeholder();",
|
|
1474
|
+
with_references_binding(),
|
|
1475
|
+
),
|
|
920
1476
|
("&tmpl_eval_fielded,", "&with_references,".to_string()),
|
|
921
|
-
(
|
|
1477
|
+
(
|
|
1478
|
+
"let tmpl_seed_projections = tmpl_seed_projections_placeholder();",
|
|
1479
|
+
seed_projections_binding(parent_aggregate),
|
|
1480
|
+
),
|
|
922
1481
|
("tmpl_seed_projections,", "seed_projections,".to_string()),
|
|
923
1482
|
("tmpl_list_field", list_field),
|
|
924
1483
|
("TmplElement", element_record),
|
|
925
|
-
(
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
(
|
|
930
|
-
|
|
1484
|
+
(
|
|
1485
|
+
"\"TmplQualifiedCommandName\"",
|
|
1486
|
+
naming::ruby_inspect_string(&qualified_command_name),
|
|
1487
|
+
),
|
|
1488
|
+
(
|
|
1489
|
+
"\"TmplQualifiedName\"",
|
|
1490
|
+
naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}")),
|
|
1491
|
+
),
|
|
1492
|
+
(
|
|
1493
|
+
"\"TmplAggregateName\"",
|
|
1494
|
+
naming::ruby_inspect_string(&aggregate_name),
|
|
1495
|
+
),
|
|
1496
|
+
(
|
|
1497
|
+
"\"TmplParentIdentityReading\"",
|
|
1498
|
+
naming::ruby_inspect_string(&parent_identity_reading),
|
|
1499
|
+
),
|
|
1500
|
+
(
|
|
1501
|
+
"\"TmplEntityName\"",
|
|
1502
|
+
naming::ruby_inspect_string(&entity_name),
|
|
1503
|
+
),
|
|
1504
|
+
(
|
|
1505
|
+
"\"TmplEntityIdentityReading\"",
|
|
1506
|
+
naming::ruby_inspect_string(&entity_identity_reading),
|
|
1507
|
+
),
|
|
931
1508
|
("tmpl_given_spec_placeholder(),", given_specs.join("\n")),
|
|
932
1509
|
("tmpl_transition_placeholder()", transition_arg),
|
|
933
|
-
(
|
|
1510
|
+
(
|
|
1511
|
+
"tmpl_entity_mutation_lines_placeholder(record);",
|
|
1512
|
+
mutation_lines.join("\n"),
|
|
1513
|
+
),
|
|
934
1514
|
("tmpl_ensures_spec_placeholder(),", ensures_specs.join("\n")),
|
|
935
|
-
(
|
|
1515
|
+
(
|
|
1516
|
+
"tmpl_invariants_placeholder()",
|
|
1517
|
+
format!("{}()", invariants_fn_name(parent_aggregate)),
|
|
1518
|
+
),
|
|
936
1519
|
("tmpl_emit_placeholder()", emits_expr),
|
|
937
1520
|
],
|
|
938
1521
|
);
|
|
939
1522
|
|
|
940
1523
|
[
|
|
941
|
-
crate::fielded::emit_fielded_flat(
|
|
942
|
-
|
|
943
|
-
|
|
1524
|
+
crate::fielded::emit_fielded_flat(
|
|
1525
|
+
exemplar,
|
|
1526
|
+
&args_struct_name,
|
|
1527
|
+
attrs,
|
|
1528
|
+
value_objects_by_name,
|
|
1529
|
+
&[],
|
|
1530
|
+
),
|
|
1531
|
+
format!("#[derive(Debug, Clone)]\n{args_struct}"),
|
|
1532
|
+
crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs),
|
|
944
1533
|
{
|
|
945
1534
|
// `unknown_argument_allowlist:` as for an aggregate command, plus the entity's
|
|
946
1535
|
// identity head (`extra_identity_heads:` in Ruby's `ArgumentGate`).
|
|
947
|
-
let entity_identity_heads
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1536
|
+
let entity_identity_heads = identity_heads(entity);
|
|
1537
|
+
let allowlist = crate::json_codec::command_argument_allowlist(
|
|
1538
|
+
parent_aggregate,
|
|
1539
|
+
command,
|
|
1540
|
+
process_managers,
|
|
1541
|
+
&entity_identity_heads,
|
|
1542
|
+
);
|
|
1543
|
+
crate::json_codec::emit_from_json_flat(
|
|
1544
|
+
exemplar,
|
|
1545
|
+
&args_struct_name,
|
|
1546
|
+
attrs,
|
|
1547
|
+
value_objects_by_name,
|
|
1548
|
+
Some(&allowlist),
|
|
1549
|
+
Some(&qualified_command_name),
|
|
1550
|
+
true,
|
|
1551
|
+
true,
|
|
1552
|
+
Some(aggregates_by_name),
|
|
1553
|
+
None,
|
|
1554
|
+
)
|
|
956
1555
|
},
|
|
957
1556
|
// Argument gates, run by `kernel::decode_entity_arguments` in `EntityStep::ORDER`.
|
|
958
1557
|
{
|
|
959
|
-
let entity_identity_heads
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1558
|
+
let entity_identity_heads = identity_heads(entity);
|
|
1559
|
+
let allowlist = crate::json_codec::command_argument_allowlist(
|
|
1560
|
+
parent_aggregate,
|
|
1561
|
+
command,
|
|
1562
|
+
process_managers,
|
|
1563
|
+
&entity_identity_heads,
|
|
1564
|
+
);
|
|
1565
|
+
crate::json_codec::emit_argument_gates(
|
|
1566
|
+
&args_struct_name,
|
|
1567
|
+
&qualified_command_name,
|
|
1568
|
+
attrs,
|
|
1569
|
+
Some(&allowlist),
|
|
1570
|
+
)
|
|
968
1571
|
},
|
|
969
1572
|
entity_dispatch_fn,
|
|
970
1573
|
]
|
|
@@ -973,6 +1576,11 @@ pub fn emit_entity_command(
|
|
|
973
1576
|
|
|
974
1577
|
/// Emits the dispatch function for a command on an entity nested two levels deep.
|
|
975
1578
|
/// Takes plain `&str` ids per hop, so routed and flat-args callers can both use it.
|
|
1579
|
+
#[allow(clippy::too_many_arguments)] // established emitter signature
|
|
1580
|
+
#[allow(
|
|
1581
|
+
clippy::panic,
|
|
1582
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
1583
|
+
)]
|
|
976
1584
|
pub fn emit_nested_entity_command(
|
|
977
1585
|
exemplar: &Exemplar,
|
|
978
1586
|
command: &Json,
|
|
@@ -984,96 +1592,66 @@ pub fn emit_nested_entity_command(
|
|
|
984
1592
|
aggregates_by_name: &HashMap<String, &Json>,
|
|
985
1593
|
process_managers: &[Json],
|
|
986
1594
|
) -> String {
|
|
987
|
-
let parent_record = naming::rust_ident(parent_aggregate
|
|
988
|
-
let entity_record = naming::rust_ident(entity
|
|
989
|
-
let nested_record = naming::rust_ident(nested
|
|
990
|
-
let cmd = naming::rust_ident(command
|
|
991
|
-
|
|
992
|
-
let aggregate_name = parent_aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
|
|
993
|
-
let parent_identity_reading = parent_aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
|
|
994
|
-
let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("").to_string();
|
|
995
|
-
let entity_identity_reading = entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
|
|
996
|
-
let nested_name = nested.get("name").and_then(Json::as_str).unwrap_or("").to_string();
|
|
997
|
-
let nested_identity_reading = nested.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
|
|
998
|
-
|
|
999
|
-
let parent_attrs = parent_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
1000
|
-
let list_attr1 = parent_attrs
|
|
1001
|
-
.iter()
|
|
1002
|
-
.find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity.get("name").and_then(Json::as_str).unwrap_or(""))
|
|
1003
|
-
.unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it — unsupported_attribute_types should have caught this"));
|
|
1004
|
-
let list_field1 = naming::rust_ident_field(crate::attr::name(list_attr1));
|
|
1595
|
+
let parent_record = naming::rust_ident(name_of(parent_aggregate));
|
|
1596
|
+
let entity_record = naming::rust_ident(name_of(entity));
|
|
1597
|
+
let nested_record = naming::rust_ident(name_of(nested));
|
|
1598
|
+
let cmd = naming::rust_ident(name_of(command));
|
|
1005
1599
|
|
|
1006
|
-
let
|
|
1007
|
-
let
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
let
|
|
1600
|
+
let aggregate_name = name_of(parent_aggregate).to_string();
|
|
1601
|
+
let parent_identity_reading = identity_reading(parent_aggregate);
|
|
1602
|
+
let entity_name = name_of(entity).to_string();
|
|
1603
|
+
let entity_identity_reading = identity_reading(entity);
|
|
1604
|
+
let nested_name = name_of(nested).to_string();
|
|
1605
|
+
let nested_identity_reading = identity_reading(nested);
|
|
1012
1606
|
|
|
1013
|
-
let
|
|
1607
|
+
let list_field1 = list_field_of(parent_aggregate, entity);
|
|
1608
|
+
let list_field2 = list_field_of(entity, nested);
|
|
1014
1609
|
|
|
1015
|
-
let
|
|
1016
|
-
let mut args_struct = vec![format!("pub struct {args_struct_name} {{")];
|
|
1017
|
-
for attr in attrs {
|
|
1018
|
-
let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
|
|
1019
|
-
if crate::attr::optional(attr) {
|
|
1020
|
-
ty = format!("Option<{ty}>");
|
|
1021
|
-
}
|
|
1022
|
-
args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
|
|
1023
|
-
}
|
|
1024
|
-
args_struct.push("}".to_string());
|
|
1610
|
+
let args_struct_name = format!("{nested_record}{cmd}NestedEntityArgs");
|
|
1025
1611
|
|
|
1026
|
-
let
|
|
1612
|
+
let attrs = command.get("attributes").map_or_default(Json::each);
|
|
1613
|
+
let args_struct = args_struct_text(exemplar, &args_struct_name, attrs);
|
|
1027
1614
|
|
|
1028
|
-
let
|
|
1029
|
-
|
|
1030
|
-
.iter()
|
|
1031
|
-
.map(|g| {
|
|
1032
|
-
let description = g.get("description").and_then(Json::as_str).unwrap_or("");
|
|
1033
|
-
let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
|
|
1034
|
-
format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
|
|
1035
|
-
})
|
|
1036
|
-
.collect();
|
|
1615
|
+
let invariant_checks =
|
|
1616
|
+
invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
|
|
1037
1617
|
|
|
1038
|
-
let
|
|
1039
|
-
let ensures_specs
|
|
1040
|
-
.iter()
|
|
1041
|
-
.map(|e| {
|
|
1042
|
-
let description = e.get("description").and_then(Json::as_str).unwrap_or("");
|
|
1043
|
-
let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
|
|
1044
|
-
format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
|
|
1045
|
-
})
|
|
1046
|
-
.collect();
|
|
1618
|
+
let given_specs = given_spec_lines(command, " ");
|
|
1619
|
+
let ensures_specs = ensures_spec_lines(command, " ");
|
|
1047
1620
|
|
|
1048
1621
|
// The nested entity's lifecycle, as in `emit_entity_command`.
|
|
1049
1622
|
let transition = mutations::lifecycle_transition_for(command, nested);
|
|
1050
1623
|
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
1051
1624
|
|
|
1052
|
-
let mutations_list = command.get("mutations").
|
|
1053
|
-
let mut mutation_lines: Vec<String> = mutations_list
|
|
1625
|
+
let mutations_list = command.get("mutations").map_or_default(Json::each);
|
|
1626
|
+
let mut mutation_lines: Vec<String> = mutations_list
|
|
1627
|
+
.iter()
|
|
1628
|
+
.map(|m| {
|
|
1629
|
+
mutations::emit_mutation_line(
|
|
1630
|
+
exemplar,
|
|
1631
|
+
m,
|
|
1632
|
+
nested,
|
|
1633
|
+
command,
|
|
1634
|
+
value_objects_by_name,
|
|
1635
|
+
false,
|
|
1636
|
+
)
|
|
1637
|
+
})
|
|
1638
|
+
.collect();
|
|
1054
1639
|
if mutations::reads_pre_state(mutations_list) {
|
|
1055
1640
|
mutation_lines.insert(0, mutations::pre_state_line());
|
|
1056
1641
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
if mutation_lines.is_empty() {
|
|
1063
|
-
mutation_lines = vec![" let _ = record;".to_string()];
|
|
1064
|
-
}
|
|
1642
|
+
let mutation_lines =
|
|
1643
|
+
close_mutation_lines(mutation_lines, transition.as_ref(), " ");
|
|
1065
1644
|
|
|
1066
1645
|
// Bare, as in `emit_entity_command`.
|
|
1067
|
-
let qualified_command_name = command
|
|
1646
|
+
let qualified_command_name = name_of(command).to_string();
|
|
1068
1647
|
// The hop-2 `apply_entity_command` gets the root aggregate's qualified name only to satisfy
|
|
1069
1648
|
// the signature: its `record` is the hop-1 entity, so `corrects` admissibility is not checked
|
|
1070
1649
|
// there. Its trailing `parent_in_args` literal is `true`, byte for byte as Ruby emits.
|
|
1071
|
-
let
|
|
1072
|
-
let emits_expr = emits.iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", ");
|
|
1650
|
+
let emits_expr = emits_expr_of(command);
|
|
1073
1651
|
let fn_name = format!(
|
|
1074
1652
|
"dispatch_entity_{}_{}_{}",
|
|
1075
|
-
entity
|
|
1076
|
-
nested
|
|
1653
|
+
name_of(entity).to_lowercase(),
|
|
1654
|
+
name_of(nested).to_lowercase(),
|
|
1077
1655
|
naming::dispatch_fn_name(&cmd)
|
|
1078
1656
|
);
|
|
1079
1657
|
|
|
@@ -1154,48 +1732,76 @@ pub fn emit_nested_entity_command(
|
|
|
1154
1732
|
);
|
|
1155
1733
|
|
|
1156
1734
|
[
|
|
1157
|
-
crate::fielded::emit_fielded_flat(
|
|
1158
|
-
|
|
1159
|
-
|
|
1735
|
+
crate::fielded::emit_fielded_flat(
|
|
1736
|
+
exemplar,
|
|
1737
|
+
&args_struct_name,
|
|
1738
|
+
attrs,
|
|
1739
|
+
value_objects_by_name,
|
|
1740
|
+
&[],
|
|
1741
|
+
),
|
|
1742
|
+
format!("#[derive(Debug, Clone)]\n{args_struct}"),
|
|
1743
|
+
crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs),
|
|
1160
1744
|
{
|
|
1161
1745
|
// Both hops' identity heads, as `ctx.chain.flat_map(&:identity_heads)` in Ruby.
|
|
1162
1746
|
let mut identity_heads: Vec<String> = entity
|
|
1163
1747
|
.get("identified_by")
|
|
1164
|
-
.
|
|
1165
|
-
.unwrap_or(&[])
|
|
1748
|
+
.map_or_default(Json::each)
|
|
1166
1749
|
.iter()
|
|
1167
1750
|
.map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
|
|
1168
1751
|
.collect();
|
|
1169
1752
|
identity_heads.extend(
|
|
1170
1753
|
nested
|
|
1171
1754
|
.get("identified_by")
|
|
1172
|
-
.
|
|
1173
|
-
.unwrap_or(&[])
|
|
1755
|
+
.map_or_default(Json::each)
|
|
1174
1756
|
.iter()
|
|
1175
1757
|
.map(|p| p.to_s().split('.').next().unwrap_or("").to_string()),
|
|
1176
1758
|
);
|
|
1177
|
-
let allowlist = crate::json_codec::command_argument_allowlist(
|
|
1178
|
-
|
|
1759
|
+
let allowlist = crate::json_codec::command_argument_allowlist(
|
|
1760
|
+
parent_aggregate,
|
|
1761
|
+
command,
|
|
1762
|
+
process_managers,
|
|
1763
|
+
&identity_heads,
|
|
1764
|
+
);
|
|
1765
|
+
crate::json_codec::emit_from_json_flat(
|
|
1766
|
+
exemplar,
|
|
1767
|
+
&args_struct_name,
|
|
1768
|
+
attrs,
|
|
1769
|
+
value_objects_by_name,
|
|
1770
|
+
Some(&allowlist),
|
|
1771
|
+
Some(&qualified_command_name),
|
|
1772
|
+
true,
|
|
1773
|
+
true,
|
|
1774
|
+
Some(aggregates_by_name),
|
|
1775
|
+
None,
|
|
1776
|
+
)
|
|
1179
1777
|
},
|
|
1180
1778
|
// Argument gates, as in the one-hop entity command.
|
|
1181
1779
|
{
|
|
1182
1780
|
let mut identity_heads: Vec<String> = entity
|
|
1183
1781
|
.get("identified_by")
|
|
1184
|
-
.
|
|
1185
|
-
.unwrap_or(&[])
|
|
1782
|
+
.map_or_default(Json::each)
|
|
1186
1783
|
.iter()
|
|
1187
1784
|
.map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
|
|
1188
1785
|
.collect();
|
|
1189
1786
|
identity_heads.extend(
|
|
1190
1787
|
nested
|
|
1191
1788
|
.get("identified_by")
|
|
1192
|
-
.
|
|
1193
|
-
.unwrap_or(&[])
|
|
1789
|
+
.map_or_default(Json::each)
|
|
1194
1790
|
.iter()
|
|
1195
1791
|
.map(|p| p.to_s().split('.').next().unwrap_or("").to_string()),
|
|
1196
1792
|
);
|
|
1197
|
-
let allowlist = crate::json_codec::command_argument_allowlist(
|
|
1198
|
-
|
|
1793
|
+
let allowlist = crate::json_codec::command_argument_allowlist(
|
|
1794
|
+
parent_aggregate,
|
|
1795
|
+
command,
|
|
1796
|
+
process_managers,
|
|
1797
|
+
&identity_heads,
|
|
1798
|
+
);
|
|
1799
|
+
crate::json_codec::emit_argument_gates(
|
|
1800
|
+
&args_struct_name,
|
|
1801
|
+
&qualified_command_name,
|
|
1802
|
+
attrs,
|
|
1803
|
+
Some(&allowlist),
|
|
1804
|
+
)
|
|
1199
1805
|
},
|
|
1200
1806
|
nested_dispatch_fn,
|
|
1201
1807
|
]
|