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.
Files changed (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -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(aggregate: &Json, field: &str, value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
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.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
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 rest.is_empty() { FieldKind::String } else { FieldKind::Unknown };
84
+ return if segments.next().is_none() {
85
+ FieldKind::String
86
+ } else {
87
+ FieldKind::Unknown
88
+ };
32
89
  }
33
90
  }
34
91
 
35
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
36
- let Some(attr) = attrs.iter().find(|a| crate::attr::name(a) == head) else { return FieldKind::Unknown };
37
- if crate::attr::list(attr) {
38
- return FieldKind::Other;
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 query_type_kind(type_name: &str, segments: &[&str], value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
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 if segments.is_empty() { FieldKind::String } else { FieldKind::Unknown };
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(type_name: &str, value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
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
- _ => match value_objects_by_name.get(type_name) {
68
- Some(vo) => query_vo_collapse_kind(vo, value_objects_by_name),
69
- None => FieldKind::Unknown,
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").map(Json::each).unwrap_or(&[]);
76
- if attrs.iter().any(|a| matches!(crate::attr::type_name(a), "Integer" | "Float")) {
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 query_type_kind(crate::attr::type_name(&attrs[0]), &[], value_objects_by_name);
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(aggregate: &Json, field: &str, value_objects_by_name: &HashMap<String, &Json>) -> bool {
88
- let mut segments = field.split('.');
89
- let head = segments.next().unwrap_or("");
90
- let rest: Vec<&str> = segments.collect();
91
-
92
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
93
- let Some(attr) = attrs.iter().find(|a| crate::attr::name(a) == head) else { return false };
94
- if crate::attr::list(attr) {
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 query_type_boolean(type_name: &str, segments: &[&str], value_objects_by_name: &HashMap<String, &Json>) -> bool {
101
- if segments.is_empty() {
102
- if matches!(type_name, "TrueClass" | "FalseClass") {
103
- return true;
104
- }
105
- if naming::reference_type(type_name) {
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(aggregate: &Json, field: &str, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
130
- let mut segments = field.split('.');
131
- let head = segments.next().unwrap_or("");
132
- let rest: Vec<&str> = segments.collect();
133
-
134
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
135
- let attr = attrs.iter().find(|a| crate::attr::name(a) == head)?;
136
- if crate::attr::list(attr) {
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 query_type_numeric_type(type_name: &str, segments: &[&str], value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
143
- if segments.is_empty() {
144
- if matches!(type_name, "Integer" | "Float") {
145
- return Some(type_name.to_string());
146
- }
147
- if naming::reference_type(type_name) {
148
- return None;
149
- }
150
- let vo = value_objects_by_name.get(type_name)?;
151
- let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
152
- if let Some(numeric_member) = attrs.iter().find(|a| matches!(crate::attr::type_name(a), "Integer" | "Float")) {
153
- return Some(crate::attr::type_name(numeric_member).to_string());
154
- }
155
- if attrs.len() == 1 {
156
- return query_type_numeric_type(crate::attr::type_name(&attrs[0]), &[], value_objects_by_name);
157
- }
158
- None
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(where_clause: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
171
- let field = where_clause.get("field").map(Json::to_s).unwrap_or_default();
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('/') { "reference_hop_where" } else { "where_unrecognized_field" };
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.get("value").map(Json::to_s).unwrap_or_default();
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>(aggregate: &'a Json, field: &str, aggregates_by_name: &HashMap<String, &'a Json>) -> Option<HopPlan<'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.get("attributes").map(Json::each).unwrap_or(&[]).iter().find(|a| crate::attr::name(a) == *segment)?;
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 { via_field, target_aggregate, through: steps, target: current, inner_field: segments[segments.len() - 1].to_string() })
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)| if key == "field" { (key.clone(), Json::String(field.to_string())) } else { (key.clone(), value.clone()) })
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
- .map(Json::each)
295
- .unwrap_or(&[])
357
+ .map_or_default(Json::each)
296
358
  .iter()
297
- .map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), 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(query: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
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.iter().filter(|k| query.get(k).is_some()).copied().collect();
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.get("index_hints").map(Json::each).unwrap_or(&[]).iter().any(|_| true) {
308
- return Some(skip("index_hints", "declares use_index, out of scope for the same reason the extras above are"));
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.get("authorization").and_then(|a| a.get("tenant")).is_some();
315
- let wheres = query.get("wheres").map(Json::each).unwrap_or(&[]);
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() && query.get("limit").is_none() && query.get("offset").is_none();
321
- let takes_arguments = query.get("attributes").map(Json::each).is_some_and(|attributes| !attributes.is_empty());
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.get("field").map(Json::to_s).unwrap_or_default();
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(&with_field(where_clause, &plan.inner_field), plan.target, &target_value_objects_by_name) {
334
- return Some(reskip(&reason, format!("hop through {} to {}'s own {reason}", plan.via_field, plan.target_aggregate)));
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) = query_where_skip_reason(where_clause, aggregate, value_objects_by_name) {
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(query.get("authorization"), aggregate, value_objects_by_name) {
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) = declared_order_by_skip_reason(query.get("order_by"), aggregate, value_objects_by_name) {
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(order_by: Option<&Json>, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
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(authorization: Option<&Json>, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
405
- let tenant = authorization.and_then(|a| a.get("tenant")).map(Json::to_s)?;
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).map(|reason| skip("authorization", reason.text))
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.get("direction").map(Json::to_s).unwrap_or_default() == "desc";
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.and_then(|ns| ns.get("mode")).map(Json::to_s).unwrap_or_default();
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!("crate::kernel::query_ordering::Limit::Arg({})", naming::ruby_inspect_string(arg));
571
+ return format!(
572
+ "crate::kernel::query_ordering::Limit::Arg({})",
573
+ naming::ruby_inspect_string(arg)
574
+ );
438
575
  }
439
- format!("crate::kernel::query_ordering::Limit::Literal({})", ruby_to_i(&raw))
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").map(Json::each).unwrap_or(&[]);
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 { Some(raw_value.trim_start_matches(':').to_string()) } else { None },
485
- literal: if symbol { None } else { Some(literal::read(&raw_value)) },
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(domain_name: &str, query: &Json, aggregate: &Json, aggregates_by_name: &HashMap<String, &Json>) -> (Vec<Condition>, Vec<HopCondition>) {
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").map(Json::each).unwrap_or(&[]) {
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.get("authorization").and_then(|a| a.get("tenant")).map(Json::to_s) {
509
- local.push(Condition { field: tenant.clone(), op: "eq".to_string(), arg: Some(tenant), literal: None });
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(domain_name: &str, hop_wheres: &[&Json], aggregate: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Vec<HopCondition> {
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(aggregate, &w.get("field").map(Json::to_s).unwrap_or_default(), aggregates_by_name)?;
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.through.into_iter().map(|(via, target)| (via, format!("{domain_name}::{target}"))).collect();
529
- HopCondition { via_field: plan.via_field, target_aggregate: format!("{domain_name}::{}", plan.target_aggregate), through, condition }
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)| format!("crate::kernel::read_model::HopStep {{ via_field: {}, target_aggregate: {} }}", naming::ruby_inspect_string(via), naming::ruby_inspect_string(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.get("authorization").and_then(|a| a.get("tenant")).map(Json::to_s) {
553
- conditions.push(Condition { field: tenant.clone(), op: "eq".to_string(), arg: Some(tenant), literal: None });
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.and_then(|a| a.get("tenant")).map(Json::to_s)?;
562
- let policy = authorization.and_then(|a| a.get("policy")).map(Json::to_s).unwrap_or_default();
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!(op, "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "contains")
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!("{{{}}}", pairs.iter().map(|(k, v)| format!("{k}: {}", literal_inspect(v))).collect::<Vec<_>>().join(", ")),
604
- Literal::Array(items) => format!("[{}]", items.iter().map(literal_inspect).collect::<Vec<_>>().join(", ")),
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
- match &condition.arg {
620
- Some(arg) => format!("crate::kernel::QueryConditionValue::Arg({})", naming::ruby_inspect_string(arg)),
621
- None => {
622
- let lit = condition.literal.as_ref().unwrap();
623
- // The skip check already proved a numeric literal targets a numeric field.
624
- match lit {
625
- Literal::Int(n) => format!("crate::kernel::QueryConditionValue::NumericLiteral({})", ruby_float_inspect(*n as f64)),
626
- Literal::Float(n) => format!("crate::kernel::QueryConditionValue::NumericLiteral({})", ruby_float_inspect(*n)),
627
- _ => format!("crate::kernel::QueryConditionValue::Literal({})", literal_inspect(lit)),
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!("crate::kernel::query_comparators::QueryComparator::{}", query_comparator_variant(&condition.op));
635
- format!("crate::kernel::QueryCondition {{ field: {}, comparator: {comparator_expr}, value: {} }},", naming::ruby_inspect_string(&condition.field), emit_query_condition_value(condition))
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
- .map(Json::each)
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 = match query_defs.iter().find(|q| q.assignments) {
669
- Some(q) => format!("Some({})", naming::ruby_inspect_string(&q.verb)),
670
- None => "None".to_string(),
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().map(|q| format!("{}\n", emit_entity_query_def(q))).collect();
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.entity.as_ref().expect("an entity query def carries its scope");
692
- let conditions = query_def.conditions.iter().map(|c| format!(" {}", emit_query_condition(c))).collect::<Vec<_>>().join("\n");
693
- let keys = entity.identity_keys.iter().map(|key| naming::ruby_inspect_string(key)).collect::<Vec<_>>().join(", ");
694
- let wrap = |value: &Option<String>| match value {
695
- Some(v) => format!("Some({v})"),
696
- None => "None".to_string(),
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(query: &Json, entity: &Json, holds_list: bool, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
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("entity_query", format!("{entity_name} is held in no list attribute on its aggregate — nothing to flatten")));
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("entity_query_authorization", "declares authorize — an entity query's tenant scope is not generated yet"));
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.conditions.iter().map(|c| format!(" {}", emit_query_condition(c))).collect::<Vec<_>>().join("\n");
723
- let reference_hop_conditions = query_def.reference_hop_conditions.iter().map(|h| format!(" {}", emit_reference_hop_condition(h))).collect::<Vec<_>>().join("\n");
724
- let order_by = match &query_def.order_by {
725
- Some(o) => format!("Some({o})"),
726
- None => "None".to_string(),
727
- };
728
- let offset = match &query_def.offset {
729
- Some(o) => format!("Some({o})"),
730
- None => "None".to_string(),
731
- };
732
- let limit = match &query_def.limit {
733
- Some(l) => format!("Some({l})"),
734
- None => "None".to_string(),
735
- };
736
- let authorization = match &query_def.authorization {
737
- Some(a) => format!("Some({a})"),
738
- None => "None".to_string(),
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>) = query_defs.iter().partition(|q| q.entity.is_some());
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("query_table", &[(QUERY_TABLE_ROW_PLACEHOLDER, rows.join("\n"))]),
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(query: &Json, mod_path: &str, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
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
- .map(Json::each)
766
- .unwrap_or(&[])
1043
+ .map_or_default(Json::each)
767
1044
  .iter()
768
1045
  .filter_map(|attr| {
769
- if crate::attr::list(attr) || !matches!(attr.get("relationship"), None | Some(Json::Null)) {
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 = crate::json_codec::composite_from_json_expr(attr, value_objects_by_name, "x");
775
- let check = if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) { "" } else { ".check_invariants()?" };
776
- Some(format!("if let Some(x) = args.get({}) {{ {mod_path}::{build}{check}; }}", naming::ruby_inspect_string(&key)))
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.arg_checks.iter().map(|line| format!(" {line}")).collect::<Vec<_>>().join("\n");
788
- format!(" {} => {{\n{lines}\n Ok(())\n }}", naming::ruby_inspect_string(&q.verb))
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
+ }