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
data/rust/codegen/src/queries.rs
CHANGED
|
@@ -4,8 +4,10 @@ use crate::exemplar::Exemplar;
|
|
|
4
4
|
use crate::json::Json;
|
|
5
5
|
use crate::literal::{self, Literal};
|
|
6
6
|
use crate::naming;
|
|
7
|
+
use crate::shared::ruby_to_i;
|
|
7
8
|
use crate::skip_reason::{reskip, skip, SkipReason};
|
|
8
9
|
use std::collections::HashMap;
|
|
10
|
+
use std::fmt::Write as _;
|
|
9
11
|
|
|
10
12
|
const COMPARATORS_NEEDING_NUMERIC_FIDELITY: &[&str] = &["gt", "gte", "lt", "lte"];
|
|
11
13
|
const COMPARATORS_EXEMPT_FROM_LITERAL_TYPING: &[&str] = &["in", "contains"];
|
|
@@ -18,160 +20,198 @@ pub enum FieldKind {
|
|
|
18
20
|
Unknown,
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/// Where a dotted field path ends.
|
|
24
|
+
enum PathEnd<'a> {
|
|
25
|
+
/// The path lands on a member of this type.
|
|
26
|
+
Type(&'a str),
|
|
27
|
+
/// The path's head is a list attribute.
|
|
28
|
+
HeadList,
|
|
29
|
+
/// The path names nothing: an undeclared attribute or member, a list member, or a step
|
|
30
|
+
/// through a type with no members (a scalar, or a reference, which is never a value object).
|
|
31
|
+
Broken,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Walks `field` from `aggregate`: its head names an attribute, and any later segments step
|
|
35
|
+
/// through nested value objects. The one walk the kind, boolean and numeric classifiers share;
|
|
36
|
+
/// each then judges the type it lands on.
|
|
37
|
+
fn path_end<'a>(
|
|
38
|
+
aggregate: &'a Json,
|
|
39
|
+
field: &str,
|
|
40
|
+
value_objects_by_name: &HashMap<String, &'a Json>,
|
|
41
|
+
) -> PathEnd<'a> {
|
|
42
|
+
let mut segments = field.split('.');
|
|
43
|
+
let head = segments.next().unwrap_or("");
|
|
44
|
+
let attrs = aggregate.get("attributes").map_or_default(Json::each);
|
|
45
|
+
let Some(attr) = attrs.iter().find(|a| crate::attr::name(a) == head) else {
|
|
46
|
+
return PathEnd::Broken;
|
|
47
|
+
};
|
|
48
|
+
if crate::attr::list(attr) {
|
|
49
|
+
return PathEnd::HeadList;
|
|
50
|
+
}
|
|
51
|
+
let mut type_name = crate::attr::type_name(attr);
|
|
52
|
+
for segment in segments {
|
|
53
|
+
let Some(vo) = value_objects_by_name.get(type_name) else {
|
|
54
|
+
return PathEnd::Broken;
|
|
55
|
+
};
|
|
56
|
+
let members = vo.get("attributes").map_or_default(Json::each);
|
|
57
|
+
let Some(member) = members.iter().find(|a| crate::attr::name(a) == segment) else {
|
|
58
|
+
return PathEnd::Broken;
|
|
59
|
+
};
|
|
60
|
+
if crate::attr::list(member) {
|
|
61
|
+
return PathEnd::Broken;
|
|
62
|
+
}
|
|
63
|
+
type_name = crate::attr::type_name(member);
|
|
64
|
+
}
|
|
65
|
+
PathEnd::Type(type_name)
|
|
66
|
+
}
|
|
67
|
+
|
|
21
68
|
/// Classifies `field`: its head names an attribute or the lifecycle field, and any
|
|
22
69
|
/// later segments walk nested value objects.
|
|
23
|
-
pub fn query_field_kind(
|
|
70
|
+
pub fn query_field_kind(
|
|
71
|
+
aggregate: &Json,
|
|
72
|
+
field: &str,
|
|
73
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
74
|
+
) -> FieldKind {
|
|
24
75
|
let mut segments = field.split('.');
|
|
25
76
|
let head = segments.next().unwrap_or("");
|
|
26
|
-
let rest: Vec<&str> = segments.collect();
|
|
27
77
|
|
|
28
|
-
let lifecycle_field = aggregate
|
|
78
|
+
let lifecycle_field = aggregate
|
|
79
|
+
.get("lifecycle")
|
|
80
|
+
.and_then(|l| l.get("field"))
|
|
81
|
+
.map(Json::to_s);
|
|
29
82
|
if let Some(lf) = &lifecycle_field {
|
|
30
83
|
if lf == head {
|
|
31
|
-
return if
|
|
84
|
+
return if segments.next().is_none() {
|
|
85
|
+
FieldKind::String
|
|
86
|
+
} else {
|
|
87
|
+
FieldKind::Unknown
|
|
88
|
+
};
|
|
32
89
|
}
|
|
33
90
|
}
|
|
34
91
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
92
|
+
match path_end(aggregate, field, value_objects_by_name) {
|
|
93
|
+
PathEnd::HeadList => FieldKind::Other,
|
|
94
|
+
PathEnd::Broken => FieldKind::Unknown,
|
|
95
|
+
PathEnd::Type(type_name) => type_kind(type_name, value_objects_by_name),
|
|
39
96
|
}
|
|
40
|
-
|
|
41
|
-
query_type_kind(crate::attr::type_name(attr), &rest, value_objects_by_name)
|
|
42
97
|
}
|
|
43
98
|
|
|
44
|
-
fn
|
|
99
|
+
fn type_kind(type_name: &str, value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
|
|
45
100
|
if naming::reference_type(type_name) {
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if segments.is_empty() {
|
|
50
|
-
return query_scalar_or_vo_kind(type_name, value_objects_by_name);
|
|
101
|
+
return FieldKind::String;
|
|
51
102
|
}
|
|
52
|
-
|
|
53
|
-
let Some(vo) = value_objects_by_name.get(type_name) else { return FieldKind::Unknown };
|
|
54
|
-
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
55
|
-
let Some(member) = attrs.iter().find(|a| crate::attr::name(a) == segments[0]) else { return FieldKind::Unknown };
|
|
56
|
-
if crate::attr::list(member) {
|
|
57
|
-
return FieldKind::Unknown;
|
|
58
|
-
}
|
|
59
|
-
query_type_kind(crate::attr::type_name(member), &segments[1..], value_objects_by_name)
|
|
103
|
+
query_scalar_or_vo_kind(type_name, value_objects_by_name)
|
|
60
104
|
}
|
|
61
105
|
|
|
62
|
-
fn query_scalar_or_vo_kind(
|
|
106
|
+
fn query_scalar_or_vo_kind(
|
|
107
|
+
type_name: &str,
|
|
108
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
109
|
+
) -> FieldKind {
|
|
63
110
|
match type_name {
|
|
64
111
|
"String" => FieldKind::String,
|
|
65
112
|
"Integer" | "Float" => FieldKind::Number,
|
|
66
113
|
"TrueClass" | "FalseClass" => FieldKind::Other,
|
|
67
|
-
_ =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
114
|
+
_ => value_objects_by_name
|
|
115
|
+
.get(type_name)
|
|
116
|
+
.map_or(FieldKind::Unknown, |vo| {
|
|
117
|
+
query_vo_collapse_kind(vo, value_objects_by_name)
|
|
118
|
+
}),
|
|
71
119
|
}
|
|
72
120
|
}
|
|
73
121
|
|
|
74
122
|
fn query_vo_collapse_kind(vo: &Json, value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
|
|
75
|
-
let attrs = vo.get("attributes").
|
|
76
|
-
if attrs
|
|
123
|
+
let attrs = vo.get("attributes").map_or_default(Json::each);
|
|
124
|
+
if attrs
|
|
125
|
+
.iter()
|
|
126
|
+
.any(|a| matches!(crate::attr::type_name(a), "Integer" | "Float"))
|
|
127
|
+
{
|
|
77
128
|
return FieldKind::Number;
|
|
78
129
|
}
|
|
79
130
|
if attrs.len() == 1 {
|
|
80
|
-
return
|
|
131
|
+
return type_kind(crate::attr::type_name(&attrs[0]), value_objects_by_name);
|
|
81
132
|
}
|
|
82
133
|
FieldKind::Other
|
|
83
134
|
}
|
|
84
135
|
|
|
85
136
|
/// Whether a field (or dotted path) reduces specifically to a `TrueClass`/`FalseClass` —
|
|
86
137
|
/// `any`/`all` need this, distinct from `query_field_kind`'s coarser `Other` bucket (ADR 0078).
|
|
87
|
-
pub fn query_field_boolean(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return false;
|
|
138
|
+
pub fn query_field_boolean(
|
|
139
|
+
aggregate: &Json,
|
|
140
|
+
field: &str,
|
|
141
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
142
|
+
) -> bool {
|
|
143
|
+
match path_end(aggregate, field, value_objects_by_name) {
|
|
144
|
+
PathEnd::Type(type_name) => type_boolean(type_name, value_objects_by_name),
|
|
145
|
+
PathEnd::HeadList | PathEnd::Broken => false,
|
|
96
146
|
}
|
|
97
|
-
query_type_boolean(crate::attr::type_name(attr), &rest, value_objects_by_name)
|
|
98
147
|
}
|
|
99
148
|
|
|
100
|
-
fn
|
|
101
|
-
if
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
let Some(vo) = value_objects_by_name.get(type_name) else { return false };
|
|
109
|
-
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
110
|
-
if attrs.len() == 1 {
|
|
111
|
-
return query_type_boolean(crate::attr::type_name(&attrs[0]), &[], value_objects_by_name);
|
|
112
|
-
}
|
|
113
|
-
false
|
|
114
|
-
} else {
|
|
115
|
-
let Some(vo) = value_objects_by_name.get(type_name) else { return false };
|
|
116
|
-
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
117
|
-
let Some(member) = attrs.iter().find(|a| crate::attr::name(a) == segments[0]) else { return false };
|
|
118
|
-
if crate::attr::list(member) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
query_type_boolean(crate::attr::type_name(member), &segments[1..], value_objects_by_name)
|
|
149
|
+
fn type_boolean(type_name: &str, value_objects_by_name: &HashMap<String, &Json>) -> bool {
|
|
150
|
+
if matches!(type_name, "TrueClass" | "FalseClass") {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if naming::reference_type(type_name) {
|
|
154
|
+
return false;
|
|
122
155
|
}
|
|
156
|
+
let Some(vo) = value_objects_by_name.get(type_name) else {
|
|
157
|
+
return false;
|
|
158
|
+
};
|
|
159
|
+
let attrs = vo.get("attributes").map_or_default(Json::each);
|
|
160
|
+
attrs.len() == 1 && type_boolean(crate::attr::type_name(&attrs[0]), value_objects_by_name)
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
/// The winning member's own type name for a numeric field ("the sole numeric member wins",
|
|
126
164
|
/// mirroring `query_vo_collapse_kind`) — "Integer" or "Float" distinctly, where
|
|
127
165
|
/// `query_field_kind`'s own `Number` collapses both. `sum`/`avg` need the distinction
|
|
128
166
|
/// (ADR 0078).
|
|
129
|
-
pub fn query_field_numeric_type(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return None;
|
|
167
|
+
pub fn query_field_numeric_type(
|
|
168
|
+
aggregate: &Json,
|
|
169
|
+
field: &str,
|
|
170
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
171
|
+
) -> Option<String> {
|
|
172
|
+
match path_end(aggregate, field, value_objects_by_name) {
|
|
173
|
+
PathEnd::Type(type_name) => type_numeric(type_name, value_objects_by_name),
|
|
174
|
+
PathEnd::HeadList | PathEnd::Broken => None,
|
|
138
175
|
}
|
|
139
|
-
query_type_numeric_type(crate::attr::type_name(attr), &rest, value_objects_by_name)
|
|
140
176
|
}
|
|
141
177
|
|
|
142
|
-
fn
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} else {
|
|
160
|
-
let vo = value_objects_by_name.get(type_name)?;
|
|
161
|
-
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
162
|
-
let member = attrs.iter().find(|a| crate::attr::name(a) == segments[0])?;
|
|
163
|
-
if crate::attr::list(member) {
|
|
164
|
-
return None;
|
|
165
|
-
}
|
|
166
|
-
query_type_numeric_type(crate::attr::type_name(member), &segments[1..], value_objects_by_name)
|
|
178
|
+
fn type_numeric(type_name: &str, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
|
|
179
|
+
if matches!(type_name, "Integer" | "Float") {
|
|
180
|
+
return Some(type_name.to_string());
|
|
181
|
+
}
|
|
182
|
+
if naming::reference_type(type_name) {
|
|
183
|
+
return None;
|
|
184
|
+
}
|
|
185
|
+
let vo = value_objects_by_name.get(type_name)?;
|
|
186
|
+
let attrs = vo.get("attributes").map_or_default(Json::each);
|
|
187
|
+
if let Some(numeric_member) = attrs
|
|
188
|
+
.iter()
|
|
189
|
+
.find(|a| matches!(crate::attr::type_name(a), "Integer" | "Float"))
|
|
190
|
+
{
|
|
191
|
+
return Some(crate::attr::type_name(numeric_member).to_string());
|
|
192
|
+
}
|
|
193
|
+
if attrs.len() == 1 {
|
|
194
|
+
return type_numeric(crate::attr::type_name(&attrs[0]), value_objects_by_name);
|
|
167
195
|
}
|
|
196
|
+
None
|
|
168
197
|
}
|
|
169
198
|
|
|
170
|
-
pub fn query_where_skip_reason(
|
|
171
|
-
|
|
199
|
+
pub fn query_where_skip_reason(
|
|
200
|
+
where_clause: &Json,
|
|
201
|
+
aggregate: &Json,
|
|
202
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
203
|
+
) -> Option<SkipReason> {
|
|
204
|
+
let field = where_clause
|
|
205
|
+
.get("field")
|
|
206
|
+
.map(Json::to_s)
|
|
207
|
+
.unwrap_or_default();
|
|
172
208
|
let kind = query_field_kind(aggregate, &field, value_objects_by_name);
|
|
173
209
|
if kind == FieldKind::Unknown {
|
|
174
|
-
let construct = if field.contains('/') {
|
|
210
|
+
let construct = if field.contains('/') {
|
|
211
|
+
"reference_hop_where"
|
|
212
|
+
} else {
|
|
213
|
+
"where_unrecognized_field"
|
|
214
|
+
};
|
|
175
215
|
return Some(skip(
|
|
176
216
|
construct,
|
|
177
217
|
format!(
|
|
@@ -181,7 +221,10 @@ pub fn query_where_skip_reason(where_clause: &Json, aggregate: &Json, value_obje
|
|
|
181
221
|
));
|
|
182
222
|
}
|
|
183
223
|
|
|
184
|
-
let raw_value = where_clause
|
|
224
|
+
let raw_value = where_clause
|
|
225
|
+
.get("value")
|
|
226
|
+
.map(Json::to_s)
|
|
227
|
+
.unwrap_or_default();
|
|
185
228
|
if raw_value.starts_with(':') {
|
|
186
229
|
return None;
|
|
187
230
|
}
|
|
@@ -232,7 +275,7 @@ pub fn query_where_skip_reason(where_clause: &Json, aggregate: &Json, value_obje
|
|
|
232
275
|
))
|
|
233
276
|
}
|
|
234
277
|
|
|
235
|
-
fn kind_name(kind: FieldKind) -> &'static str {
|
|
278
|
+
const fn kind_name(kind: FieldKind) -> &'static str {
|
|
236
279
|
match kind {
|
|
237
280
|
FieldKind::String => "string",
|
|
238
281
|
FieldKind::Number => "number",
|
|
@@ -254,7 +297,11 @@ pub struct HopPlan<'a> {
|
|
|
254
297
|
// Matches `HopPath::MAX_HOPS`.
|
|
255
298
|
const HOP_CHAIN_LIMIT: usize = 8;
|
|
256
299
|
|
|
257
|
-
pub fn query_hop_plan<'a>(
|
|
300
|
+
pub fn query_hop_plan<'a>(
|
|
301
|
+
aggregate: &'a Json,
|
|
302
|
+
field: &str,
|
|
303
|
+
aggregates_by_name: &HashMap<String, &'a Json>,
|
|
304
|
+
) -> Option<HopPlan<'a>> {
|
|
258
305
|
let segments: Vec<&str> = field.split('/').collect();
|
|
259
306
|
if segments.len() < 2 || segments.len() - 1 > HOP_CHAIN_LIMIT {
|
|
260
307
|
return None;
|
|
@@ -262,7 +309,11 @@ pub fn query_hop_plan<'a>(aggregate: &'a Json, field: &str, aggregates_by_name:
|
|
|
262
309
|
let mut steps: Vec<(String, String)> = Vec::new();
|
|
263
310
|
let mut current: &'a Json = aggregate;
|
|
264
311
|
for segment in &segments[..segments.len() - 1] {
|
|
265
|
-
let via = current
|
|
312
|
+
let via = current
|
|
313
|
+
.get("attributes")
|
|
314
|
+
.map_or_default(Json::each)
|
|
315
|
+
.iter()
|
|
316
|
+
.find(|a| crate::attr::name(a) == *segment)?;
|
|
266
317
|
let type_name = crate::attr::type_name(via);
|
|
267
318
|
if !naming::reference_type(type_name) {
|
|
268
319
|
return None;
|
|
@@ -273,7 +324,13 @@ pub fn query_hop_plan<'a>(aggregate: &'a Json, field: &str, aggregates_by_name:
|
|
|
273
324
|
current = target;
|
|
274
325
|
}
|
|
275
326
|
let (via_field, target_aggregate) = steps.remove(0);
|
|
276
|
-
Some(HopPlan {
|
|
327
|
+
Some(HopPlan {
|
|
328
|
+
via_field,
|
|
329
|
+
target_aggregate,
|
|
330
|
+
through: steps,
|
|
331
|
+
target: current,
|
|
332
|
+
inner_field: segments[segments.len() - 1].to_string(),
|
|
333
|
+
})
|
|
277
334
|
}
|
|
278
335
|
|
|
279
336
|
pub fn with_field(where_clause: &Json, field: &str) -> Json {
|
|
@@ -281,7 +338,13 @@ pub fn with_field(where_clause: &Json, field: &str) -> Json {
|
|
|
281
338
|
Json::Object(pairs) => Json::Object(
|
|
282
339
|
pairs
|
|
283
340
|
.iter()
|
|
284
|
-
.map(|(key, value)|
|
|
341
|
+
.map(|(key, value)| {
|
|
342
|
+
if key == "field" {
|
|
343
|
+
(key.clone(), Json::String(field.to_string()))
|
|
344
|
+
} else {
|
|
345
|
+
(key.clone(), value.clone())
|
|
346
|
+
}
|
|
347
|
+
})
|
|
285
348
|
.collect(),
|
|
286
349
|
),
|
|
287
350
|
other => other.clone(),
|
|
@@ -291,34 +354,66 @@ pub fn with_field(where_clause: &Json, field: &str) -> Json {
|
|
|
291
354
|
pub fn value_objects_of(aggregate: &Json) -> HashMap<String, &Json> {
|
|
292
355
|
aggregate
|
|
293
356
|
.get("value_objects")
|
|
294
|
-
.
|
|
295
|
-
.unwrap_or(&[])
|
|
357
|
+
.map_or_default(Json::each)
|
|
296
358
|
.iter()
|
|
297
|
-
.map(|vo|
|
|
359
|
+
.map(|vo| {
|
|
360
|
+
(
|
|
361
|
+
vo.get("name")
|
|
362
|
+
.and_then(Json::as_str)
|
|
363
|
+
.unwrap_or("")
|
|
364
|
+
.to_string(),
|
|
365
|
+
vo,
|
|
366
|
+
)
|
|
367
|
+
})
|
|
298
368
|
.collect()
|
|
299
369
|
}
|
|
300
370
|
|
|
301
|
-
pub fn query_skip_reason(
|
|
371
|
+
pub fn query_skip_reason(
|
|
372
|
+
query: &Json,
|
|
373
|
+
aggregate: &Json,
|
|
374
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
375
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
376
|
+
) -> Option<SkipReason> {
|
|
302
377
|
let extra_keys = ["cursor", "consistency", "freshness", "inspection"];
|
|
303
|
-
let extras: Vec<&str> = extra_keys
|
|
378
|
+
let extras: Vec<&str> = extra_keys
|
|
379
|
+
.iter()
|
|
380
|
+
.filter(|k| query.get(k).is_some())
|
|
381
|
+
.copied()
|
|
382
|
+
.collect();
|
|
304
383
|
if !extras.is_empty() {
|
|
305
384
|
return Some(skip(extras[0], format!("declares {} — out of scope for this generator (rust/codegen/src/queries.rs's own header has the full argument)", extras.join(", "))));
|
|
306
385
|
}
|
|
307
|
-
if query
|
|
308
|
-
|
|
386
|
+
if query
|
|
387
|
+
.get("index_hints")
|
|
388
|
+
.map_or_default(Json::each)
|
|
389
|
+
.iter()
|
|
390
|
+
.any(|_| true)
|
|
391
|
+
{
|
|
392
|
+
return Some(skip(
|
|
393
|
+
"index_hints",
|
|
394
|
+
"declares use_index, out of scope for the same reason the extras above are",
|
|
395
|
+
));
|
|
309
396
|
}
|
|
310
397
|
|
|
311
398
|
// A declared tenant synthesizes its own where clause (`query_conditions_with_authorization`),
|
|
312
399
|
// so a query with no ordinary wheres but a tenant gate is still generable. The tenant
|
|
313
400
|
// field itself is validated afterward by `declared_authorization_skip_reason`.
|
|
314
|
-
let declared_tenant = query
|
|
315
|
-
|
|
401
|
+
let declared_tenant = query
|
|
402
|
+
.get("authorization")
|
|
403
|
+
.and_then(|a| a.get("tenant"))
|
|
404
|
+
.is_some();
|
|
405
|
+
let wheres = query.get("wheres").map_or_default(Json::each);
|
|
316
406
|
// A query with no `where` answers every record, ordered and bounded as declared, as Ruby's
|
|
317
407
|
// `QueryInterpreter` does. Ruby refuses at boot one that takes arguments yet filters, orders
|
|
318
408
|
// and bounds nothing (`Registry::QueryVerification#derivable?`: it reads none of them), so no
|
|
319
409
|
// answer exists to generate for it.
|
|
320
|
-
let selects_nothing = query.get("order_by").is_none()
|
|
321
|
-
|
|
410
|
+
let selects_nothing = query.get("order_by").is_none()
|
|
411
|
+
&& query.get("limit").is_none()
|
|
412
|
+
&& query.get("offset").is_none();
|
|
413
|
+
let takes_arguments = query
|
|
414
|
+
.get("attributes")
|
|
415
|
+
.map(Json::each)
|
|
416
|
+
.is_some_and(|attributes| !attributes.is_empty());
|
|
322
417
|
if wheres.is_empty() && !declared_tenant && selects_nothing && takes_arguments {
|
|
323
418
|
return Some(skip(
|
|
324
419
|
"no_wheres",
|
|
@@ -327,24 +422,45 @@ pub fn query_skip_reason(query: &Json, aggregate: &Json, value_objects_by_name:
|
|
|
327
422
|
}
|
|
328
423
|
|
|
329
424
|
for where_clause in wheres {
|
|
330
|
-
let field = where_clause
|
|
425
|
+
let field = where_clause
|
|
426
|
+
.get("field")
|
|
427
|
+
.map(Json::to_s)
|
|
428
|
+
.unwrap_or_default();
|
|
331
429
|
if let Some(plan) = query_hop_plan(aggregate, &field, aggregates_by_name) {
|
|
332
430
|
let target_value_objects_by_name = value_objects_of(plan.target);
|
|
333
|
-
if let Some(reason) = query_where_skip_reason(
|
|
334
|
-
|
|
431
|
+
if let Some(reason) = query_where_skip_reason(
|
|
432
|
+
&with_field(where_clause, &plan.inner_field),
|
|
433
|
+
plan.target,
|
|
434
|
+
&target_value_objects_by_name,
|
|
435
|
+
) {
|
|
436
|
+
return Some(reskip(
|
|
437
|
+
&reason,
|
|
438
|
+
format!(
|
|
439
|
+
"hop through {} to {}'s own {reason}",
|
|
440
|
+
plan.via_field, plan.target_aggregate
|
|
441
|
+
),
|
|
442
|
+
));
|
|
335
443
|
}
|
|
336
444
|
continue;
|
|
337
445
|
}
|
|
338
|
-
if let Some(reason) =
|
|
446
|
+
if let Some(reason) =
|
|
447
|
+
query_where_skip_reason(where_clause, aggregate, value_objects_by_name)
|
|
448
|
+
{
|
|
339
449
|
return Some(reason);
|
|
340
450
|
}
|
|
341
451
|
}
|
|
342
452
|
|
|
343
|
-
if let Some(reason) = declared_authorization_skip_reason(
|
|
453
|
+
if let Some(reason) = declared_authorization_skip_reason(
|
|
454
|
+
query.get("authorization"),
|
|
455
|
+
aggregate,
|
|
456
|
+
value_objects_by_name,
|
|
457
|
+
) {
|
|
344
458
|
return Some(reason);
|
|
345
459
|
}
|
|
346
460
|
|
|
347
|
-
if let Some(reason) =
|
|
461
|
+
if let Some(reason) =
|
|
462
|
+
declared_order_by_skip_reason(query.get("order_by"), aggregate, value_objects_by_name)
|
|
463
|
+
{
|
|
348
464
|
return Some(reason);
|
|
349
465
|
}
|
|
350
466
|
|
|
@@ -355,7 +471,11 @@ pub fn query_skip_reason(query: &Json, aggregate: &Json, value_objects_by_name:
|
|
|
355
471
|
declared_limit_skip_reason(query.get("limit"))
|
|
356
472
|
}
|
|
357
473
|
|
|
358
|
-
pub fn declared_order_by_skip_reason(
|
|
474
|
+
pub fn declared_order_by_skip_reason(
|
|
475
|
+
order_by: Option<&Json>,
|
|
476
|
+
aggregate: &Json,
|
|
477
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
478
|
+
) -> Option<SkipReason> {
|
|
359
479
|
let order_by = order_by?;
|
|
360
480
|
let field = order_by.get("field").map(Json::to_s).unwrap_or_default();
|
|
361
481
|
let kind = query_field_kind(aggregate, &field, value_objects_by_name);
|
|
@@ -401,18 +521,29 @@ fn is_plain_integer(raw: &str) -> bool {
|
|
|
401
521
|
|
|
402
522
|
// A `nil` tenant is a no-op, never disqualifying. A real tenant is checked by running a
|
|
403
523
|
// synthetic arg-bound where through `query_where_skip_reason`.
|
|
404
|
-
pub fn declared_authorization_skip_reason(
|
|
405
|
-
|
|
524
|
+
pub fn declared_authorization_skip_reason(
|
|
525
|
+
authorization: Option<&Json>,
|
|
526
|
+
aggregate: &Json,
|
|
527
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
528
|
+
) -> Option<SkipReason> {
|
|
529
|
+
let tenant = authorization
|
|
530
|
+
.and_then(|a| a.get("tenant"))
|
|
531
|
+
.map(Json::to_s)?;
|
|
406
532
|
let synthetic_where = Json::Object(vec![
|
|
407
533
|
("field".to_string(), Json::String(tenant.clone())),
|
|
408
534
|
("op".to_string(), Json::String("eq".to_string())),
|
|
409
535
|
("value".to_string(), Json::String(format!(":{tenant}"))),
|
|
410
536
|
]);
|
|
411
|
-
query_where_skip_reason(&synthetic_where, aggregate, value_objects_by_name)
|
|
537
|
+
query_where_skip_reason(&synthetic_where, aggregate, value_objects_by_name)
|
|
538
|
+
.map(|reason| skip("authorization", reason.text))
|
|
412
539
|
}
|
|
413
540
|
|
|
414
541
|
pub fn emit_query_order_by(order_by: &Json, null_semantics: Option<&Json>) -> String {
|
|
415
|
-
let descending = order_by
|
|
542
|
+
let descending = order_by
|
|
543
|
+
.get("direction")
|
|
544
|
+
.map(Json::to_s)
|
|
545
|
+
.unwrap_or_default()
|
|
546
|
+
== "desc";
|
|
416
547
|
format!(
|
|
417
548
|
"crate::kernel::query_ordering::OrderBy {{ field: {}, descending: {descending}, nulls: {} }}",
|
|
418
549
|
naming::ruby_inspect_string(&order_by.get("field").map(Json::to_s).unwrap_or_default()),
|
|
@@ -423,7 +554,10 @@ pub fn emit_query_order_by(order_by: &Json, null_semantics: Option<&Json>) -> St
|
|
|
423
554
|
// `nulls(mode)` is unvalidated, so an unrecognized mode falls back to `Native`
|
|
424
555
|
// like `NullPolicy.order`'s `else` arm.
|
|
425
556
|
pub fn null_semantics_variant(null_semantics: Option<&Json>) -> &'static str {
|
|
426
|
-
let mode = null_semantics
|
|
557
|
+
let mode = null_semantics
|
|
558
|
+
.and_then(|ns| ns.get("mode"))
|
|
559
|
+
.map(Json::to_s)
|
|
560
|
+
.unwrap_or_default();
|
|
427
561
|
match mode.as_str() {
|
|
428
562
|
"first" => "crate::kernel::query_ordering::NullsMode::First",
|
|
429
563
|
"last" => "crate::kernel::query_ordering::NullsMode::Last",
|
|
@@ -434,9 +568,15 @@ pub fn null_semantics_variant(null_semantics: Option<&Json>) -> &'static str {
|
|
|
434
568
|
pub fn emit_query_limit(limit: &Json) -> String {
|
|
435
569
|
let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
|
|
436
570
|
if let Some(arg) = raw.strip_prefix(':') {
|
|
437
|
-
return format!(
|
|
571
|
+
return format!(
|
|
572
|
+
"crate::kernel::query_ordering::Limit::Arg({})",
|
|
573
|
+
naming::ruby_inspect_string(arg)
|
|
574
|
+
);
|
|
438
575
|
}
|
|
439
|
-
format!(
|
|
576
|
+
format!(
|
|
577
|
+
"crate::kernel::query_ordering::Limit::Literal({})",
|
|
578
|
+
ruby_to_i(&raw)
|
|
579
|
+
)
|
|
440
580
|
}
|
|
441
581
|
|
|
442
582
|
// `Offset` aliases `Limit`; only the spelled type name is swapped so a generated
|
|
@@ -445,23 +585,6 @@ pub fn emit_query_offset(offset: &Json) -> String {
|
|
|
445
585
|
emit_query_limit(offset).replace("query_ordering::Limit::", "query_ordering::Offset::")
|
|
446
586
|
}
|
|
447
587
|
|
|
448
|
-
fn ruby_to_i(s: &str) -> i64 {
|
|
449
|
-
let trimmed = s.trim_start();
|
|
450
|
-
let bytes = trimmed.as_bytes();
|
|
451
|
-
let mut end = 0;
|
|
452
|
-
if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
|
|
453
|
-
end += 1;
|
|
454
|
-
}
|
|
455
|
-
let digits_start = end;
|
|
456
|
-
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
457
|
-
end += 1;
|
|
458
|
-
}
|
|
459
|
-
if end == digits_start {
|
|
460
|
-
return 0;
|
|
461
|
-
}
|
|
462
|
-
trimmed[..end].parse().unwrap_or(0)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
588
|
pub struct Condition {
|
|
466
589
|
pub field: String,
|
|
467
590
|
pub op: String,
|
|
@@ -471,7 +594,7 @@ pub struct Condition {
|
|
|
471
594
|
|
|
472
595
|
// Assumes `query_skip_reason` already accepted the query.
|
|
473
596
|
pub fn query_conditions(query: &Json) -> Vec<Condition> {
|
|
474
|
-
let wheres = query.get("wheres").
|
|
597
|
+
let wheres = query.get("wheres").map_or_default(Json::each);
|
|
475
598
|
wheres.iter().map(condition_for).collect()
|
|
476
599
|
}
|
|
477
600
|
|
|
@@ -481,8 +604,16 @@ fn condition_for(w: &Json) -> Condition {
|
|
|
481
604
|
Condition {
|
|
482
605
|
field: w.get("field").map(Json::to_s).unwrap_or_default(),
|
|
483
606
|
op: w.get("op").map(Json::to_s).unwrap_or_default(),
|
|
484
|
-
arg: if symbol {
|
|
485
|
-
|
|
607
|
+
arg: if symbol {
|
|
608
|
+
Some(raw_value.trim_start_matches(':').to_string())
|
|
609
|
+
} else {
|
|
610
|
+
None
|
|
611
|
+
},
|
|
612
|
+
literal: if symbol {
|
|
613
|
+
None
|
|
614
|
+
} else {
|
|
615
|
+
Some(literal::read(&raw_value))
|
|
616
|
+
},
|
|
486
617
|
}
|
|
487
618
|
}
|
|
488
619
|
|
|
@@ -495,28 +626,51 @@ pub struct HopCondition {
|
|
|
495
626
|
pub condition: Condition,
|
|
496
627
|
}
|
|
497
628
|
|
|
498
|
-
pub fn query_conditions_and_hops(
|
|
629
|
+
pub fn query_conditions_and_hops(
|
|
630
|
+
domain_name: &str,
|
|
631
|
+
query: &Json,
|
|
632
|
+
aggregate: &Json,
|
|
633
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
634
|
+
) -> (Vec<Condition>, Vec<HopCondition>) {
|
|
499
635
|
let mut local = Vec::new();
|
|
500
636
|
let mut hops = Vec::new();
|
|
501
|
-
for w in query.get("wheres").
|
|
637
|
+
for w in query.get("wheres").map_or_default(Json::each) {
|
|
502
638
|
let field = w.get("field").map(Json::to_s).unwrap_or_default();
|
|
503
639
|
match query_hop_plan(aggregate, &field, aggregates_by_name) {
|
|
504
640
|
Some(plan) => hops.push(hop_condition(domain_name, plan, w)),
|
|
505
641
|
None => local.push(condition_for(w)),
|
|
506
642
|
}
|
|
507
643
|
}
|
|
508
|
-
if let Some(tenant) = query
|
|
509
|
-
|
|
644
|
+
if let Some(tenant) = query
|
|
645
|
+
.get("authorization")
|
|
646
|
+
.and_then(|a| a.get("tenant"))
|
|
647
|
+
.map(Json::to_s)
|
|
648
|
+
{
|
|
649
|
+
local.push(Condition {
|
|
650
|
+
field: tenant.clone(),
|
|
651
|
+
op: "eq".to_string(),
|
|
652
|
+
arg: Some(tenant),
|
|
653
|
+
literal: None,
|
|
654
|
+
});
|
|
510
655
|
}
|
|
511
656
|
(local, hops)
|
|
512
657
|
}
|
|
513
658
|
|
|
514
659
|
// Assumes the skip check already confirmed every `hop_wheres` entry resolves.
|
|
515
|
-
pub fn read_model_hop_conditions(
|
|
660
|
+
pub fn read_model_hop_conditions(
|
|
661
|
+
domain_name: &str,
|
|
662
|
+
hop_wheres: &[&Json],
|
|
663
|
+
aggregate: &Json,
|
|
664
|
+
aggregates_by_name: &HashMap<String, &Json>,
|
|
665
|
+
) -> Vec<HopCondition> {
|
|
516
666
|
hop_wheres
|
|
517
667
|
.iter()
|
|
518
668
|
.filter_map(|w| {
|
|
519
|
-
let plan = query_hop_plan(
|
|
669
|
+
let plan = query_hop_plan(
|
|
670
|
+
aggregate,
|
|
671
|
+
&w.get("field").map(Json::to_s).unwrap_or_default(),
|
|
672
|
+
aggregates_by_name,
|
|
673
|
+
)?;
|
|
520
674
|
Some(hop_condition(domain_name, plan, w))
|
|
521
675
|
})
|
|
522
676
|
.collect()
|
|
@@ -525,15 +679,30 @@ pub fn read_model_hop_conditions(domain_name: &str, hop_wheres: &[&Json], aggreg
|
|
|
525
679
|
fn hop_condition(domain_name: &str, plan: HopPlan<'_>, w: &Json) -> HopCondition {
|
|
526
680
|
let mut condition = condition_for(w);
|
|
527
681
|
condition.field = plan.inner_field;
|
|
528
|
-
let through = plan
|
|
529
|
-
|
|
682
|
+
let through = plan
|
|
683
|
+
.through
|
|
684
|
+
.into_iter()
|
|
685
|
+
.map(|(via, target)| (via, format!("{domain_name}::{target}")))
|
|
686
|
+
.collect();
|
|
687
|
+
HopCondition {
|
|
688
|
+
via_field: plan.via_field,
|
|
689
|
+
target_aggregate: format!("{domain_name}::{}", plan.target_aggregate),
|
|
690
|
+
through,
|
|
691
|
+
condition,
|
|
692
|
+
}
|
|
530
693
|
}
|
|
531
694
|
|
|
532
695
|
pub fn emit_reference_hop_condition(hop: &HopCondition) -> String {
|
|
533
696
|
let through = hop
|
|
534
697
|
.through
|
|
535
698
|
.iter()
|
|
536
|
-
.map(|(via, target)|
|
|
699
|
+
.map(|(via, target)| {
|
|
700
|
+
format!(
|
|
701
|
+
"crate::kernel::read_model::HopStep {{ via_field: {}, target_aggregate: {} }}",
|
|
702
|
+
naming::ruby_inspect_string(via),
|
|
703
|
+
naming::ruby_inspect_string(target)
|
|
704
|
+
)
|
|
705
|
+
})
|
|
537
706
|
.collect::<Vec<_>>()
|
|
538
707
|
.join(", ");
|
|
539
708
|
format!(
|
|
@@ -549,8 +718,17 @@ pub fn emit_reference_hop_condition(hop: &HopCondition) -> String {
|
|
|
549
718
|
// Adds `Runtime::TenantScope.apply`'s synthetic tenant clause at codegen time.
|
|
550
719
|
pub fn query_conditions_with_authorization(query: &Json) -> Vec<Condition> {
|
|
551
720
|
let mut conditions = query_conditions(query);
|
|
552
|
-
if let Some(tenant) = query
|
|
553
|
-
|
|
721
|
+
if let Some(tenant) = query
|
|
722
|
+
.get("authorization")
|
|
723
|
+
.and_then(|a| a.get("tenant"))
|
|
724
|
+
.map(Json::to_s)
|
|
725
|
+
{
|
|
726
|
+
conditions.push(Condition {
|
|
727
|
+
field: tenant.clone(),
|
|
728
|
+
op: "eq".to_string(),
|
|
729
|
+
arg: Some(tenant),
|
|
730
|
+
literal: None,
|
|
731
|
+
});
|
|
554
732
|
}
|
|
555
733
|
conditions
|
|
556
734
|
}
|
|
@@ -558,8 +736,13 @@ pub fn query_conditions_with_authorization(query: &Json) -> Vec<Condition> {
|
|
|
558
736
|
// `None` unless a tenant is declared. `policy` is carried on the wire but not enforced,
|
|
559
737
|
// matching Ruby's `TenantScope.apply`; an absent policy becomes `""`.
|
|
560
738
|
pub fn emit_query_authorization(query_name: &str, authorization: Option<&Json>) -> Option<String> {
|
|
561
|
-
let tenant = authorization
|
|
562
|
-
|
|
739
|
+
let tenant = authorization
|
|
740
|
+
.and_then(|a| a.get("tenant"))
|
|
741
|
+
.map(Json::to_s)?;
|
|
742
|
+
let policy = authorization
|
|
743
|
+
.and_then(|a| a.get("policy"))
|
|
744
|
+
.map(Json::to_s)
|
|
745
|
+
.unwrap_or_default();
|
|
563
746
|
Some(format!(
|
|
564
747
|
"crate::kernel::named_query::TenantAuth {{ query_name: {}, tenant_field: {}, policy: {} }}",
|
|
565
748
|
naming::ruby_inspect_string(query_name),
|
|
@@ -572,9 +755,16 @@ pub fn emit_query_authorization(query_name: &str, authorization: Option<&Json>)
|
|
|
572
755
|
// cross-domain search list, so it would answer every row `true`. `query_where_skip_reason`
|
|
573
756
|
// checks this first, making the `panic!` in `query_comparator_variant` a backstop.
|
|
574
757
|
fn known_query_comparator(op: &str) -> bool {
|
|
575
|
-
matches!(
|
|
758
|
+
matches!(
|
|
759
|
+
op,
|
|
760
|
+
"eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "contains"
|
|
761
|
+
)
|
|
576
762
|
}
|
|
577
763
|
|
|
764
|
+
#[allow(
|
|
765
|
+
clippy::panic,
|
|
766
|
+
reason = "a drifted exemplar or invalid IR aborts generation on purpose"
|
|
767
|
+
)]
|
|
578
768
|
fn query_comparator_variant(op: &str) -> &'static str {
|
|
579
769
|
match op {
|
|
580
770
|
"eq" => "Eq",
|
|
@@ -600,8 +790,22 @@ fn literal_inspect(lit: &Literal) -> String {
|
|
|
600
790
|
Literal::Bool(b) => b.to_string(),
|
|
601
791
|
Literal::Nil => "nil".to_string(),
|
|
602
792
|
Literal::Symbol(s) => format!(":{s}"),
|
|
603
|
-
Literal::Hash(pairs) => format!(
|
|
604
|
-
|
|
793
|
+
Literal::Hash(pairs) => format!(
|
|
794
|
+
"{{{}}}",
|
|
795
|
+
pairs
|
|
796
|
+
.iter()
|
|
797
|
+
.map(|(k, v)| format!("{k}: {}", literal_inspect(v)))
|
|
798
|
+
.collect::<Vec<_>>()
|
|
799
|
+
.join(", ")
|
|
800
|
+
),
|
|
801
|
+
Literal::Array(items) => format!(
|
|
802
|
+
"[{}]",
|
|
803
|
+
items
|
|
804
|
+
.iter()
|
|
805
|
+
.map(literal_inspect)
|
|
806
|
+
.collect::<Vec<_>>()
|
|
807
|
+
.join(", ")
|
|
808
|
+
),
|
|
605
809
|
}
|
|
606
810
|
}
|
|
607
811
|
|
|
@@ -615,24 +819,47 @@ fn ruby_float_inspect(n: f64) -> String {
|
|
|
615
819
|
}
|
|
616
820
|
}
|
|
617
821
|
|
|
822
|
+
#[allow(
|
|
823
|
+
clippy::option_if_let_else,
|
|
824
|
+
clippy::unwrap_used,
|
|
825
|
+
reason = "a map_or_else closure would bury this long body; the value was checked just before; None would mean invalid IR"
|
|
826
|
+
)]
|
|
618
827
|
pub fn emit_query_condition_value(condition: &Condition) -> String {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
828
|
+
if let Some(arg) = &condition.arg {
|
|
829
|
+
format!(
|
|
830
|
+
"crate::kernel::QueryConditionValue::Arg({})",
|
|
831
|
+
naming::ruby_inspect_string(arg)
|
|
832
|
+
)
|
|
833
|
+
} else {
|
|
834
|
+
let lit = condition.literal.as_ref().unwrap();
|
|
835
|
+
// The skip check already proved a numeric literal targets a numeric field.
|
|
836
|
+
match lit {
|
|
837
|
+
Literal::Int(n) => format!(
|
|
838
|
+
"crate::kernel::QueryConditionValue::NumericLiteral({})",
|
|
839
|
+
ruby_float_inspect(*n as f64)
|
|
840
|
+
),
|
|
841
|
+
Literal::Float(n) => format!(
|
|
842
|
+
"crate::kernel::QueryConditionValue::NumericLiteral({})",
|
|
843
|
+
ruby_float_inspect(*n)
|
|
844
|
+
),
|
|
845
|
+
_ => format!(
|
|
846
|
+
"crate::kernel::QueryConditionValue::Literal({})",
|
|
847
|
+
literal_inspect(lit)
|
|
848
|
+
),
|
|
629
849
|
}
|
|
630
850
|
}
|
|
631
851
|
}
|
|
632
852
|
|
|
633
853
|
pub fn emit_query_condition(condition: &Condition) -> String {
|
|
634
|
-
let comparator_expr = format!(
|
|
635
|
-
|
|
854
|
+
let comparator_expr = format!(
|
|
855
|
+
"crate::kernel::query_comparators::QueryComparator::{}",
|
|
856
|
+
query_comparator_variant(&condition.op)
|
|
857
|
+
);
|
|
858
|
+
format!(
|
|
859
|
+
"crate::kernel::QueryCondition {{ field: {}, comparator: {comparator_expr}, value: {} }},",
|
|
860
|
+
naming::ruby_inspect_string(&condition.field),
|
|
861
|
+
emit_query_condition_value(condition)
|
|
862
|
+
)
|
|
636
863
|
}
|
|
637
864
|
|
|
638
865
|
pub struct QueryDef {
|
|
@@ -653,8 +880,7 @@ pub struct QueryDef {
|
|
|
653
880
|
|
|
654
881
|
pub fn provided_assignments(ir: &Json) -> Option<String> {
|
|
655
882
|
ir.get("provides")
|
|
656
|
-
.
|
|
657
|
-
.unwrap_or(&[])
|
|
883
|
+
.map_or_default(Json::each)
|
|
658
884
|
.iter()
|
|
659
885
|
.find(|row| {
|
|
660
886
|
row.get("capability").and_then(Json::as_str) == Some("authorization")
|
|
@@ -665,10 +891,10 @@ pub fn provided_assignments(ir: &Json) -> Option<String> {
|
|
|
665
891
|
}
|
|
666
892
|
|
|
667
893
|
pub fn emit_authorization_assignments(query_defs: &[QueryDef]) -> String {
|
|
668
|
-
let value =
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
894
|
+
let value = query_defs.iter().find(|q| q.assignments).map_or_else(
|
|
895
|
+
|| "None".to_string(),
|
|
896
|
+
|q| format!("Some({})", naming::ruby_inspect_string(&q.verb)),
|
|
897
|
+
);
|
|
672
898
|
format!(
|
|
673
899
|
"/// `provides \"authorization\", assignments:` — the query `kernel::check_role_via` reads; `None` when no chapter here declares one.\npub const AUTHORIZATION_ASSIGNMENTS: Option<&str> = {value};\n"
|
|
674
900
|
)
|
|
@@ -681,19 +907,40 @@ pub struct EntityScope {
|
|
|
681
907
|
}
|
|
682
908
|
|
|
683
909
|
pub fn emit_entity_query_table(entity_defs: &[&QueryDef]) -> String {
|
|
684
|
-
let rows: String = entity_defs.iter().
|
|
910
|
+
let rows: String = entity_defs.iter().fold(String::new(), |mut rows, q| {
|
|
911
|
+
let _ = writeln!(rows, "{}", emit_entity_query_def(q));
|
|
912
|
+
rows
|
|
913
|
+
});
|
|
685
914
|
format!(
|
|
686
915
|
"/// Declared entity queries (`Aggregate.Entity.Query`) — `kernel::named_query::run_entity`.\npub const ENTITY_QUERIES: &[crate::kernel::named_query::EntityQueryDef] = &[\n{rows}];\n"
|
|
687
916
|
)
|
|
688
917
|
}
|
|
689
918
|
|
|
919
|
+
#[allow(
|
|
920
|
+
clippy::expect_used,
|
|
921
|
+
reason = "the message names an invariant an earlier check in this generator established"
|
|
922
|
+
)]
|
|
690
923
|
fn emit_entity_query_def(query_def: &QueryDef) -> String {
|
|
691
|
-
let entity = query_def
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
924
|
+
let entity = query_def
|
|
925
|
+
.entity
|
|
926
|
+
.as_ref()
|
|
927
|
+
.expect("an entity query def carries its scope");
|
|
928
|
+
let conditions = query_def
|
|
929
|
+
.conditions
|
|
930
|
+
.iter()
|
|
931
|
+
.map(|c| format!(" {}", emit_query_condition(c)))
|
|
932
|
+
.collect::<Vec<_>>()
|
|
933
|
+
.join("\n");
|
|
934
|
+
let keys = entity
|
|
935
|
+
.identity_keys
|
|
936
|
+
.iter()
|
|
937
|
+
.map(|key| naming::ruby_inspect_string(key))
|
|
938
|
+
.collect::<Vec<_>>()
|
|
939
|
+
.join(", ");
|
|
940
|
+
let wrap = |value: &Option<String>| {
|
|
941
|
+
value
|
|
942
|
+
.as_ref()
|
|
943
|
+
.map_or_else(|| "None".to_string(), |v| format!("Some({v})"))
|
|
697
944
|
};
|
|
698
945
|
format!(
|
|
699
946
|
"crate::kernel::named_query::EntityQueryDef {{\n verb: {},\n aggregate: {},\n list_field: {},\n parent_key: {},\n identity_keys: &[{keys}],\n conditions: &[\n{conditions}\n ],\n order_by: {},\n offset: {},\n limit: {},\n}},",
|
|
@@ -707,36 +954,59 @@ fn emit_entity_query_def(query_def: &QueryDef) -> String {
|
|
|
707
954
|
)
|
|
708
955
|
}
|
|
709
956
|
|
|
710
|
-
pub fn entity_query_skip_reason(
|
|
957
|
+
pub fn entity_query_skip_reason(
|
|
958
|
+
query: &Json,
|
|
959
|
+
entity: &Json,
|
|
960
|
+
holds_list: bool,
|
|
961
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
962
|
+
) -> Option<SkipReason> {
|
|
711
963
|
let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
|
|
712
964
|
if !holds_list {
|
|
713
|
-
return Some(skip(
|
|
965
|
+
return Some(skip(
|
|
966
|
+
"entity_query",
|
|
967
|
+
format!(
|
|
968
|
+
"{entity_name} is held in no list attribute on its aggregate — nothing to flatten"
|
|
969
|
+
),
|
|
970
|
+
));
|
|
714
971
|
}
|
|
715
972
|
if query.get("authorization").is_some() {
|
|
716
|
-
return Some(skip(
|
|
973
|
+
return Some(skip(
|
|
974
|
+
"entity_query_authorization",
|
|
975
|
+
"declares authorize — an entity query's tenant scope is not generated yet",
|
|
976
|
+
));
|
|
717
977
|
}
|
|
718
978
|
query_skip_reason(query, entity, value_objects_by_name, &HashMap::new())
|
|
719
979
|
}
|
|
720
980
|
|
|
721
981
|
pub fn emit_query_def(query_def: &QueryDef) -> String {
|
|
722
|
-
let conditions = query_def
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
let
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
982
|
+
let conditions = query_def
|
|
983
|
+
.conditions
|
|
984
|
+
.iter()
|
|
985
|
+
.map(|c| format!(" {}", emit_query_condition(c)))
|
|
986
|
+
.collect::<Vec<_>>()
|
|
987
|
+
.join("\n");
|
|
988
|
+
let reference_hop_conditions = query_def
|
|
989
|
+
.reference_hop_conditions
|
|
990
|
+
.iter()
|
|
991
|
+
.map(|h| format!(" {}", emit_reference_hop_condition(h)))
|
|
992
|
+
.collect::<Vec<_>>()
|
|
993
|
+
.join("\n");
|
|
994
|
+
let order_by = query_def
|
|
995
|
+
.order_by
|
|
996
|
+
.as_ref()
|
|
997
|
+
.map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
|
|
998
|
+
let offset = query_def
|
|
999
|
+
.offset
|
|
1000
|
+
.as_ref()
|
|
1001
|
+
.map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
|
|
1002
|
+
let limit = query_def
|
|
1003
|
+
.limit
|
|
1004
|
+
.as_ref()
|
|
1005
|
+
.map_or_else(|| "None".to_string(), |l| format!("Some({l})"));
|
|
1006
|
+
let authorization = query_def
|
|
1007
|
+
.authorization
|
|
1008
|
+
.as_ref()
|
|
1009
|
+
.map_or_else(|| "None".to_string(), |a| format!("Some({a})"));
|
|
740
1010
|
|
|
741
1011
|
format!(
|
|
742
1012
|
"crate::kernel::QueryDef {{\n verb: {},\n aggregate: {},\n conditions: &[\n{conditions}\n ],\n reference_hop_conditions: &[\n{reference_hop_conditions}\n ],\n order_by: {order_by},\n offset: {offset},\n limit: {limit},\n authorization: {authorization},\n}},",
|
|
@@ -748,32 +1018,49 @@ pub fn emit_query_def(query_def: &QueryDef) -> String {
|
|
|
748
1018
|
const QUERY_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::QueryDef {\n verb: \"tmpl_verb\",\n aggregate: \"tmpl_aggregate\",\n conditions: &[\n crate::kernel::QueryCondition {\n field: \"tmpl_field\",\n comparator: crate::kernel::query_comparators::QueryComparator::Eq,\n value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\"),\n },\n ],\n reference_hop_conditions: &[\n crate::kernel::read_model::ReferenceHopCondition {\n via_field: \"tmpl_via_field\",\n target_aggregate: \"tmpl_target_aggregate\",\n through: &[crate::kernel::read_model::HopStep { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\" }],\n inner_field: \"tmpl_inner_field\",\n inner_comparator: crate::kernel::query_comparators::QueryComparator::Eq,\n inner_value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\"),\n },\n ],\n order_by: Some(crate::kernel::query_ordering::OrderBy { field: \"tmpl_order_field\", descending: true, nulls: crate::kernel::query_ordering::NullsMode::Last }),\n offset: Some(crate::kernel::query_ordering::Offset::Literal(1)),\n limit: Some(crate::kernel::query_ordering::Limit::Literal(5)),\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n},";
|
|
749
1019
|
|
|
750
1020
|
pub fn emit_query_table(exemplar: &Exemplar, query_defs: &[QueryDef]) -> String {
|
|
751
|
-
let (entity_defs, aggregate_defs): (Vec<&QueryDef>, Vec<&QueryDef>) =
|
|
1021
|
+
let (entity_defs, aggregate_defs): (Vec<&QueryDef>, Vec<&QueryDef>) =
|
|
1022
|
+
query_defs.iter().partition(|q| q.entity.is_some());
|
|
752
1023
|
let rows: Vec<String> = aggregate_defs.into_iter().map(emit_query_def).collect();
|
|
753
1024
|
format!(
|
|
754
1025
|
"{}\n{}{}",
|
|
755
|
-
exemplar.render(
|
|
1026
|
+
exemplar.render(
|
|
1027
|
+
"query_table",
|
|
1028
|
+
&[(QUERY_TABLE_ROW_PLACEHOLDER, rows.join("\n"))]
|
|
1029
|
+
),
|
|
756
1030
|
emit_authorization_assignments(query_defs),
|
|
757
1031
|
emit_entity_query_table(&entity_defs)
|
|
758
1032
|
)
|
|
759
1033
|
}
|
|
760
1034
|
|
|
761
1035
|
// One `if let` per value-object argument: built and invariant-checked, then dropped (ADR 0037).
|
|
762
|
-
pub fn query_arg_checks(
|
|
1036
|
+
pub fn query_arg_checks(
|
|
1037
|
+
query: &Json,
|
|
1038
|
+
mod_path: &str,
|
|
1039
|
+
value_objects_by_name: &HashMap<String, &Json>,
|
|
1040
|
+
) -> Vec<String> {
|
|
763
1041
|
query
|
|
764
1042
|
.get("attributes")
|
|
765
|
-
.
|
|
766
|
-
.unwrap_or(&[])
|
|
1043
|
+
.map_or_default(Json::each)
|
|
767
1044
|
.iter()
|
|
768
1045
|
.filter_map(|attr| {
|
|
769
|
-
if crate::attr::list(attr)
|
|
1046
|
+
if crate::attr::list(attr)
|
|
1047
|
+
|| !matches!(attr.get("relationship"), None | Some(Json::Null))
|
|
1048
|
+
{
|
|
770
1049
|
return None;
|
|
771
1050
|
}
|
|
772
1051
|
let vo = value_objects_by_name.get(crate::attr::type_name(attr))?;
|
|
773
1052
|
let key = naming::rust_field(crate::attr::name(attr));
|
|
774
|
-
let build =
|
|
775
|
-
|
|
776
|
-
|
|
1053
|
+
let build =
|
|
1054
|
+
crate::json_codec::composite_from_json_expr(attr, value_objects_by_name, "x");
|
|
1055
|
+
let check = if vo.get("closed_set").is_some_and(Json::as_bool) {
|
|
1056
|
+
""
|
|
1057
|
+
} else {
|
|
1058
|
+
".check_invariants()?"
|
|
1059
|
+
};
|
|
1060
|
+
Some(format!(
|
|
1061
|
+
"if let Some(x) = args.get({}) {{ {mod_path}::{build}{check}; }}",
|
|
1062
|
+
naming::ruby_inspect_string(&key)
|
|
1063
|
+
))
|
|
777
1064
|
})
|
|
778
1065
|
.collect()
|
|
779
1066
|
}
|
|
@@ -784,8 +1071,16 @@ pub fn emit_query_arg_check_table(query_defs: &[QueryDef]) -> String {
|
|
|
784
1071
|
.iter()
|
|
785
1072
|
.filter(|q| !q.arg_checks.is_empty())
|
|
786
1073
|
.map(|q| {
|
|
787
|
-
let lines = q
|
|
788
|
-
|
|
1074
|
+
let lines = q
|
|
1075
|
+
.arg_checks
|
|
1076
|
+
.iter()
|
|
1077
|
+
.map(|line| format!(" {line}"))
|
|
1078
|
+
.collect::<Vec<_>>()
|
|
1079
|
+
.join("\n");
|
|
1080
|
+
format!(
|
|
1081
|
+
" {} => {{\n{lines}\n Ok(())\n }}",
|
|
1082
|
+
naming::ruby_inspect_string(&q.verb)
|
|
1083
|
+
)
|
|
789
1084
|
})
|
|
790
1085
|
.collect();
|
|
791
1086
|
format!(
|
|
@@ -793,3 +1088,83 @@ pub fn emit_query_arg_check_table(query_defs: &[QueryDef]) -> String {
|
|
|
793
1088
|
arms.join("\n")
|
|
794
1089
|
)
|
|
795
1090
|
}
|
|
1091
|
+
|
|
1092
|
+
#[cfg(test)]
|
|
1093
|
+
mod path_tests {
|
|
1094
|
+
use super::*;
|
|
1095
|
+
|
|
1096
|
+
const AGGREGATE: &str = r#"{"lifecycle":{"field":"status"},"attributes":[
|
|
1097
|
+
{"name":"status","type":"String","list":false},
|
|
1098
|
+
{"name":"title","type":"String","list":false},
|
|
1099
|
+
{"name":"count","type":"Integer","list":false},
|
|
1100
|
+
{"name":"flag","type":"TrueClass","list":false},
|
|
1101
|
+
{"name":"owner","type":"Reference<Person>","list":false},
|
|
1102
|
+
{"name":"tags","type":"String","list":true},
|
|
1103
|
+
{"name":"money","type":"Money","list":false},
|
|
1104
|
+
{"name":"nest","type":"Nest","list":false}]}"#;
|
|
1105
|
+
const VALUE_OBJECTS: [&str; 3] = [
|
|
1106
|
+
r#"{"name":"Money","attributes":[{"name":"amount","type":"Float","list":false},{"name":"currency","type":"String","list":false}]}"#,
|
|
1107
|
+
r#"{"name":"Nest","attributes":[{"name":"items","type":"Money","list":true},{"name":"flag","type":"Flag","list":false}]}"#,
|
|
1108
|
+
r#"{"name":"Flag","attributes":[{"name":"on","type":"TrueClass","list":false}]}"#,
|
|
1109
|
+
];
|
|
1110
|
+
|
|
1111
|
+
/// Runs `check` over the classifiers' common inputs.
|
|
1112
|
+
fn with_schema(check: impl Fn(&Json, &HashMap<String, &Json>)) {
|
|
1113
|
+
let aggregate = Json::parse(AGGREGATE).unwrap();
|
|
1114
|
+
let parsed: Vec<Json> = VALUE_OBJECTS
|
|
1115
|
+
.iter()
|
|
1116
|
+
.map(|text| Json::parse(text).unwrap())
|
|
1117
|
+
.collect();
|
|
1118
|
+
let by_name: HashMap<String, &Json> = parsed
|
|
1119
|
+
.iter()
|
|
1120
|
+
.map(|vo| {
|
|
1121
|
+
(
|
|
1122
|
+
vo.get("name").and_then(Json::as_str).unwrap().to_string(),
|
|
1123
|
+
vo,
|
|
1124
|
+
)
|
|
1125
|
+
})
|
|
1126
|
+
.collect();
|
|
1127
|
+
check(&aggregate, &by_name);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
#[test]
|
|
1131
|
+
fn a_path_is_classified_by_the_type_it_lands_on() {
|
|
1132
|
+
with_schema(|aggregate, vos| {
|
|
1133
|
+
let kind = |path: &str| query_field_kind(aggregate, path, vos);
|
|
1134
|
+
assert_eq!(kind("title"), FieldKind::String);
|
|
1135
|
+
assert_eq!(kind("owner"), FieldKind::String);
|
|
1136
|
+
assert_eq!(kind("status"), FieldKind::String);
|
|
1137
|
+
assert_eq!(kind("count"), FieldKind::Number);
|
|
1138
|
+
assert_eq!(kind("money"), FieldKind::Number);
|
|
1139
|
+
assert_eq!(kind("nest.flag"), FieldKind::Other);
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
#[test]
|
|
1144
|
+
fn a_path_that_names_nothing_is_unknown_and_a_list_head_is_other() {
|
|
1145
|
+
with_schema(|aggregate, vos| {
|
|
1146
|
+
let kind = |path: &str| query_field_kind(aggregate, path, vos);
|
|
1147
|
+
assert_eq!(kind("missing"), FieldKind::Unknown);
|
|
1148
|
+
assert_eq!(kind("status.x"), FieldKind::Unknown);
|
|
1149
|
+
assert_eq!(kind("owner.x"), FieldKind::Unknown);
|
|
1150
|
+
assert_eq!(kind("nest.items"), FieldKind::Unknown);
|
|
1151
|
+
assert_eq!(kind("tags"), FieldKind::Other);
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#[test]
|
|
1156
|
+
fn booleans_and_numeric_types_follow_a_single_member_down() {
|
|
1157
|
+
with_schema(|aggregate, vos| {
|
|
1158
|
+
assert!(query_field_boolean(aggregate, "flag", vos));
|
|
1159
|
+
assert!(query_field_boolean(aggregate, "nest.flag", vos));
|
|
1160
|
+
assert!(query_field_boolean(aggregate, "nest.flag.on", vos));
|
|
1161
|
+
assert!(!query_field_boolean(aggregate, "money", vos));
|
|
1162
|
+
assert!(!query_field_boolean(aggregate, "tags", vos));
|
|
1163
|
+
let numeric = |path: &str| query_field_numeric_type(aggregate, path, vos);
|
|
1164
|
+
assert_eq!(numeric("count").as_deref(), Some("Integer"));
|
|
1165
|
+
assert_eq!(numeric("money").as_deref(), Some("Float"));
|
|
1166
|
+
assert_eq!(numeric("title"), None);
|
|
1167
|
+
assert_eq!(numeric("tags"), None);
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
}
|