hecks 3.13.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/named_pattern.rb +33 -0
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +3 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/framework/bluebook/change.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +58 -37
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +15 -92
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +10 -16
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +51 -51
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value.rb +13 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/site/bluebook/site.bluebook +5 -5
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +300 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +145 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +990 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +191 -0
- data/rust/host/src/payments/tests.rs +1445 -275
- data/rust/host/src/payments.rs +409 -121
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +483 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +26 -0
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -510
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +43 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +229 -112
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +230 -148
- data/rust/src/kernel/mod.rs +46 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +85 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
- data/rust/host/src/approval.rs +0 -652
data/rust/host/src/expr_json.rs
CHANGED
|
@@ -32,50 +32,146 @@ pub enum BlockMode {
|
|
|
32
32
|
#[derive(Debug, Clone)]
|
|
33
33
|
#[allow(dead_code)]
|
|
34
34
|
pub enum Expr {
|
|
35
|
-
Or {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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)
|
|
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|
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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 {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"
|
|
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 {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
|
|
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 =>
|
|
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 {
|
|
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")
|
|
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")
|
|
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 =>
|
|
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 {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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<
|
|
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,
|
|
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<
|
|
388
|
+
fn from_json(json: &Json) -> Result<Self, String> {
|
|
196
389
|
match json {
|
|
197
|
-
Json::Null => Ok(
|
|
198
|
-
Json::Bool(b) => Ok(
|
|
199
|
-
Json::Number(n) =>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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()
|
|
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)) =>
|
|
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)) =>
|
|
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()
|
|
240
|
-
(
|
|
241
|
-
|
|
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!(
|
|
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:
|
|
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(_) =>
|
|
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
|
|
296
|
-
|
|
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
|
-
(
|
|
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!(
|
|
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 =>
|
|
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!(
|
|
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!(
|
|
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(||
|
|
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!(
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
let result = float_op(
|
|
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!(
|
|
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)) {
|
|
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) {
|
|
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() {
|
|
458
|
-
|
|
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(
|
|
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
|
|
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) =>
|
|
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|
|
|
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
|
|
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(
|
|
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 } =>
|
|
607
|
-
|
|
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(
|
|
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(
|
|
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!(
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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 {
|
|
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 } =>
|
|
659
|
-
|
|
660
|
-
|
|
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 {
|
|
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!(
|
|
997
|
+
other => Err(format!(
|
|
998
|
+
"start_with? expects a string, got {}",
|
|
999
|
+
describe(&other)
|
|
1000
|
+
)),
|
|
679
1001
|
},
|
|
680
|
-
Expr::EndsWith {
|
|
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!(
|
|
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 {
|
|
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 =>
|
|
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 {
|
|
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() {
|
|
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(
|
|
724
|
-
|
|
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:
|
|
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
|
|
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(_) =>
|
|
1121
|
+
Value::Array(_) | Value::Int(_) => {
|
|
1122
|
+
return Err(format!(
|
|
1123
|
+
"cannot read {} from {}",
|
|
1124
|
+
ruby_inspect(segment),
|
|
1125
|
+
describe(¤t)
|
|
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
|
-
|
|
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!(
|
|
799
|
-
|
|
800
|
-
|
|
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(
|
|
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!(
|
|
830
|
-
|
|
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!(
|
|
840
|
-
|
|
841
|
-
|
|
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!(
|
|
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!(
|
|
897
|
-
|
|
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!(
|
|
904
|
-
|
|
905
|
-
|
|
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(
|
|
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!(
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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(
|
|
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!(
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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!(
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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!(
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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!(
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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![
|
|
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!(
|
|
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!(
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
assert_eq!(
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
assert_eq!(
|
|
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!(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
assert_eq!(
|
|
1010
|
-
|
|
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(
|
|
1016
|
-
|
|
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!(
|
|
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!(
|
|
1048
|
-
|
|
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!(
|
|
1057
|
-
|
|
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!(
|
|
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!(
|
|
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
|
|
1085
|
-
assert_eq!(
|
|
1086
|
-
|
|
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!(
|
|
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 =
|
|
1101
|
-
|
|
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!(
|
|
1113
|
-
|
|
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!(
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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!(
|
|
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!(
|
|
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!(
|
|
1131
|
-
|
|
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!(
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
assert_eq!(
|
|
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|
|
|
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
|
-
|
|
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!(
|
|
1161
|
-
|
|
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!(
|
|
1164
|
-
|
|
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!(
|
|
1172
|
-
|
|
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!(
|
|
1176
|
-
|
|
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!(
|
|
1182
|
-
|
|
1183
|
-
|
|
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(
|
|
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(
|
|
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!(
|
|
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
|
-
(
|
|
1209
|
-
(
|
|
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
|
-
(
|
|
1952
|
+
(123_456_789.123_456_79, "123456789.12345679"),
|
|
1217
1953
|
] {
|
|
1218
1954
|
assert_eq!(ruby_float(f), text);
|
|
1219
|
-
assert_eq!(
|
|
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!(
|
|
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(
|
|
1231
|
-
|
|
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
|
}
|