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
@@ -32,50 +32,146 @@ pub enum BlockMode {
32
32
  #[derive(Debug, Clone)]
33
33
  #[allow(dead_code)]
34
34
  pub enum Expr {
35
- Or { left: Box<Expr>, right: Box<Expr> },
36
- And { left: Box<Expr>, right: Box<Expr> },
37
- Not { expr: Box<Expr> },
38
- Compare { cmp: Comparison, left: Box<Expr>, right: Box<Expr> },
39
- Include { haystack: Box<Expr>, needle: Box<Expr> },
40
- Int { value: i64 },
41
- Float { value: f64 },
42
- Str { value: String },
43
- Bool { value: bool },
35
+ Or {
36
+ left: Box<Self>,
37
+ right: Box<Self>,
38
+ },
39
+ And {
40
+ left: Box<Self>,
41
+ right: Box<Self>,
42
+ },
43
+ Not {
44
+ expr: Box<Self>,
45
+ },
46
+ Compare {
47
+ cmp: Comparison,
48
+ left: Box<Self>,
49
+ right: Box<Self>,
50
+ },
51
+ Include {
52
+ haystack: Box<Self>,
53
+ needle: Box<Self>,
54
+ },
55
+ Int {
56
+ value: i64,
57
+ },
58
+ Float {
59
+ value: f64,
60
+ },
61
+ Str {
62
+ value: String,
63
+ },
64
+ Bool {
65
+ value: bool,
66
+ },
44
67
  Nil,
45
- Add { left: Box<Expr>, right: Box<Expr> },
46
- Sub { left: Box<Expr>, right: Box<Expr> },
47
- Mul { left: Box<Expr>, right: Box<Expr> },
48
- Div { left: Box<Expr>, right: Box<Expr> },
49
- SignTest { cmp: Comparison, receiver: Box<Expr> },
50
- Empty { receiver: Box<Expr> },
51
- ToS { receiver: Box<Expr> },
52
- Modulo { receiver: Box<Expr>, divisor: Box<Expr> },
53
- Size { receiver: Box<Expr> },
54
- Lookup { path: Vec<String> },
55
- BlockPredicate { mode: BlockMode, receiver: Box<Expr>, param: String, predicate: Box<Expr> },
56
- Find { receiver: Box<Expr>, param: String, predicate: Box<Expr>, path: Vec<String> },
57
- Array { elements: Vec<Expr> },
58
- MatchesRegex { receiver: Box<Expr>, pattern: String, flags: String },
59
- Presence { receiver: Box<Expr>, negated: bool },
60
- Assignment { receiver: Box<Expr>, negated: bool },
61
- Split { receiver: Box<Expr>, separator: String },
62
- Strip { receiver: Box<Expr>, side: StripSide },
63
- StartsWith { receiver: Box<Expr>, substring: String },
64
- EndsWith { receiver: Box<Expr>, substring: String },
65
- First { receiver: Box<Expr> },
66
- Last { receiver: Box<Expr> },
68
+ Add {
69
+ left: Box<Self>,
70
+ right: Box<Self>,
71
+ },
72
+ Sub {
73
+ left: Box<Self>,
74
+ right: Box<Self>,
75
+ },
76
+ Mul {
77
+ left: Box<Self>,
78
+ right: Box<Self>,
79
+ },
80
+ Div {
81
+ left: Box<Self>,
82
+ right: Box<Self>,
83
+ },
84
+ SignTest {
85
+ cmp: Comparison,
86
+ receiver: Box<Self>,
87
+ },
88
+ Empty {
89
+ receiver: Box<Self>,
90
+ },
91
+ ToS {
92
+ receiver: Box<Self>,
93
+ },
94
+ Modulo {
95
+ receiver: Box<Self>,
96
+ divisor: Box<Self>,
97
+ },
98
+ Size {
99
+ receiver: Box<Self>,
100
+ },
101
+ Lookup {
102
+ path: Vec<String>,
103
+ },
104
+ BlockPredicate {
105
+ mode: BlockMode,
106
+ receiver: Box<Self>,
107
+ param: String,
108
+ predicate: Box<Self>,
109
+ },
110
+ Find {
111
+ receiver: Box<Self>,
112
+ param: String,
113
+ predicate: Box<Self>,
114
+ path: Vec<String>,
115
+ },
116
+ Array {
117
+ elements: Vec<Self>,
118
+ },
119
+ MatchesRegex {
120
+ receiver: Box<Self>,
121
+ pattern: String,
122
+ flags: String,
123
+ },
124
+ Presence {
125
+ receiver: Box<Self>,
126
+ negated: bool,
127
+ },
128
+ Assignment {
129
+ receiver: Box<Self>,
130
+ negated: bool,
131
+ },
132
+ Split {
133
+ receiver: Box<Self>,
134
+ separator: String,
135
+ },
136
+ Strip {
137
+ receiver: Box<Self>,
138
+ side: StripSide,
139
+ },
140
+ StartsWith {
141
+ receiver: Box<Self>,
142
+ substring: String,
143
+ },
144
+ EndsWith {
145
+ receiver: Box<Self>,
146
+ substring: String,
147
+ },
148
+ First {
149
+ receiver: Box<Self>,
150
+ },
151
+ Last {
152
+ receiver: Box<Self>,
153
+ },
67
154
  }
68
155
 
69
156
  /// Parses an invariant's `ast` node into an `Expr`.
70
157
  ///
71
158
  /// An unrecognized `"op"`, or a malformed shape under a known one, is an `Err`.
72
159
  pub fn parse(json: &Json) -> Result<Expr, String> {
73
- let op = json.get("op").and_then(Json::as_str).ok_or_else(|| format!("{json} has no string \"op\" field"))?;
74
-
75
- let field = |name: &str| json.get(name).ok_or_else(|| format!("{op} node has no {name:?} field: {json}"));
160
+ let op = json
161
+ .get("op")
162
+ .and_then(Json::as_str)
163
+ .ok_or_else(|| format!("{json} has no string \"op\" field"))?;
164
+
165
+ let field = |name: &str| {
166
+ json.get(name)
167
+ .ok_or_else(|| format!("{op} node has no {name:?} field: {json}"))
168
+ };
76
169
  let expr = |name: &str| parse(field(name)?);
77
170
  let str_field = |name: &str| -> Result<String, String> {
78
- field(name)?.as_str().map(str::to_string).ok_or_else(|| format!("{op}'s {name:?} isn't a string: {json}"))
171
+ field(name)?
172
+ .as_str()
173
+ .map(str::to_string)
174
+ .ok_or_else(|| format!("{op}'s {name:?} isn't a string: {json}"))
79
175
  };
80
176
  // `lookup.path` and `find.path` are segment arrays, never dotted strings.
81
177
  let path_field = |name: &str| -> Result<Vec<String>, String> {
@@ -83,45 +179,117 @@ pub fn parse(json: &Json) -> Result<Expr, String> {
83
179
  .as_array()
84
180
  .ok_or_else(|| format!("{op}'s {name:?} isn't an array: {json}"))?
85
181
  .iter()
86
- .map(|v| v.as_str().map(str::to_string).ok_or_else(|| format!("{op}'s {name:?} has a non-string element: {json}")))
182
+ .map(|v| {
183
+ v.as_str()
184
+ .map(str::to_string)
185
+ .ok_or_else(|| format!("{op}'s {name:?} has a non-string element: {json}"))
186
+ })
87
187
  .collect::<Result<Vec<_>, _>>()
88
188
  };
89
189
  let comparison = || -> Result<Comparison, String> {
90
190
  let cmp = field("cmp")?;
91
191
  Ok(Comparison {
92
- less_than: cmp.get("less_than").and_then(Json::as_bool).ok_or_else(|| format!("{op}'s cmp has no less_than: {json}"))?,
93
- equal: cmp.get("equal").and_then(Json::as_bool).ok_or_else(|| format!("{op}'s cmp has no equal: {json}"))?,
94
- negated: cmp.get("negated").and_then(Json::as_bool).ok_or_else(|| format!("{op}'s cmp has no negated: {json}"))?,
192
+ less_than: cmp
193
+ .get("less_than")
194
+ .and_then(Json::as_bool)
195
+ .ok_or_else(|| format!("{op}'s cmp has no less_than: {json}"))?,
196
+ equal: cmp
197
+ .get("equal")
198
+ .and_then(Json::as_bool)
199
+ .ok_or_else(|| format!("{op}'s cmp has no equal: {json}"))?,
200
+ negated: cmp
201
+ .get("negated")
202
+ .and_then(Json::as_bool)
203
+ .ok_or_else(|| format!("{op}'s cmp has no negated: {json}"))?,
95
204
  })
96
205
  };
97
206
 
98
207
  Ok(match op {
99
- "or" => Expr::Or { left: Box::new(expr("left")?), right: Box::new(expr("right")?) },
100
- "and" => Expr::And { left: Box::new(expr("left")?), right: Box::new(expr("right")?) },
101
- "not" => Expr::Not { expr: Box::new(expr("expr")?) },
102
- "compare" => Expr::Compare { cmp: comparison()?, left: Box::new(expr("left")?), right: Box::new(expr("right")?) },
103
- "include" => Expr::Include { haystack: Box::new(expr("haystack")?), needle: Box::new(expr("needle")?) },
104
- "int" => Expr::Int { value: field("value")?.as_i64().ok_or_else(|| format!("int's value isn't an integer: {json}"))? },
105
- "float" => Expr::Float { value: field("value")?.as_f64().ok_or_else(|| format!("float's value isn't a number: {json}"))? },
106
- "str" => Expr::Str { value: str_field("value")? },
107
- "bool" => Expr::Bool { value: field("value")?.as_bool().ok_or_else(|| format!("bool's value isn't a boolean: {json}"))? },
208
+ "or" => Expr::Or {
209
+ left: Box::new(expr("left")?),
210
+ right: Box::new(expr("right")?),
211
+ },
212
+ "and" => Expr::And {
213
+ left: Box::new(expr("left")?),
214
+ right: Box::new(expr("right")?),
215
+ },
216
+ "not" => Expr::Not {
217
+ expr: Box::new(expr("expr")?),
218
+ },
219
+ "compare" => Expr::Compare {
220
+ cmp: comparison()?,
221
+ left: Box::new(expr("left")?),
222
+ right: Box::new(expr("right")?),
223
+ },
224
+ "include" => Expr::Include {
225
+ haystack: Box::new(expr("haystack")?),
226
+ needle: Box::new(expr("needle")?),
227
+ },
228
+ "int" => Expr::Int {
229
+ value: field("value")?
230
+ .as_i64()
231
+ .ok_or_else(|| format!("int's value isn't an integer: {json}"))?,
232
+ },
233
+ "float" => Expr::Float {
234
+ value: field("value")?
235
+ .as_f64()
236
+ .ok_or_else(|| format!("float's value isn't a number: {json}"))?,
237
+ },
238
+ "str" => Expr::Str {
239
+ value: str_field("value")?,
240
+ },
241
+ "bool" => Expr::Bool {
242
+ value: field("value")?
243
+ .as_bool()
244
+ .ok_or_else(|| format!("bool's value isn't a boolean: {json}"))?,
245
+ },
108
246
  "nil" => Expr::Nil,
109
- "add" => Expr::Add { left: Box::new(expr("left")?), right: Box::new(expr("right")?) },
110
- "sub" => Expr::Sub { left: Box::new(expr("left")?), right: Box::new(expr("right")?) },
111
- "mul" => Expr::Mul { left: Box::new(expr("left")?), right: Box::new(expr("right")?) },
112
- "div" => Expr::Div { left: Box::new(expr("left")?), right: Box::new(expr("right")?) },
113
- "sign_test" => Expr::SignTest { cmp: comparison()?, receiver: Box::new(expr("receiver")?) },
114
- "empty" => Expr::Empty { receiver: Box::new(expr("receiver")?) },
115
- "to_s" => Expr::ToS { receiver: Box::new(expr("receiver")?) },
116
- "modulo" => Expr::Modulo { receiver: Box::new(expr("receiver")?), divisor: Box::new(expr("divisor")?) },
117
- "size" => Expr::Size { receiver: Box::new(expr("receiver")?) },
118
- "lookup" => Expr::Lookup { path: path_field("path")? },
247
+ "add" => Expr::Add {
248
+ left: Box::new(expr("left")?),
249
+ right: Box::new(expr("right")?),
250
+ },
251
+ "sub" => Expr::Sub {
252
+ left: Box::new(expr("left")?),
253
+ right: Box::new(expr("right")?),
254
+ },
255
+ "mul" => Expr::Mul {
256
+ left: Box::new(expr("left")?),
257
+ right: Box::new(expr("right")?),
258
+ },
259
+ "div" => Expr::Div {
260
+ left: Box::new(expr("left")?),
261
+ right: Box::new(expr("right")?),
262
+ },
263
+ "sign_test" => Expr::SignTest {
264
+ cmp: comparison()?,
265
+ receiver: Box::new(expr("receiver")?),
266
+ },
267
+ "empty" => Expr::Empty {
268
+ receiver: Box::new(expr("receiver")?),
269
+ },
270
+ "to_s" => Expr::ToS {
271
+ receiver: Box::new(expr("receiver")?),
272
+ },
273
+ "modulo" => Expr::Modulo {
274
+ receiver: Box::new(expr("receiver")?),
275
+ divisor: Box::new(expr("divisor")?),
276
+ },
277
+ "size" => Expr::Size {
278
+ receiver: Box::new(expr("receiver")?),
279
+ },
280
+ "lookup" => Expr::Lookup {
281
+ path: path_field("path")?,
282
+ },
119
283
  "block_predicate" => Expr::BlockPredicate {
120
284
  mode: match str_field("mode")?.as_str() {
121
285
  "all" => BlockMode::All,
122
286
  "any" => BlockMode::Any,
123
287
  "none" => BlockMode::None,
124
- other => return Err(format!("block_predicate's mode {other:?} is none of all/any/none: {json}")),
288
+ other => {
289
+ return Err(format!(
290
+ "block_predicate's mode {other:?} is none of all/any/none: {json}"
291
+ ))
292
+ }
125
293
  },
126
294
  receiver: Box::new(expr("receiver")?),
127
295
  param: str_field("param")?,
@@ -141,29 +309,54 @@ pub fn parse(json: &Json) -> Result<Expr, String> {
141
309
  .map(parse)
142
310
  .collect::<Result<Vec<_>, _>>()?,
143
311
  },
144
- "matches_regex" => Expr::MatchesRegex { receiver: Box::new(expr("receiver")?), pattern: str_field("pattern")?, flags: str_field("flags")? },
312
+ "matches_regex" => Expr::MatchesRegex {
313
+ receiver: Box::new(expr("receiver")?),
314
+ pattern: str_field("pattern")?,
315
+ flags: str_field("flags")?,
316
+ },
145
317
  "presence" => Expr::Presence {
146
318
  receiver: Box::new(expr("receiver")?),
147
- negated: field("negated")?.as_bool().ok_or_else(|| format!("presence's negated isn't a boolean: {json}"))?,
319
+ negated: field("negated")?
320
+ .as_bool()
321
+ .ok_or_else(|| format!("presence's negated isn't a boolean: {json}"))?,
148
322
  },
149
323
  "assignment" => Expr::Assignment {
150
324
  receiver: Box::new(expr("receiver")?),
151
- negated: field("negated")?.as_bool().ok_or_else(|| format!("assignment's negated isn't a boolean: {json}"))?,
325
+ negated: field("negated")?
326
+ .as_bool()
327
+ .ok_or_else(|| format!("assignment's negated isn't a boolean: {json}"))?,
328
+ },
329
+ "split" => Expr::Split {
330
+ receiver: Box::new(expr("receiver")?),
331
+ separator: str_field("separator")?,
152
332
  },
153
- "split" => Expr::Split { receiver: Box::new(expr("receiver")?), separator: str_field("separator")? },
154
333
  "strip" => Expr::Strip {
155
334
  receiver: Box::new(expr("receiver")?),
156
335
  side: match str_field("side")?.as_str() {
157
336
  "both" => StripSide::Both,
158
337
  "left" => StripSide::Left,
159
338
  "right" => StripSide::Right,
160
- other => return Err(format!("strip's side {other:?} is none of both/left/right: {json}")),
339
+ other => {
340
+ return Err(format!(
341
+ "strip's side {other:?} is none of both/left/right: {json}"
342
+ ))
343
+ }
161
344
  },
162
345
  },
163
- "starts_with" => Expr::StartsWith { receiver: Box::new(expr("receiver")?), substring: str_field("substring")? },
164
- "ends_with" => Expr::EndsWith { receiver: Box::new(expr("receiver")?), substring: str_field("substring")? },
165
- "first" => Expr::First { receiver: Box::new(expr("receiver")?) },
166
- "last" => Expr::Last { receiver: Box::new(expr("receiver")?) },
346
+ "starts_with" => Expr::StartsWith {
347
+ receiver: Box::new(expr("receiver")?),
348
+ substring: str_field("substring")?,
349
+ },
350
+ "ends_with" => Expr::EndsWith {
351
+ receiver: Box::new(expr("receiver")?),
352
+ substring: str_field("substring")?,
353
+ },
354
+ "first" => Expr::First {
355
+ receiver: Box::new(expr("receiver")?),
356
+ },
357
+ "last" => Expr::Last {
358
+ receiver: Box::new(expr("receiver")?),
359
+ },
167
360
  other => return Err(format!("unrecognized expression op {other:?}: {json}")),
168
361
  })
169
362
  }
@@ -180,7 +373,7 @@ pub enum Value {
180
373
  Str(String),
181
374
  Bool(bool),
182
375
  Nil,
183
- Array(Vec<Value>),
376
+ Array(Vec<Self>),
184
377
  Object(Json),
185
378
  }
186
379
 
@@ -188,32 +381,33 @@ impl Value {
188
381
  /// Ruby truthiness: everything but `Nil` and `false`.
189
382
  ///
190
383
  /// Invariant results are checked for truthiness, not `== true`, as `given`/`ensures` are.
191
- pub(crate) fn truthy(&self) -> bool {
192
- !matches!(self, Value::Nil | Value::Bool(false))
384
+ pub(crate) const fn truthy(&self) -> bool {
385
+ !matches!(self, Self::Nil | Self::Bool(false))
193
386
  }
194
387
 
195
- fn from_json(json: &Json) -> Result<Value, String> {
388
+ fn from_json(json: &Json) -> Result<Self, String> {
196
389
  match json {
197
- Json::Null => Ok(Value::Nil),
198
- Json::Bool(b) => Ok(Value::Bool(*b)),
199
- Json::Number(n) => {
200
- if let Some(i) = n.as_i64() {
201
- Ok(Value::Int(i))
202
- } else if let Some(f) = n.as_f64() {
203
- Ok(Value::Float(f))
204
- } else {
205
- Err(format!("{n} is a number this interpreter cannot represent"))
206
- }
207
- }
208
- Json::String(s) => Ok(Value::Str(s.clone())),
209
- Json::Array(items) => Ok(Value::Array(items.iter().map(Value::from_json).collect::<Result<Vec<_>, _>>()?)),
210
- Json::Object(_) => Ok(Value::Object(json.clone())),
390
+ Json::Null => Ok(Self::Nil),
391
+ Json::Bool(b) => Ok(Self::Bool(*b)),
392
+ Json::Number(n) => n
393
+ .as_i64()
394
+ .map(Self::Int)
395
+ .or_else(|| n.as_f64().map(Self::Float))
396
+ .ok_or_else(|| format!("{n} is a number this interpreter cannot represent")),
397
+ Json::String(s) => Ok(Self::Str(s.clone())),
398
+ Json::Array(items) => Ok(Self::Array(
399
+ items
400
+ .iter()
401
+ .map(Self::from_json)
402
+ .collect::<Result<Vec<_>, _>>()?,
403
+ )),
404
+ Json::Object(_) => Ok(Self::Object(json.clone())),
211
405
  }
212
406
  }
213
407
  }
214
408
 
215
409
  /// The value as an `f64` when it is an `Int` or `Float`.
216
- fn numeric(v: &Value) -> Option<f64> {
410
+ const fn numeric(v: &Value) -> Option<f64> {
217
411
  match v {
218
412
  Value::Int(i) => Some(*i as f64),
219
413
  Value::Float(f) => Some(*f),
@@ -223,23 +417,32 @@ fn numeric(v: &Value) -> Option<f64> {
223
417
 
224
418
  /// An `Int` and a `Float` are equal only when the float is a whole number that is that integer.
225
419
  fn int_equals_float(i: i64, f: f64) -> bool {
226
- f.is_finite() && f.fract() == 0.0 && (-9.223_372_036_854_775_808e18..9.223_372_036_854_775_808e18).contains(&f) && f as i64 == i
420
+ f.is_finite()
421
+ && f.fract() == 0.0
422
+ && (-9.223_372_036_854_776e18..9.223_372_036_854_776e18).contains(&f)
423
+ && f as i64 == i
227
424
  }
228
425
 
229
426
  /// Ruby's `==` after `Evaluator.equal?`: numbers compare across kinds, and lists and objects
230
427
  /// compare element by element with the same rule, so `[1] == [1.0]`.
428
+ #[allow(clippy::float_cmp)] // reason: Ruby's `Float#==` is exact, and this mirrors it
231
429
  fn values_equal(l: &Value, r: &Value) -> bool {
232
430
  match (l, r) {
233
431
  (Value::Int(a), Value::Int(b)) => a == b,
234
- (Value::Int(i), Value::Float(f)) | (Value::Float(f), Value::Int(i)) => int_equals_float(*i, *f),
432
+ (Value::Int(i), Value::Float(f)) | (Value::Float(f), Value::Int(i)) => {
433
+ int_equals_float(*i, *f)
434
+ }
235
435
  (Value::Float(a), Value::Float(b)) => a == b,
236
- (Value::Array(a), Value::Array(b)) => a.len() == b.len() && a.iter().zip(b).all(|(x, y)| values_equal(x, y)),
436
+ (Value::Array(a), Value::Array(b)) => {
437
+ a.len() == b.len() && a.iter().zip(b).all(|(x, y)| values_equal(x, y))
438
+ }
237
439
  (Value::Object(Json::Object(a)), Value::Object(Json::Object(b))) => {
238
440
  a.len() == b.len()
239
- && a.iter().all(|(key, x)| match (b.get(key), Value::from_json(x)) {
240
- (Some(y), Ok(x)) => Value::from_json(y).is_ok_and(|y| values_equal(&x, &y)),
241
- _ => false,
242
- })
441
+ && a.iter()
442
+ .all(|(key, x)| match (b.get(key), Value::from_json(x)) {
443
+ (Some(y), Ok(x)) => Value::from_json(y).is_ok_and(|y| values_equal(&x, &y)),
444
+ _ => false,
445
+ })
243
446
  }
244
447
  _ => l == r,
245
448
  }
@@ -253,14 +456,18 @@ fn less_than(l: &Value, r: &Value) -> Result<bool, String> {
253
456
  (Some(a), Some(b)) => Ok(a < b),
254
457
  _ => match (l, r) {
255
458
  (Value::Str(a), Value::Str(b)) => Ok(a < b),
256
- _ => Err(format!("comparison of {} with {} failed", class_of(l), describe(r))),
459
+ _ => Err(format!(
460
+ "comparison of {} with {} failed",
461
+ class_of(l),
462
+ describe(r)
463
+ )),
257
464
  },
258
465
  },
259
466
  }
260
467
  }
261
468
 
262
469
  /// ORs `<` and `==`, then negates if the operator says so; `SignTest` reuses it against 0.
263
- fn apply_comparison(op: &Comparison, l: &Value, r: &Value) -> Result<bool, String> {
470
+ fn apply_comparison(op: Comparison, l: &Value, r: &Value) -> Result<bool, String> {
264
471
  let lt = op.less_than && less_than(l, r)?;
265
472
  let eq = op.equal && values_equal(l, r);
266
473
  Ok(if op.negated { !(lt || eq) } else { lt || eq })
@@ -274,12 +481,15 @@ fn to_s(v: &Value) -> Result<String, String> {
274
481
  Value::Float(f) => Ok(ruby_float(*f)),
275
482
  Value::Bool(b) => Ok(b.to_string()),
276
483
  Value::Nil => Ok(String::new()),
277
- Value::Array(_) | Value::Object(_) => Err(format!("to_s expects a scalar, got {}", describe(v))),
484
+ Value::Array(_) | Value::Object(_) => {
485
+ Err(format!("to_s expects a scalar, got {}", describe(v)))
486
+ }
278
487
  }
279
488
  }
280
489
 
281
490
  /// Ruby's `Float#to_s`: shortest round-trip digits, fixed notation for exponents -4 through 14,
282
491
  /// `d.ddde+XX` outside that.
492
+ #[allow(clippy::expect_used)] // reason: Rust's exponent format always has `e` and an integer exponent
283
493
  fn ruby_float(f: f64) -> String {
284
494
  if f.is_nan() {
285
495
  return "NaN".to_string();
@@ -292,8 +502,10 @@ fn ruby_float(f: f64) -> String {
292
502
  return format!("{sign}0.0");
293
503
  }
294
504
  let scientific = format!("{:e}", f.abs());
295
- let (mantissa, exponent) = scientific.split_once('e').expect("`{:e}` always has an exponent");
296
- let exponent: i32 = exponent.parse().expect("`{:e}` exponent is an integer");
505
+ let (mantissa, exponent) = scientific
506
+ .split_once('e')
507
+ .expect("exponent form always has an exponent");
508
+ let exponent: i32 = exponent.parse().expect("exponent is an integer");
297
509
  let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
298
510
  if (-4..15).contains(&exponent) {
299
511
  if exponent >= 0 {
@@ -301,7 +513,10 @@ fn ruby_float(f: f64) -> String {
301
513
  let (int_part, frac_part) = if digits.len() > whole {
302
514
  (digits[..whole].to_string(), digits[whole..].to_string())
303
515
  } else {
304
- (format!("{digits}{}", "0".repeat(whole - digits.len())), "0".to_string())
516
+ (
517
+ format!("{digits}{}", "0".repeat(whole - digits.len())),
518
+ "0".to_string(),
519
+ )
305
520
  };
306
521
  format!("{sign}{int_part}.{frac_part}")
307
522
  } else {
@@ -309,12 +524,18 @@ fn ruby_float(f: f64) -> String {
309
524
  }
310
525
  } else {
311
526
  let frac = if digits.len() > 1 { &digits[1..] } else { "0" };
312
- format!("{sign}{}.{frac}e{}{:02}", &digits[..1], if exponent < 0 { '-' } else { '+' }, exponent.abs())
527
+ format!(
528
+ "{sign}{}.{frac}e{}{:02}",
529
+ &digits[..1],
530
+ if exponent < 0 { '-' } else { '+' },
531
+ exponent.abs()
532
+ )
313
533
  }
314
534
  }
315
535
 
316
536
  /// Ruby's `String#inspect` for a UTF-8 string.
317
537
  fn ruby_inspect(s: &str) -> String {
538
+ use std::fmt::Write;
318
539
  let mut out = String::from("\"");
319
540
  let mut chars = s.chars().peekable();
320
541
  while let Some(c) = chars.next() {
@@ -330,7 +551,9 @@ fn ruby_inspect(s: &str) -> String {
330
551
  '\u{0c}' => out.push_str("\\f"),
331
552
  '\u{1b}' => out.push_str("\\e"),
332
553
  '#' if matches!(chars.peek(), Some('{' | '$' | '@')) => out.push_str("\\#"),
333
- c if (c as u32) < 0x20 || c as u32 == 0x7f => out.push_str(&format!("\\u{:04X}", c as u32)),
554
+ c if (c as u32) < 0x20 || c as u32 == 0x7f => {
555
+ let _ = write!(out, "\\u{:04X}", c as u32); // writing to a String cannot fail
556
+ }
334
557
  c => out.push(c),
335
558
  }
336
559
  }
@@ -346,7 +569,10 @@ fn generate(v: &Value) -> String {
346
569
  Value::Int(i) => i.to_string(),
347
570
  Value::Float(f) => ruby_float(*f),
348
571
  Value::Str(s) => Json::String(s.clone()).to_string(),
349
- Value::Array(items) => format!("[{}]", items.iter().map(generate).collect::<Vec<_>>().join(",")),
572
+ Value::Array(items) => format!(
573
+ "[{}]",
574
+ items.iter().map(generate).collect::<Vec<_>>().join(",")
575
+ ),
350
576
  Value::Object(json) => json.to_string(),
351
577
  }
352
578
  }
@@ -361,7 +587,7 @@ fn describe(v: &Value) -> String {
361
587
  }
362
588
 
363
589
  /// The Ruby class name an error message gives for a value; `nil` for nil, as `class_of` does.
364
- fn class_of(v: &Value) -> &'static str {
590
+ const fn class_of(v: &Value) -> &'static str {
365
591
  match v {
366
592
  Value::Nil => "nil",
367
593
  Value::Int(_) => "Integer",
@@ -379,7 +605,10 @@ fn class_of(v: &Value) -> &'static str {
379
605
  fn require_number(v: &Value, operation: &str) -> Result<Value, String> {
380
606
  match v {
381
607
  Value::Int(_) | Value::Float(_) => Ok(v.clone()),
382
- other => Err(format!("{operation} expects a number, got {}", describe(other))),
608
+ other => Err(format!(
609
+ "{operation} expects a number, got {}",
610
+ describe(other)
611
+ )),
383
612
  }
384
613
  }
385
614
 
@@ -388,13 +617,19 @@ fn add(left: &Value, right: &Value) -> Result<Value, String> {
388
617
  let lhs = require_number(left, "addition")?;
389
618
  let rhs = require_number(right, "addition")?;
390
619
  if let (Value::Int(a), Value::Int(b)) = (&lhs, &rhs) {
391
- return a.checked_add(*b).map(Value::Int).ok_or_else(|| format!("addition overflowed: {a} + {b} does not fit in a 64-bit integer"));
620
+ return a.checked_add(*b).map(Value::Int).ok_or_else(|| {
621
+ format!("addition overflowed: {a} + {b} does not fit in a 64-bit integer")
622
+ });
392
623
  }
393
624
  let sum = numeric(&lhs).unwrap_or_default() + numeric(&rhs).unwrap_or_default();
394
625
  if sum.is_finite() {
395
626
  Ok(Value::Float(sum))
396
627
  } else {
397
- Err(format!("addition overflowed: {} + {} is not a finite number", to_s(&lhs)?, to_s(&rhs)?))
628
+ Err(format!(
629
+ "addition overflowed: {} + {} is not a finite number",
630
+ to_s(&lhs)?,
631
+ to_s(&rhs)?
632
+ ))
398
633
  }
399
634
  }
400
635
 
@@ -411,15 +646,22 @@ fn arithmetic(
411
646
  let lhs = require_number(left, name)?;
412
647
  let rhs = require_number(right, name)?;
413
648
  if let (Value::Int(a), Value::Int(b)) = (&lhs, &rhs) {
414
- return int_op(*a, *b)
415
- .map(Value::Int)
416
- .ok_or_else(|| format!("{name} overflowed: {a} {symbol} {b} does not fit in a 64-bit integer"));
417
- }
418
- let result = float_op(numeric(&lhs).unwrap_or_default(), numeric(&rhs).unwrap_or_default());
649
+ return int_op(*a, *b).map(Value::Int).ok_or_else(|| {
650
+ format!("{name} overflowed: {a} {symbol} {b} does not fit in a 64-bit integer")
651
+ });
652
+ }
653
+ let result = float_op(
654
+ numeric(&lhs).unwrap_or_default(),
655
+ numeric(&rhs).unwrap_or_default(),
656
+ );
419
657
  if result.is_finite() {
420
658
  Ok(Value::Float(result))
421
659
  } else {
422
- Err(format!("{name} overflowed: {} {symbol} {} is not a finite number", to_s(&lhs)?, to_s(&rhs)?))
660
+ Err(format!(
661
+ "{name} overflowed: {} {symbol} {} is not a finite number",
662
+ to_s(&lhs)?,
663
+ to_s(&rhs)?
664
+ ))
423
665
  }
424
666
  }
425
667
 
@@ -434,7 +676,11 @@ fn divide(left: &Value, right: &Value) -> Result<Value, String> {
434
676
  /// Floored integer division; `None` only for `i64::MIN / -1`, which does not fit in 64 bits.
435
677
  fn floor_div(a: i64, b: i64) -> Option<i64> {
436
678
  let quotient = a.checked_div(b)?;
437
- Some(if a % b != 0 && ((a < 0) != (b < 0)) { quotient - 1 } else { quotient })
679
+ Some(if a % b != 0 && ((a < 0) != (b < 0)) {
680
+ quotient - 1
681
+ } else {
682
+ quotient
683
+ })
438
684
  }
439
685
 
440
686
  /// `Resolver.apply_modulo`: floored like Ruby's `%`, so the result takes the divisor's sign.
@@ -450,12 +696,24 @@ fn modulo(receiver: &Value, divisor: &Value) -> Result<Value, String> {
450
696
  return Ok(Value::Int(0));
451
697
  }
452
698
  let raw = r % d;
453
- return Ok(Value::Int(if raw != 0 && (raw < 0) != (*d < 0) { raw + d } else { raw }));
699
+ return Ok(Value::Int(if raw != 0 && (raw < 0) != (*d < 0) {
700
+ raw + d
701
+ } else {
702
+ raw
703
+ }));
454
704
  }
455
705
  let x = numeric(&receiver).unwrap_or_default();
456
706
  let y = numeric(&divisor).unwrap_or_default();
457
- let remainder = if y.is_infinite() && !x.is_infinite() { x } else { x % y };
458
- Ok(Value::Float(if y * remainder < 0.0 { remainder + y } else { remainder }))
707
+ let remainder = if y.is_infinite() && !x.is_infinite() {
708
+ x
709
+ } else {
710
+ x % y
711
+ };
712
+ Ok(Value::Float(if y * remainder < 0.0 {
713
+ remainder + y
714
+ } else {
715
+ remainder
716
+ }))
459
717
  }
460
718
 
461
719
  /// `Resolver.blank?`: nil, false, and an empty string, list or object.
@@ -467,7 +725,7 @@ fn blank(v: &Value) -> bool {
467
725
  Value::Nil | Value::Bool(false) => true,
468
726
  Value::Str(s) => s.is_empty(),
469
727
  Value::Array(items) => items.is_empty(),
470
- Value::Object(json) => json.as_object().is_some_and(|fields| fields.is_empty()),
728
+ Value::Object(json) => json.as_object().is_some_and(serde_json::Map::is_empty),
471
729
  _ => false,
472
730
  }
473
731
  }
@@ -476,7 +734,11 @@ fn blank(v: &Value) -> bool {
476
734
  /// characters, anything else on the literal text; trailing empty fields are dropped.
477
735
  fn split(text: &str, separator: &str) -> Vec<Value> {
478
736
  let mut parts: Vec<String> = match separator {
479
- " " => text.split([' ', '\t', '\n', '\u{0b}', '\u{0c}', '\r']).filter(|p| !p.is_empty()).map(str::to_string).collect(),
737
+ " " => text
738
+ .split([' ', '\t', '\n', '\u{0b}', '\u{0c}', '\r'])
739
+ .filter(|p| !p.is_empty())
740
+ .map(str::to_string)
741
+ .collect(),
480
742
  "" => text.chars().map(String::from).collect(),
481
743
  sep => text.split(sep).map(str::to_string).collect(),
482
744
  };
@@ -504,6 +766,7 @@ fn strip(text: &str, side: StripSide) -> String {
504
766
  /// the classes as Unicode and the anchors as text anchors, so both are rewritten. `m` is
505
767
  /// Ruby's dot-matches-newline; `\Z` (end, or before a final newline) becomes `\n?\z`.
506
768
  fn ruby_regex(pattern: &str, flags: &str) -> Result<regex::Regex, String> {
769
+ use std::fmt::Write;
507
770
  let mut source = String::new();
508
771
  let mut in_class = false;
509
772
  let mut class_start = 0;
@@ -535,7 +798,9 @@ fn ruby_regex(pattern: &str, flags: &str) -> Result<regex::Regex, String> {
535
798
  };
536
799
  match (next.is_ascii_uppercase(), in_class) {
537
800
  (false, true) => source.push_str(positive),
538
- (false, false) => source.push_str(&format!("[{positive}]")),
801
+ (false, false) => {
802
+ let _ = write!(source, "[{positive}]"); // writing to a String cannot fail
803
+ }
539
804
  (true, _) => source.push_str(negative),
540
805
  }
541
806
  continue;
@@ -564,14 +829,19 @@ fn ruby_regex(pattern: &str, flags: &str) -> Result<regex::Regex, String> {
564
829
  .dot_matches_new_line(flags.contains('m'))
565
830
  .ignore_whitespace(flags.contains('x'))
566
831
  .build()
567
- .map_err(|e| format!("match? given an invalid pattern {} — {e}", ruby_inspect(pattern)))
832
+ .map_err(|e| {
833
+ format!(
834
+ "match? given an invalid pattern {} — {e}",
835
+ ruby_inspect(pattern)
836
+ )
837
+ })
568
838
  }
569
839
 
570
840
  /// One block parameter bound to its element, innermost first; `attrs` in `Resolver#fetch`.
571
841
  struct Scope<'a> {
572
842
  name: &'a str,
573
843
  value: Value,
574
- outer: Option<&'a Scope<'a>>,
844
+ outer: Option<&'a Self>,
575
845
  }
576
846
 
577
847
  impl Scope<'_> {
@@ -601,52 +871,91 @@ fn eval(expr: &Expr, instance: &Json, scope: Option<&Scope>) -> Result<Value, St
601
871
  Expr::Str { value } => Ok(Value::Str(value.clone())),
602
872
  Expr::Bool { value } => Ok(Value::Bool(*value)),
603
873
  Expr::Nil => Ok(Value::Nil),
604
- Expr::Array { elements } => Ok(Value::Array(elements.iter().map(recur).collect::<Result<Vec<_>, _>>()?)),
874
+ Expr::Array { elements } => Ok(Value::Array(
875
+ elements.iter().map(recur).collect::<Result<Vec<_>, _>>()?,
876
+ )),
605
877
  Expr::Lookup { path } => lookup(path, instance, scope),
606
- Expr::Or { left, right } => Ok(Value::Bool(recur(left)?.truthy() || recur(right)?.truthy())),
607
- Expr::And { left, right } => Ok(Value::Bool(recur(left)?.truthy() && recur(right)?.truthy())),
878
+ Expr::Or { left, right } => {
879
+ Ok(Value::Bool(recur(left)?.truthy() || recur(right)?.truthy()))
880
+ }
881
+ Expr::And { left, right } => {
882
+ Ok(Value::Bool(recur(left)?.truthy() && recur(right)?.truthy()))
883
+ }
608
884
  Expr::Not { expr } => Ok(Value::Bool(!recur(expr)?.truthy())),
609
- Expr::Compare { cmp, left, right } => Ok(Value::Bool(apply_comparison(cmp, &recur(left)?, &recur(right)?)?)),
885
+ Expr::Compare { cmp, left, right } => Ok(Value::Bool(apply_comparison(
886
+ *cmp,
887
+ &recur(left)?,
888
+ &recur(right)?,
889
+ )?)),
610
890
  // The needle resolves before the haystack, as `Evaluator.includes?` does.
611
891
  Expr::Include { haystack, needle } => {
612
892
  let wanted = recur(needle)?;
613
893
  match recur(haystack)? {
614
- Value::Array(items) => Ok(Value::Bool(items.iter().any(|item| values_equal(item, &wanted)))),
894
+ Value::Array(items) => Ok(Value::Bool(
895
+ items.iter().any(|item| values_equal(item, &wanted)),
896
+ )),
615
897
  Value::Str(text) => match &wanted {
616
898
  Value::Str(part) => Ok(Value::Bool(text.contains(part.as_str()))),
617
- other => Err(format!("no implicit conversion of {} into String", class_of(other))),
899
+ other => Err(format!(
900
+ "no implicit conversion of {} into String",
901
+ class_of(other)
902
+ )),
618
903
  },
619
904
  _ => Ok(Value::Bool(false)),
620
905
  }
621
906
  }
622
907
  Expr::Add { left, right } => add(&recur(left)?, &recur(right)?),
623
- Expr::Sub { left, right } => {
624
- arithmetic("subtraction", "-", &recur(left)?, &recur(right)?, i64::checked_sub, |a, b| a - b)
625
- }
626
- Expr::Mul { left, right } => {
627
- arithmetic("multiplication", "*", &recur(left)?, &recur(right)?, i64::checked_mul, |a, b| a * b)
628
- }
908
+ Expr::Sub { left, right } => arithmetic(
909
+ "subtraction",
910
+ "-",
911
+ &recur(left)?,
912
+ &recur(right)?,
913
+ i64::checked_sub,
914
+ |a, b| a - b,
915
+ ),
916
+ Expr::Mul { left, right } => arithmetic(
917
+ "multiplication",
918
+ "*",
919
+ &recur(left)?,
920
+ &recur(right)?,
921
+ i64::checked_mul,
922
+ |a, b| a * b,
923
+ ),
629
924
  Expr::Div { left, right } => divide(&recur(left)?, &recur(right)?),
630
925
  Expr::Modulo { receiver, divisor } => modulo(&recur(receiver)?, &recur(divisor)?),
631
926
  Expr::SignTest { cmp, receiver } => {
632
927
  let v = recur(receiver)?;
633
928
  if numeric(&v).is_none() {
634
- return Err(format!("{} expects a number, got {}", sign_test_name(cmp), describe(&v)));
929
+ return Err(format!(
930
+ "{} expects a number, got {}",
931
+ sign_test_name(*cmp),
932
+ describe(&v)
933
+ ));
635
934
  }
636
- Ok(Value::Bool(apply_comparison(cmp, &v, &Value::Int(0))?))
935
+ Ok(Value::Bool(apply_comparison(*cmp, &v, &Value::Int(0))?))
637
936
  }
638
937
  Expr::Empty { receiver } => match recur(receiver)? {
639
938
  Value::Str(s) => Ok(Value::Bool(s.is_empty())),
640
939
  Value::Array(items) => Ok(Value::Bool(items.is_empty())),
641
- other => Err(format!("empty? expects a list or string, got {}", describe(&other))),
940
+ other => Err(format!(
941
+ "empty? expects a list or string, got {}",
942
+ describe(&other)
943
+ )),
642
944
  },
643
945
  Expr::Size { receiver } => match recur(receiver)? {
644
946
  Value::Str(s) => Ok(Value::Int(s.chars().count() as i64)),
645
947
  Value::Array(items) => Ok(Value::Int(items.len() as i64)),
646
- other => Err(format!("size expects a list or string, got {}", describe(&other))),
948
+ other => Err(format!(
949
+ "size expects a list or string, got {}",
950
+ describe(&other)
951
+ )),
647
952
  },
648
953
  Expr::ToS { receiver } => Ok(Value::Str(to_s(&recur(receiver)?)?)),
649
- Expr::MatchesRegex { receiver, pattern, flags } => {
954
+ Expr::MatchesRegex {
955
+ receiver,
956
+ pattern,
957
+ flags,
958
+ } => {
650
959
  let text = match recur(receiver)? {
651
960
  Value::Str(s) => s,
652
961
  v @ (Value::Int(_) | Value::Float(_)) => to_s(&v)?,
@@ -655,9 +964,16 @@ fn eval(expr: &Expr, instance: &Json, scope: Option<&Scope>) -> Result<Value, St
655
964
  };
656
965
  Ok(Value::Bool(ruby_regex(pattern, flags)?.is_match(&text)))
657
966
  }
658
- Expr::Presence { receiver, negated } => Ok(Value::Bool(blank(&recur(receiver)?) == *negated)),
659
- Expr::Assignment { receiver, negated } => Ok(Value::Bool((recur(receiver)? == Value::Nil) == *negated)),
660
- Expr::Split { receiver, separator } => match recur(receiver)? {
967
+ Expr::Presence { receiver, negated } => {
968
+ Ok(Value::Bool(blank(&recur(receiver)?) == *negated))
969
+ }
970
+ Expr::Assignment { receiver, negated } => {
971
+ Ok(Value::Bool((recur(receiver)? == Value::Nil) == *negated))
972
+ }
973
+ Expr::Split {
974
+ receiver,
975
+ separator,
976
+ } => match recur(receiver)? {
661
977
  Value::Str(text) => Ok(Value::Array(split(&text, separator))),
662
978
  other => Err(format!("split expects a string, got {}", describe(&other))),
663
979
  },
@@ -673,17 +989,34 @@ fn eval(expr: &Expr, instance: &Json, scope: Option<&Scope>) -> Result<Value, St
673
989
  Value::Str(text) => Ok(Value::Str(strip(&text, *side))),
674
990
  other => Err(format!("strip expects a string, got {}", describe(&other))),
675
991
  },
676
- Expr::StartsWith { receiver, substring } => match recur(receiver)? {
992
+ Expr::StartsWith {
993
+ receiver,
994
+ substring,
995
+ } => match recur(receiver)? {
677
996
  Value::Str(text) => Ok(Value::Bool(text.starts_with(substring.as_str()))),
678
- other => Err(format!("start_with? expects a string, got {}", describe(&other))),
997
+ other => Err(format!(
998
+ "start_with? expects a string, got {}",
999
+ describe(&other)
1000
+ )),
679
1001
  },
680
- Expr::EndsWith { receiver, substring } => match recur(receiver)? {
1002
+ Expr::EndsWith {
1003
+ receiver,
1004
+ substring,
1005
+ } => match recur(receiver)? {
681
1006
  Value::Str(text) => Ok(Value::Bool(text.ends_with(substring.as_str()))),
682
- other => Err(format!("end_with? expects a string, got {}", describe(&other))),
1007
+ other => Err(format!(
1008
+ "end_with? expects a string, got {}",
1009
+ describe(&other)
1010
+ )),
683
1011
  },
684
1012
  // Every element is evaluated before aggregating, so an error on any element surfaces as
685
1013
  // it does in Ruby.
686
- Expr::BlockPredicate { mode, receiver, param, predicate } => {
1014
+ Expr::BlockPredicate {
1015
+ mode,
1016
+ receiver,
1017
+ param,
1018
+ predicate,
1019
+ } => {
687
1020
  let mode_name = match mode {
688
1021
  BlockMode::All => "all",
689
1022
  BlockMode::Any => "any",
@@ -691,7 +1024,12 @@ fn eval(expr: &Expr, instance: &Json, scope: Option<&Scope>) -> Result<Value, St
691
1024
  };
692
1025
  let items = match recur(receiver)? {
693
1026
  Value::Array(items) => items,
694
- other => return Err(format!("{mode_name}? expects a list, got {}", describe(&other))),
1027
+ other => {
1028
+ return Err(format!(
1029
+ "{mode_name}? expects a list, got {}",
1030
+ describe(&other)
1031
+ ))
1032
+ }
695
1033
  };
696
1034
  let mut outcomes = Vec::with_capacity(items.len());
697
1035
  for item in items {
@@ -704,14 +1042,23 @@ fn eval(expr: &Expr, instance: &Json, scope: Option<&Scope>) -> Result<Value, St
704
1042
  }))
705
1043
  }
706
1044
  // The first accepted element ends the scan; a miss is nil, not an error.
707
- Expr::Find { receiver, param, predicate, path } => {
1045
+ Expr::Find {
1046
+ receiver,
1047
+ param,
1048
+ predicate,
1049
+ path,
1050
+ } => {
708
1051
  let items = match recur(receiver)? {
709
1052
  Value::Array(items) => items,
710
1053
  other => return Err(format!("find expects a list, got {}", describe(&other))),
711
1054
  };
712
1055
  for item in items {
713
1056
  if bound(param, item.clone(), instance, scope, predicate)?.truthy() {
714
- return if path.is_empty() { Ok(unwrap_scalar(item)) } else { Ok(unwrap_scalar(walk_path(item, path)?)) };
1057
+ return if path.is_empty() {
1058
+ Ok(unwrap_scalar(item))
1059
+ } else {
1060
+ Ok(unwrap_scalar(walk_path(item, path)?))
1061
+ };
715
1062
  }
716
1063
  }
717
1064
  Ok(Value::Nil)
@@ -720,14 +1067,24 @@ fn eval(expr: &Expr, instance: &Json, scope: Option<&Scope>) -> Result<Value, St
720
1067
  }
721
1068
 
722
1069
  /// Evaluates a block's `predicate` with `param` bound to `item`, shadowing any same-named field.
723
- fn bound(param: &str, item: Value, instance: &Json, scope: Option<&Scope>, predicate: &Expr) -> Result<Value, String> {
724
- let inner = Scope { name: param, value: item, outer: scope };
1070
+ fn bound(
1071
+ param: &str,
1072
+ item: Value,
1073
+ instance: &Json,
1074
+ scope: Option<&Scope>,
1075
+ predicate: &Expr,
1076
+ ) -> Result<Value, String> {
1077
+ let inner = Scope {
1078
+ name: param,
1079
+ value: item,
1080
+ outer: scope,
1081
+ };
725
1082
  eval(predicate, instance, Some(&inner))
726
1083
  }
727
1084
 
728
1085
  /// The `sign_test` a comparison's flags spell, for the error message: `positive?` is
729
1086
  /// `!(< 0 || == 0)`, `negative?` is `< 0`, `zero?` is `== 0`.
730
- fn sign_test_name(cmp: &Comparison) -> &'static str {
1087
+ const fn sign_test_name(cmp: Comparison) -> &'static str {
731
1088
  match (cmp.less_than, cmp.equal) {
732
1089
  (true, true) => "positive?",
733
1090
  (true, false) => "negative?",
@@ -738,7 +1095,10 @@ fn sign_test_name(cmp: &Comparison) -> &'static str {
738
1095
  /// `Resolver.unwrap_scalar`: a one-field value object reads as its one value.
739
1096
  fn unwrap_scalar(v: Value) -> Value {
740
1097
  let sole = match &v {
741
- Value::Object(Json::Object(fields)) if fields.len() == 1 => fields.values().next().and_then(|only| Value::from_json(only).ok()),
1098
+ Value::Object(Json::Object(fields)) if fields.len() == 1 => fields
1099
+ .values()
1100
+ .next()
1101
+ .and_then(|only| Value::from_json(only).ok()),
742
1102
  _ => None,
743
1103
  };
744
1104
  sole.unwrap_or(v)
@@ -758,7 +1118,13 @@ fn walk_path(start: Value, segments: &[String]) -> Result<Value, String> {
758
1118
  },
759
1119
  Value::Str(text) if text.contains(segment.as_str()) => Value::Str(segment.clone()),
760
1120
  Value::Str(_) => Value::Nil,
761
- Value::Array(_) | Value::Int(_) => return Err(format!("cannot read {} from {}", ruby_inspect(segment), describe(&current))),
1121
+ Value::Array(_) | Value::Int(_) => {
1122
+ return Err(format!(
1123
+ "cannot read {} from {}",
1124
+ ruby_inspect(segment),
1125
+ describe(&current)
1126
+ ))
1127
+ }
762
1128
  _ => return Ok(Value::Nil),
763
1129
  };
764
1130
  }
@@ -773,9 +1139,12 @@ fn lookup(path: &[String], instance: &Json, scope: Option<&Scope>) -> Result<Val
773
1139
  };
774
1140
  let start = match scope.and_then(|s| s.get(head)) {
775
1141
  Some(value) => value.clone(),
776
- None => Value::from_json(
777
- instance.get(head).ok_or_else(|| format!("cannot resolve {} — no such attribute or argument", ruby_inspect(head)))?,
778
- )?,
1142
+ None => Value::from_json(instance.get(head).ok_or_else(|| {
1143
+ format!(
1144
+ "cannot resolve {} — no such attribute or argument",
1145
+ ruby_inspect(head)
1146
+ )
1147
+ })?)?,
779
1148
  };
780
1149
  Ok(unwrap_scalar(walk_path(start, rest)?))
781
1150
  }
@@ -784,6 +1153,7 @@ fn lookup(path: &[String], instance: &Json, scope: Option<&Scope>) -> Result<Val
784
1153
  mod tests {
785
1154
  use super::*;
786
1155
 
1156
+ #[allow(clippy::needless_pass_by_value)] // reason: test helper called with inline literals
787
1157
  fn field(name: &str, value: Json) -> Json {
788
1158
  serde_json::json!({ name: value })
789
1159
  }
@@ -795,15 +1165,30 @@ mod tests {
795
1165
  "left":{"op":"lookup","path":["cents"]},"right":{"op":"int","value":0}});
796
1166
  let expr = parse(&ast).expect("valid Expr JSON");
797
1167
 
798
- assert_eq!(interpret(&expr, &field("cents", serde_json::json!(1200))), Ok(Value::Bool(true)));
799
- assert_eq!(interpret(&expr, &field("cents", serde_json::json!(0))), Ok(Value::Bool(true)));
800
- assert_eq!(interpret(&expr, &field("cents", serde_json::json!(-1))), Ok(Value::Bool(false)));
1168
+ assert_eq!(
1169
+ interpret(&expr, &field("cents", serde_json::json!(1200))),
1170
+ Ok(Value::Bool(true))
1171
+ );
1172
+ assert_eq!(
1173
+ interpret(&expr, &field("cents", serde_json::json!(0))),
1174
+ Ok(Value::Bool(true))
1175
+ );
1176
+ assert_eq!(
1177
+ interpret(&expr, &field("cents", serde_json::json!(-1))),
1178
+ Ok(Value::Bool(false))
1179
+ );
801
1180
  }
802
1181
 
803
1182
  #[test]
804
1183
  fn a_compound_and_of_four_comparisons_matches_chess_square_bounds() {
805
1184
  // `file >= 0 && file <= 7 && rank >= 0 && rank <= 7`, chess.bluebook's `Square`.
806
- fn cmp(less_than: bool, equal: bool, negated: bool, field: &str, n: i64) -> serde_json::Value {
1185
+ fn cmp(
1186
+ less_than: bool,
1187
+ equal: bool,
1188
+ negated: bool,
1189
+ field: &str,
1190
+ n: i64,
1191
+ ) -> serde_json::Value {
807
1192
  serde_json::json!({"op":"compare","cmp":{"less_than":less_than,"equal":equal,"negated":negated},
808
1193
  "left":{"op":"lookup","path":[field]},"right":{"op":"int","value":n}})
809
1194
  }
@@ -826,8 +1211,14 @@ mod tests {
826
1211
  let ast = serde_json::json!({"op":"not","expr":{"op":"empty","receiver":{"op":"to_s","receiver":{"op":"lookup","path":["value"]}}}});
827
1212
  let expr = parse(&ast).expect("valid Expr JSON");
828
1213
 
829
- assert_eq!(interpret(&expr, &field("value", serde_json::json!("Margherita"))), Ok(Value::Bool(true)));
830
- assert_eq!(interpret(&expr, &field("value", serde_json::json!(""))), Ok(Value::Bool(false)));
1214
+ assert_eq!(
1215
+ interpret(&expr, &field("value", serde_json::json!("Margherita"))),
1216
+ Ok(Value::Bool(true))
1217
+ );
1218
+ assert_eq!(
1219
+ interpret(&expr, &field("value", serde_json::json!(""))),
1220
+ Ok(Value::Bool(false))
1221
+ );
831
1222
  }
832
1223
 
833
1224
  #[test]
@@ -836,9 +1227,18 @@ mod tests {
836
1227
  let ast = serde_json::json!({"op":"sign_test","cmp":{"less_than":true,"equal":true,"negated":true},"receiver":{"op":"lookup","path":["value"]}});
837
1228
  let expr = parse(&ast).expect("valid Expr JSON");
838
1229
 
839
- assert_eq!(interpret(&expr, &field("value", serde_json::json!(5))), Ok(Value::Bool(true)));
840
- assert_eq!(interpret(&expr, &field("value", serde_json::json!(0))), Ok(Value::Bool(false)));
841
- assert_eq!(interpret(&expr, &field("value", serde_json::json!(-5))), Ok(Value::Bool(false)));
1230
+ assert_eq!(
1231
+ interpret(&expr, &field("value", serde_json::json!(5))),
1232
+ Ok(Value::Bool(true))
1233
+ );
1234
+ assert_eq!(
1235
+ interpret(&expr, &field("value", serde_json::json!(0))),
1236
+ Ok(Value::Bool(false))
1237
+ );
1238
+ assert_eq!(
1239
+ interpret(&expr, &field("value", serde_json::json!(-5))),
1240
+ Ok(Value::Bool(false))
1241
+ );
842
1242
  }
843
1243
 
844
1244
  #[test]
@@ -847,7 +1247,10 @@ mod tests {
847
1247
  "left":{"op":"lookup","path":["cents"]},"right":{"op":"float","value":3.0}});
848
1248
  let expr = parse(&ast).expect("valid Expr JSON");
849
1249
 
850
- assert_eq!(interpret(&expr, &field("cents", serde_json::json!(3))), Ok(Value::Bool(true)));
1250
+ assert_eq!(
1251
+ interpret(&expr, &field("cents", serde_json::json!(3))),
1252
+ Ok(Value::Bool(true))
1253
+ );
851
1254
  }
852
1255
 
853
1256
  #[test]
@@ -861,6 +1264,7 @@ mod tests {
861
1264
  // differential spec `spec/rust_host_expr_json_conformance_spec.rb` re-checks them live.
862
1265
  use serde_json::json;
863
1266
 
1267
+ #[allow(clippy::needless_pass_by_value)] // reason: test helper called with inline literals
864
1268
  fn run(ast: Json, instance: Json) -> Result<Value, String> {
865
1269
  interpret(&parse(&ast).expect("valid Expr JSON"), &instance)
866
1270
  }
@@ -877,10 +1281,12 @@ mod tests {
877
1281
  json!({"op":"str","value":s})
878
1282
  }
879
1283
 
1284
+ #[allow(clippy::needless_pass_by_value)] // reason: test helper called with inline literals
880
1285
  fn call(op: &str, receiver: Json) -> Json {
881
1286
  json!({"op":op,"receiver":receiver})
882
1287
  }
883
1288
 
1289
+ #[allow(clippy::needless_pass_by_value)] // reason: test helper called with inline literals
884
1290
  fn eq(left: Json, right: Json) -> Json {
885
1291
  json!({"op":"compare","cmp":{"less_than":false,"equal":true,"negated":false},"left":left,"right":right})
886
1292
  }
@@ -893,19 +1299,37 @@ mod tests {
893
1299
  fn add_sums_numbers_and_promotes_to_float() {
894
1300
  let add = |l: Json, r: Json| json!({"op":"add","left":l,"right":r});
895
1301
  assert_eq!(run(add(int(2), int(3)), json!({})), Ok(Value::Int(5)));
896
- assert_eq!(run(add(int(2), json!({"op":"float","value":1.5})), json!({})), Ok(Value::Float(3.5)));
897
- assert_eq!(run(add(lookup_of("a"), int(1)), json!({"a": 9})), Ok(Value::Int(10)));
1302
+ assert_eq!(
1303
+ run(add(int(2), json!({"op":"float","value":1.5})), json!({})),
1304
+ Ok(Value::Float(3.5))
1305
+ );
1306
+ assert_eq!(
1307
+ run(add(lookup_of("a"), int(1)), json!({"a": 9})),
1308
+ Ok(Value::Int(10))
1309
+ );
898
1310
  }
899
1311
 
900
1312
  #[test]
901
1313
  fn add_refuses_a_non_number_and_an_overflow_with_rubys_words() {
902
1314
  let add = |l: Json, r: Json| json!({"op":"add","left":l,"right":r});
903
- assert_eq!(run(add(string("a"), int(1)), json!({})), Err("addition expects a number, got \"a\"".to_string()));
904
- assert_eq!(run(add(lookup_of("a"), int(1)), json!({"a": null})), Err("addition expects a number, got nil".to_string()));
905
- assert_eq!(run(add(int(1), lookup_of("a")), json!({"a": [1]})), Err("addition expects a number, got [1]".to_string()));
1315
+ assert_eq!(
1316
+ run(add(string("a"), int(1)), json!({})),
1317
+ Err("addition expects a number, got \"a\"".to_string())
1318
+ );
1319
+ assert_eq!(
1320
+ run(add(lookup_of("a"), int(1)), json!({"a": null})),
1321
+ Err("addition expects a number, got nil".to_string())
1322
+ );
1323
+ assert_eq!(
1324
+ run(add(int(1), lookup_of("a")), json!({"a": [1]})),
1325
+ Err("addition expects a number, got [1]".to_string())
1326
+ );
906
1327
  assert_eq!(
907
1328
  run(add(int(i64::MAX), int(1)), json!({})),
908
- Err("addition overflowed: 9223372036854775807 + 1 does not fit in a 64-bit integer".to_string())
1329
+ Err(
1330
+ "addition overflowed: 9223372036854775807 + 1 does not fit in a 64-bit integer"
1331
+ .to_string()
1332
+ )
909
1333
  );
910
1334
  let huge = json!({"op":"float","value":1.7e308});
911
1335
  assert_eq!(
@@ -918,19 +1342,40 @@ mod tests {
918
1342
  fn sub_mul_div_follow_rubys_limits_and_floor_integer_division() {
919
1343
  let op = |name: &str, l: Json, r: Json| json!({"op":name,"left":l,"right":r});
920
1344
  assert_eq!(run(op("sub", int(5), int(2)), json!({})), Ok(Value::Int(3)));
921
- assert_eq!(run(op("mul", int(6), json!({"op":"float","value":0.5})), json!({})), Ok(Value::Float(3.0)));
922
- assert_eq!(run(op("div", int(-7), int(2)), json!({})), Ok(Value::Int(-4)));
923
- assert_eq!(run(op("div", int(7), int(-2)), json!({})), Ok(Value::Int(-4)));
924
- assert_eq!(run(op("div", int(7), int(0)), json!({})), Err("divided by 0".to_string()));
1345
+ assert_eq!(
1346
+ run(
1347
+ op("mul", int(6), json!({"op":"float","value":0.5})),
1348
+ json!({})
1349
+ ),
1350
+ Ok(Value::Float(3.0))
1351
+ );
1352
+ assert_eq!(
1353
+ run(op("div", int(-7), int(2)), json!({})),
1354
+ Ok(Value::Int(-4))
1355
+ );
1356
+ assert_eq!(
1357
+ run(op("div", int(7), int(-2)), json!({})),
1358
+ Ok(Value::Int(-4))
1359
+ );
1360
+ assert_eq!(
1361
+ run(op("div", int(7), int(0)), json!({})),
1362
+ Err("divided by 0".to_string())
1363
+ );
925
1364
  assert_eq!(
926
1365
  run(op("mul", int(i64::MAX), int(2)), json!({})),
927
1366
  Err("multiplication overflowed: 9223372036854775807 * 2 does not fit in a 64-bit integer".to_string())
928
1367
  );
929
1368
  assert_eq!(
930
1369
  run(op("div", int(i64::MIN), int(-1)), json!({})),
931
- Err("division overflowed: -9223372036854775808 / -1 does not fit in a 64-bit integer".to_string())
1370
+ Err(
1371
+ "division overflowed: -9223372036854775808 / -1 does not fit in a 64-bit integer"
1372
+ .to_string()
1373
+ )
1374
+ );
1375
+ assert_eq!(
1376
+ run(op("sub", lookup_of("a"), int(1)), json!({"a": null})),
1377
+ Err("subtraction expects a number, got nil".to_string())
932
1378
  );
933
- assert_eq!(run(op("sub", lookup_of("a"), int(1)), json!({"a": null})), Err("subtraction expects a number, got nil".to_string()));
934
1379
  }
935
1380
 
936
1381
  #[test]
@@ -938,39 +1383,117 @@ mod tests {
938
1383
  let modulo = |r: Json, d: Json| json!({"op":"modulo","receiver":r,"divisor":d});
939
1384
  assert_eq!(run(modulo(int(-7), int(3)), json!({})), Ok(Value::Int(2)));
940
1385
  assert_eq!(run(modulo(int(7), int(-3)), json!({})), Ok(Value::Int(-2)));
941
- assert_eq!(run(modulo(int(i64::MIN), int(-1)), json!({})), Ok(Value::Int(0)));
942
- assert_eq!(run(modulo(json!({"op":"float","value":5.5}), int(2)), json!({})), Ok(Value::Float(1.5)));
943
- assert_eq!(run(modulo(int(7), json!({"op":"float","value":-2.5})), json!({})), Ok(Value::Float(-0.5)));
944
- assert_eq!(run(modulo(json!({"op":"float","value":-1.0}), int(2)), json!({})), Ok(Value::Float(1.0)));
1386
+ assert_eq!(
1387
+ run(modulo(int(i64::MIN), int(-1)), json!({})),
1388
+ Ok(Value::Int(0))
1389
+ );
1390
+ assert_eq!(
1391
+ run(modulo(json!({"op":"float","value":5.5}), int(2)), json!({})),
1392
+ Ok(Value::Float(1.5))
1393
+ );
1394
+ assert_eq!(
1395
+ run(
1396
+ modulo(int(7), json!({"op":"float","value":-2.5})),
1397
+ json!({})
1398
+ ),
1399
+ Ok(Value::Float(-0.5))
1400
+ );
1401
+ assert_eq!(
1402
+ run(
1403
+ modulo(json!({"op":"float","value":-1.0}), int(2)),
1404
+ json!({})
1405
+ ),
1406
+ Ok(Value::Float(1.0))
1407
+ );
945
1408
  }
946
1409
 
947
1410
  #[test]
948
1411
  fn modulo_refuses_a_zero_divisor_and_a_non_number() {
949
1412
  let modulo = |r: Json, d: Json| json!({"op":"modulo","receiver":r,"divisor":d});
950
- assert_eq!(run(modulo(int(5), int(0)), json!({})), Err("divided by 0".to_string()));
951
- assert_eq!(run(modulo(int(5), json!({"op":"float","value":0.0})), json!({})), Err("divided by 0".to_string()));
952
- assert_eq!(run(modulo(string("a"), int(2)), json!({})), Err("modulo expects a number, got \"a\"".to_string()));
953
- assert_eq!(run(modulo(int(2), lookup_of("d")), json!({"d": null})), Err("modulo expects a number, got nil".to_string()));
1413
+ assert_eq!(
1414
+ run(modulo(int(5), int(0)), json!({})),
1415
+ Err("divided by 0".to_string())
1416
+ );
1417
+ assert_eq!(
1418
+ run(modulo(int(5), json!({"op":"float","value":0.0})), json!({})),
1419
+ Err("divided by 0".to_string())
1420
+ );
1421
+ assert_eq!(
1422
+ run(modulo(string("a"), int(2)), json!({})),
1423
+ Err("modulo expects a number, got \"a\"".to_string())
1424
+ );
1425
+ assert_eq!(
1426
+ run(modulo(int(2), lookup_of("d")), json!({"d": null})),
1427
+ Err("modulo expects a number, got nil".to_string())
1428
+ );
954
1429
  }
955
1430
 
956
1431
  #[test]
957
1432
  fn include_tests_list_membership_with_numeric_coercion_and_substrings() {
958
1433
  let include = |h: Json, n: Json| json!({"op":"include","haystack":h,"needle":n});
959
- assert_eq!(run(include(lookup_of("tags"), string("b")), json!({"tags": ["a", "b"]})), Ok(Value::Bool(true)));
960
- assert_eq!(run(include(lookup_of("tags"), string("z")), json!({"tags": ["a", "b"]})), Ok(Value::Bool(false)));
961
- assert_eq!(run(include(lookup_of("n"), json!({"op":"float","value":2.0})), json!({"n": [1, 2]})), Ok(Value::Bool(true)));
962
- assert_eq!(run(include(lookup_of("n"), lookup_of("m")), json!({"n": [1, null], "m": null})), Ok(Value::Bool(true)));
963
- assert_eq!(run(include(lookup_of("s"), string("ell")), json!({"s": "hello"})), Ok(Value::Bool(true)));
964
- assert_eq!(run(include(lookup_of("s"), string("")), json!({"s": "hello"})), Ok(Value::Bool(true)));
1434
+ assert_eq!(
1435
+ run(
1436
+ include(lookup_of("tags"), string("b")),
1437
+ json!({"tags": ["a", "b"]})
1438
+ ),
1439
+ Ok(Value::Bool(true))
1440
+ );
1441
+ assert_eq!(
1442
+ run(
1443
+ include(lookup_of("tags"), string("z")),
1444
+ json!({"tags": ["a", "b"]})
1445
+ ),
1446
+ Ok(Value::Bool(false))
1447
+ );
1448
+ assert_eq!(
1449
+ run(
1450
+ include(lookup_of("n"), json!({"op":"float","value":2.0})),
1451
+ json!({"n": [1, 2]})
1452
+ ),
1453
+ Ok(Value::Bool(true))
1454
+ );
1455
+ assert_eq!(
1456
+ run(
1457
+ include(lookup_of("n"), lookup_of("m")),
1458
+ json!({"n": [1, null], "m": null})
1459
+ ),
1460
+ Ok(Value::Bool(true))
1461
+ );
1462
+ assert_eq!(
1463
+ run(
1464
+ include(lookup_of("s"), string("ell")),
1465
+ json!({"s": "hello"})
1466
+ ),
1467
+ Ok(Value::Bool(true))
1468
+ );
1469
+ assert_eq!(
1470
+ run(include(lookup_of("s"), string("")), json!({"s": "hello"})),
1471
+ Ok(Value::Bool(true))
1472
+ );
965
1473
  }
966
1474
 
967
1475
  #[test]
968
1476
  fn include_on_anything_but_a_list_or_string_is_false_and_a_string_needs_a_string() {
969
1477
  let include = |h: Json, n: Json| json!({"op":"include","haystack":h,"needle":n});
970
- assert_eq!(run(include(lookup_of("s"), string("a")), json!({"s": null})), Ok(Value::Bool(false)));
971
- assert_eq!(run(include(lookup_of("s"), string("a")), json!({"s": 5})), Ok(Value::Bool(false)));
972
- assert_eq!(run(include(lookup_of("s"), int(1)), json!({"s": "abc"})), Err("no implicit conversion of Integer into String".to_string()));
973
- assert_eq!(run(include(lookup_of("s"), lookup_of("n")), json!({"s": "abc", "n": null})), Err("no implicit conversion of nil into String".to_string()));
1478
+ assert_eq!(
1479
+ run(include(lookup_of("s"), string("a")), json!({"s": null})),
1480
+ Ok(Value::Bool(false))
1481
+ );
1482
+ assert_eq!(
1483
+ run(include(lookup_of("s"), string("a")), json!({"s": 5})),
1484
+ Ok(Value::Bool(false))
1485
+ );
1486
+ assert_eq!(
1487
+ run(include(lookup_of("s"), int(1)), json!({"s": "abc"})),
1488
+ Err("no implicit conversion of Integer into String".to_string())
1489
+ );
1490
+ assert_eq!(
1491
+ run(
1492
+ include(lookup_of("s"), lookup_of("n")),
1493
+ json!({"s": "abc", "n": null})
1494
+ ),
1495
+ Err("no implicit conversion of nil into String".to_string())
1496
+ );
974
1497
  }
975
1498
 
976
1499
  #[test]
@@ -978,42 +1501,108 @@ mod tests {
978
1501
  let array = json!({"op":"array","elements":[int(1), json!({"op":"float","value":2.5}), string("a"), json!({"op":"nil"})]});
979
1502
  assert_eq!(
980
1503
  run(array.clone(), json!({})),
981
- Ok(Value::Array(vec![Value::Int(1), Value::Float(2.5), Value::Str("a".to_string()), Value::Nil]))
1504
+ Ok(Value::Array(vec![
1505
+ Value::Int(1),
1506
+ Value::Float(2.5),
1507
+ Value::Str("a".to_string()),
1508
+ Value::Nil
1509
+ ]))
1510
+ );
1511
+ assert_eq!(
1512
+ run(call("first", array.clone()), json!({})),
1513
+ Ok(Value::Int(1))
982
1514
  );
983
- assert_eq!(run(call("first", array.clone()), json!({})), Ok(Value::Int(1)));
984
1515
  assert_eq!(run(call("last", array), json!({})), Ok(Value::Nil));
985
- assert_eq!(run(json!({"op":"array","elements":[]}), json!({})), Ok(Value::Array(vec![])));
1516
+ assert_eq!(
1517
+ run(json!({"op":"array","elements":[]}), json!({})),
1518
+ Ok(Value::Array(vec![]))
1519
+ );
986
1520
  }
987
1521
 
988
1522
  #[test]
989
1523
  fn matches_regex_reads_ruby_anchors_flags_and_ascii_classes() {
990
1524
  let matches = |pattern: &str, flags: &str| json!({"op":"matches_regex","receiver":lookup_of("x"),"pattern":pattern,"flags":flags});
991
- assert_eq!(run(matches("^b$", ""), json!({"x": "a\nb"})), Ok(Value::Bool(true)));
992
- assert_eq!(run(matches("\\Ab\\z", ""), json!({"x": "a\nb"})), Ok(Value::Bool(false)));
993
- assert_eq!(run(matches("a.b", ""), json!({"x": "a\nb"})), Ok(Value::Bool(false)));
994
- assert_eq!(run(matches("a.b", "m"), json!({"x": "a\nb"})), Ok(Value::Bool(true)));
995
- assert_eq!(run(matches("A", "i"), json!({"x": "a"})), Ok(Value::Bool(true)));
996
- assert_eq!(run(matches("\\d", ""), json!({"x": "\u{663}"})), Ok(Value::Bool(false)));
997
- assert_eq!(run(matches("\\A[\\w.]+\\z", ""), json!({"x": "a.b_c"})), Ok(Value::Bool(true)));
998
- assert_eq!(run(matches("a b", "x"), json!({"x": "ab"})), Ok(Value::Bool(true)));
999
- assert_eq!(run(matches("\\Aa\\Z", ""), json!({"x": "a\n"})), Ok(Value::Bool(true)));
1525
+ assert_eq!(
1526
+ run(matches("^b$", ""), json!({"x": "a\nb"})),
1527
+ Ok(Value::Bool(true))
1528
+ );
1529
+ assert_eq!(
1530
+ run(matches("\\Ab\\z", ""), json!({"x": "a\nb"})),
1531
+ Ok(Value::Bool(false))
1532
+ );
1533
+ assert_eq!(
1534
+ run(matches("a.b", ""), json!({"x": "a\nb"})),
1535
+ Ok(Value::Bool(false))
1536
+ );
1537
+ assert_eq!(
1538
+ run(matches("a.b", "m"), json!({"x": "a\nb"})),
1539
+ Ok(Value::Bool(true))
1540
+ );
1541
+ assert_eq!(
1542
+ run(matches("A", "i"), json!({"x": "a"})),
1543
+ Ok(Value::Bool(true))
1544
+ );
1545
+ assert_eq!(
1546
+ run(matches("\\d", ""), json!({"x": "\u{663}"})),
1547
+ Ok(Value::Bool(false))
1548
+ );
1549
+ assert_eq!(
1550
+ run(matches("\\A[\\w.]+\\z", ""), json!({"x": "a.b_c"})),
1551
+ Ok(Value::Bool(true))
1552
+ );
1553
+ assert_eq!(
1554
+ run(matches("a b", "x"), json!({"x": "ab"})),
1555
+ Ok(Value::Bool(true))
1556
+ );
1557
+ assert_eq!(
1558
+ run(matches("\\Aa\\Z", ""), json!({"x": "a\n"})),
1559
+ Ok(Value::Bool(true))
1560
+ );
1000
1561
  }
1001
1562
 
1002
1563
  #[test]
1003
1564
  fn matches_regex_coerces_scalars_and_refuses_the_rest_by_class() {
1004
1565
  let matches = json!({"op":"matches_regex","receiver":lookup_of("x"),"pattern":"\\A[0-9.]+\\z","flags":""});
1005
- assert_eq!(run(matches.clone(), json!({"x": 12})), Ok(Value::Bool(true)));
1006
- assert_eq!(run(matches.clone(), json!({"x": 1.5})), Ok(Value::Bool(true)));
1007
- assert_eq!(run(json!({"op":"matches_regex","receiver":lookup_of("x"),"pattern":"\\A\\z","flags":""}), json!({"x": null})), Ok(Value::Bool(true)));
1008
- assert_eq!(run(matches.clone(), json!({"x": true})), Err("match? expects a scalar, got TrueClass".to_string()));
1009
- assert_eq!(run(matches.clone(), json!({"x": false})), Err("match? expects a scalar, got FalseClass".to_string()));
1010
- assert_eq!(run(matches, json!({"x": [1]})), Err("match? expects a scalar, got Array".to_string()));
1566
+ assert_eq!(
1567
+ run(matches.clone(), json!({"x": 12})),
1568
+ Ok(Value::Bool(true))
1569
+ );
1570
+ assert_eq!(
1571
+ run(matches.clone(), json!({"x": 1.5})),
1572
+ Ok(Value::Bool(true))
1573
+ );
1574
+ assert_eq!(
1575
+ run(
1576
+ json!({"op":"matches_regex","receiver":lookup_of("x"),"pattern":"\\A\\z","flags":""}),
1577
+ json!({"x": null})
1578
+ ),
1579
+ Ok(Value::Bool(true))
1580
+ );
1581
+ assert_eq!(
1582
+ run(matches.clone(), json!({"x": true})),
1583
+ Err("match? expects a scalar, got TrueClass".to_string())
1584
+ );
1585
+ assert_eq!(
1586
+ run(matches.clone(), json!({"x": false})),
1587
+ Err("match? expects a scalar, got FalseClass".to_string())
1588
+ );
1589
+ assert_eq!(
1590
+ run(matches, json!({"x": [1]})),
1591
+ Err("match? expects a scalar, got Array".to_string())
1592
+ );
1011
1593
  }
1012
1594
 
1013
1595
  #[test]
1014
1596
  fn matches_regex_refuses_an_invalid_pattern_by_name() {
1015
- let err = run(json!({"op":"matches_regex","receiver":string("a"),"pattern":"(","flags":""}), json!({})).unwrap_err();
1016
- assert!(err.starts_with("match? given an invalid pattern \"(\" — "), "{err}");
1597
+ let err = run(
1598
+ json!({"op":"matches_regex","receiver":string("a"),"pattern":"(","flags":""}),
1599
+ json!({}),
1600
+ )
1601
+ .unwrap_err();
1602
+ assert!(
1603
+ err.starts_with("match? given an invalid pattern \"(\" — "),
1604
+ "{err}"
1605
+ );
1017
1606
  }
1018
1607
 
1019
1608
  #[test]
@@ -1037,15 +1626,24 @@ mod tests {
1037
1626
  // `all?` would stop at the first false in a short-circuit loop; Ruby maps first.
1038
1627
  let ast = json!({"op":"block_predicate","mode":"all","receiver":lookup_of("xs"),"param":"x",
1039
1628
  "predicate":json!({"op":"sign_test","cmp":{"less_than":true,"equal":true,"negated":true},"receiver":lookup_of("x")})});
1040
- assert_eq!(run(ast, json!({"xs": [-1, "a"]})), Err("positive? expects a number, got \"a\"".to_string()));
1629
+ assert_eq!(
1630
+ run(ast, json!({"xs": [-1, "a"]})),
1631
+ Err("positive? expects a number, got \"a\"".to_string())
1632
+ );
1041
1633
  }
1042
1634
 
1043
1635
  #[test]
1044
1636
  fn a_block_predicate_over_a_non_list_refuses_with_the_mode_named() {
1045
1637
  for (mode, word) in [("all", "all?"), ("any", "any?"), ("none", "none?")] {
1046
1638
  let ast = json!({"op":"block_predicate","mode":mode,"receiver":lookup_of("x"),"param":"y","predicate":{"op":"bool","value":true}});
1047
- assert_eq!(run(ast.clone(), json!({"x": 5})), Err(format!("{word} expects a list, got 5")));
1048
- assert_eq!(run(ast, json!({"x": null})), Err(format!("{word} expects a list, got nil")));
1639
+ assert_eq!(
1640
+ run(ast.clone(), json!({"x": 5})),
1641
+ Err(format!("{word} expects a list, got 5"))
1642
+ );
1643
+ assert_eq!(
1644
+ run(ast, json!({"x": null})),
1645
+ Err(format!("{word} expects a list, got nil"))
1646
+ );
1049
1647
  }
1050
1648
  }
1051
1649
 
@@ -1053,8 +1651,14 @@ mod tests {
1053
1651
  fn the_block_parameter_shadows_a_same_named_field_only_inside_the_block() {
1054
1652
  let ast = json!({"op":"block_predicate","mode":"any","receiver":lookup_of("xs"),"param":"n",
1055
1653
  "predicate":{"op":"and","left":eq(lookup_of("n"), int(2)),"right":eq(lookup_of("m"), int(9))}});
1056
- assert_eq!(run(ast.clone(), json!({"xs": [1, 2], "m": 9, "n": 100})), Ok(Value::Bool(true)));
1057
- assert_eq!(run(ast, json!({"xs": [1, 2], "m": 8})), Ok(Value::Bool(false)));
1654
+ assert_eq!(
1655
+ run(ast.clone(), json!({"xs": [1, 2], "m": 9, "n": 100})),
1656
+ Ok(Value::Bool(true))
1657
+ );
1658
+ assert_eq!(
1659
+ run(ast, json!({"xs": [1, 2], "m": 8})),
1660
+ Ok(Value::Bool(false))
1661
+ );
1058
1662
  }
1059
1663
 
1060
1664
  #[test]
@@ -1069,7 +1673,10 @@ mod tests {
1069
1673
  #[test]
1070
1674
  fn an_unbound_name_in_a_block_refuses_as_ruby_does() {
1071
1675
  let ast = json!({"op":"block_predicate","mode":"any","receiver":lookup_of("xs"),"param":"x","predicate":lookup_of("ghost")});
1072
- assert_eq!(run(ast, json!({"xs": [1]})), Err("cannot resolve \"ghost\" — no such attribute or argument".to_string()));
1676
+ assert_eq!(
1677
+ run(ast, json!({"xs": [1]})),
1678
+ Err("cannot resolve \"ghost\" — no such attribute or argument".to_string())
1679
+ );
1073
1680
  }
1074
1681
 
1075
1682
  #[test]
@@ -1079,11 +1686,23 @@ mod tests {
1079
1686
  "predicate":eq(json!({"op":"lookup","path":["l","open"]}), json!({"op":"bool","value":wanted}))})
1080
1687
  };
1081
1688
  let legs = json!({"legs": [{"to": "A", "open": false}, {"to": "B", "open": true}, {"to": "C", "open": true}]});
1082
- assert_eq!(run(find(true, &["to"]), legs.clone()), Ok(Value::Str("B".to_string())));
1689
+ assert_eq!(
1690
+ run(find(true, &["to"]), legs.clone()),
1691
+ Ok(Value::Str("B".to_string()))
1692
+ );
1083
1693
  assert_eq!(run(find(true, &["nope"]), legs.clone()), Ok(Value::Nil));
1084
- assert!(matches!(run(find(true, &[]), legs.clone()), Ok(Value::Object(_))));
1085
- assert_eq!(run(find(true, &["to"]), json!({"legs": [{"to": "A", "open": false}]})), Ok(Value::Nil));
1086
- assert_eq!(run(find(true, &["to"]), json!({"legs": []})), Ok(Value::Nil));
1694
+ assert!(matches!(run(find(true, &[]), legs), Ok(Value::Object(_))));
1695
+ assert_eq!(
1696
+ run(
1697
+ find(true, &["to"]),
1698
+ json!({"legs": [{"to": "A", "open": false}]})
1699
+ ),
1700
+ Ok(Value::Nil)
1701
+ );
1702
+ assert_eq!(
1703
+ run(find(true, &["to"]), json!({"legs": []})),
1704
+ Ok(Value::Nil)
1705
+ );
1087
1706
  }
1088
1707
 
1089
1708
  #[test]
@@ -1092,13 +1711,18 @@ mod tests {
1092
1711
  let ast = json!({"op":"find","receiver":lookup_of("xs"),"param":"x","path":[],
1093
1712
  "predicate":json!({"op":"sign_test","cmp":{"less_than":true,"equal":true,"negated":true},"receiver":lookup_of("x")})});
1094
1713
  assert_eq!(run(ast.clone(), json!({"xs": [3, "a"]})), Ok(Value::Int(3)));
1095
- assert_eq!(run(ast, json!({"xs": "abc"})), Err("find expects a list, got \"abc\"".to_string()));
1714
+ assert_eq!(
1715
+ run(ast, json!({"xs": "abc"})),
1716
+ Err("find expects a list, got \"abc\"".to_string())
1717
+ );
1096
1718
  }
1097
1719
 
1098
1720
  #[test]
1099
1721
  fn presence_and_assignment_follow_blank_and_nil() {
1100
- let presence = |negated: bool| json!({"op":"presence","receiver":lookup_of("x"),"negated":negated});
1101
- let assignment = |negated: bool| json!({"op":"assignment","receiver":lookup_of("x"),"negated":negated});
1722
+ let presence =
1723
+ |negated: bool| json!({"op":"presence","receiver":lookup_of("x"),"negated":negated});
1724
+ let assignment =
1725
+ |negated: bool| json!({"op":"assignment","receiver":lookup_of("x"),"negated":negated});
1102
1726
  for (value, present) in [
1103
1727
  (json!(null), false),
1104
1728
  (json!(false), false),
@@ -1109,46 +1733,104 @@ mod tests {
1109
1733
  (json!(true), true),
1110
1734
  (json!([0]), true),
1111
1735
  ] {
1112
- assert_eq!(run(presence(false), json!({"x": value.clone()})), Ok(Value::Bool(present)), "present? of {value}");
1113
- assert_eq!(run(presence(true), json!({"x": value.clone()})), Ok(Value::Bool(!present)), "blank? of {value}");
1736
+ assert_eq!(
1737
+ run(presence(false), json!({"x": value.clone()})),
1738
+ Ok(Value::Bool(present)),
1739
+ "present? of {value}"
1740
+ );
1741
+ assert_eq!(
1742
+ run(presence(true), json!({"x": value.clone()})),
1743
+ Ok(Value::Bool(!present)),
1744
+ "blank? of {value}"
1745
+ );
1114
1746
  }
1115
1747
  // `set?` asks only `!nil?`: an assigned empty or false value is set.
1116
- assert_eq!(run(assignment(false), json!({"x": ""})), Ok(Value::Bool(true)));
1117
- assert_eq!(run(assignment(false), json!({"x": false})), Ok(Value::Bool(true)));
1118
- assert_eq!(run(assignment(false), json!({"x": null})), Ok(Value::Bool(false)));
1119
- assert_eq!(run(assignment(true), json!({"x": null})), Ok(Value::Bool(true)));
1748
+ assert_eq!(
1749
+ run(assignment(false), json!({"x": ""})),
1750
+ Ok(Value::Bool(true))
1751
+ );
1752
+ assert_eq!(
1753
+ run(assignment(false), json!({"x": false})),
1754
+ Ok(Value::Bool(true))
1755
+ );
1756
+ assert_eq!(
1757
+ run(assignment(false), json!({"x": null})),
1758
+ Ok(Value::Bool(false))
1759
+ );
1760
+ assert_eq!(
1761
+ run(assignment(true), json!({"x": null})),
1762
+ Ok(Value::Bool(true))
1763
+ );
1120
1764
  }
1121
1765
 
1122
1766
  #[test]
1123
1767
  fn split_follows_ruby_for_awk_empty_and_literal_separators() {
1124
1768
  let split = |sep: &str| json!({"op":"split","receiver":lookup_of("x"),"separator":sep});
1125
- assert_eq!(run(split(" "), json!({"x": " a b c "})), Ok(strs(&["a", "b", "c"])));
1769
+ assert_eq!(
1770
+ run(split(" "), json!({"x": " a b c "})),
1771
+ Ok(strs(&["a", "b", "c"]))
1772
+ );
1126
1773
  assert_eq!(run(split(""), json!({"x": "aé"})), Ok(strs(&["a", "é"])));
1127
- assert_eq!(run(split(","), json!({"x": "a,b,,"})), Ok(strs(&["a", "b"])));
1774
+ assert_eq!(
1775
+ run(split(","), json!({"x": "a,b,,"})),
1776
+ Ok(strs(&["a", "b"]))
1777
+ );
1128
1778
  assert_eq!(run(split(","), json!({"x": ",a"})), Ok(strs(&["", "a"])));
1129
1779
  assert_eq!(run(split(","), json!({"x": ""})), Ok(strs(&[])));
1130
- assert_eq!(run(split(","), json!({"x": 5})), Err("split expects a string, got 5".to_string()));
1131
- assert_eq!(run(split(","), json!({"x": null})), Err("split expects a string, got nil".to_string()));
1780
+ assert_eq!(
1781
+ run(split(","), json!({"x": 5})),
1782
+ Err("split expects a string, got 5".to_string())
1783
+ );
1784
+ assert_eq!(
1785
+ run(split(","), json!({"x": null})),
1786
+ Err("split expects a string, got nil".to_string())
1787
+ );
1132
1788
  }
1133
1789
 
1134
1790
  #[test]
1135
1791
  fn first_and_last_answer_nil_for_an_empty_list_and_refuse_a_non_list() {
1136
- assert_eq!(run(call("first", lookup_of("x")), json!({"x": [7, 8]})), Ok(Value::Int(7)));
1137
- assert_eq!(run(call("last", lookup_of("x")), json!({"x": [7, 8]})), Ok(Value::Int(8)));
1138
- assert_eq!(run(call("first", lookup_of("x")), json!({"x": []})), Ok(Value::Nil));
1139
- assert_eq!(run(call("last", lookup_of("x")), json!({"x": "abc"})), Err("last expects a list, got \"abc\"".to_string()));
1140
- assert_eq!(run(call("first", lookup_of("x")), json!({"x": null})), Err("first expects a list, got nil".to_string()));
1792
+ assert_eq!(
1793
+ run(call("first", lookup_of("x")), json!({"x": [7, 8]})),
1794
+ Ok(Value::Int(7))
1795
+ );
1796
+ assert_eq!(
1797
+ run(call("last", lookup_of("x")), json!({"x": [7, 8]})),
1798
+ Ok(Value::Int(8))
1799
+ );
1800
+ assert_eq!(
1801
+ run(call("first", lookup_of("x")), json!({"x": []})),
1802
+ Ok(Value::Nil)
1803
+ );
1804
+ assert_eq!(
1805
+ run(call("last", lookup_of("x")), json!({"x": "abc"})),
1806
+ Err("last expects a list, got \"abc\"".to_string())
1807
+ );
1808
+ assert_eq!(
1809
+ run(call("first", lookup_of("x")), json!({"x": null})),
1810
+ Err("first expects a list, got nil".to_string())
1811
+ );
1141
1812
  }
1142
1813
 
1143
1814
  #[test]
1144
1815
  fn strip_trims_rubys_whitespace_and_needs_a_string() {
1145
- let strip_of = |side: &str, text: &str| run(json!({"op":"strip","receiver":lookup_of("x"),"side":side}), json!({"x": text}));
1816
+ let strip_of = |side: &str, text: &str| {
1817
+ run(
1818
+ json!({"op":"strip","receiver":lookup_of("x"),"side":side}),
1819
+ json!({"x": text}),
1820
+ )
1821
+ };
1146
1822
  assert_eq!(strip_of("both", " \t a b \0"), Ok(Value::Str("a b".into())));
1147
1823
  assert_eq!(strip_of("left", " a "), Ok(Value::Str("a ".into())));
1148
1824
  assert_eq!(strip_of("right", " a "), Ok(Value::Str(" a".into())));
1149
- assert_eq!(strip_of("both", "\u{a0}a"), Ok(Value::Str("\u{a0}a".into())));
1150
1825
  assert_eq!(
1151
- run(json!({"op":"strip","receiver":lookup_of("x"),"side":"both"}), json!({"x": null})),
1826
+ strip_of("both", "\u{a0}a"),
1827
+ Ok(Value::Str("\u{a0}a".into()))
1828
+ );
1829
+ assert_eq!(
1830
+ run(
1831
+ json!({"op":"strip","receiver":lookup_of("x"),"side":"both"}),
1832
+ json!({"x": null})
1833
+ ),
1152
1834
  Err("strip expects a string, got nil".to_string())
1153
1835
  );
1154
1836
  }
@@ -1157,46 +1839,100 @@ mod tests {
1157
1839
  fn starts_with_and_ends_with_need_a_string() {
1158
1840
  let starts = json!({"op":"starts_with","receiver":lookup_of("x"),"substring":"ab"});
1159
1841
  let ends = json!({"op":"ends_with","receiver":lookup_of("x"),"substring":"bc"});
1160
- assert_eq!(run(starts.clone(), json!({"x": "abc"})), Ok(Value::Bool(true)));
1161
- assert_eq!(run(ends.clone(), json!({"x": "abc"})), Ok(Value::Bool(true)));
1842
+ assert_eq!(
1843
+ run(starts.clone(), json!({"x": "abc"})),
1844
+ Ok(Value::Bool(true))
1845
+ );
1846
+ assert_eq!(
1847
+ run(ends.clone(), json!({"x": "abc"})),
1848
+ Ok(Value::Bool(true))
1849
+ );
1162
1850
  assert_eq!(run(ends, json!({"x": "abd"})), Ok(Value::Bool(false)));
1163
- assert_eq!(run(starts.clone(), json!({"x": null})), Err("start_with? expects a string, got nil".to_string()));
1164
- assert_eq!(run(starts, json!({"x": [1]})), Err("start_with? expects a string, got [1]".to_string()));
1851
+ assert_eq!(
1852
+ run(starts.clone(), json!({"x": null})),
1853
+ Err("start_with? expects a string, got nil".to_string())
1854
+ );
1855
+ assert_eq!(
1856
+ run(starts, json!({"x": [1]})),
1857
+ Err("start_with? expects a string, got [1]".to_string())
1858
+ );
1165
1859
  }
1166
1860
 
1167
1861
  #[test]
1168
1862
  fn a_path_through_a_string_list_or_integer_walks_like_ruby() {
1169
1863
  let path = |segments: &[&str]| json!({"op":"lookup","path":segments});
1170
1864
  // `String#[]` answers the segment when it is a substring.
1171
- assert_eq!(run(path(&["x", "foo"]), json!({"x": "abcfoo"})), Ok(Value::Str("foo".to_string())));
1172
- assert_eq!(run(path(&["x", "nil?"]), json!({"x": "abc"})), Ok(Value::Nil));
1865
+ assert_eq!(
1866
+ run(path(&["x", "foo"]), json!({"x": "abcfoo"})),
1867
+ Ok(Value::Str("foo".to_string()))
1868
+ );
1869
+ assert_eq!(
1870
+ run(path(&["x", "nil?"]), json!({"x": "abc"})),
1871
+ Ok(Value::Nil)
1872
+ );
1173
1873
  assert_eq!(run(path(&["x", "a"]), json!({"x": 1.5})), Ok(Value::Nil));
1174
1874
  assert_eq!(run(path(&["x", "a"]), json!({"x": true})), Ok(Value::Nil));
1175
- assert_eq!(run(path(&["x", "a"]), json!({"x": [1]})), Err("cannot read \"a\" from [1]".to_string()));
1176
- assert_eq!(run(path(&["x", "a"]), json!({"x": 5})), Err("cannot read \"a\" from 5".to_string()));
1875
+ assert_eq!(
1876
+ run(path(&["x", "a"]), json!({"x": [1]})),
1877
+ Err("cannot read \"a\" from [1]".to_string())
1878
+ );
1879
+ assert_eq!(
1880
+ run(path(&["x", "a"]), json!({"x": 5})),
1881
+ Err("cannot read \"a\" from 5".to_string())
1882
+ );
1177
1883
  }
1178
1884
 
1179
1885
  #[test]
1180
1886
  fn a_one_field_object_reads_as_its_value_and_a_wider_one_stays_an_object() {
1181
- assert_eq!(run(lookup_of("x"), json!({"x": {"value": 7}})), Ok(Value::Int(7)));
1182
- assert!(matches!(run(lookup_of("x"), json!({"x": {"a": 1, "b": 2}})), Ok(Value::Object(_))));
1183
- assert_eq!(run(json!({"op":"lookup","path":["x","b"]}), json!({"x": {"a": 1, "b": 2}})), Ok(Value::Int(2)));
1887
+ assert_eq!(
1888
+ run(lookup_of("x"), json!({"x": {"value": 7}})),
1889
+ Ok(Value::Int(7))
1890
+ );
1891
+ assert!(matches!(
1892
+ run(lookup_of("x"), json!({"x": {"a": 1, "b": 2}})),
1893
+ Ok(Value::Object(_))
1894
+ ));
1895
+ assert_eq!(
1896
+ run(
1897
+ json!({"op":"lookup","path":["x","b"]}),
1898
+ json!({"x": {"a": 1, "b": 2}})
1899
+ ),
1900
+ Ok(Value::Int(2))
1901
+ );
1184
1902
  }
1185
1903
 
1186
1904
  #[test]
1187
1905
  fn errors_are_worded_as_rubys_describe() {
1188
- assert_eq!(run(call("size", lookup_of("x")), json!({"x": 5})), Err("size expects a list or string, got 5".to_string()));
1189
- assert_eq!(run(call("empty", lookup_of("x")), json!({"x": null})), Err("empty? expects a list or string, got nil".to_string()));
1190
- assert_eq!(run(call("to_s", lookup_of("x")), json!({"x": [1, "a"]})), Err("to_s expects a scalar, got [1,\"a\"]".to_string()));
1191
1906
  assert_eq!(
1192
- run(json!({"op":"compare","cmp":{"less_than":true,"equal":false,"negated":false},"left":lookup_of("x"),"right":int(1)}), json!({"x": "a"})),
1907
+ run(call("size", lookup_of("x")), json!({"x": 5})),
1908
+ Err("size expects a list or string, got 5".to_string())
1909
+ );
1910
+ assert_eq!(
1911
+ run(call("empty", lookup_of("x")), json!({"x": null})),
1912
+ Err("empty? expects a list or string, got nil".to_string())
1913
+ );
1914
+ assert_eq!(
1915
+ run(call("to_s", lookup_of("x")), json!({"x": [1, "a"]})),
1916
+ Err("to_s expects a scalar, got [1,\"a\"]".to_string())
1917
+ );
1918
+ assert_eq!(
1919
+ run(
1920
+ json!({"op":"compare","cmp":{"less_than":true,"equal":false,"negated":false},"left":lookup_of("x"),"right":int(1)}),
1921
+ json!({"x": "a"})
1922
+ ),
1193
1923
  Err("comparison of String with 1 failed".to_string())
1194
1924
  );
1195
1925
  assert_eq!(
1196
- run(json!({"op":"sign_test","cmp":{"less_than":false,"equal":true,"negated":false},"receiver":lookup_of("x")}), json!({"x": "a"})),
1926
+ run(
1927
+ json!({"op":"sign_test","cmp":{"less_than":false,"equal":true,"negated":false},"receiver":lookup_of("x")}),
1928
+ json!({"x": "a"})
1929
+ ),
1197
1930
  Err("zero? expects a number, got \"a\"".to_string())
1198
1931
  );
1199
- assert_eq!(run(lookup_of("ghost"), json!({})), Err("cannot resolve \"ghost\" — no such attribute or argument".to_string()));
1932
+ assert_eq!(
1933
+ run(lookup_of("ghost"), json!({})),
1934
+ Err("cannot resolve \"ghost\" — no such attribute or argument".to_string())
1935
+ );
1200
1936
  }
1201
1937
 
1202
1938
  #[test]
@@ -1205,30 +1941,42 @@ mod tests {
1205
1941
  (3.0, "3.0"),
1206
1942
  (0.5, "0.5"),
1207
1943
  (-0.0, "-0.0"),
1208
- (100000000000000.0, "100000000000000.0"),
1209
- (999999999999999.9, "999999999999999.9"),
1944
+ (100_000_000_000_000.0, "100000000000000.0"),
1945
+ (999_999_999_999_999.9, "999999999999999.9"),
1210
1946
  (1e15, "1.0e+15"),
1211
1947
  (1.5e15, "1.5e+15"),
1212
1948
  (1e20, "1.0e+20"),
1213
1949
  (0.0001, "0.0001"),
1214
1950
  (0.00001, "1.0e-05"),
1215
1951
  (1.5e-7, "1.5e-07"),
1216
- (123456789.123456789, "123456789.12345679"),
1952
+ (123_456_789.123_456_79, "123456789.12345679"),
1217
1953
  ] {
1218
1954
  assert_eq!(ruby_float(f), text);
1219
- assert_eq!(run(call("to_s", lookup_of("x")), json!({"x": f})), Ok(Value::Str(text.to_string())));
1955
+ assert_eq!(
1956
+ run(call("to_s", lookup_of("x")), json!({"x": f})),
1957
+ Ok(Value::Str(text.to_string()))
1958
+ );
1220
1959
  }
1221
1960
  }
1222
1961
 
1223
1962
  #[test]
1224
1963
  fn strings_inspect_as_ruby_inspects_them() {
1225
- assert_eq!(ruby_inspect("a\"b\\c\n\u{1}#{x}\u{7f}é"), "\"a\\\"b\\\\c\\n\\u0001\\#{x}\\u007Fé\"");
1964
+ assert_eq!(
1965
+ ruby_inspect("a\"b\\c\n\u{1}#{x}\u{7f}é"),
1966
+ "\"a\\\"b\\\\c\\n\\u0001\\#{x}\\u007Fé\""
1967
+ );
1226
1968
  }
1227
1969
 
1228
1970
  #[test]
1229
1971
  fn equality_compares_nested_numbers_across_kinds_like_ruby() {
1230
- assert!(values_equal(&Value::Array(vec![Value::Int(1)]), &Value::Array(vec![Value::Float(1.0)])));
1231
- assert!(!values_equal(&Value::Int(9_007_199_254_740_993), &Value::Float(9_007_199_254_740_992.0)));
1972
+ assert!(values_equal(
1973
+ &Value::Array(vec![Value::Int(1)]),
1974
+ &Value::Array(vec![Value::Float(1.0)])
1975
+ ));
1976
+ assert!(!values_equal(
1977
+ &Value::Int(9_007_199_254_740_993),
1978
+ &Value::Float(9_007_199_254_740_992.0)
1979
+ ));
1232
1980
  assert!(values_equal(&Value::Int(3), &Value::Float(3.0)));
1233
1981
  assert!(!values_equal(&Value::Int(3), &Value::Float(3.5)));
1234
1982
  }