hecks 3.13.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- 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/named_pattern.rb +33 -0
- 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 +3 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- 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/deploy/bluebook/deploy.bluebook +34 -34
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/framework/bluebook/change.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +58 -37
- 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 +15 -92
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +10 -16
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +51 -51
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value.rb +13 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/site/bluebook/site.bluebook +5 -5
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- 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/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +300 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +145 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +990 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +191 -0
- data/rust/host/src/payments/tests.rs +1445 -275
- data/rust/host/src/payments.rs +409 -121
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +483 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +26 -0
- 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 -510
- 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 +43 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +229 -112
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +230 -148
- data/rust/src/kernel/mod.rs +46 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +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/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
- data/rust/host/src/approval.rs +0 -652
|
@@ -4,38 +4,92 @@
|
|
|
4
4
|
use crate::exemplar::Exemplar;
|
|
5
5
|
use crate::json::Json;
|
|
6
6
|
use crate::queries;
|
|
7
|
+
use crate::shared::ruby_to_i;
|
|
7
8
|
use crate::skip_reason::{reskip, skip, SkipReason};
|
|
8
9
|
use std::collections::HashMap;
|
|
9
10
|
|
|
10
11
|
const READ_MODEL_BARE_KEYS: &[&str] = &[
|
|
11
|
-
"name",
|
|
12
|
-
"
|
|
12
|
+
"name",
|
|
13
|
+
"description",
|
|
14
|
+
"reference_name",
|
|
15
|
+
"reference_target",
|
|
16
|
+
"query_name",
|
|
17
|
+
"aggregate_heads",
|
|
18
|
+
"wheres",
|
|
19
|
+
"order_by",
|
|
20
|
+
"offset",
|
|
21
|
+
"limit",
|
|
22
|
+
"freshness",
|
|
23
|
+
"index_hints",
|
|
24
|
+
"group_by",
|
|
25
|
+
"null_semantics",
|
|
26
|
+
"authorization",
|
|
27
|
+
"count",
|
|
28
|
+
"median_field",
|
|
29
|
+
"sum_field",
|
|
30
|
+
"avg_field",
|
|
31
|
+
"min_field",
|
|
32
|
+
"max_field",
|
|
33
|
+
"percentile_field",
|
|
34
|
+
"percentile_at",
|
|
35
|
+
"any_field",
|
|
36
|
+
"all_field",
|
|
13
37
|
];
|
|
14
38
|
|
|
15
39
|
// Every reduction beyond `count` (which needs no field) — mirrors
|
|
16
40
|
// Runtime::ReadModelInterpreter::REDUCTION_WORD/INTEGER_ONLY_REDUCTIONS/BOOLEAN_REDUCTIONS
|
|
17
41
|
// (ADR 0078).
|
|
18
|
-
const AGGREGATION_FIELD_KEYS: &[&str] = &[
|
|
42
|
+
const AGGREGATION_FIELD_KEYS: &[&str] = &[
|
|
43
|
+
"median_field",
|
|
44
|
+
"sum_field",
|
|
45
|
+
"avg_field",
|
|
46
|
+
"min_field",
|
|
47
|
+
"max_field",
|
|
48
|
+
"percentile_field",
|
|
49
|
+
"any_field",
|
|
50
|
+
"all_field",
|
|
51
|
+
];
|
|
19
52
|
const INTEGER_ONLY_AGGREGATION_FIELDS: &[&str] = &["sum_field", "avg_field"];
|
|
20
53
|
const BOOLEAN_AGGREGATION_FIELDS: &[&str] = &["any_field", "all_field"];
|
|
21
54
|
|
|
22
|
-
pub fn read_model_skip_reason(
|
|
55
|
+
pub fn read_model_skip_reason(
|
|
56
|
+
read_model: &Json,
|
|
57
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
58
|
+
unsupported_names: &[String],
|
|
59
|
+
) -> Option<SkipReason> {
|
|
23
60
|
let keys: Vec<&str> = match read_model {
|
|
24
|
-
Json::Object(pairs) => pairs
|
|
61
|
+
Json::Object(pairs) => pairs
|
|
62
|
+
.iter()
|
|
63
|
+
.filter(|(_, v)| !matches!(v, Json::Null))
|
|
64
|
+
.map(|(k, _)| k.as_str())
|
|
65
|
+
.collect(),
|
|
25
66
|
_ => Vec::new(),
|
|
26
67
|
};
|
|
27
|
-
let extra: Vec<&str> = keys
|
|
68
|
+
let extra: Vec<&str> = keys
|
|
69
|
+
.into_iter()
|
|
70
|
+
.filter(|k| !READ_MODEL_BARE_KEYS.contains(k))
|
|
71
|
+
.collect();
|
|
28
72
|
if !extra.is_empty() {
|
|
29
73
|
return Some(read_model_options_skip_reason(&extra));
|
|
30
74
|
}
|
|
31
75
|
|
|
32
|
-
if read_model
|
|
76
|
+
if read_model
|
|
77
|
+
.get("group_by")
|
|
78
|
+
.map_or_default(Json::each)
|
|
79
|
+
.iter()
|
|
80
|
+
.any(|_| true)
|
|
81
|
+
{
|
|
33
82
|
return group_by_skip_reason(read_model, aggregates_by_name, unsupported_names);
|
|
34
83
|
}
|
|
35
84
|
|
|
36
|
-
let heads = read_model.get("aggregate_heads").
|
|
37
|
-
let reference_target = read_model
|
|
38
|
-
|
|
85
|
+
let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
86
|
+
let reference_target = read_model
|
|
87
|
+
.get("reference_target")
|
|
88
|
+
.map(Json::to_s)
|
|
89
|
+
.unwrap_or_default();
|
|
90
|
+
let root = heads
|
|
91
|
+
.iter()
|
|
92
|
+
.find(|h| h.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target);
|
|
39
93
|
// Rootless (no `reference_to`) is generated — `read_models.rb`'s own
|
|
40
94
|
// comment on this check.
|
|
41
95
|
if root.is_none() && !reference_target.is_empty() {
|
|
@@ -45,13 +99,20 @@ pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<St
|
|
|
45
99
|
));
|
|
46
100
|
}
|
|
47
101
|
|
|
48
|
-
let root_name = root
|
|
49
|
-
|
|
102
|
+
let root_name = root
|
|
103
|
+
.and_then(|h| h.get("aggregate"))
|
|
104
|
+
.map(Json::to_s)
|
|
105
|
+
.unwrap_or_default();
|
|
106
|
+
if !aggregates_by_name.contains_key(&root_name)
|
|
107
|
+
&& nested_entity_names(aggregates_by_name).contains(&root_name)
|
|
108
|
+
{
|
|
50
109
|
return Some(entity_head_skip_reason(&root_name, "root", "fetch by id"));
|
|
51
110
|
}
|
|
52
111
|
|
|
53
112
|
for head in heads {
|
|
54
|
-
if let Some(reason) =
|
|
113
|
+
if let Some(reason) =
|
|
114
|
+
read_model_head_skip_reason(head, aggregates_by_name, unsupported_names)
|
|
115
|
+
{
|
|
55
116
|
return Some(reason);
|
|
56
117
|
}
|
|
57
118
|
}
|
|
@@ -65,7 +126,7 @@ pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<St
|
|
|
65
126
|
|
|
66
127
|
fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
|
|
67
128
|
let mut sorted: Vec<&str> = extra.to_vec();
|
|
68
|
-
sorted.
|
|
129
|
+
sorted.sort_unstable();
|
|
69
130
|
skip(sorted[0], format!(
|
|
70
131
|
"declares {} — out of scope for this generator: cursor/consistency/inspection are real capabilities Ports::Query::InMemory/Ports::Query::Ordering/TenantScope implement that this generator does not port (this file's own header has the full argument, the same boundary queries.rb already draws for a declared AGGREGATE query); freshness/use_index are never disqualifying on their own — neither is read by the in-memory interpreter path this kernel matches",
|
|
71
132
|
sorted.join(", ")
|
|
@@ -74,7 +135,12 @@ fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
|
|
|
74
135
|
|
|
75
136
|
/// Every many-side head this read model declares.
|
|
76
137
|
fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
|
|
77
|
-
read_model
|
|
138
|
+
read_model
|
|
139
|
+
.get("aggregate_heads")
|
|
140
|
+
.map_or_default(Json::each)
|
|
141
|
+
.iter()
|
|
142
|
+
.filter(|h| h.get("many").is_some_and(Json::as_bool))
|
|
143
|
+
.collect()
|
|
78
144
|
}
|
|
79
145
|
|
|
80
146
|
/// Whether one declared option (its own `:target`, or `None` when untargeted) applies to
|
|
@@ -84,10 +150,13 @@ fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
|
|
|
84
150
|
/// reaching codegen at all is proof there's exactly one to mean.
|
|
85
151
|
fn option_target_matches_head(target: Option<&str>, head: &Json, many_heads: &[&Json]) -> bool {
|
|
86
152
|
let head_as = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
153
|
+
target.map_or_else(
|
|
154
|
+
|| {
|
|
155
|
+
many_heads.len() == 1
|
|
156
|
+
&& many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head_as
|
|
157
|
+
},
|
|
158
|
+
|target| target == head.get("aggregate").map(Json::to_s).unwrap_or_default(),
|
|
159
|
+
)
|
|
91
160
|
}
|
|
92
161
|
|
|
93
162
|
fn json_target(option: &Json) -> Option<String> {
|
|
@@ -98,8 +167,11 @@ fn json_target(option: &Json) -> Option<String> {
|
|
|
98
167
|
/// many-side head only, a declared `authorize ..., tenant:` — `on:` doesn't extend to
|
|
99
168
|
/// `tenant:`) targeting it.
|
|
100
169
|
fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[&Json]) -> bool {
|
|
101
|
-
let wheres = read_model.get("wheres").
|
|
102
|
-
if wheres
|
|
170
|
+
let wheres = read_model.get("wheres").map_or_default(Json::each);
|
|
171
|
+
if wheres
|
|
172
|
+
.iter()
|
|
173
|
+
.any(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads))
|
|
174
|
+
{
|
|
103
175
|
return true;
|
|
104
176
|
}
|
|
105
177
|
if let Some(order_by) = read_model.get("order_by") {
|
|
@@ -117,8 +189,14 @@ fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[
|
|
|
117
189
|
return true;
|
|
118
190
|
}
|
|
119
191
|
}
|
|
120
|
-
if read_model
|
|
121
|
-
|
|
192
|
+
if read_model
|
|
193
|
+
.get("authorization")
|
|
194
|
+
.and_then(|a| a.get("tenant"))
|
|
195
|
+
.is_some()
|
|
196
|
+
&& many_heads.len() == 1
|
|
197
|
+
{
|
|
198
|
+
return many_heads[0].get("as").map(Json::to_s).unwrap_or_default()
|
|
199
|
+
== head.get("as").map(Json::to_s).unwrap_or_default();
|
|
122
200
|
}
|
|
123
201
|
false
|
|
124
202
|
}
|
|
@@ -127,7 +205,11 @@ fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[
|
|
|
127
205
|
/// declared order — one entry when there's a single many-side head (the pre-ADR-0055 shape),
|
|
128
206
|
/// more than one only when `on:` targets several.
|
|
129
207
|
fn read_model_filtered_heads<'a>(read_model: &Json, many_heads: &[&'a Json]) -> Vec<&'a Json> {
|
|
130
|
-
many_heads
|
|
208
|
+
many_heads
|
|
209
|
+
.iter()
|
|
210
|
+
.copied()
|
|
211
|
+
.filter(|head| read_model_option_targets_head(read_model, head, many_heads))
|
|
212
|
+
.collect()
|
|
131
213
|
}
|
|
132
214
|
|
|
133
215
|
/// One targeted head's own where/order_by/offset/limit, pulled out of the read model's full
|
|
@@ -141,31 +223,73 @@ struct HeadQuery<'a> {
|
|
|
141
223
|
authorization: Option<&'a Json>,
|
|
142
224
|
}
|
|
143
225
|
|
|
144
|
-
fn read_model_head_query<'a>(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let
|
|
226
|
+
fn read_model_head_query<'a>(
|
|
227
|
+
read_model: &'a Json,
|
|
228
|
+
head: &Json,
|
|
229
|
+
many_heads: &[&Json],
|
|
230
|
+
) -> HeadQuery<'a> {
|
|
231
|
+
let wheres = read_model
|
|
232
|
+
.get("wheres")
|
|
233
|
+
.map_or_default(Json::each)
|
|
234
|
+
.iter()
|
|
235
|
+
.filter(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads))
|
|
236
|
+
.collect();
|
|
237
|
+
let order_by = read_model
|
|
238
|
+
.get("order_by")
|
|
239
|
+
.filter(|ob| option_target_matches_head(json_target(ob).as_deref(), head, many_heads));
|
|
240
|
+
let limit = read_model
|
|
241
|
+
.get("limit")
|
|
242
|
+
.filter(|l| option_target_matches_head(json_target(l).as_deref(), head, many_heads));
|
|
243
|
+
let offset = read_model
|
|
244
|
+
.get("offset")
|
|
245
|
+
.filter(|o| option_target_matches_head(json_target(o).as_deref(), head, many_heads));
|
|
246
|
+
let authorization = if many_heads.len() == 1
|
|
247
|
+
&& many_heads[0].get("as").map(Json::to_s).unwrap_or_default()
|
|
248
|
+
== head.get("as").map(Json::to_s).unwrap_or_default()
|
|
249
|
+
{
|
|
150
250
|
read_model.get("authorization")
|
|
151
251
|
} else {
|
|
152
252
|
None
|
|
153
253
|
};
|
|
154
|
-
HeadQuery {
|
|
254
|
+
HeadQuery {
|
|
255
|
+
wheres,
|
|
256
|
+
order_by,
|
|
257
|
+
limit,
|
|
258
|
+
offset,
|
|
259
|
+
authorization,
|
|
260
|
+
}
|
|
155
261
|
}
|
|
156
262
|
|
|
157
263
|
/// Checks every targeted head's own where/order_by/limit for generability against ITS
|
|
158
264
|
/// aggregate (not the read model's root); `None` means clean.
|
|
159
|
-
fn read_model_options_content_skip_reason(
|
|
265
|
+
fn read_model_options_content_skip_reason(
|
|
266
|
+
read_model: &Json,
|
|
267
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
268
|
+
) -> Option<SkipReason> {
|
|
160
269
|
let many_heads = read_model_many_heads(read_model);
|
|
161
270
|
|
|
162
271
|
for head in read_model_filtered_heads(read_model, &many_heads) {
|
|
163
272
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
164
273
|
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
165
|
-
return Some(entity_head_skip_reason(
|
|
274
|
+
return Some(entity_head_skip_reason(
|
|
275
|
+
&aggregate_name,
|
|
276
|
+
"filtered head",
|
|
277
|
+
"filter, order or authorize",
|
|
278
|
+
));
|
|
166
279
|
};
|
|
167
|
-
let vos = aggregate.get("value_objects").
|
|
168
|
-
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
280
|
+
let vos = aggregate.get("value_objects").map_or_default(Json::each);
|
|
281
|
+
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
282
|
+
.iter()
|
|
283
|
+
.map(|vo| {
|
|
284
|
+
(
|
|
285
|
+
vo.get("name")
|
|
286
|
+
.and_then(Json::as_str)
|
|
287
|
+
.unwrap_or("")
|
|
288
|
+
.to_string(),
|
|
289
|
+
vo,
|
|
290
|
+
)
|
|
291
|
+
})
|
|
292
|
+
.collect();
|
|
169
293
|
let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
170
294
|
let head_label = format!("head {aggregate_name} (as {as_name})");
|
|
171
295
|
let head_query = read_model_head_query(read_model, head, &many_heads);
|
|
@@ -173,7 +297,10 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
|
|
|
173
297
|
// A hop through a reference is generated when `query_hop_plan` resolves
|
|
174
298
|
// it — `read_models.rb`'s own loop, check for check.
|
|
175
299
|
for where_clause in head_query.wheres.iter().copied() {
|
|
176
|
-
let field = where_clause
|
|
300
|
+
let field = where_clause
|
|
301
|
+
.get("field")
|
|
302
|
+
.map(Json::to_s)
|
|
303
|
+
.unwrap_or_default();
|
|
177
304
|
let hop = queries::query_hop_plan(aggregate, &field, aggregates_by_name);
|
|
178
305
|
if hop.is_none() && field.contains('/') {
|
|
179
306
|
return Some(skip(
|
|
@@ -187,22 +314,42 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
|
|
|
187
314
|
|
|
188
315
|
if let Some(plan) = hop {
|
|
189
316
|
let target_value_objects_by_name = queries::value_objects_of(plan.target);
|
|
190
|
-
if let Some(reason) = queries::query_where_skip_reason(
|
|
191
|
-
|
|
317
|
+
if let Some(reason) = queries::query_where_skip_reason(
|
|
318
|
+
&queries::with_field(where_clause, &plan.inner_field),
|
|
319
|
+
plan.target,
|
|
320
|
+
&target_value_objects_by_name,
|
|
321
|
+
) {
|
|
322
|
+
return Some(reskip(
|
|
323
|
+
&reason,
|
|
324
|
+
format!(
|
|
325
|
+
"{head_label}'s own hop through {} to {}'s own {reason}",
|
|
326
|
+
plan.via_field, plan.target_aggregate
|
|
327
|
+
),
|
|
328
|
+
));
|
|
192
329
|
}
|
|
193
330
|
continue;
|
|
194
331
|
}
|
|
195
332
|
|
|
196
|
-
if let Some(reason) =
|
|
333
|
+
if let Some(reason) =
|
|
334
|
+
queries::query_where_skip_reason(where_clause, aggregate, &value_objects_by_name)
|
|
335
|
+
{
|
|
197
336
|
return Some(reskip(&reason, format!("{head_label}'s own {reason}")));
|
|
198
337
|
}
|
|
199
338
|
}
|
|
200
339
|
|
|
201
|
-
if let Some(reason) = queries::declared_authorization_skip_reason(
|
|
340
|
+
if let Some(reason) = queries::declared_authorization_skip_reason(
|
|
341
|
+
head_query.authorization,
|
|
342
|
+
aggregate,
|
|
343
|
+
&value_objects_by_name,
|
|
344
|
+
) {
|
|
202
345
|
return Some(reason);
|
|
203
346
|
}
|
|
204
347
|
|
|
205
|
-
if let Some(reason) = queries::declared_order_by_skip_reason(
|
|
348
|
+
if let Some(reason) = queries::declared_order_by_skip_reason(
|
|
349
|
+
head_query.order_by,
|
|
350
|
+
aggregate,
|
|
351
|
+
&value_objects_by_name,
|
|
352
|
+
) {
|
|
206
353
|
return Some(reason);
|
|
207
354
|
}
|
|
208
355
|
|
|
@@ -221,19 +368,45 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
|
|
|
221
368
|
// `seal_aggregation` (Ruby, build time) already guarantees exactly one
|
|
222
369
|
// many-side head and mutual exclusion with `group_by` — only the one declared
|
|
223
370
|
// reduction's own field needs checking here (ADR 0078).
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
371
|
+
#[allow(
|
|
372
|
+
clippy::expect_used,
|
|
373
|
+
reason = "the message names an invariant an earlier check in this generator established"
|
|
374
|
+
)]
|
|
375
|
+
fn aggregation_skip_reason(
|
|
376
|
+
read_model: &Json,
|
|
377
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
378
|
+
) -> Option<SkipReason> {
|
|
379
|
+
let field_key = AGGREGATION_FIELD_KEYS
|
|
380
|
+
.iter()
|
|
381
|
+
.find(|key| read_model.get(key).is_some())?;
|
|
382
|
+
let field = Json::to_s(read_model.get(field_key).expect("checked by find above"));
|
|
227
383
|
let word = field_key.strip_suffix("_field").unwrap_or(field_key);
|
|
228
384
|
|
|
229
|
-
let heads = read_model.get("aggregate_heads").
|
|
230
|
-
let target = heads
|
|
385
|
+
let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
386
|
+
let target = heads
|
|
387
|
+
.iter()
|
|
388
|
+
.find(|h| h.get("many").is_some_and(Json::as_bool))?;
|
|
231
389
|
let aggregate_name = target.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
232
390
|
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
233
|
-
return Some(entity_head_skip_reason(
|
|
391
|
+
return Some(entity_head_skip_reason(
|
|
392
|
+
&aggregate_name,
|
|
393
|
+
&format!("{word} target"),
|
|
394
|
+
"reduce",
|
|
395
|
+
));
|
|
234
396
|
};
|
|
235
|
-
let vos = aggregate.get("value_objects").
|
|
236
|
-
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
397
|
+
let vos = aggregate.get("value_objects").map_or_default(Json::each);
|
|
398
|
+
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
399
|
+
.iter()
|
|
400
|
+
.map(|vo| {
|
|
401
|
+
(
|
|
402
|
+
vo.get("name")
|
|
403
|
+
.and_then(Json::as_str)
|
|
404
|
+
.unwrap_or("")
|
|
405
|
+
.to_string(),
|
|
406
|
+
vo,
|
|
407
|
+
)
|
|
408
|
+
})
|
|
409
|
+
.collect();
|
|
237
410
|
|
|
238
411
|
let kind = queries::query_field_kind(aggregate, &field, &value_objects_by_name);
|
|
239
412
|
if kind == queries::FieldKind::Unknown {
|
|
@@ -255,13 +428,14 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
|
|
|
255
428
|
));
|
|
256
429
|
}
|
|
257
430
|
|
|
258
|
-
if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key)
|
|
259
|
-
|
|
260
|
-
|
|
431
|
+
if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key)
|
|
432
|
+
&& queries::query_field_numeric_type(aggregate, &field, &value_objects_by_name).as_deref()
|
|
433
|
+
!= Some("Integer")
|
|
434
|
+
{
|
|
435
|
+
return Some(skip(
|
|
261
436
|
*field_key,
|
|
262
437
|
format!("{word} names {field:?} on {aggregate_name}, which is a Float field — {word} admits only an Integer field (summing/averaging Floats cannot be made to agree, byte for byte, between Ruby and Rust) — not generated yet"),
|
|
263
438
|
));
|
|
264
|
-
}
|
|
265
439
|
}
|
|
266
440
|
|
|
267
441
|
None
|
|
@@ -269,8 +443,12 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
|
|
|
269
443
|
|
|
270
444
|
// Mirrors the retired Ruby generator's `read_models.rb`'s `group_by_skip_reason`: the one
|
|
271
445
|
// shape the corpus declares — a single rootless head, group_by alone.
|
|
272
|
-
fn group_by_skip_reason(
|
|
273
|
-
|
|
446
|
+
fn group_by_skip_reason(
|
|
447
|
+
read_model: &Json,
|
|
448
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
449
|
+
unsupported_names: &[String],
|
|
450
|
+
) -> Option<SkipReason> {
|
|
451
|
+
let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
274
452
|
if heads.len() != 1 {
|
|
275
453
|
return Some(skip("group_by", format!("declares group_by across {} aggregate heads — not generated yet (only a single, rootless head is)", heads.len())));
|
|
276
454
|
}
|
|
@@ -278,15 +456,36 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
|
|
|
278
456
|
if reference_target.is_some() && !matches!(reference_target, Some(Json::Null)) {
|
|
279
457
|
return Some(skip("group_by", format!("declares group_by on a NON-rootless read model (reference_to {}) — not generated yet", reference_target.map(Json::to_s).unwrap_or_default())));
|
|
280
458
|
}
|
|
281
|
-
if read_model.get("count").is_some()
|
|
282
|
-
|
|
459
|
+
if read_model.get("count").is_some()
|
|
460
|
+
|| AGGREGATION_FIELD_KEYS
|
|
461
|
+
.iter()
|
|
462
|
+
.any(|key| read_model.get(key).is_some())
|
|
463
|
+
{
|
|
464
|
+
return Some(skip(
|
|
465
|
+
"group_by",
|
|
466
|
+
"declares group_by alongside a reduction — not generated yet",
|
|
467
|
+
));
|
|
283
468
|
}
|
|
284
|
-
let has_wheres = read_model
|
|
285
|
-
|
|
286
|
-
|
|
469
|
+
let has_wheres = read_model
|
|
470
|
+
.get("wheres")
|
|
471
|
+
.map_or_default(Json::each)
|
|
472
|
+
.iter()
|
|
473
|
+
.any(|_| true);
|
|
474
|
+
if has_wheres
|
|
475
|
+
|| read_model.get("order_by").is_some()
|
|
476
|
+
|| read_model.get("limit").is_some()
|
|
477
|
+
|| read_model.get("offset").is_some()
|
|
478
|
+
{
|
|
479
|
+
return Some(skip(
|
|
480
|
+
"group_by",
|
|
481
|
+
"declares group_by alongside where/order_by/limit/offset — not generated yet",
|
|
482
|
+
));
|
|
287
483
|
}
|
|
288
484
|
if read_model.get("authorization").is_some() {
|
|
289
|
-
return Some(skip(
|
|
485
|
+
return Some(skip(
|
|
486
|
+
"group_by",
|
|
487
|
+
"declares group_by with an authorize policy — not generated yet",
|
|
488
|
+
));
|
|
290
489
|
}
|
|
291
490
|
|
|
292
491
|
let head = &heads[0];
|
|
@@ -296,11 +495,18 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
|
|
|
296
495
|
|
|
297
496
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
298
497
|
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
299
|
-
return Some(entity_head_skip_reason(
|
|
498
|
+
return Some(entity_head_skip_reason(
|
|
499
|
+
&aggregate_name,
|
|
500
|
+
"group_by head",
|
|
501
|
+
"group",
|
|
502
|
+
));
|
|
300
503
|
};
|
|
301
|
-
let attrs = aggregate.get("attributes").
|
|
302
|
-
let lifecycle_field = aggregate
|
|
303
|
-
|
|
504
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
505
|
+
let lifecycle_field = aggregate
|
|
506
|
+
.get("lifecycle")
|
|
507
|
+
.and_then(|l| l.get("field"))
|
|
508
|
+
.map(Json::to_s);
|
|
509
|
+
for row in read_model.get("group_by").map_or_default(Json::each) {
|
|
304
510
|
let field = row.get("field").map(Json::to_s).unwrap_or_default();
|
|
305
511
|
let is_attr = attrs.iter().any(|a| crate::attr::name(a) == field);
|
|
306
512
|
let is_lifecycle = lifecycle_field.as_deref() == Some(field.as_str());
|
|
@@ -314,7 +520,7 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
|
|
|
314
520
|
// Every entity name nested at any depth under a declared aggregate.
|
|
315
521
|
fn nested_entity_names(aggregates_by_name: &HashMap<String, &Json>) -> Vec<String> {
|
|
316
522
|
fn collect(owner: &Json, out: &mut Vec<String>) {
|
|
317
|
-
for entity in owner.get("entities").
|
|
523
|
+
for entity in owner.get("entities").map_or_default(Json::each) {
|
|
318
524
|
out.push(entity.get("name").map(Json::to_s).unwrap_or_default());
|
|
319
525
|
collect(entity, out);
|
|
320
526
|
}
|
|
@@ -334,13 +540,20 @@ fn entity_head_skip_reason(aggregate_name: &str, role: &str, purpose: &str) -> S
|
|
|
334
540
|
)
|
|
335
541
|
}
|
|
336
542
|
|
|
337
|
-
fn read_model_head_skip_reason(
|
|
543
|
+
fn read_model_head_skip_reason(
|
|
544
|
+
head: &Json,
|
|
545
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
546
|
+
unsupported_names: &[String],
|
|
547
|
+
) -> Option<SkipReason> {
|
|
338
548
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
339
549
|
if !aggregates_by_name.contains_key(&aggregate_name) {
|
|
340
550
|
if nested_entity_names(aggregates_by_name).contains(&aggregate_name) {
|
|
341
551
|
return None;
|
|
342
552
|
}
|
|
343
|
-
return Some(skip(
|
|
553
|
+
return Some(skip(
|
|
554
|
+
"include_undeclared_aggregate",
|
|
555
|
+
format!("includes {aggregate_name}, which this domain never declares"),
|
|
556
|
+
));
|
|
344
557
|
}
|
|
345
558
|
if unsupported_names.contains(&aggregate_name) {
|
|
346
559
|
return Some(skip(
|
|
@@ -357,28 +570,51 @@ pub struct ReferenceField {
|
|
|
357
570
|
}
|
|
358
571
|
|
|
359
572
|
fn read_model_reference_fields(head_aggregate: &Json) -> Vec<ReferenceField> {
|
|
360
|
-
let attrs = head_aggregate.get("attributes").
|
|
573
|
+
let attrs = head_aggregate.get("attributes").map_or_default(Json::each);
|
|
361
574
|
attrs
|
|
362
575
|
.iter()
|
|
363
576
|
.filter_map(|attr| {
|
|
364
577
|
let target = crate::naming::reference_target(crate::attr::type_name(attr))?;
|
|
365
|
-
Some(ReferenceField {
|
|
578
|
+
Some(ReferenceField {
|
|
579
|
+
target: target.to_string(),
|
|
580
|
+
field: crate::attr::name(attr).to_string(),
|
|
581
|
+
})
|
|
366
582
|
})
|
|
367
583
|
.collect()
|
|
368
584
|
}
|
|
369
585
|
|
|
370
586
|
fn emit_reference_field(domain_name: &str, rf: &ReferenceField) -> String {
|
|
371
587
|
let qualified_target = format!("{domain_name}::{}", rf.target);
|
|
372
|
-
format!(
|
|
588
|
+
format!(
|
|
589
|
+
"crate::kernel::read_model::ReferenceField {{ target_aggregate: {}, field: {} }}",
|
|
590
|
+
crate::naming::ruby_inspect_string(&qualified_target),
|
|
591
|
+
crate::naming::ruby_inspect_string(&rf.field)
|
|
592
|
+
)
|
|
373
593
|
}
|
|
374
594
|
|
|
375
|
-
fn emit_read_model_head(
|
|
595
|
+
fn emit_read_model_head(
|
|
596
|
+
domain_name: &str,
|
|
597
|
+
head: &Json,
|
|
598
|
+
is_root: bool,
|
|
599
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
600
|
+
) -> String {
|
|
376
601
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
377
|
-
let reference_fields: Vec<ReferenceField> = if is_root {
|
|
378
|
-
|
|
602
|
+
let reference_fields: Vec<ReferenceField> = if is_root {
|
|
603
|
+
Vec::new()
|
|
604
|
+
} else {
|
|
605
|
+
aggregates_by_name
|
|
606
|
+
.get(&aggregate_name)
|
|
607
|
+
.map(|a| read_model_reference_fields(a))
|
|
608
|
+
.unwrap_or_default()
|
|
609
|
+
};
|
|
610
|
+
let reference_fields_expr = reference_fields
|
|
611
|
+
.iter()
|
|
612
|
+
.map(|rf| emit_reference_field(domain_name, rf))
|
|
613
|
+
.collect::<Vec<_>>()
|
|
614
|
+
.join(", ");
|
|
379
615
|
let qualified_aggregate = format!("{domain_name}::{aggregate_name}");
|
|
380
616
|
let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
381
|
-
let many = head.get("many").
|
|
617
|
+
let many = head.get("many").is_some_and(Json::as_bool);
|
|
382
618
|
|
|
383
619
|
format!(
|
|
384
620
|
"crate::kernel::read_model::ReadModelHead {{ aggregate: {}, as_name: {}, many: {many}, is_root: {is_root}, reference_fields: &[{reference_fields_expr}] }}",
|
|
@@ -388,7 +624,11 @@ fn emit_read_model_head(domain_name: &str, head: &Json, is_root: bool, aggregate
|
|
|
388
624
|
}
|
|
389
625
|
|
|
390
626
|
fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> String {
|
|
391
|
-
let descending = order_by
|
|
627
|
+
let descending = order_by
|
|
628
|
+
.get("direction")
|
|
629
|
+
.map(Json::to_s)
|
|
630
|
+
.unwrap_or_default()
|
|
631
|
+
== "desc";
|
|
392
632
|
format!(
|
|
393
633
|
"crate::kernel::read_model::ReadModelOrderBy {{ field: {}, descending: {descending}, nulls: {} }}",
|
|
394
634
|
crate::naming::ruby_inspect_string(&order_by.get("field").map(Json::to_s).unwrap_or_default()),
|
|
@@ -399,32 +639,24 @@ fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> S
|
|
|
399
639
|
fn emit_read_model_limit(limit: &Json) -> String {
|
|
400
640
|
let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
|
|
401
641
|
if let Some(arg) = raw.strip_prefix(':') {
|
|
402
|
-
return format!(
|
|
642
|
+
return format!(
|
|
643
|
+
"crate::kernel::read_model::ReadModelLimit::Arg({})",
|
|
644
|
+
crate::naming::ruby_inspect_string(arg)
|
|
645
|
+
);
|
|
403
646
|
}
|
|
404
|
-
format!(
|
|
647
|
+
format!(
|
|
648
|
+
"crate::kernel::read_model::ReadModelLimit::Literal({})",
|
|
649
|
+
ruby_to_i(&raw)
|
|
650
|
+
)
|
|
405
651
|
}
|
|
406
652
|
|
|
407
653
|
// `ReadModelOffset` is a type alias for `ReadModelLimit`'s own type, so this
|
|
408
654
|
// reuses `emit_read_model_limit`'s computation and swaps the type name.
|
|
409
655
|
fn emit_read_model_offset(offset: &Json) -> String {
|
|
410
|
-
emit_read_model_limit(offset).replace(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let trimmed = s.trim_start();
|
|
415
|
-
let bytes = trimmed.as_bytes();
|
|
416
|
-
let mut end = 0;
|
|
417
|
-
if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
|
|
418
|
-
end += 1;
|
|
419
|
-
}
|
|
420
|
-
let digits_start = end;
|
|
421
|
-
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
422
|
-
end += 1;
|
|
423
|
-
}
|
|
424
|
-
if end == digits_start {
|
|
425
|
-
return 0;
|
|
426
|
-
}
|
|
427
|
-
trimmed[..end].parse().unwrap_or(0)
|
|
656
|
+
emit_read_model_limit(offset).replace(
|
|
657
|
+
"read_model::ReadModelLimit::",
|
|
658
|
+
"read_model::ReadModelOffset::",
|
|
659
|
+
)
|
|
428
660
|
}
|
|
429
661
|
|
|
430
662
|
/// One targeted many-side head's own where/order_by/offset/limit — the per-head analogue of
|
|
@@ -447,64 +679,155 @@ pub struct ReadModelDef {
|
|
|
447
679
|
pub authorization: Option<String>,
|
|
448
680
|
pub group_by_fn: Option<String>,
|
|
449
681
|
pub group_by_fn_body: Option<String>,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
pub
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
682
|
+
/// The kernel's `Reduction` variant this read model declares, as the expression the
|
|
683
|
+
/// generated table holds (`Sum("total")`); `None` when it declares none.
|
|
684
|
+
pub reduction: Option<String>,
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/// The reduction a read model's IR declares, in the kernel's own precedence: `count`, then the
|
|
688
|
+
/// `<word> :field` reductions.
|
|
689
|
+
#[allow(
|
|
690
|
+
clippy::panic,
|
|
691
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
692
|
+
)]
|
|
693
|
+
fn reduction_of(read_model: &Json) -> Option<String> {
|
|
694
|
+
if read_model.get("count").is_some() {
|
|
695
|
+
return Some("Count".to_string());
|
|
696
|
+
}
|
|
697
|
+
let field = |key: &str| {
|
|
698
|
+
read_model
|
|
699
|
+
.get(key)
|
|
700
|
+
.map(|v| crate::naming::ruby_inspect_string(&v.to_s()))
|
|
701
|
+
};
|
|
702
|
+
let single = [
|
|
703
|
+
("median_field", "Median"),
|
|
704
|
+
("sum_field", "Sum"),
|
|
705
|
+
("avg_field", "Avg"),
|
|
706
|
+
("min_field", "Min"),
|
|
707
|
+
("max_field", "Max"),
|
|
708
|
+
];
|
|
709
|
+
if let Some(found) = single
|
|
710
|
+
.iter()
|
|
711
|
+
.find_map(|(key, variant)| field(key).map(|f| format!("{variant}({f})")))
|
|
712
|
+
{
|
|
713
|
+
return Some(found);
|
|
714
|
+
}
|
|
715
|
+
if let Some(f) = field("percentile_field") {
|
|
716
|
+
let Some(at) = read_model.get("percentile_at").map(Json::to_s) else {
|
|
717
|
+
panic!("percentile_field {f} declares no percentile_at");
|
|
718
|
+
};
|
|
719
|
+
return Some(format!("Percentile {{ field: {f}, at: {at}_f64 }}"));
|
|
720
|
+
}
|
|
721
|
+
[("any_field", "Any"), ("all_field", "All")]
|
|
722
|
+
.iter()
|
|
723
|
+
.find_map(|(key, variant)| field(key).map(|f| format!("{variant}({f})")))
|
|
460
724
|
}
|
|
461
725
|
|
|
462
726
|
/// `local_wheres, hop_wheres` compiled for one targeted head, mirroring
|
|
463
727
|
/// `read_models.rb#filtered_head_def`.
|
|
464
|
-
fn filtered_head_def(
|
|
728
|
+
fn filtered_head_def(
|
|
729
|
+
domain_name: &str,
|
|
730
|
+
read_model: &Json,
|
|
731
|
+
head: &Json,
|
|
732
|
+
many_heads: &[&Json],
|
|
733
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
734
|
+
) -> FilteredHeadDef {
|
|
465
735
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
466
736
|
let aggregate = aggregates_by_name[&aggregate_name];
|
|
467
737
|
let head_query = read_model_head_query(read_model, head, many_heads);
|
|
468
738
|
let (local_wheres, hop_wheres): (Vec<&Json>, Vec<&Json>) =
|
|
469
|
-
head_query.wheres.iter().copied().partition(|w|
|
|
739
|
+
head_query.wheres.iter().copied().partition(|w| {
|
|
740
|
+
queries::query_hop_plan(
|
|
741
|
+
aggregate,
|
|
742
|
+
&w.get("field").map(Json::to_s).unwrap_or_default(),
|
|
743
|
+
aggregates_by_name,
|
|
744
|
+
)
|
|
745
|
+
.is_none()
|
|
746
|
+
});
|
|
470
747
|
let synthetic = with_wheres_and_authorization(&local_wheres, head_query.authorization);
|
|
471
|
-
let conditions = if head_query.authorization.is_some() {
|
|
748
|
+
let conditions = if head_query.authorization.is_some() {
|
|
749
|
+
queries::query_conditions_with_authorization(&synthetic)
|
|
750
|
+
} else {
|
|
751
|
+
queries::query_conditions(&synthetic)
|
|
752
|
+
};
|
|
472
753
|
|
|
473
754
|
FilteredHeadDef {
|
|
474
755
|
as_name: head.get("as").map(Json::to_s).unwrap_or_default(),
|
|
475
756
|
conditions,
|
|
476
|
-
reference_hop_conditions: queries::read_model_hop_conditions(
|
|
477
|
-
|
|
757
|
+
reference_hop_conditions: queries::read_model_hop_conditions(
|
|
758
|
+
domain_name,
|
|
759
|
+
&hop_wheres,
|
|
760
|
+
aggregate,
|
|
761
|
+
aggregates_by_name,
|
|
762
|
+
),
|
|
763
|
+
order_by: head_query
|
|
764
|
+
.order_by
|
|
765
|
+
.map(|ob| emit_read_model_order_by(ob, read_model.get("null_semantics"))),
|
|
478
766
|
offset: head_query.offset.map(emit_read_model_offset),
|
|
479
767
|
limit: head_query.limit.map(emit_read_model_limit),
|
|
480
768
|
}
|
|
481
769
|
}
|
|
482
770
|
|
|
483
|
-
pub fn read_model_def(
|
|
484
|
-
|
|
485
|
-
|
|
771
|
+
pub fn read_model_def(
|
|
772
|
+
domain_name: &str,
|
|
773
|
+
read_model: &Json,
|
|
774
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
775
|
+
) -> ReadModelDef {
|
|
776
|
+
let reference_target = read_model
|
|
777
|
+
.get("reference_target")
|
|
778
|
+
.map(Json::to_s)
|
|
779
|
+
.unwrap_or_default();
|
|
780
|
+
let heads_json = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
486
781
|
let heads: Vec<String> = heads_json
|
|
487
782
|
.iter()
|
|
488
783
|
.map(|head| {
|
|
489
|
-
let is_root =
|
|
784
|
+
let is_root =
|
|
785
|
+
head.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target;
|
|
490
786
|
emit_read_model_head(domain_name, head, is_root, aggregates_by_name)
|
|
491
787
|
})
|
|
492
788
|
.collect();
|
|
493
789
|
|
|
494
790
|
let many_heads = read_model_many_heads(read_model);
|
|
495
|
-
let filtered_heads: Vec<FilteredHeadDef> =
|
|
496
|
-
|
|
791
|
+
let filtered_heads: Vec<FilteredHeadDef> = read_model_filtered_heads(read_model, &many_heads)
|
|
792
|
+
.into_iter()
|
|
793
|
+
.map(|head| {
|
|
794
|
+
filtered_head_def(
|
|
795
|
+
domain_name,
|
|
796
|
+
read_model,
|
|
797
|
+
head,
|
|
798
|
+
&many_heads,
|
|
799
|
+
aggregates_by_name,
|
|
800
|
+
)
|
|
801
|
+
})
|
|
802
|
+
.collect();
|
|
497
803
|
let sole_head_as: bool = many_heads.len() == 1;
|
|
498
804
|
|
|
499
805
|
let read_model_name = read_model.get("name").map(Json::to_s).unwrap_or_default();
|
|
500
|
-
let group_by_fields: Vec<String> = read_model
|
|
501
|
-
|
|
806
|
+
let group_by_fields: Vec<String> = read_model
|
|
807
|
+
.get("group_by")
|
|
808
|
+
.map_or_default(Json::each)
|
|
809
|
+
.iter()
|
|
810
|
+
.map(|row| row.get("field").map(Json::to_s).unwrap_or_default())
|
|
811
|
+
.collect();
|
|
812
|
+
let (group_by_fn, group_by_fn_body) = if group_by_fields.is_empty() {
|
|
813
|
+
(None, None)
|
|
814
|
+
} else {
|
|
502
815
|
let fn_name = format!("group_by_{}", read_model_name.to_lowercase());
|
|
503
|
-
let aggregate_name = heads_json
|
|
816
|
+
let aggregate_name = heads_json
|
|
817
|
+
.first()
|
|
818
|
+
.and_then(|h| h.get("aggregate"))
|
|
819
|
+
.map(Json::to_s)
|
|
820
|
+
.unwrap_or_default();
|
|
504
821
|
let aggregate = aggregates_by_name[&aggregate_name];
|
|
505
|
-
(
|
|
506
|
-
|
|
507
|
-
|
|
822
|
+
(
|
|
823
|
+
Some(fn_name.clone()),
|
|
824
|
+
Some(emit_group_by_transform(
|
|
825
|
+
&fn_name,
|
|
826
|
+
&read_model_name,
|
|
827
|
+
aggregate,
|
|
828
|
+
&group_by_fields,
|
|
829
|
+
)),
|
|
830
|
+
)
|
|
508
831
|
};
|
|
509
832
|
|
|
510
833
|
ReadModelDef {
|
|
@@ -512,19 +835,14 @@ pub fn read_model_def(domain_name: &str, read_model: &Json, aggregates_by_name:
|
|
|
512
835
|
reference_name: read_model.get("reference_name").map(Json::to_s),
|
|
513
836
|
heads,
|
|
514
837
|
filtered_heads,
|
|
515
|
-
authorization: if sole_head_as {
|
|
838
|
+
authorization: if sole_head_as {
|
|
839
|
+
queries::emit_query_authorization(&read_model_name, read_model.get("authorization"))
|
|
840
|
+
} else {
|
|
841
|
+
None
|
|
842
|
+
},
|
|
516
843
|
group_by_fn,
|
|
517
844
|
group_by_fn_body,
|
|
518
|
-
|
|
519
|
-
median_field: read_model.get("median_field").map(Json::to_s),
|
|
520
|
-
sum_field: read_model.get("sum_field").map(Json::to_s),
|
|
521
|
-
avg_field: read_model.get("avg_field").map(Json::to_s),
|
|
522
|
-
min_field: read_model.get("min_field").map(Json::to_s),
|
|
523
|
-
max_field: read_model.get("max_field").map(Json::to_s),
|
|
524
|
-
percentile_field: read_model.get("percentile_field").map(Json::to_s),
|
|
525
|
-
percentile_at: if read_model.get("percentile_field").is_some() { read_model.get("percentile_at").map(Json::to_s) } else { None },
|
|
526
|
-
any_field: read_model.get("any_field").map(Json::to_s),
|
|
527
|
-
all_field: read_model.get("all_field").map(Json::to_s),
|
|
845
|
+
reduction: reduction_of(read_model),
|
|
528
846
|
}
|
|
529
847
|
}
|
|
530
848
|
|
|
@@ -541,40 +859,37 @@ fn with_wheres_and_authorization(wheres: &[&Json], authorization: Option<&Json>)
|
|
|
541
859
|
}
|
|
542
860
|
|
|
543
861
|
pub fn emit_read_model_def(rmd: &ReadModelDef) -> String {
|
|
544
|
-
let heads = rmd
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
let
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
let
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
let
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
};
|
|
573
|
-
let any_field = field_some(&rmd.any_field);
|
|
574
|
-
let all_field = field_some(&rmd.all_field);
|
|
862
|
+
let heads = rmd
|
|
863
|
+
.heads
|
|
864
|
+
.iter()
|
|
865
|
+
.map(|h| format!(" {h},"))
|
|
866
|
+
.collect::<Vec<_>>()
|
|
867
|
+
.join("\n");
|
|
868
|
+
let filtered_heads = rmd
|
|
869
|
+
.filtered_heads
|
|
870
|
+
.iter()
|
|
871
|
+
.map(|fh| format!(" {}", emit_filtered_head(fh)))
|
|
872
|
+
.collect::<Vec<_>>()
|
|
873
|
+
.join("\n");
|
|
874
|
+
let authorization = rmd
|
|
875
|
+
.authorization
|
|
876
|
+
.as_ref()
|
|
877
|
+
.map_or_else(|| "None".to_string(), |a| format!("Some({a})"));
|
|
878
|
+
let reference_name = rmd.reference_name.as_ref().map_or_else(
|
|
879
|
+
|| "None".to_string(),
|
|
880
|
+
|r| format!("Some({})", crate::naming::ruby_inspect_string(r)),
|
|
881
|
+
);
|
|
882
|
+
let group_by = rmd
|
|
883
|
+
.group_by_fn
|
|
884
|
+
.as_ref()
|
|
885
|
+
.map_or_else(|| "None".to_string(), |f| format!("Some({f})"));
|
|
886
|
+
let reduction = rmd.reduction.as_ref().map_or_else(
|
|
887
|
+
|| "None".to_string(),
|
|
888
|
+
|r| format!("Some(crate::kernel::read_model::Reduction::{r})"),
|
|
889
|
+
);
|
|
575
890
|
|
|
576
891
|
format!(
|
|
577
|
-
"crate::kernel::read_model::ReadModelDef {{\n verb: {},\n reference_name: {reference_name},\n heads: &[\n{heads}\n ],\n filtered_heads: &[\n{filtered_heads}\n ],\n authorization: {authorization},\n group_by: {group_by},\n
|
|
892
|
+
"crate::kernel::read_model::ReadModelDef {{\n verb: {},\n reference_name: {reference_name},\n heads: &[\n{heads}\n ],\n filtered_heads: &[\n{filtered_heads}\n ],\n authorization: {authorization},\n group_by: {group_by},\n reduction: {reduction},\n}},",
|
|
578
893
|
crate::naming::ruby_inspect_string(&rmd.verb)
|
|
579
894
|
)
|
|
580
895
|
}
|
|
@@ -584,20 +899,30 @@ pub fn emit_read_model_def(rmd: &ReadModelDef) -> String {
|
|
|
584
899
|
/// `ReferenceHopCondition`, `QueryCondition` are all single-line too; only the top-level
|
|
585
900
|
/// `ReadModelDef`/`QueryDef` get pretty multi-line formatting with their arrays broken apart).
|
|
586
901
|
fn emit_filtered_head(fh: &FilteredHeadDef) -> String {
|
|
587
|
-
let conditions = fh
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
let
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
902
|
+
let conditions = fh
|
|
903
|
+
.conditions
|
|
904
|
+
.iter()
|
|
905
|
+
.map(queries::emit_query_condition)
|
|
906
|
+
.collect::<Vec<_>>()
|
|
907
|
+
.join(" ");
|
|
908
|
+
let reference_hop_conditions = fh
|
|
909
|
+
.reference_hop_conditions
|
|
910
|
+
.iter()
|
|
911
|
+
.map(queries::emit_reference_hop_condition)
|
|
912
|
+
.collect::<Vec<_>>()
|
|
913
|
+
.join(" ");
|
|
914
|
+
let order_by = fh
|
|
915
|
+
.order_by
|
|
916
|
+
.as_ref()
|
|
917
|
+
.map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
|
|
918
|
+
let offset = fh
|
|
919
|
+
.offset
|
|
920
|
+
.as_ref()
|
|
921
|
+
.map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
|
|
922
|
+
let limit = fh
|
|
923
|
+
.limit
|
|
924
|
+
.as_ref()
|
|
925
|
+
.map_or_else(|| "None".to_string(), |l| format!("Some({l})"));
|
|
601
926
|
|
|
602
927
|
format!(
|
|
603
928
|
"crate::kernel::read_model::FilteredHead {{ as_name: {}, conditions: &[{conditions}], reference_hop_conditions: &[{reference_hop_conditions}], order_by: {order_by}, offset: {offset}, limit: {limit} }},",
|
|
@@ -608,30 +933,68 @@ fn emit_filtered_head(fh: &FilteredHeadDef) -> String {
|
|
|
608
933
|
// Keeps this aggregate's declared attributes plus any `projects` field, id
|
|
609
934
|
// and lifecycle field, excluding other capabilities' synthetic fields (like
|
|
610
935
|
// `corrects`'s `emitted_*` flags), and unwraps value objects recursively.
|
|
611
|
-
fn emit_group_by_transform(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
936
|
+
fn emit_group_by_transform(
|
|
937
|
+
fn_name: &str,
|
|
938
|
+
read_model_name: &str,
|
|
939
|
+
aggregate: &Json,
|
|
940
|
+
group_by_fields: &[String],
|
|
941
|
+
) -> String {
|
|
942
|
+
let value_objects: Vec<&Json> = aggregate
|
|
943
|
+
.get("value_objects")
|
|
944
|
+
.map_or_default(Json::each)
|
|
945
|
+
.iter()
|
|
946
|
+
.collect();
|
|
947
|
+
let value_objects_by_name: HashMap<String, &Json> = value_objects
|
|
948
|
+
.iter()
|
|
949
|
+
.map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), *vo))
|
|
950
|
+
.collect();
|
|
951
|
+
let lifecycle_field = aggregate
|
|
952
|
+
.get("lifecycle")
|
|
953
|
+
.and_then(|l| l.get("field"))
|
|
954
|
+
.map(Json::to_s);
|
|
955
|
+
let mut fields: Vec<Json> = aggregate
|
|
956
|
+
.get("attributes")
|
|
957
|
+
.map_or_default(Json::each)
|
|
958
|
+
.to_vec();
|
|
616
959
|
fields.extend(crate::types::projected_field_pseudo_attributes(aggregate));
|
|
617
960
|
|
|
618
|
-
let mut kept_keys: Vec<String> = fields
|
|
961
|
+
let mut kept_keys: Vec<String> = fields
|
|
962
|
+
.iter()
|
|
963
|
+
.map(|a| crate::attr::name(a).to_string())
|
|
964
|
+
.collect();
|
|
619
965
|
kept_keys.push("id".to_string());
|
|
620
966
|
if let Some(lf) = &lifecycle_field {
|
|
621
967
|
kept_keys.push(lf.clone());
|
|
622
968
|
}
|
|
623
|
-
let keep_cond = kept_keys
|
|
969
|
+
let keep_cond = kept_keys
|
|
970
|
+
.iter()
|
|
971
|
+
.map(|k| format!("k == {k:?}"))
|
|
972
|
+
.collect::<Vec<_>>()
|
|
973
|
+
.join(" || ");
|
|
624
974
|
|
|
625
975
|
let arms: Vec<String> = fields
|
|
626
976
|
.iter()
|
|
627
977
|
.map(|a| {
|
|
628
|
-
let unwrapped = unwrap_json_expr(
|
|
978
|
+
let unwrapped = unwrap_json_expr(
|
|
979
|
+
"v",
|
|
980
|
+
crate::attr::type_name(a),
|
|
981
|
+
crate::attr::list(a),
|
|
982
|
+
aggregate,
|
|
983
|
+
&value_objects_by_name,
|
|
984
|
+
);
|
|
629
985
|
format!("{:?} => {unwrapped},", crate::attr::name(a))
|
|
630
986
|
})
|
|
631
987
|
.collect();
|
|
632
988
|
let arms = arms.join("\n ");
|
|
633
989
|
|
|
634
|
-
let fields_literal = format!(
|
|
990
|
+
let fields_literal = format!(
|
|
991
|
+
"&[{}]",
|
|
992
|
+
group_by_fields
|
|
993
|
+
.iter()
|
|
994
|
+
.map(|f| format!("{f:?}"))
|
|
995
|
+
.collect::<Vec<_>>()
|
|
996
|
+
.join(", ")
|
|
997
|
+
);
|
|
635
998
|
let leaf_check = group_by_leaf_check(read_model_name, aggregate, group_by_fields);
|
|
636
999
|
|
|
637
1000
|
format!(
|
|
@@ -641,10 +1004,19 @@ fn emit_group_by_transform(fn_name: &str, read_model_name: &str, aggregate: &Jso
|
|
|
641
1004
|
|
|
642
1005
|
// A key path naming every identity head cannot collide, so its leaves go
|
|
643
1006
|
// unchecked; any other refuses a second row (ADR 0061).
|
|
644
|
-
fn group_by_leaf_check(
|
|
1007
|
+
fn group_by_leaf_check(
|
|
1008
|
+
read_model_name: &str,
|
|
1009
|
+
aggregate: &Json,
|
|
1010
|
+
group_by_fields: &[String],
|
|
1011
|
+
) -> String {
|
|
645
1012
|
let mut identity: Vec<String> = Vec::new();
|
|
646
|
-
for path in aggregate.get("identified_by").
|
|
647
|
-
let head = path
|
|
1013
|
+
for path in aggregate.get("identified_by").map_or_default(Json::each) {
|
|
1014
|
+
let head = path
|
|
1015
|
+
.to_s()
|
|
1016
|
+
.split('.')
|
|
1017
|
+
.next()
|
|
1018
|
+
.unwrap_or_default()
|
|
1019
|
+
.to_string();
|
|
648
1020
|
if !identity.contains(&head) {
|
|
649
1021
|
identity.push(head);
|
|
650
1022
|
}
|
|
@@ -652,36 +1024,62 @@ fn group_by_leaf_check(read_model_name: &str, aggregate: &Json, group_by_fields:
|
|
|
652
1024
|
if !identity.is_empty() && identity.iter().all(|head| group_by_fields.contains(head)) {
|
|
653
1025
|
return "crate::kernel::read_model::LeafCheck::IdentityCovered".to_string();
|
|
654
1026
|
}
|
|
655
|
-
format!(
|
|
1027
|
+
format!(
|
|
1028
|
+
"crate::kernel::read_model::LeafCheck::RefuseCollision({})",
|
|
1029
|
+
crate::naming::ruby_inspect_string(read_model_name)
|
|
1030
|
+
)
|
|
656
1031
|
}
|
|
657
1032
|
|
|
658
1033
|
// Port of `Value.materialize_unwrapped` (Ruby).
|
|
659
|
-
fn unwrap_json_expr(
|
|
1034
|
+
fn unwrap_json_expr(
|
|
1035
|
+
expr: &str,
|
|
1036
|
+
type_name: &str,
|
|
1037
|
+
list: bool,
|
|
1038
|
+
aggregate: &Json,
|
|
1039
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1040
|
+
) -> String {
|
|
660
1041
|
if list {
|
|
661
1042
|
let inner = unwrap_json_expr("item", type_name, false, aggregate, value_objects_by_name);
|
|
662
1043
|
return format!("match {expr} {{ crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| {inner}).collect()), other => other }}");
|
|
663
1044
|
}
|
|
664
1045
|
|
|
665
1046
|
let vo = value_objects_by_name.get(type_name).copied();
|
|
666
|
-
let entity = aggregate
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1047
|
+
let entity = aggregate
|
|
1048
|
+
.get("entities")
|
|
1049
|
+
.map_or_default(Json::each)
|
|
1050
|
+
.iter()
|
|
1051
|
+
.find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == type_name);
|
|
1052
|
+
let fields_meta: Option<&[Json]> = vo.map_or_else(
|
|
1053
|
+
|| entity.and_then(|e| e.get("attributes").map(Json::each)),
|
|
1054
|
+
|vo| vo.get("attributes").map(Json::each),
|
|
1055
|
+
);
|
|
1056
|
+
let Some(fields_meta) = fields_meta else {
|
|
1057
|
+
return expr.to_string();
|
|
671
1058
|
};
|
|
672
|
-
let Some(fields_meta) = fields_meta else { return expr.to_string() };
|
|
673
1059
|
|
|
674
1060
|
if vo.is_some() && fields_meta.len() == 1 {
|
|
675
1061
|
let field = &fields_meta[0];
|
|
676
|
-
let unwrapped = unwrap_json_expr(
|
|
1062
|
+
let unwrapped = unwrap_json_expr(
|
|
1063
|
+
"field_value",
|
|
1064
|
+
crate::attr::type_name(field),
|
|
1065
|
+
crate::attr::list(field),
|
|
1066
|
+
aggregate,
|
|
1067
|
+
value_objects_by_name,
|
|
1068
|
+
);
|
|
677
1069
|
let field_name = crate::attr::name(field);
|
|
678
|
-
return format!("match {expr} {{ crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == {field_name:?}).
|
|
1070
|
+
return format!("match {expr} {{ crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == {field_name:?}).map_or(crate::kernel::Json::Null, |(_, field_value)| {unwrapped}), other => other }}");
|
|
679
1071
|
}
|
|
680
1072
|
|
|
681
1073
|
let inner_arms: Vec<String> = fields_meta
|
|
682
1074
|
.iter()
|
|
683
1075
|
.map(|f| {
|
|
684
|
-
let unwrapped = unwrap_json_expr(
|
|
1076
|
+
let unwrapped = unwrap_json_expr(
|
|
1077
|
+
"v",
|
|
1078
|
+
crate::attr::type_name(f),
|
|
1079
|
+
crate::attr::list(f),
|
|
1080
|
+
aggregate,
|
|
1081
|
+
value_objects_by_name,
|
|
1082
|
+
);
|
|
685
1083
|
format!("{:?} => {unwrapped},", crate::attr::name(f))
|
|
686
1084
|
})
|
|
687
1085
|
.collect();
|
|
@@ -689,9 +1087,12 @@ fn unwrap_json_expr(expr: &str, type_name: &str, list: bool, aggregate: &Json, v
|
|
|
689
1087
|
format!("match {expr} {{ crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(fields.into_iter().map(|(k, v)| {{ let new_v = match k.as_str() {{ {inner_arms} _ => v }}; (k, new_v) }}).collect()), other => other }}")
|
|
690
1088
|
}
|
|
691
1089
|
|
|
692
|
-
const READ_MODEL_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::read_model::ReadModelDef {\n verb: \"tmpl_verb\",\n reference_name: Some(\"tmpl_reference_name\"),\n heads: &[\n crate::kernel::read_model::ReadModelHead {\n aggregate: \"tmpl_aggregate\",\n as_name: \"tmpl_as_name\",\n many: true,\n is_root: false,\n reference_fields: &[\n crate::kernel::read_model::ReferenceField { target_aggregate: \"tmpl_target_aggregate\", field: \"tmpl_field\" },\n ],\n },\n ],\n filtered_heads: &[\n crate::kernel::read_model::FilteredHead { as_name: \"tmpl_as_name\", conditions: &[crate::kernel::QueryCondition { field: \"tmpl_field\", comparator: crate::kernel::query_comparators::QueryComparator::Eq, value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], reference_hop_conditions: &[crate::kernel::read_model::ReferenceHopCondition { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\", through: &[crate::kernel::read_model::HopStep { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\" }], inner_field: \"tmpl_inner_field\", inner_comparator: crate::kernel::query_comparators::QueryComparator::Eq, inner_value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], order_by: Some(crate::kernel::read_model::ReadModelOrderBy { field: \"tmpl_order_field\", descending: true, nulls: crate::kernel::query_ordering::NullsMode::Last }), offset: Some(crate::kernel::read_model::ReadModelOffset::Literal(1)), limit: Some(crate::kernel::read_model::ReadModelLimit::Literal(5)) },\n ],\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n group_by: None,\n
|
|
1090
|
+
const READ_MODEL_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::read_model::ReadModelDef {\n verb: \"tmpl_verb\",\n reference_name: Some(\"tmpl_reference_name\"),\n heads: &[\n crate::kernel::read_model::ReadModelHead {\n aggregate: \"tmpl_aggregate\",\n as_name: \"tmpl_as_name\",\n many: true,\n is_root: false,\n reference_fields: &[\n crate::kernel::read_model::ReferenceField { target_aggregate: \"tmpl_target_aggregate\", field: \"tmpl_field\" },\n ],\n },\n ],\n filtered_heads: &[\n crate::kernel::read_model::FilteredHead { as_name: \"tmpl_as_name\", conditions: &[crate::kernel::QueryCondition { field: \"tmpl_field\", comparator: crate::kernel::query_comparators::QueryComparator::Eq, value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], reference_hop_conditions: &[crate::kernel::read_model::ReferenceHopCondition { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\", through: &[crate::kernel::read_model::HopStep { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\" }], inner_field: \"tmpl_inner_field\", inner_comparator: crate::kernel::query_comparators::QueryComparator::Eq, inner_value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], order_by: Some(crate::kernel::read_model::ReadModelOrderBy { field: \"tmpl_order_field\", descending: true, nulls: crate::kernel::query_ordering::NullsMode::Last }), offset: Some(crate::kernel::read_model::ReadModelOffset::Literal(1)), limit: Some(crate::kernel::read_model::ReadModelLimit::Literal(5)) },\n ],\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n group_by: None,\n reduction: None,\n},";
|
|
693
1091
|
|
|
694
1092
|
pub fn emit_read_model_table(exemplar: &Exemplar, read_model_defs: &[ReadModelDef]) -> String {
|
|
695
1093
|
let rows: Vec<String> = read_model_defs.iter().map(emit_read_model_def).collect();
|
|
696
|
-
exemplar.render(
|
|
1094
|
+
exemplar.render(
|
|
1095
|
+
"read_model_table",
|
|
1096
|
+
&[(READ_MODEL_TABLE_ROW_PLACEHOLDER, rows.join("\n"))],
|
|
1097
|
+
)
|
|
697
1098
|
}
|