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
|
@@ -3,39 +3,94 @@
|
|
|
3
3
|
|
|
4
4
|
use crate::exemplar::Exemplar;
|
|
5
5
|
use crate::json::Json;
|
|
6
|
+
use crate::presenters::unwrap_json_expr;
|
|
6
7
|
use crate::queries;
|
|
8
|
+
use crate::shared::ruby_to_i;
|
|
7
9
|
use crate::skip_reason::{reskip, skip, SkipReason};
|
|
8
10
|
use std::collections::HashMap;
|
|
9
11
|
|
|
10
12
|
const READ_MODEL_BARE_KEYS: &[&str] = &[
|
|
11
|
-
"name",
|
|
12
|
-
"
|
|
13
|
+
"name",
|
|
14
|
+
"description",
|
|
15
|
+
"reference_name",
|
|
16
|
+
"reference_target",
|
|
17
|
+
"query_name",
|
|
18
|
+
"aggregate_heads",
|
|
19
|
+
"wheres",
|
|
20
|
+
"order_by",
|
|
21
|
+
"offset",
|
|
22
|
+
"limit",
|
|
23
|
+
"freshness",
|
|
24
|
+
"index_hints",
|
|
25
|
+
"group_by",
|
|
26
|
+
"null_semantics",
|
|
27
|
+
"authorization",
|
|
28
|
+
"count",
|
|
29
|
+
"median_field",
|
|
30
|
+
"sum_field",
|
|
31
|
+
"avg_field",
|
|
32
|
+
"min_field",
|
|
33
|
+
"max_field",
|
|
34
|
+
"percentile_field",
|
|
35
|
+
"percentile_at",
|
|
36
|
+
"any_field",
|
|
37
|
+
"all_field",
|
|
13
38
|
];
|
|
14
39
|
|
|
15
40
|
// Every reduction beyond `count` (which needs no field) — mirrors
|
|
16
41
|
// Runtime::ReadModelInterpreter::REDUCTION_WORD/INTEGER_ONLY_REDUCTIONS/BOOLEAN_REDUCTIONS
|
|
17
42
|
// (ADR 0078).
|
|
18
|
-
const AGGREGATION_FIELD_KEYS: &[&str] = &[
|
|
43
|
+
const AGGREGATION_FIELD_KEYS: &[&str] = &[
|
|
44
|
+
"median_field",
|
|
45
|
+
"sum_field",
|
|
46
|
+
"avg_field",
|
|
47
|
+
"min_field",
|
|
48
|
+
"max_field",
|
|
49
|
+
"percentile_field",
|
|
50
|
+
"any_field",
|
|
51
|
+
"all_field",
|
|
52
|
+
];
|
|
19
53
|
const INTEGER_ONLY_AGGREGATION_FIELDS: &[&str] = &["sum_field", "avg_field"];
|
|
20
54
|
const BOOLEAN_AGGREGATION_FIELDS: &[&str] = &["any_field", "all_field"];
|
|
21
55
|
|
|
22
|
-
pub fn read_model_skip_reason(
|
|
56
|
+
pub fn read_model_skip_reason(
|
|
57
|
+
read_model: &Json,
|
|
58
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
59
|
+
unsupported_names: &[String],
|
|
60
|
+
) -> Option<SkipReason> {
|
|
23
61
|
let keys: Vec<&str> = match read_model {
|
|
24
|
-
Json::Object(pairs) => pairs
|
|
62
|
+
Json::Object(pairs) => pairs
|
|
63
|
+
.iter()
|
|
64
|
+
.filter(|(_, v)| !matches!(v, Json::Null))
|
|
65
|
+
.map(|(k, _)| k.as_str())
|
|
66
|
+
.collect(),
|
|
25
67
|
_ => Vec::new(),
|
|
26
68
|
};
|
|
27
|
-
let extra: Vec<&str> = keys
|
|
69
|
+
let extra: Vec<&str> = keys
|
|
70
|
+
.into_iter()
|
|
71
|
+
.filter(|k| !READ_MODEL_BARE_KEYS.contains(k))
|
|
72
|
+
.collect();
|
|
28
73
|
if !extra.is_empty() {
|
|
29
74
|
return Some(read_model_options_skip_reason(&extra));
|
|
30
75
|
}
|
|
31
76
|
|
|
32
|
-
if read_model
|
|
77
|
+
if read_model
|
|
78
|
+
.get("group_by")
|
|
79
|
+
.map_or_default(Json::each)
|
|
80
|
+
.iter()
|
|
81
|
+
.any(|_| true)
|
|
82
|
+
{
|
|
33
83
|
return group_by_skip_reason(read_model, aggregates_by_name, unsupported_names);
|
|
34
84
|
}
|
|
35
85
|
|
|
36
|
-
let heads = read_model.get("aggregate_heads").
|
|
37
|
-
let reference_target = read_model
|
|
38
|
-
|
|
86
|
+
let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
87
|
+
let reference_target = read_model
|
|
88
|
+
.get("reference_target")
|
|
89
|
+
.map(Json::to_s)
|
|
90
|
+
.unwrap_or_default();
|
|
91
|
+
let root = heads
|
|
92
|
+
.iter()
|
|
93
|
+
.find(|h| h.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target);
|
|
39
94
|
// Rootless (no `reference_to`) is generated — `read_models.rb`'s own
|
|
40
95
|
// comment on this check.
|
|
41
96
|
if root.is_none() && !reference_target.is_empty() {
|
|
@@ -45,13 +100,20 @@ pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<St
|
|
|
45
100
|
));
|
|
46
101
|
}
|
|
47
102
|
|
|
48
|
-
let root_name = root
|
|
49
|
-
|
|
103
|
+
let root_name = root
|
|
104
|
+
.and_then(|h| h.get("aggregate"))
|
|
105
|
+
.map(Json::to_s)
|
|
106
|
+
.unwrap_or_default();
|
|
107
|
+
if !aggregates_by_name.contains_key(&root_name)
|
|
108
|
+
&& nested_entity_names(aggregates_by_name).contains(&root_name)
|
|
109
|
+
{
|
|
50
110
|
return Some(entity_head_skip_reason(&root_name, "root", "fetch by id"));
|
|
51
111
|
}
|
|
52
112
|
|
|
53
113
|
for head in heads {
|
|
54
|
-
if let Some(reason) =
|
|
114
|
+
if let Some(reason) =
|
|
115
|
+
read_model_head_skip_reason(head, aggregates_by_name, unsupported_names)
|
|
116
|
+
{
|
|
55
117
|
return Some(reason);
|
|
56
118
|
}
|
|
57
119
|
}
|
|
@@ -65,7 +127,7 @@ pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<St
|
|
|
65
127
|
|
|
66
128
|
fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
|
|
67
129
|
let mut sorted: Vec<&str> = extra.to_vec();
|
|
68
|
-
sorted.
|
|
130
|
+
sorted.sort_unstable();
|
|
69
131
|
skip(sorted[0], format!(
|
|
70
132
|
"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
133
|
sorted.join(", ")
|
|
@@ -74,7 +136,12 @@ fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
|
|
|
74
136
|
|
|
75
137
|
/// Every many-side head this read model declares.
|
|
76
138
|
fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
|
|
77
|
-
read_model
|
|
139
|
+
read_model
|
|
140
|
+
.get("aggregate_heads")
|
|
141
|
+
.map_or_default(Json::each)
|
|
142
|
+
.iter()
|
|
143
|
+
.filter(|h| h.get("many").is_some_and(Json::as_bool))
|
|
144
|
+
.collect()
|
|
78
145
|
}
|
|
79
146
|
|
|
80
147
|
/// Whether one declared option (its own `:target`, or `None` when untargeted) applies to
|
|
@@ -84,10 +151,13 @@ fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
|
|
|
84
151
|
/// reaching codegen at all is proof there's exactly one to mean.
|
|
85
152
|
fn option_target_matches_head(target: Option<&str>, head: &Json, many_heads: &[&Json]) -> bool {
|
|
86
153
|
let head_as = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
154
|
+
target.map_or_else(
|
|
155
|
+
|| {
|
|
156
|
+
many_heads.len() == 1
|
|
157
|
+
&& many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head_as
|
|
158
|
+
},
|
|
159
|
+
|target| target == head.get("aggregate").map(Json::to_s).unwrap_or_default(),
|
|
160
|
+
)
|
|
91
161
|
}
|
|
92
162
|
|
|
93
163
|
fn json_target(option: &Json) -> Option<String> {
|
|
@@ -98,8 +168,11 @@ fn json_target(option: &Json) -> Option<String> {
|
|
|
98
168
|
/// many-side head only, a declared `authorize ..., tenant:` — `on:` doesn't extend to
|
|
99
169
|
/// `tenant:`) targeting it.
|
|
100
170
|
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
|
|
171
|
+
let wheres = read_model.get("wheres").map_or_default(Json::each);
|
|
172
|
+
if wheres
|
|
173
|
+
.iter()
|
|
174
|
+
.any(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads))
|
|
175
|
+
{
|
|
103
176
|
return true;
|
|
104
177
|
}
|
|
105
178
|
if let Some(order_by) = read_model.get("order_by") {
|
|
@@ -117,8 +190,14 @@ fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[
|
|
|
117
190
|
return true;
|
|
118
191
|
}
|
|
119
192
|
}
|
|
120
|
-
if read_model
|
|
121
|
-
|
|
193
|
+
if read_model
|
|
194
|
+
.get("authorization")
|
|
195
|
+
.and_then(|a| a.get("tenant"))
|
|
196
|
+
.is_some()
|
|
197
|
+
&& many_heads.len() == 1
|
|
198
|
+
{
|
|
199
|
+
return many_heads[0].get("as").map(Json::to_s).unwrap_or_default()
|
|
200
|
+
== head.get("as").map(Json::to_s).unwrap_or_default();
|
|
122
201
|
}
|
|
123
202
|
false
|
|
124
203
|
}
|
|
@@ -127,7 +206,11 @@ fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[
|
|
|
127
206
|
/// declared order — one entry when there's a single many-side head (the pre-ADR-0055 shape),
|
|
128
207
|
/// more than one only when `on:` targets several.
|
|
129
208
|
fn read_model_filtered_heads<'a>(read_model: &Json, many_heads: &[&'a Json]) -> Vec<&'a Json> {
|
|
130
|
-
many_heads
|
|
209
|
+
many_heads
|
|
210
|
+
.iter()
|
|
211
|
+
.copied()
|
|
212
|
+
.filter(|head| read_model_option_targets_head(read_model, head, many_heads))
|
|
213
|
+
.collect()
|
|
131
214
|
}
|
|
132
215
|
|
|
133
216
|
/// One targeted head's own where/order_by/offset/limit, pulled out of the read model's full
|
|
@@ -141,31 +224,73 @@ struct HeadQuery<'a> {
|
|
|
141
224
|
authorization: Option<&'a Json>,
|
|
142
225
|
}
|
|
143
226
|
|
|
144
|
-
fn read_model_head_query<'a>(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let
|
|
227
|
+
fn read_model_head_query<'a>(
|
|
228
|
+
read_model: &'a Json,
|
|
229
|
+
head: &Json,
|
|
230
|
+
many_heads: &[&Json],
|
|
231
|
+
) -> HeadQuery<'a> {
|
|
232
|
+
let wheres = read_model
|
|
233
|
+
.get("wheres")
|
|
234
|
+
.map_or_default(Json::each)
|
|
235
|
+
.iter()
|
|
236
|
+
.filter(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads))
|
|
237
|
+
.collect();
|
|
238
|
+
let order_by = read_model
|
|
239
|
+
.get("order_by")
|
|
240
|
+
.filter(|ob| option_target_matches_head(json_target(ob).as_deref(), head, many_heads));
|
|
241
|
+
let limit = read_model
|
|
242
|
+
.get("limit")
|
|
243
|
+
.filter(|l| option_target_matches_head(json_target(l).as_deref(), head, many_heads));
|
|
244
|
+
let offset = read_model
|
|
245
|
+
.get("offset")
|
|
246
|
+
.filter(|o| option_target_matches_head(json_target(o).as_deref(), head, many_heads));
|
|
247
|
+
let authorization = if many_heads.len() == 1
|
|
248
|
+
&& many_heads[0].get("as").map(Json::to_s).unwrap_or_default()
|
|
249
|
+
== head.get("as").map(Json::to_s).unwrap_or_default()
|
|
250
|
+
{
|
|
150
251
|
read_model.get("authorization")
|
|
151
252
|
} else {
|
|
152
253
|
None
|
|
153
254
|
};
|
|
154
|
-
HeadQuery {
|
|
255
|
+
HeadQuery {
|
|
256
|
+
wheres,
|
|
257
|
+
order_by,
|
|
258
|
+
limit,
|
|
259
|
+
offset,
|
|
260
|
+
authorization,
|
|
261
|
+
}
|
|
155
262
|
}
|
|
156
263
|
|
|
157
264
|
/// Checks every targeted head's own where/order_by/limit for generability against ITS
|
|
158
265
|
/// aggregate (not the read model's root); `None` means clean.
|
|
159
|
-
fn read_model_options_content_skip_reason(
|
|
266
|
+
fn read_model_options_content_skip_reason(
|
|
267
|
+
read_model: &Json,
|
|
268
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
269
|
+
) -> Option<SkipReason> {
|
|
160
270
|
let many_heads = read_model_many_heads(read_model);
|
|
161
271
|
|
|
162
272
|
for head in read_model_filtered_heads(read_model, &many_heads) {
|
|
163
273
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
164
274
|
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
165
|
-
return Some(entity_head_skip_reason(
|
|
275
|
+
return Some(entity_head_skip_reason(
|
|
276
|
+
&aggregate_name,
|
|
277
|
+
"filtered head",
|
|
278
|
+
"filter, order or authorize",
|
|
279
|
+
));
|
|
166
280
|
};
|
|
167
|
-
let vos = aggregate.get("value_objects").
|
|
168
|
-
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
281
|
+
let vos = aggregate.get("value_objects").map_or_default(Json::each);
|
|
282
|
+
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
283
|
+
.iter()
|
|
284
|
+
.map(|vo| {
|
|
285
|
+
(
|
|
286
|
+
vo.get("name")
|
|
287
|
+
.and_then(Json::as_str)
|
|
288
|
+
.unwrap_or("")
|
|
289
|
+
.to_string(),
|
|
290
|
+
vo,
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
.collect();
|
|
169
294
|
let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
170
295
|
let head_label = format!("head {aggregate_name} (as {as_name})");
|
|
171
296
|
let head_query = read_model_head_query(read_model, head, &many_heads);
|
|
@@ -173,7 +298,10 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
|
|
|
173
298
|
// A hop through a reference is generated when `query_hop_plan` resolves
|
|
174
299
|
// it — `read_models.rb`'s own loop, check for check.
|
|
175
300
|
for where_clause in head_query.wheres.iter().copied() {
|
|
176
|
-
let field = where_clause
|
|
301
|
+
let field = where_clause
|
|
302
|
+
.get("field")
|
|
303
|
+
.map(Json::to_s)
|
|
304
|
+
.unwrap_or_default();
|
|
177
305
|
let hop = queries::query_hop_plan(aggregate, &field, aggregates_by_name);
|
|
178
306
|
if hop.is_none() && field.contains('/') {
|
|
179
307
|
return Some(skip(
|
|
@@ -187,22 +315,42 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
|
|
|
187
315
|
|
|
188
316
|
if let Some(plan) = hop {
|
|
189
317
|
let target_value_objects_by_name = queries::value_objects_of(plan.target);
|
|
190
|
-
if let Some(reason) = queries::query_where_skip_reason(
|
|
191
|
-
|
|
318
|
+
if let Some(reason) = queries::query_where_skip_reason(
|
|
319
|
+
&queries::with_field(where_clause, &plan.inner_field),
|
|
320
|
+
plan.target,
|
|
321
|
+
&target_value_objects_by_name,
|
|
322
|
+
) {
|
|
323
|
+
return Some(reskip(
|
|
324
|
+
&reason,
|
|
325
|
+
format!(
|
|
326
|
+
"{head_label}'s own hop through {} to {}'s own {reason}",
|
|
327
|
+
plan.via_field, plan.target_aggregate
|
|
328
|
+
),
|
|
329
|
+
));
|
|
192
330
|
}
|
|
193
331
|
continue;
|
|
194
332
|
}
|
|
195
333
|
|
|
196
|
-
if let Some(reason) =
|
|
334
|
+
if let Some(reason) =
|
|
335
|
+
queries::query_where_skip_reason(where_clause, aggregate, &value_objects_by_name)
|
|
336
|
+
{
|
|
197
337
|
return Some(reskip(&reason, format!("{head_label}'s own {reason}")));
|
|
198
338
|
}
|
|
199
339
|
}
|
|
200
340
|
|
|
201
|
-
if let Some(reason) = queries::declared_authorization_skip_reason(
|
|
341
|
+
if let Some(reason) = queries::declared_authorization_skip_reason(
|
|
342
|
+
head_query.authorization,
|
|
343
|
+
aggregate,
|
|
344
|
+
&value_objects_by_name,
|
|
345
|
+
) {
|
|
202
346
|
return Some(reason);
|
|
203
347
|
}
|
|
204
348
|
|
|
205
|
-
if let Some(reason) = queries::declared_order_by_skip_reason(
|
|
349
|
+
if let Some(reason) = queries::declared_order_by_skip_reason(
|
|
350
|
+
head_query.order_by,
|
|
351
|
+
aggregate,
|
|
352
|
+
&value_objects_by_name,
|
|
353
|
+
) {
|
|
206
354
|
return Some(reason);
|
|
207
355
|
}
|
|
208
356
|
|
|
@@ -221,19 +369,45 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
|
|
|
221
369
|
// `seal_aggregation` (Ruby, build time) already guarantees exactly one
|
|
222
370
|
// many-side head and mutual exclusion with `group_by` — only the one declared
|
|
223
371
|
// reduction's own field needs checking here (ADR 0078).
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
372
|
+
#[allow(
|
|
373
|
+
clippy::expect_used,
|
|
374
|
+
reason = "the message names an invariant an earlier check in this generator established"
|
|
375
|
+
)]
|
|
376
|
+
fn aggregation_skip_reason(
|
|
377
|
+
read_model: &Json,
|
|
378
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
379
|
+
) -> Option<SkipReason> {
|
|
380
|
+
let field_key = AGGREGATION_FIELD_KEYS
|
|
381
|
+
.iter()
|
|
382
|
+
.find(|key| read_model.get(key).is_some())?;
|
|
383
|
+
let field = Json::to_s(read_model.get(field_key).expect("checked by find above"));
|
|
227
384
|
let word = field_key.strip_suffix("_field").unwrap_or(field_key);
|
|
228
385
|
|
|
229
|
-
let heads = read_model.get("aggregate_heads").
|
|
230
|
-
let target = heads
|
|
386
|
+
let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
387
|
+
let target = heads
|
|
388
|
+
.iter()
|
|
389
|
+
.find(|h| h.get("many").is_some_and(Json::as_bool))?;
|
|
231
390
|
let aggregate_name = target.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
232
391
|
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
233
|
-
return Some(entity_head_skip_reason(
|
|
392
|
+
return Some(entity_head_skip_reason(
|
|
393
|
+
&aggregate_name,
|
|
394
|
+
&format!("{word} target"),
|
|
395
|
+
"reduce",
|
|
396
|
+
));
|
|
234
397
|
};
|
|
235
|
-
let vos = aggregate.get("value_objects").
|
|
236
|
-
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
398
|
+
let vos = aggregate.get("value_objects").map_or_default(Json::each);
|
|
399
|
+
let value_objects_by_name: HashMap<String, &Json> = vos
|
|
400
|
+
.iter()
|
|
401
|
+
.map(|vo| {
|
|
402
|
+
(
|
|
403
|
+
vo.get("name")
|
|
404
|
+
.and_then(Json::as_str)
|
|
405
|
+
.unwrap_or("")
|
|
406
|
+
.to_string(),
|
|
407
|
+
vo,
|
|
408
|
+
)
|
|
409
|
+
})
|
|
410
|
+
.collect();
|
|
237
411
|
|
|
238
412
|
let kind = queries::query_field_kind(aggregate, &field, &value_objects_by_name);
|
|
239
413
|
if kind == queries::FieldKind::Unknown {
|
|
@@ -255,13 +429,14 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
|
|
|
255
429
|
));
|
|
256
430
|
}
|
|
257
431
|
|
|
258
|
-
if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key)
|
|
259
|
-
|
|
260
|
-
|
|
432
|
+
if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key)
|
|
433
|
+
&& queries::query_field_numeric_type(aggregate, &field, &value_objects_by_name).as_deref()
|
|
434
|
+
!= Some("Integer")
|
|
435
|
+
{
|
|
436
|
+
return Some(skip(
|
|
261
437
|
*field_key,
|
|
262
438
|
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
439
|
));
|
|
264
|
-
}
|
|
265
440
|
}
|
|
266
441
|
|
|
267
442
|
None
|
|
@@ -269,8 +444,12 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
|
|
|
269
444
|
|
|
270
445
|
// Mirrors the retired Ruby generator's `read_models.rb`'s `group_by_skip_reason`: the one
|
|
271
446
|
// shape the corpus declares — a single rootless head, group_by alone.
|
|
272
|
-
fn group_by_skip_reason(
|
|
273
|
-
|
|
447
|
+
fn group_by_skip_reason(
|
|
448
|
+
read_model: &Json,
|
|
449
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
450
|
+
unsupported_names: &[String],
|
|
451
|
+
) -> Option<SkipReason> {
|
|
452
|
+
let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
274
453
|
if heads.len() != 1 {
|
|
275
454
|
return Some(skip("group_by", format!("declares group_by across {} aggregate heads — not generated yet (only a single, rootless head is)", heads.len())));
|
|
276
455
|
}
|
|
@@ -278,15 +457,36 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
|
|
|
278
457
|
if reference_target.is_some() && !matches!(reference_target, Some(Json::Null)) {
|
|
279
458
|
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
459
|
}
|
|
281
|
-
if read_model.get("count").is_some()
|
|
282
|
-
|
|
460
|
+
if read_model.get("count").is_some()
|
|
461
|
+
|| AGGREGATION_FIELD_KEYS
|
|
462
|
+
.iter()
|
|
463
|
+
.any(|key| read_model.get(key).is_some())
|
|
464
|
+
{
|
|
465
|
+
return Some(skip(
|
|
466
|
+
"group_by",
|
|
467
|
+
"declares group_by alongside a reduction — not generated yet",
|
|
468
|
+
));
|
|
283
469
|
}
|
|
284
|
-
let has_wheres = read_model
|
|
285
|
-
|
|
286
|
-
|
|
470
|
+
let has_wheres = read_model
|
|
471
|
+
.get("wheres")
|
|
472
|
+
.map_or_default(Json::each)
|
|
473
|
+
.iter()
|
|
474
|
+
.any(|_| true);
|
|
475
|
+
if has_wheres
|
|
476
|
+
|| read_model.get("order_by").is_some()
|
|
477
|
+
|| read_model.get("limit").is_some()
|
|
478
|
+
|| read_model.get("offset").is_some()
|
|
479
|
+
{
|
|
480
|
+
return Some(skip(
|
|
481
|
+
"group_by",
|
|
482
|
+
"declares group_by alongside where/order_by/limit/offset — not generated yet",
|
|
483
|
+
));
|
|
287
484
|
}
|
|
288
485
|
if read_model.get("authorization").is_some() {
|
|
289
|
-
return Some(skip(
|
|
486
|
+
return Some(skip(
|
|
487
|
+
"group_by",
|
|
488
|
+
"declares group_by with an authorize policy — not generated yet",
|
|
489
|
+
));
|
|
290
490
|
}
|
|
291
491
|
|
|
292
492
|
let head = &heads[0];
|
|
@@ -296,11 +496,18 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
|
|
|
296
496
|
|
|
297
497
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
298
498
|
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
299
|
-
return Some(entity_head_skip_reason(
|
|
499
|
+
return Some(entity_head_skip_reason(
|
|
500
|
+
&aggregate_name,
|
|
501
|
+
"group_by head",
|
|
502
|
+
"group",
|
|
503
|
+
));
|
|
300
504
|
};
|
|
301
|
-
let attrs = aggregate.get("attributes").
|
|
302
|
-
let lifecycle_field = aggregate
|
|
303
|
-
|
|
505
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
506
|
+
let lifecycle_field = aggregate
|
|
507
|
+
.get("lifecycle")
|
|
508
|
+
.and_then(|l| l.get("field"))
|
|
509
|
+
.map(Json::to_s);
|
|
510
|
+
for row in read_model.get("group_by").map_or_default(Json::each) {
|
|
304
511
|
let field = row.get("field").map(Json::to_s).unwrap_or_default();
|
|
305
512
|
let is_attr = attrs.iter().any(|a| crate::attr::name(a) == field);
|
|
306
513
|
let is_lifecycle = lifecycle_field.as_deref() == Some(field.as_str());
|
|
@@ -314,7 +521,7 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
|
|
|
314
521
|
// Every entity name nested at any depth under a declared aggregate.
|
|
315
522
|
fn nested_entity_names(aggregates_by_name: &HashMap<String, &Json>) -> Vec<String> {
|
|
316
523
|
fn collect(owner: &Json, out: &mut Vec<String>) {
|
|
317
|
-
for entity in owner.get("entities").
|
|
524
|
+
for entity in owner.get("entities").map_or_default(Json::each) {
|
|
318
525
|
out.push(entity.get("name").map(Json::to_s).unwrap_or_default());
|
|
319
526
|
collect(entity, out);
|
|
320
527
|
}
|
|
@@ -334,13 +541,20 @@ fn entity_head_skip_reason(aggregate_name: &str, role: &str, purpose: &str) -> S
|
|
|
334
541
|
)
|
|
335
542
|
}
|
|
336
543
|
|
|
337
|
-
fn read_model_head_skip_reason(
|
|
544
|
+
fn read_model_head_skip_reason(
|
|
545
|
+
head: &Json,
|
|
546
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
547
|
+
unsupported_names: &[String],
|
|
548
|
+
) -> Option<SkipReason> {
|
|
338
549
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
339
550
|
if !aggregates_by_name.contains_key(&aggregate_name) {
|
|
340
551
|
if nested_entity_names(aggregates_by_name).contains(&aggregate_name) {
|
|
341
552
|
return None;
|
|
342
553
|
}
|
|
343
|
-
return Some(skip(
|
|
554
|
+
return Some(skip(
|
|
555
|
+
"include_undeclared_aggregate",
|
|
556
|
+
format!("includes {aggregate_name}, which this domain never declares"),
|
|
557
|
+
));
|
|
344
558
|
}
|
|
345
559
|
if unsupported_names.contains(&aggregate_name) {
|
|
346
560
|
return Some(skip(
|
|
@@ -357,28 +571,51 @@ pub struct ReferenceField {
|
|
|
357
571
|
}
|
|
358
572
|
|
|
359
573
|
fn read_model_reference_fields(head_aggregate: &Json) -> Vec<ReferenceField> {
|
|
360
|
-
let attrs = head_aggregate.get("attributes").
|
|
574
|
+
let attrs = head_aggregate.get("attributes").map_or_default(Json::each);
|
|
361
575
|
attrs
|
|
362
576
|
.iter()
|
|
363
577
|
.filter_map(|attr| {
|
|
364
578
|
let target = crate::naming::reference_target(crate::attr::type_name(attr))?;
|
|
365
|
-
Some(ReferenceField {
|
|
579
|
+
Some(ReferenceField {
|
|
580
|
+
target: target.to_string(),
|
|
581
|
+
field: crate::attr::name(attr).to_string(),
|
|
582
|
+
})
|
|
366
583
|
})
|
|
367
584
|
.collect()
|
|
368
585
|
}
|
|
369
586
|
|
|
370
587
|
fn emit_reference_field(domain_name: &str, rf: &ReferenceField) -> String {
|
|
371
588
|
let qualified_target = format!("{domain_name}::{}", rf.target);
|
|
372
|
-
format!(
|
|
589
|
+
format!(
|
|
590
|
+
"crate::kernel::read_model::ReferenceField {{ target_aggregate: {}, field: {} }}",
|
|
591
|
+
crate::naming::ruby_inspect_string(&qualified_target),
|
|
592
|
+
crate::naming::ruby_inspect_string(&rf.field)
|
|
593
|
+
)
|
|
373
594
|
}
|
|
374
595
|
|
|
375
|
-
fn emit_read_model_head(
|
|
596
|
+
fn emit_read_model_head(
|
|
597
|
+
domain_name: &str,
|
|
598
|
+
head: &Json,
|
|
599
|
+
is_root: bool,
|
|
600
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
601
|
+
) -> String {
|
|
376
602
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
377
|
-
let reference_fields: Vec<ReferenceField> = if is_root {
|
|
378
|
-
|
|
603
|
+
let reference_fields: Vec<ReferenceField> = if is_root {
|
|
604
|
+
Vec::new()
|
|
605
|
+
} else {
|
|
606
|
+
aggregates_by_name
|
|
607
|
+
.get(&aggregate_name)
|
|
608
|
+
.map(|a| read_model_reference_fields(a))
|
|
609
|
+
.unwrap_or_default()
|
|
610
|
+
};
|
|
611
|
+
let reference_fields_expr = reference_fields
|
|
612
|
+
.iter()
|
|
613
|
+
.map(|rf| emit_reference_field(domain_name, rf))
|
|
614
|
+
.collect::<Vec<_>>()
|
|
615
|
+
.join(", ");
|
|
379
616
|
let qualified_aggregate = format!("{domain_name}::{aggregate_name}");
|
|
380
617
|
let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
381
|
-
let many = head.get("many").
|
|
618
|
+
let many = head.get("many").is_some_and(Json::as_bool);
|
|
382
619
|
|
|
383
620
|
format!(
|
|
384
621
|
"crate::kernel::read_model::ReadModelHead {{ aggregate: {}, as_name: {}, many: {many}, is_root: {is_root}, reference_fields: &[{reference_fields_expr}] }}",
|
|
@@ -388,7 +625,11 @@ fn emit_read_model_head(domain_name: &str, head: &Json, is_root: bool, aggregate
|
|
|
388
625
|
}
|
|
389
626
|
|
|
390
627
|
fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> String {
|
|
391
|
-
let descending = order_by
|
|
628
|
+
let descending = order_by
|
|
629
|
+
.get("direction")
|
|
630
|
+
.map(Json::to_s)
|
|
631
|
+
.unwrap_or_default()
|
|
632
|
+
== "desc";
|
|
392
633
|
format!(
|
|
393
634
|
"crate::kernel::read_model::ReadModelOrderBy {{ field: {}, descending: {descending}, nulls: {} }}",
|
|
394
635
|
crate::naming::ruby_inspect_string(&order_by.get("field").map(Json::to_s).unwrap_or_default()),
|
|
@@ -399,32 +640,24 @@ fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> S
|
|
|
399
640
|
fn emit_read_model_limit(limit: &Json) -> String {
|
|
400
641
|
let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
|
|
401
642
|
if let Some(arg) = raw.strip_prefix(':') {
|
|
402
|
-
return format!(
|
|
643
|
+
return format!(
|
|
644
|
+
"crate::kernel::read_model::ReadModelLimit::Arg({})",
|
|
645
|
+
crate::naming::ruby_inspect_string(arg)
|
|
646
|
+
);
|
|
403
647
|
}
|
|
404
|
-
format!(
|
|
648
|
+
format!(
|
|
649
|
+
"crate::kernel::read_model::ReadModelLimit::Literal({})",
|
|
650
|
+
ruby_to_i(&raw)
|
|
651
|
+
)
|
|
405
652
|
}
|
|
406
653
|
|
|
407
654
|
// `ReadModelOffset` is a type alias for `ReadModelLimit`'s own type, so this
|
|
408
655
|
// reuses `emit_read_model_limit`'s computation and swaps the type name.
|
|
409
656
|
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)
|
|
657
|
+
emit_read_model_limit(offset).replace(
|
|
658
|
+
"read_model::ReadModelLimit::",
|
|
659
|
+
"read_model::ReadModelOffset::",
|
|
660
|
+
)
|
|
428
661
|
}
|
|
429
662
|
|
|
430
663
|
/// One targeted many-side head's own where/order_by/offset/limit — the per-head analogue of
|
|
@@ -447,64 +680,155 @@ pub struct ReadModelDef {
|
|
|
447
680
|
pub authorization: Option<String>,
|
|
448
681
|
pub group_by_fn: Option<String>,
|
|
449
682
|
pub group_by_fn_body: Option<String>,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
pub
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
683
|
+
/// The kernel's `Reduction` variant this read model declares, as the expression the
|
|
684
|
+
/// generated table holds (`Sum("total")`); `None` when it declares none.
|
|
685
|
+
pub reduction: Option<String>,
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/// The reduction a read model's IR declares, in the kernel's own precedence: `count`, then the
|
|
689
|
+
/// `<word> :field` reductions.
|
|
690
|
+
#[allow(
|
|
691
|
+
clippy::panic,
|
|
692
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
693
|
+
)]
|
|
694
|
+
fn reduction_of(read_model: &Json) -> Option<String> {
|
|
695
|
+
if read_model.get("count").is_some() {
|
|
696
|
+
return Some("Count".to_string());
|
|
697
|
+
}
|
|
698
|
+
let field = |key: &str| {
|
|
699
|
+
read_model
|
|
700
|
+
.get(key)
|
|
701
|
+
.map(|v| crate::naming::ruby_inspect_string(&v.to_s()))
|
|
702
|
+
};
|
|
703
|
+
let single = [
|
|
704
|
+
("median_field", "Median"),
|
|
705
|
+
("sum_field", "Sum"),
|
|
706
|
+
("avg_field", "Avg"),
|
|
707
|
+
("min_field", "Min"),
|
|
708
|
+
("max_field", "Max"),
|
|
709
|
+
];
|
|
710
|
+
if let Some(found) = single
|
|
711
|
+
.iter()
|
|
712
|
+
.find_map(|(key, variant)| field(key).map(|f| format!("{variant}({f})")))
|
|
713
|
+
{
|
|
714
|
+
return Some(found);
|
|
715
|
+
}
|
|
716
|
+
if let Some(f) = field("percentile_field") {
|
|
717
|
+
let Some(at) = read_model.get("percentile_at").map(Json::to_s) else {
|
|
718
|
+
panic!("percentile_field {f} declares no percentile_at");
|
|
719
|
+
};
|
|
720
|
+
return Some(format!("Percentile {{ field: {f}, at: {at}_f64 }}"));
|
|
721
|
+
}
|
|
722
|
+
[("any_field", "Any"), ("all_field", "All")]
|
|
723
|
+
.iter()
|
|
724
|
+
.find_map(|(key, variant)| field(key).map(|f| format!("{variant}({f})")))
|
|
460
725
|
}
|
|
461
726
|
|
|
462
727
|
/// `local_wheres, hop_wheres` compiled for one targeted head, mirroring
|
|
463
728
|
/// `read_models.rb#filtered_head_def`.
|
|
464
|
-
fn filtered_head_def(
|
|
729
|
+
fn filtered_head_def(
|
|
730
|
+
domain_name: &str,
|
|
731
|
+
read_model: &Json,
|
|
732
|
+
head: &Json,
|
|
733
|
+
many_heads: &[&Json],
|
|
734
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
735
|
+
) -> FilteredHeadDef {
|
|
465
736
|
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
466
737
|
let aggregate = aggregates_by_name[&aggregate_name];
|
|
467
738
|
let head_query = read_model_head_query(read_model, head, many_heads);
|
|
468
739
|
let (local_wheres, hop_wheres): (Vec<&Json>, Vec<&Json>) =
|
|
469
|
-
head_query.wheres.iter().copied().partition(|w|
|
|
740
|
+
head_query.wheres.iter().copied().partition(|w| {
|
|
741
|
+
queries::query_hop_plan(
|
|
742
|
+
aggregate,
|
|
743
|
+
&w.get("field").map(Json::to_s).unwrap_or_default(),
|
|
744
|
+
aggregates_by_name,
|
|
745
|
+
)
|
|
746
|
+
.is_none()
|
|
747
|
+
});
|
|
470
748
|
let synthetic = with_wheres_and_authorization(&local_wheres, head_query.authorization);
|
|
471
|
-
let conditions = if head_query.authorization.is_some() {
|
|
749
|
+
let conditions = if head_query.authorization.is_some() {
|
|
750
|
+
queries::query_conditions_with_authorization(&synthetic)
|
|
751
|
+
} else {
|
|
752
|
+
queries::query_conditions(&synthetic)
|
|
753
|
+
};
|
|
472
754
|
|
|
473
755
|
FilteredHeadDef {
|
|
474
756
|
as_name: head.get("as").map(Json::to_s).unwrap_or_default(),
|
|
475
757
|
conditions,
|
|
476
|
-
reference_hop_conditions: queries::read_model_hop_conditions(
|
|
477
|
-
|
|
758
|
+
reference_hop_conditions: queries::read_model_hop_conditions(
|
|
759
|
+
domain_name,
|
|
760
|
+
&hop_wheres,
|
|
761
|
+
aggregate,
|
|
762
|
+
aggregates_by_name,
|
|
763
|
+
),
|
|
764
|
+
order_by: head_query
|
|
765
|
+
.order_by
|
|
766
|
+
.map(|ob| emit_read_model_order_by(ob, read_model.get("null_semantics"))),
|
|
478
767
|
offset: head_query.offset.map(emit_read_model_offset),
|
|
479
768
|
limit: head_query.limit.map(emit_read_model_limit),
|
|
480
769
|
}
|
|
481
770
|
}
|
|
482
771
|
|
|
483
|
-
pub fn read_model_def(
|
|
484
|
-
|
|
485
|
-
|
|
772
|
+
pub fn read_model_def(
|
|
773
|
+
domain_name: &str,
|
|
774
|
+
read_model: &Json,
|
|
775
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
776
|
+
) -> ReadModelDef {
|
|
777
|
+
let reference_target = read_model
|
|
778
|
+
.get("reference_target")
|
|
779
|
+
.map(Json::to_s)
|
|
780
|
+
.unwrap_or_default();
|
|
781
|
+
let heads_json = read_model.get("aggregate_heads").map_or_default(Json::each);
|
|
486
782
|
let heads: Vec<String> = heads_json
|
|
487
783
|
.iter()
|
|
488
784
|
.map(|head| {
|
|
489
|
-
let is_root =
|
|
785
|
+
let is_root =
|
|
786
|
+
head.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target;
|
|
490
787
|
emit_read_model_head(domain_name, head, is_root, aggregates_by_name)
|
|
491
788
|
})
|
|
492
789
|
.collect();
|
|
493
790
|
|
|
494
791
|
let many_heads = read_model_many_heads(read_model);
|
|
495
|
-
let filtered_heads: Vec<FilteredHeadDef> =
|
|
496
|
-
|
|
792
|
+
let filtered_heads: Vec<FilteredHeadDef> = read_model_filtered_heads(read_model, &many_heads)
|
|
793
|
+
.into_iter()
|
|
794
|
+
.map(|head| {
|
|
795
|
+
filtered_head_def(
|
|
796
|
+
domain_name,
|
|
797
|
+
read_model,
|
|
798
|
+
head,
|
|
799
|
+
&many_heads,
|
|
800
|
+
aggregates_by_name,
|
|
801
|
+
)
|
|
802
|
+
})
|
|
803
|
+
.collect();
|
|
497
804
|
let sole_head_as: bool = many_heads.len() == 1;
|
|
498
805
|
|
|
499
806
|
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
|
-
|
|
807
|
+
let group_by_fields: Vec<String> = read_model
|
|
808
|
+
.get("group_by")
|
|
809
|
+
.map_or_default(Json::each)
|
|
810
|
+
.iter()
|
|
811
|
+
.map(|row| row.get("field").map(Json::to_s).unwrap_or_default())
|
|
812
|
+
.collect();
|
|
813
|
+
let (group_by_fn, group_by_fn_body) = if group_by_fields.is_empty() {
|
|
814
|
+
(None, None)
|
|
815
|
+
} else {
|
|
502
816
|
let fn_name = format!("group_by_{}", read_model_name.to_lowercase());
|
|
503
|
-
let aggregate_name = heads_json
|
|
817
|
+
let aggregate_name = heads_json
|
|
818
|
+
.first()
|
|
819
|
+
.and_then(|h| h.get("aggregate"))
|
|
820
|
+
.map(Json::to_s)
|
|
821
|
+
.unwrap_or_default();
|
|
504
822
|
let aggregate = aggregates_by_name[&aggregate_name];
|
|
505
|
-
(
|
|
506
|
-
|
|
507
|
-
|
|
823
|
+
(
|
|
824
|
+
Some(fn_name.clone()),
|
|
825
|
+
Some(emit_group_by_transform(
|
|
826
|
+
&fn_name,
|
|
827
|
+
&read_model_name,
|
|
828
|
+
aggregate,
|
|
829
|
+
&group_by_fields,
|
|
830
|
+
)),
|
|
831
|
+
)
|
|
508
832
|
};
|
|
509
833
|
|
|
510
834
|
ReadModelDef {
|
|
@@ -512,19 +836,14 @@ pub fn read_model_def(domain_name: &str, read_model: &Json, aggregates_by_name:
|
|
|
512
836
|
reference_name: read_model.get("reference_name").map(Json::to_s),
|
|
513
837
|
heads,
|
|
514
838
|
filtered_heads,
|
|
515
|
-
authorization: if sole_head_as {
|
|
839
|
+
authorization: if sole_head_as {
|
|
840
|
+
queries::emit_query_authorization(&read_model_name, read_model.get("authorization"))
|
|
841
|
+
} else {
|
|
842
|
+
None
|
|
843
|
+
},
|
|
516
844
|
group_by_fn,
|
|
517
845
|
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),
|
|
846
|
+
reduction: reduction_of(read_model),
|
|
528
847
|
}
|
|
529
848
|
}
|
|
530
849
|
|
|
@@ -541,40 +860,37 @@ fn with_wheres_and_authorization(wheres: &[&Json], authorization: Option<&Json>)
|
|
|
541
860
|
}
|
|
542
861
|
|
|
543
862
|
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);
|
|
863
|
+
let heads = rmd
|
|
864
|
+
.heads
|
|
865
|
+
.iter()
|
|
866
|
+
.map(|h| format!(" {h},"))
|
|
867
|
+
.collect::<Vec<_>>()
|
|
868
|
+
.join("\n");
|
|
869
|
+
let filtered_heads = rmd
|
|
870
|
+
.filtered_heads
|
|
871
|
+
.iter()
|
|
872
|
+
.map(|fh| format!(" {}", emit_filtered_head(fh)))
|
|
873
|
+
.collect::<Vec<_>>()
|
|
874
|
+
.join("\n");
|
|
875
|
+
let authorization = rmd
|
|
876
|
+
.authorization
|
|
877
|
+
.as_ref()
|
|
878
|
+
.map_or_else(|| "None".to_string(), |a| format!("Some({a})"));
|
|
879
|
+
let reference_name = rmd.reference_name.as_ref().map_or_else(
|
|
880
|
+
|| "None".to_string(),
|
|
881
|
+
|r| format!("Some({})", crate::naming::ruby_inspect_string(r)),
|
|
882
|
+
);
|
|
883
|
+
let group_by = rmd
|
|
884
|
+
.group_by_fn
|
|
885
|
+
.as_ref()
|
|
886
|
+
.map_or_else(|| "None".to_string(), |f| format!("Some({f})"));
|
|
887
|
+
let reduction = rmd.reduction.as_ref().map_or_else(
|
|
888
|
+
|| "None".to_string(),
|
|
889
|
+
|r| format!("Some(crate::kernel::read_model::Reduction::{r})"),
|
|
890
|
+
);
|
|
575
891
|
|
|
576
892
|
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
|
|
893
|
+
"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
894
|
crate::naming::ruby_inspect_string(&rmd.verb)
|
|
579
895
|
)
|
|
580
896
|
}
|
|
@@ -584,20 +900,30 @@ pub fn emit_read_model_def(rmd: &ReadModelDef) -> String {
|
|
|
584
900
|
/// `ReferenceHopCondition`, `QueryCondition` are all single-line too; only the top-level
|
|
585
901
|
/// `ReadModelDef`/`QueryDef` get pretty multi-line formatting with their arrays broken apart).
|
|
586
902
|
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
|
-
|
|
903
|
+
let conditions = fh
|
|
904
|
+
.conditions
|
|
905
|
+
.iter()
|
|
906
|
+
.map(queries::emit_query_condition)
|
|
907
|
+
.collect::<Vec<_>>()
|
|
908
|
+
.join(" ");
|
|
909
|
+
let reference_hop_conditions = fh
|
|
910
|
+
.reference_hop_conditions
|
|
911
|
+
.iter()
|
|
912
|
+
.map(queries::emit_reference_hop_condition)
|
|
913
|
+
.collect::<Vec<_>>()
|
|
914
|
+
.join(" ");
|
|
915
|
+
let order_by = fh
|
|
916
|
+
.order_by
|
|
917
|
+
.as_ref()
|
|
918
|
+
.map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
|
|
919
|
+
let offset = fh
|
|
920
|
+
.offset
|
|
921
|
+
.as_ref()
|
|
922
|
+
.map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
|
|
923
|
+
let limit = fh
|
|
924
|
+
.limit
|
|
925
|
+
.as_ref()
|
|
926
|
+
.map_or_else(|| "None".to_string(), |l| format!("Some({l})"));
|
|
601
927
|
|
|
602
928
|
format!(
|
|
603
929
|
"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 +934,68 @@ fn emit_filtered_head(fh: &FilteredHeadDef) -> String {
|
|
|
608
934
|
// Keeps this aggregate's declared attributes plus any `projects` field, id
|
|
609
935
|
// and lifecycle field, excluding other capabilities' synthetic fields (like
|
|
610
936
|
// `corrects`'s `emitted_*` flags), and unwraps value objects recursively.
|
|
611
|
-
fn emit_group_by_transform(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
937
|
+
fn emit_group_by_transform(
|
|
938
|
+
fn_name: &str,
|
|
939
|
+
read_model_name: &str,
|
|
940
|
+
aggregate: &Json,
|
|
941
|
+
group_by_fields: &[String],
|
|
942
|
+
) -> String {
|
|
943
|
+
let value_objects: Vec<&Json> = aggregate
|
|
944
|
+
.get("value_objects")
|
|
945
|
+
.map_or_default(Json::each)
|
|
946
|
+
.iter()
|
|
947
|
+
.collect();
|
|
948
|
+
let value_objects_by_name: HashMap<String, &Json> = value_objects
|
|
949
|
+
.iter()
|
|
950
|
+
.map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), *vo))
|
|
951
|
+
.collect();
|
|
952
|
+
let lifecycle_field = aggregate
|
|
953
|
+
.get("lifecycle")
|
|
954
|
+
.and_then(|l| l.get("field"))
|
|
955
|
+
.map(Json::to_s);
|
|
956
|
+
let mut fields: Vec<Json> = aggregate
|
|
957
|
+
.get("attributes")
|
|
958
|
+
.map_or_default(Json::each)
|
|
959
|
+
.to_vec();
|
|
616
960
|
fields.extend(crate::types::projected_field_pseudo_attributes(aggregate));
|
|
617
961
|
|
|
618
|
-
let mut kept_keys: Vec<String> = fields
|
|
962
|
+
let mut kept_keys: Vec<String> = fields
|
|
963
|
+
.iter()
|
|
964
|
+
.map(|a| crate::attr::name(a).to_string())
|
|
965
|
+
.collect();
|
|
619
966
|
kept_keys.push("id".to_string());
|
|
620
967
|
if let Some(lf) = &lifecycle_field {
|
|
621
968
|
kept_keys.push(lf.clone());
|
|
622
969
|
}
|
|
623
|
-
let keep_cond = kept_keys
|
|
970
|
+
let keep_cond = kept_keys
|
|
971
|
+
.iter()
|
|
972
|
+
.map(|k| format!("k == {k:?}"))
|
|
973
|
+
.collect::<Vec<_>>()
|
|
974
|
+
.join(" || ");
|
|
624
975
|
|
|
625
976
|
let arms: Vec<String> = fields
|
|
626
977
|
.iter()
|
|
627
978
|
.map(|a| {
|
|
628
|
-
let unwrapped = unwrap_json_expr(
|
|
979
|
+
let unwrapped = unwrap_json_expr(
|
|
980
|
+
"v",
|
|
981
|
+
crate::attr::type_name(a),
|
|
982
|
+
crate::attr::list(a),
|
|
983
|
+
aggregate,
|
|
984
|
+
&value_objects_by_name,
|
|
985
|
+
);
|
|
629
986
|
format!("{:?} => {unwrapped},", crate::attr::name(a))
|
|
630
987
|
})
|
|
631
988
|
.collect();
|
|
632
989
|
let arms = arms.join("\n ");
|
|
633
990
|
|
|
634
|
-
let fields_literal = format!(
|
|
991
|
+
let fields_literal = format!(
|
|
992
|
+
"&[{}]",
|
|
993
|
+
group_by_fields
|
|
994
|
+
.iter()
|
|
995
|
+
.map(|f| format!("{f:?}"))
|
|
996
|
+
.collect::<Vec<_>>()
|
|
997
|
+
.join(", ")
|
|
998
|
+
);
|
|
635
999
|
let leaf_check = group_by_leaf_check(read_model_name, aggregate, group_by_fields);
|
|
636
1000
|
|
|
637
1001
|
format!(
|
|
@@ -641,10 +1005,19 @@ fn emit_group_by_transform(fn_name: &str, read_model_name: &str, aggregate: &Jso
|
|
|
641
1005
|
|
|
642
1006
|
// A key path naming every identity head cannot collide, so its leaves go
|
|
643
1007
|
// unchecked; any other refuses a second row (ADR 0061).
|
|
644
|
-
fn group_by_leaf_check(
|
|
1008
|
+
fn group_by_leaf_check(
|
|
1009
|
+
read_model_name: &str,
|
|
1010
|
+
aggregate: &Json,
|
|
1011
|
+
group_by_fields: &[String],
|
|
1012
|
+
) -> String {
|
|
645
1013
|
let mut identity: Vec<String> = Vec::new();
|
|
646
|
-
for path in aggregate.get("identified_by").
|
|
647
|
-
let head = path
|
|
1014
|
+
for path in aggregate.get("identified_by").map_or_default(Json::each) {
|
|
1015
|
+
let head = path
|
|
1016
|
+
.to_s()
|
|
1017
|
+
.split('.')
|
|
1018
|
+
.next()
|
|
1019
|
+
.unwrap_or_default()
|
|
1020
|
+
.to_string();
|
|
648
1021
|
if !identity.contains(&head) {
|
|
649
1022
|
identity.push(head);
|
|
650
1023
|
}
|
|
@@ -652,46 +1025,18 @@ fn group_by_leaf_check(read_model_name: &str, aggregate: &Json, group_by_fields:
|
|
|
652
1025
|
if !identity.is_empty() && identity.iter().all(|head| group_by_fields.contains(head)) {
|
|
653
1026
|
return "crate::kernel::read_model::LeafCheck::IdentityCovered".to_string();
|
|
654
1027
|
}
|
|
655
|
-
format!(
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
fn unwrap_json_expr(expr: &str, type_name: &str, list: bool, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
|
|
660
|
-
if list {
|
|
661
|
-
let inner = unwrap_json_expr("item", type_name, false, aggregate, value_objects_by_name);
|
|
662
|
-
return format!("match {expr} {{ crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| {inner}).collect()), other => other }}");
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
let vo = value_objects_by_name.get(type_name).copied();
|
|
666
|
-
let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == type_name);
|
|
667
|
-
let fields_meta: Option<&[Json]> = if let Some(vo) = vo {
|
|
668
|
-
vo.get("attributes").map(Json::each)
|
|
669
|
-
} else {
|
|
670
|
-
entity.and_then(|e| e.get("attributes").map(Json::each))
|
|
671
|
-
};
|
|
672
|
-
let Some(fields_meta) = fields_meta else { return expr.to_string() };
|
|
673
|
-
|
|
674
|
-
if vo.is_some() && fields_meta.len() == 1 {
|
|
675
|
-
let field = &fields_meta[0];
|
|
676
|
-
let unwrapped = unwrap_json_expr("field_value", crate::attr::type_name(field), crate::attr::list(field), aggregate, value_objects_by_name);
|
|
677
|
-
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:?}).map(|(_, field_value)| {unwrapped}).unwrap_or(crate::kernel::Json::Null), other => other }}");
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
let inner_arms: Vec<String> = fields_meta
|
|
682
|
-
.iter()
|
|
683
|
-
.map(|f| {
|
|
684
|
-
let unwrapped = unwrap_json_expr("v", crate::attr::type_name(f), crate::attr::list(f), aggregate, value_objects_by_name);
|
|
685
|
-
format!("{:?} => {unwrapped},", crate::attr::name(f))
|
|
686
|
-
})
|
|
687
|
-
.collect();
|
|
688
|
-
let inner_arms = inner_arms.join(" ");
|
|
689
|
-
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 }}")
|
|
1028
|
+
format!(
|
|
1029
|
+
"crate::kernel::read_model::LeafCheck::RefuseCollision({})",
|
|
1030
|
+
crate::naming::ruby_inspect_string(read_model_name)
|
|
1031
|
+
)
|
|
690
1032
|
}
|
|
691
1033
|
|
|
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
|
|
1034
|
+
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
1035
|
|
|
694
1036
|
pub fn emit_read_model_table(exemplar: &Exemplar, read_model_defs: &[ReadModelDef]) -> String {
|
|
695
1037
|
let rows: Vec<String> = read_model_defs.iter().map(emit_read_model_def).collect();
|
|
696
|
-
exemplar.render(
|
|
1038
|
+
exemplar.render(
|
|
1039
|
+
"read_model_table",
|
|
1040
|
+
&[(READ_MODEL_TABLE_ROW_PLACEHOLDER, rows.join("\n"))],
|
|
1041
|
+
)
|
|
697
1042
|
}
|