hecks 3.14.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/exe/hecks +1 -1
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +9 -16
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +2 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/app/responses.rb +3 -1
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +3 -80
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +7 -13
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value/unwrapping.rb +60 -0
- data/lib/hecks/runtime/value.rb +10 -21
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
- data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +306 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +151 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/presenters.rs +335 -0
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +574 -229
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +1037 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +39 -7
- data/rust/host/src/payments/tests.rs +1427 -282
- data/rust/host/src/payments.rs +391 -112
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +555 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -1
- data/rust/parser/src/build/named_pattern.rs +4 -1
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -516
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +22 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +244 -114
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +244 -148
- data/rust/src/kernel/mod.rs +47 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/present.rs +261 -0
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +85 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/rust/host/src/approval.rs +0 -652
data/rust/src/kernel/json.rs
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
//! Kept separate from `expr::Value`; zero Cargo dependencies (ADR 0012).
|
|
3
3
|
|
|
4
4
|
use super::Refusal;
|
|
5
|
+
use std::fmt::Write as _;
|
|
5
6
|
|
|
6
7
|
#[derive(Debug, Clone)]
|
|
7
8
|
pub enum Json {
|
|
8
|
-
Object(Vec<(String,
|
|
9
|
-
Array(Vec<
|
|
9
|
+
Object(Vec<(String, Self)>),
|
|
10
|
+
Array(Vec<Self>),
|
|
10
11
|
Str(String),
|
|
11
12
|
/// The second field is the exact source text (BUG#147) — set only when the literal's
|
|
12
13
|
/// magnitude means the `f64` approximation would not reproduce it, e.g. an integer past
|
|
@@ -28,14 +29,14 @@ impl PartialEq for Json {
|
|
|
28
29
|
/// in the first place; otherwise the numeric comparison is unchanged.
|
|
29
30
|
fn eq(&self, other: &Self) -> bool {
|
|
30
31
|
match (self, other) {
|
|
31
|
-
(
|
|
32
|
-
(
|
|
33
|
-
(
|
|
34
|
-
(
|
|
35
|
-
(
|
|
36
|
-
(
|
|
37
|
-
(
|
|
38
|
-
(
|
|
32
|
+
(Self::Object(a), Self::Object(b)) => a == b,
|
|
33
|
+
(Self::Array(a), Self::Array(b)) => a == b,
|
|
34
|
+
(Self::Str(a), Self::Str(b)) => a == b,
|
|
35
|
+
(Self::Num(_, Some(araw)), Self::Num(_, Some(braw))) => araw == braw,
|
|
36
|
+
(Self::Num(a, _), Self::Num(b, _)) => a == b,
|
|
37
|
+
(Self::Float(a), Self::Float(b)) => a == b,
|
|
38
|
+
(Self::Bool(a), Self::Bool(b)) => a == b,
|
|
39
|
+
(Self::Null, Self::Null) => true,
|
|
39
40
|
_ => false,
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -45,42 +46,46 @@ impl PartialEq for Json {
|
|
|
45
46
|
impl super::Fielded for Json {
|
|
46
47
|
fn field(&self, name: &str) -> Option<super::Field<'_>> {
|
|
47
48
|
use super::{Field, Value};
|
|
48
|
-
let
|
|
49
|
+
let Self::Object(pairs) = self else {
|
|
50
|
+
return None;
|
|
51
|
+
};
|
|
49
52
|
let (_, found) = pairs.iter().find(|(k, _)| k == name)?;
|
|
50
53
|
Some(match found {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
},
|
|
54
|
+
Self::Object(_) => Field::Nested(found),
|
|
55
|
+
Self::Array(items) => Field::Value(Value::List(items.len())),
|
|
56
|
+
Self::Str(s) => Field::Value(Value::Str(s.clone())),
|
|
57
|
+
Self::Num(n, raw) => {
|
|
58
|
+
Field::Value(exact_i64(*n, raw.as_deref()).map_or(Value::Float(*n), Value::Int))
|
|
59
|
+
}
|
|
58
60
|
// Float already carries the Integer-or-Float answer that `Num` must guess.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
Self::Float(n) => Field::Value(Value::Float(*n)),
|
|
62
|
+
Self::Bool(b) => Field::Value(Value::Bool(*b)),
|
|
63
|
+
Self::Null => Field::Value(Value::Nil),
|
|
62
64
|
})
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
fn items(&self, name: &str) -> Option<Vec<super::Field<'_>>> {
|
|
66
68
|
use super::{Field, Value};
|
|
67
|
-
let
|
|
69
|
+
let Self::Object(pairs) = self else {
|
|
70
|
+
return None;
|
|
71
|
+
};
|
|
68
72
|
let (_, found) = pairs.iter().find(|(k, _)| k == name)?;
|
|
69
|
-
let
|
|
73
|
+
let Self::Array(items) = found else {
|
|
74
|
+
return None;
|
|
75
|
+
};
|
|
70
76
|
Some(
|
|
71
77
|
items
|
|
72
78
|
.iter()
|
|
73
79
|
.map(|item| match item {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Json::Null => Field::Value(Value::Nil),
|
|
80
|
+
Self::Object(_) => Field::Nested(item),
|
|
81
|
+
Self::Array(inner) => Field::Value(Value::List(inner.len())),
|
|
82
|
+
Self::Str(s) => Field::Value(Value::Str(s.clone())),
|
|
83
|
+
Self::Num(n, raw) => Field::Value(
|
|
84
|
+
exact_i64(*n, raw.as_deref()).map_or(Value::Float(*n), Value::Int),
|
|
85
|
+
),
|
|
86
|
+
Self::Float(n) => Field::Value(Value::Float(*n)),
|
|
87
|
+
Self::Bool(b) => Field::Value(Value::Bool(*b)),
|
|
88
|
+
Self::Null => Field::Value(Value::Nil),
|
|
84
89
|
})
|
|
85
90
|
.collect(),
|
|
86
91
|
)
|
|
@@ -88,7 +93,9 @@ impl super::Fielded for Json {
|
|
|
88
93
|
|
|
89
94
|
// A `Json` has no declaration to consult, so one key of any name is the whole shape test.
|
|
90
95
|
fn as_scalar(&self) -> Option<super::Value> {
|
|
91
|
-
let
|
|
96
|
+
let Self::Object(pairs) = self else {
|
|
97
|
+
return None;
|
|
98
|
+
};
|
|
92
99
|
if pairs.len() != 1 {
|
|
93
100
|
return None;
|
|
94
101
|
}
|
|
@@ -100,28 +107,33 @@ impl super::Fielded for Json {
|
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
impl Json {
|
|
103
|
-
pub fn obj(fields: Vec<(&str,
|
|
104
|
-
|
|
110
|
+
pub fn obj(fields: Vec<(&str, Self)>) -> Self {
|
|
111
|
+
Self::Object(
|
|
112
|
+
fields
|
|
113
|
+
.into_iter()
|
|
114
|
+
.map(|(k, v)| (k.to_string(), v))
|
|
115
|
+
.collect(),
|
|
116
|
+
)
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
pub fn str(s: impl Into<String>) ->
|
|
108
|
-
|
|
119
|
+
pub fn str(s: impl Into<String>) -> Self {
|
|
120
|
+
Self::Str(s.into())
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
/// The raw text is always the exact `i64`, not derived from the `f64` cast: `i as f64`
|
|
112
124
|
/// itself rounds near the `i64` boundaries, which would otherwise reintroduce BUG#147's
|
|
113
125
|
/// class of loss for an in-range value built straight from a Rust integer.
|
|
114
|
-
pub fn int(i: i64) ->
|
|
115
|
-
|
|
126
|
+
pub fn int(i: i64) -> Self {
|
|
127
|
+
Self::Num(i as f64, Some(i.to_string()))
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
pub fn float(f: f64) ->
|
|
119
|
-
|
|
130
|
+
pub const fn float(f: f64) -> Self {
|
|
131
|
+
Self::Float(f)
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
pub fn get(&self, key: &str) -> Option<&
|
|
134
|
+
pub fn get(&self, key: &str) -> Option<&Self> {
|
|
123
135
|
match self {
|
|
124
|
-
|
|
136
|
+
Self::Object(fields) => fields.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
|
125
137
|
_ => None,
|
|
126
138
|
}
|
|
127
139
|
}
|
|
@@ -129,11 +141,11 @@ impl Json {
|
|
|
129
141
|
/// A tolerant dotted-path walk: a non-object reached mid-path is the answer.
|
|
130
142
|
///
|
|
131
143
|
/// A `Reference<X>` argument arrives as a bare string where a wrapped value is expected.
|
|
132
|
-
pub fn dig(&self, path: &str) -> Option<&
|
|
144
|
+
pub fn dig(&self, path: &str) -> Option<&Self> {
|
|
133
145
|
let mut current = self;
|
|
134
146
|
for segment in path.split('.') {
|
|
135
147
|
match current {
|
|
136
|
-
|
|
148
|
+
Self::Object(_) => current = current.get(segment)?,
|
|
137
149
|
_ => return Some(current),
|
|
138
150
|
}
|
|
139
151
|
}
|
|
@@ -146,13 +158,13 @@ impl Json {
|
|
|
146
158
|
/// an Object stays compact JSON.
|
|
147
159
|
pub fn inspect(&self) -> String {
|
|
148
160
|
match self {
|
|
149
|
-
|
|
161
|
+
Self::Str(s) => {
|
|
150
162
|
let mut out = String::new();
|
|
151
163
|
write_escaped_string(s, &mut out);
|
|
152
164
|
out
|
|
153
165
|
}
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
Self::Null => "nil".to_string(),
|
|
167
|
+
Self::Array(items) => {
|
|
156
168
|
let mut out = String::from("[");
|
|
157
169
|
for (i, item) in items.iter().enumerate() {
|
|
158
170
|
if i > 0 {
|
|
@@ -173,17 +185,17 @@ impl Json {
|
|
|
173
185
|
/// Arrays, Objects and fractions fall back to compact JSON; they never match a member.
|
|
174
186
|
pub fn ruby_to_s(&self) -> String {
|
|
175
187
|
match self {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
Self::Str(s) => s.clone(),
|
|
189
|
+
Self::Null => String::new(),
|
|
190
|
+
Self::Bool(b) => b.to_string(),
|
|
191
|
+
Self::Num(n, _) if n.fract() == 0.0 && n.abs() < 1e15 => format!("{}", *n as i64),
|
|
180
192
|
_ => self.to_json_string(),
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
pub fn as_str(&self) -> Option<&str> {
|
|
185
197
|
match self {
|
|
186
|
-
|
|
198
|
+
Self::Str(s) => Some(s),
|
|
187
199
|
_ => None,
|
|
188
200
|
}
|
|
189
201
|
}
|
|
@@ -194,28 +206,28 @@ impl Json {
|
|
|
194
206
|
/// range and `i64::MAX` round-trips through `f64` inexactly even though it fits in `i64`.
|
|
195
207
|
pub fn as_i64(&self) -> Option<i64> {
|
|
196
208
|
match self {
|
|
197
|
-
|
|
209
|
+
Self::Num(n, raw) => exact_i64(*n, raw.as_deref()),
|
|
198
210
|
_ => None,
|
|
199
211
|
}
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
pub fn as_f64(&self) -> Option<f64> {
|
|
214
|
+
pub const fn as_f64(&self) -> Option<f64> {
|
|
203
215
|
match self {
|
|
204
|
-
|
|
216
|
+
Self::Num(n, _) | Self::Float(n) => Some(*n),
|
|
205
217
|
_ => None,
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
|
|
209
|
-
pub fn as_bool(&self) -> Option<bool> {
|
|
221
|
+
pub const fn as_bool(&self) -> Option<bool> {
|
|
210
222
|
match self {
|
|
211
|
-
|
|
223
|
+
Self::Bool(b) => Some(*b),
|
|
212
224
|
_ => None,
|
|
213
225
|
}
|
|
214
226
|
}
|
|
215
227
|
|
|
216
|
-
pub fn as_array(&self) -> Option<&[
|
|
228
|
+
pub fn as_array(&self) -> Option<&[Self]> {
|
|
217
229
|
match self {
|
|
218
|
-
|
|
230
|
+
Self::Array(items) => Some(items),
|
|
219
231
|
_ => None,
|
|
220
232
|
}
|
|
221
233
|
}
|
|
@@ -223,17 +235,17 @@ impl Json {
|
|
|
223
235
|
/// Wraps a bare scalar as a one-field object under `field_name`; an object passes through.
|
|
224
236
|
///
|
|
225
237
|
/// Mirrors `Value.for_attribute`'s bare-value admission for a single-attribute value object.
|
|
226
|
-
pub fn coerce_single_field(&self, field_name: &str) ->
|
|
238
|
+
pub fn coerce_single_field(&self, field_name: &str) -> Self {
|
|
227
239
|
match self {
|
|
228
|
-
|
|
229
|
-
other =>
|
|
240
|
+
Self::Object(_) => self.clone(),
|
|
241
|
+
other => Self::obj(vec![(field_name, other.clone())]),
|
|
230
242
|
}
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
/// How a refusal quotes an offered value: compact JSON for an Array or Object, else `inspect`.
|
|
234
246
|
pub fn describe(&self) -> String {
|
|
235
247
|
match self {
|
|
236
|
-
|
|
248
|
+
Self::Array(_) | Self::Object(_) => self.to_json_string(),
|
|
237
249
|
other => other.inspect(),
|
|
238
250
|
}
|
|
239
251
|
}
|
|
@@ -241,9 +253,9 @@ impl Json {
|
|
|
241
253
|
/// Refuses `TypeMismatch` when a multi-field value object is offered as a non-object.
|
|
242
254
|
///
|
|
243
255
|
/// Runs at the `from_json` call site, since only the caller knows `name`.
|
|
244
|
-
pub fn expect_value_object_shape(&self, name: &str, type_name: &str) -> Result<&
|
|
256
|
+
pub fn expect_value_object_shape(&self, name: &str, type_name: &str) -> Result<&Self, Refusal> {
|
|
245
257
|
match self {
|
|
246
|
-
|
|
258
|
+
Self::Object(_) => Ok(self),
|
|
247
259
|
other => Err(Refusal::TypeMismatch(
|
|
248
260
|
super::refusal_wording::TypeMismatchValueObjectShapeArgs {
|
|
249
261
|
name,
|
|
@@ -256,9 +268,10 @@ impl Json {
|
|
|
256
268
|
}
|
|
257
269
|
|
|
258
270
|
/// A required-field lookup that refuses `TypeMismatch` when the key is missing.
|
|
259
|
-
pub fn require(&self, key: &str, struct_name: &str) -> Result<&
|
|
260
|
-
self.get(key)
|
|
261
|
-
|
|
271
|
+
pub fn require(&self, key: &str, struct_name: &str) -> Result<&Self, Refusal> {
|
|
272
|
+
self.get(key).ok_or_else(|| {
|
|
273
|
+
Refusal::TypeMismatch(format!("{struct_name}.{key}: missing from JSON args"))
|
|
274
|
+
})
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
/// The keys of a command's args that are not in `known`, sorted.
|
|
@@ -266,8 +279,12 @@ impl Json {
|
|
|
266
279
|
/// Sorted because refusal wording is pinned by corpus fixtures and cannot depend on key order.
|
|
267
280
|
pub fn unknown_keys(&self, known: &[&str]) -> Vec<String> {
|
|
268
281
|
match self {
|
|
269
|
-
|
|
270
|
-
let mut unknown: Vec<String> = fields
|
|
282
|
+
Self::Object(fields) => {
|
|
283
|
+
let mut unknown: Vec<String> = fields
|
|
284
|
+
.iter()
|
|
285
|
+
.filter(|(k, _)| !known.contains(&k.as_str()))
|
|
286
|
+
.map(|(k, _)| k.clone())
|
|
287
|
+
.collect();
|
|
271
288
|
unknown.sort();
|
|
272
289
|
unknown
|
|
273
290
|
}
|
|
@@ -279,8 +296,8 @@ impl Json {
|
|
|
279
296
|
///
|
|
280
297
|
/// The typed args carry declared defaults that the raw `args_json` lacks, while the raw
|
|
281
298
|
/// args keep identity/reference arguments no attribute names. Non-objects return `base`.
|
|
282
|
-
pub fn overlay(base: &
|
|
283
|
-
let (
|
|
299
|
+
pub fn overlay(base: &Self, patch: &Self) -> Self {
|
|
300
|
+
let (Self::Object(base_fields), Self::Object(patch_fields)) = (base, patch) else {
|
|
284
301
|
return base.clone();
|
|
285
302
|
};
|
|
286
303
|
let mut merged = base_fields.clone();
|
|
@@ -290,25 +307,31 @@ impl Json {
|
|
|
290
307
|
None => merged.push((key.clone(), value.clone())),
|
|
291
308
|
}
|
|
292
309
|
}
|
|
293
|
-
|
|
310
|
+
Self::Object(merged)
|
|
294
311
|
}
|
|
295
312
|
|
|
296
313
|
/// Adds each target key of `pairs` holding its source key's value, keeping existing args.
|
|
297
314
|
///
|
|
298
315
|
/// A source key that is absent adds nothing. Non-objects pass through.
|
|
299
|
-
pub fn with_aliases(&self, pairs: &[(&str, &str)]) ->
|
|
300
|
-
let
|
|
316
|
+
pub fn with_aliases(&self, pairs: &[(&str, &str)]) -> Self {
|
|
317
|
+
let Self::Object(fields) = self else {
|
|
301
318
|
return self.clone();
|
|
302
319
|
};
|
|
303
320
|
let mut merged = fields.clone();
|
|
304
321
|
for (target_key, source_key) in pairs {
|
|
305
|
-
let Some(value) = fields
|
|
322
|
+
let Some(value) = fields
|
|
323
|
+
.iter()
|
|
324
|
+
.find(|(k, _)| k == source_key)
|
|
325
|
+
.map(|(_, v)| v.clone())
|
|
326
|
+
else {
|
|
327
|
+
continue;
|
|
328
|
+
};
|
|
306
329
|
match merged.iter_mut().find(|(k, _)| k == target_key) {
|
|
307
330
|
Some(entry) => entry.1 = value,
|
|
308
331
|
None => merged.push((target_key.to_string(), value)),
|
|
309
332
|
}
|
|
310
333
|
}
|
|
311
|
-
|
|
334
|
+
Self::Object(merged)
|
|
312
335
|
}
|
|
313
336
|
|
|
314
337
|
/// Stringifies a scalar leaf as an identity component.
|
|
@@ -319,19 +342,21 @@ impl Json {
|
|
|
319
342
|
/// carries it at its full, arbitrary-precision magnitude.
|
|
320
343
|
pub fn to_id_component(&self) -> Result<String, Refusal> {
|
|
321
344
|
match self {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
other => Err(Refusal::TypeMismatch(format!(
|
|
345
|
+
Self::Str(s) if s.is_empty() => Err(Refusal::TypeMismatch(
|
|
346
|
+
"identity component must not be empty".to_string(),
|
|
347
|
+
)),
|
|
348
|
+
Self::Str(s) => Ok(s.clone()),
|
|
349
|
+
// In range: a plain integer, as Ruby prints it. Out of range or fractional: the
|
|
350
|
+
// exact source text when there is one (a Bignum past i64), else the float's own
|
|
351
|
+
// digits; `as i64` would saturate.
|
|
352
|
+
Self::Num(n, raw) => Ok(exact_i64(*n, raw.as_deref()).map_or_else(
|
|
353
|
+
|| raw.clone().unwrap_or_else(|| n.to_string()),
|
|
354
|
+
|i| i.to_string(),
|
|
355
|
+
)),
|
|
356
|
+
Self::Bool(b) => Ok(b.to_string()),
|
|
357
|
+
other => Err(Refusal::TypeMismatch(format!(
|
|
358
|
+
"cannot use {other:?} as an identity component"
|
|
359
|
+
))),
|
|
335
360
|
}
|
|
336
361
|
}
|
|
337
362
|
|
|
@@ -340,13 +365,28 @@ impl Json {
|
|
|
340
365
|
/// A present-but-blank key is an ordinary non-matching value there; non-scalars still refuse.
|
|
341
366
|
pub fn to_id_component_lenient(&self) -> Result<String, Refusal> {
|
|
342
367
|
match self {
|
|
343
|
-
|
|
368
|
+
Self::Str(s) => Ok(s.clone()),
|
|
344
369
|
other => other.to_id_component(),
|
|
345
370
|
}
|
|
346
371
|
}
|
|
347
372
|
|
|
348
|
-
pub fn parse(input: &str) -> Result<
|
|
349
|
-
|
|
373
|
+
pub fn parse(input: &str) -> Result<Self, String> {
|
|
374
|
+
Self::parse_with(input, false)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// Parses `input` keeping every number literal's source text, so writing the value back
|
|
378
|
+
/// reproduces each number exactly as given (`0.0` stays `0.0`, not `0`). For a document that
|
|
379
|
+
/// is only reshaped and written out again, never evaluated.
|
|
380
|
+
pub fn parse_keeping_number_text(input: &str) -> Result<Self, String> {
|
|
381
|
+
Self::parse_with(input, true)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn parse_with(input: &str, keep_number_text: bool) -> Result<Self, String> {
|
|
385
|
+
let mut parser = Parser {
|
|
386
|
+
chars: input.chars().peekable(),
|
|
387
|
+
depth: 0,
|
|
388
|
+
keep_number_text,
|
|
389
|
+
};
|
|
350
390
|
let value = parser.parse_value()?;
|
|
351
391
|
parser.skip_ws();
|
|
352
392
|
Ok(value)
|
|
@@ -360,7 +400,7 @@ impl Json {
|
|
|
360
400
|
|
|
361
401
|
fn write(&self, out: &mut String) {
|
|
362
402
|
match self {
|
|
363
|
-
|
|
403
|
+
Self::Object(fields) => {
|
|
364
404
|
out.push('{');
|
|
365
405
|
for (i, (k, v)) in fields.iter().enumerate() {
|
|
366
406
|
if i > 0 {
|
|
@@ -372,7 +412,7 @@ impl Json {
|
|
|
372
412
|
}
|
|
373
413
|
out.push('}');
|
|
374
414
|
}
|
|
375
|
-
|
|
415
|
+
Self::Array(items) => {
|
|
376
416
|
out.push('[');
|
|
377
417
|
for (i, v) in items.iter().enumerate() {
|
|
378
418
|
if i > 0 {
|
|
@@ -382,21 +422,21 @@ impl Json {
|
|
|
382
422
|
}
|
|
383
423
|
out.push(']');
|
|
384
424
|
}
|
|
385
|
-
|
|
425
|
+
Self::Str(s) => write_escaped_string(s, out),
|
|
386
426
|
// The exact source text wins when there is one (BUG#147): re-deriving digits from
|
|
387
427
|
// `n` would re-round a value the `f64` approximation already lost precision on.
|
|
388
|
-
|
|
389
|
-
|
|
428
|
+
Self::Num(_, Some(raw)) => out.push_str(raw),
|
|
429
|
+
Self::Num(n, None) => out.push_str(&canonical_integer_digits(*n)),
|
|
390
430
|
// Always a decimal point (`10.0`), as Ruby's Float#to_json; `Num` must not grow one.
|
|
391
|
-
|
|
431
|
+
Self::Float(n) => {
|
|
392
432
|
let rendered = n.to_string();
|
|
393
433
|
out.push_str(&rendered);
|
|
394
434
|
if !rendered.contains('.') && !rendered.contains('e') && !rendered.contains('E') {
|
|
395
435
|
out.push_str(".0");
|
|
396
436
|
}
|
|
397
437
|
}
|
|
398
|
-
|
|
399
|
-
|
|
438
|
+
Self::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
|
439
|
+
Self::Null => out.push_str("null"),
|
|
400
440
|
}
|
|
401
441
|
}
|
|
402
442
|
}
|
|
@@ -419,8 +459,9 @@ fn integral_i64(n: f64) -> Option<i64> {
|
|
|
419
459
|
/// wrong `i64` via `integral_i64`'s `f64` path alone. Parsing the original digits sidesteps that
|
|
420
460
|
/// gap; `raw` is `None` for anything short enough that the gap never opens, so this is the same
|
|
421
461
|
/// answer `integral_i64` already gave for every value the old, one-field `Num` could represent.
|
|
422
|
-
fn exact_i64(n: f64, raw:
|
|
423
|
-
raw.
|
|
462
|
+
fn exact_i64(n: f64, raw: Option<&str>) -> Option<i64> {
|
|
463
|
+
raw.and_then(|r| r.parse::<i64>().ok())
|
|
464
|
+
.or_else(|| integral_i64(n))
|
|
424
465
|
}
|
|
425
466
|
|
|
426
467
|
/// The digits `write` prints for a `Num` with no exact source text: whichever of the two
|
|
@@ -456,16 +497,24 @@ mod integral_i64_tests {
|
|
|
456
497
|
fn accepts_i64_boundaries() {
|
|
457
498
|
assert_eq!(integral_i64(i64::MIN as f64), Some(i64::MIN));
|
|
458
499
|
// Largest f64 below 2^63; `i64::MAX` itself rounds up to 2^63 in f64.
|
|
459
|
-
let largest_safe =
|
|
500
|
+
let largest_safe = 9_223_372_036_854_774_784.0_f64;
|
|
460
501
|
assert_eq!(integral_i64(largest_safe), Some(largest_safe as i64));
|
|
461
502
|
}
|
|
462
503
|
|
|
463
504
|
#[test]
|
|
464
505
|
fn accepts_i64_saturation_boundary() {
|
|
465
506
|
// `as i64` would saturate on either of these.
|
|
466
|
-
assert_eq!(
|
|
507
|
+
assert_eq!(
|
|
508
|
+
integral_i64(2f64.powi(63)),
|
|
509
|
+
None,
|
|
510
|
+
"2^63 must not saturate to i64::MAX"
|
|
511
|
+
);
|
|
467
512
|
// Near 2^63 a small nudge rounds back to the same float, so use 2^64.
|
|
468
|
-
assert_eq!(
|
|
513
|
+
assert_eq!(
|
|
514
|
+
integral_i64(-(2f64.powi(64))),
|
|
515
|
+
None,
|
|
516
|
+
"well below i64::MIN must not saturate to i64::MIN"
|
|
517
|
+
);
|
|
469
518
|
}
|
|
470
519
|
|
|
471
520
|
#[test]
|
|
@@ -478,18 +527,34 @@ mod integral_i64_tests {
|
|
|
478
527
|
#[test]
|
|
479
528
|
fn to_id_component_reflects_true_magnitude_instead_of_a_clamped_one() {
|
|
480
529
|
let huge = Json::Num(1e30, None);
|
|
481
|
-
let id = huge
|
|
482
|
-
|
|
483
|
-
|
|
530
|
+
let id = huge
|
|
531
|
+
.to_id_component()
|
|
532
|
+
.expect("numbers are always usable as an id component");
|
|
533
|
+
assert_ne!(
|
|
534
|
+
id,
|
|
535
|
+
i64::MAX.to_string(),
|
|
536
|
+
"must not silently clamp to i64::MAX"
|
|
537
|
+
);
|
|
538
|
+
assert!(
|
|
539
|
+
id.starts_with('1'),
|
|
540
|
+
"should reflect the real magnitude, got {id:?}"
|
|
541
|
+
);
|
|
484
542
|
}
|
|
485
543
|
|
|
486
544
|
#[test]
|
|
487
545
|
fn parse_preserves_a_huge_integers_exact_digits_past_f64_precision() {
|
|
488
546
|
// BUG#147: 2^100 (2**100, the fuzzer's own INTEGER_EDGE_CASES bignum), and the
|
|
489
547
|
// signed 31-digit repro from Roster::Roster.AddSeat's routing_key mutation.
|
|
490
|
-
for literal in [
|
|
548
|
+
for literal in [
|
|
549
|
+
"-1267650600228229401496703205376",
|
|
550
|
+
"1267650600228229401496703205376",
|
|
551
|
+
] {
|
|
491
552
|
let parsed = Json::parse(literal).expect("a bare JSON integer parses");
|
|
492
|
-
assert_eq!(
|
|
553
|
+
assert_eq!(
|
|
554
|
+
parsed.to_json_string(),
|
|
555
|
+
literal,
|
|
556
|
+
"re-emitted JSON must not lose a single digit"
|
|
557
|
+
);
|
|
493
558
|
assert_eq!(
|
|
494
559
|
parsed.to_id_component().unwrap(),
|
|
495
560
|
literal,
|
|
@@ -523,8 +588,14 @@ mod integral_i64_tests {
|
|
|
523
588
|
fn a_bignum_and_an_ordinary_number_are_still_comparable_and_never_falsely_equal() {
|
|
524
589
|
let huge = Json::parse("1267650600228229401496703205376").unwrap();
|
|
525
590
|
let other_huge = Json::parse("1267650600228229401496703205377").unwrap();
|
|
526
|
-
assert_ne!(
|
|
527
|
-
|
|
591
|
+
assert_ne!(
|
|
592
|
+
huge, other_huge,
|
|
593
|
+
"different bignums must not collide via f64 rounding"
|
|
594
|
+
);
|
|
595
|
+
assert_eq!(
|
|
596
|
+
huge,
|
|
597
|
+
Json::parse("1267650600228229401496703205376").unwrap()
|
|
598
|
+
);
|
|
528
599
|
assert_ne!(huge, Json::Num(5.0, None));
|
|
529
600
|
}
|
|
530
601
|
|
|
@@ -532,7 +603,10 @@ mod integral_i64_tests {
|
|
|
532
603
|
fn to_id_component_refuses_an_empty_string() {
|
|
533
604
|
// Ruby treats a blank identity part as absent; a blank id must not be accepted.
|
|
534
605
|
let blank = Json::Str(String::new());
|
|
535
|
-
assert!(
|
|
606
|
+
assert!(
|
|
607
|
+
blank.to_id_component().is_err(),
|
|
608
|
+
"an empty string must not be usable as an identity component"
|
|
609
|
+
);
|
|
536
610
|
}
|
|
537
611
|
|
|
538
612
|
#[test]
|
|
@@ -545,14 +619,21 @@ mod integral_i64_tests {
|
|
|
545
619
|
fn to_id_component_lenient_accepts_an_empty_string() {
|
|
546
620
|
// A blank key is truthy in Ruby and flows on as a non-matching value.
|
|
547
621
|
let blank = Json::Str(String::new());
|
|
548
|
-
assert_eq!(
|
|
622
|
+
assert_eq!(
|
|
623
|
+
blank.to_id_component_lenient().unwrap(),
|
|
624
|
+
"",
|
|
625
|
+
"a present, blank string must be a valid (non-matching) component"
|
|
626
|
+
);
|
|
549
627
|
}
|
|
550
628
|
|
|
551
629
|
#[test]
|
|
552
630
|
fn to_id_component_lenient_still_refuses_a_non_scalar() {
|
|
553
631
|
// Only the blank-string case differs from `to_id_component`.
|
|
554
632
|
let list = Json::Array(vec![]);
|
|
555
|
-
assert!(
|
|
633
|
+
assert!(
|
|
634
|
+
list.to_id_component_lenient().is_err(),
|
|
635
|
+
"a non-scalar must still refuse, same as to_id_component"
|
|
636
|
+
);
|
|
556
637
|
}
|
|
557
638
|
}
|
|
558
639
|
|
|
@@ -570,7 +651,9 @@ mod value_object_shape_tests {
|
|
|
570
651
|
#[test]
|
|
571
652
|
fn an_object_passes_through_untouched() {
|
|
572
653
|
let object = Json::obj(vec![("given", Json::Str("Ada".into()))]);
|
|
573
|
-
assert!(object
|
|
654
|
+
assert!(object
|
|
655
|
+
.expect_value_object_shape("name", "PersonName")
|
|
656
|
+
.is_ok());
|
|
574
657
|
}
|
|
575
658
|
|
|
576
659
|
#[test]
|
|
@@ -579,7 +662,10 @@ mod value_object_shape_tests {
|
|
|
579
662
|
refusal_text(&Json::Str("Ada".into())),
|
|
580
663
|
"name is a PersonName — pass its fields as an object, not \"Ada\""
|
|
581
664
|
);
|
|
582
|
-
assert_eq!(
|
|
665
|
+
assert_eq!(
|
|
666
|
+
refusal_text(&Json::Num(7.0, None)),
|
|
667
|
+
"name is a PersonName — pass its fields as an object, not 7"
|
|
668
|
+
);
|
|
583
669
|
}
|
|
584
670
|
|
|
585
671
|
#[test]
|
|
@@ -616,7 +702,9 @@ fn write_escaped_string(s: &str, out: &mut String) {
|
|
|
616
702
|
// Ruby's `JSON.generate` writes these two short, not as `\u0008` / `\u000c`.
|
|
617
703
|
'\u{8}' => out.push_str("\\b"),
|
|
618
704
|
'\u{c}' => out.push_str("\\f"),
|
|
619
|
-
c if (c as u32) < 0x20 =>
|
|
705
|
+
c if (c as u32) < 0x20 => {
|
|
706
|
+
let _ = write!(out, "\\u{:04x}", c as u32);
|
|
707
|
+
}
|
|
620
708
|
c => out.push(c),
|
|
621
709
|
}
|
|
622
710
|
}
|
|
@@ -627,6 +715,7 @@ fn write_escaped_string(s: &str, out: &mut String) {
|
|
|
627
715
|
struct Parser<'a> {
|
|
628
716
|
chars: std::iter::Peekable<std::str::Chars<'a>>,
|
|
629
717
|
depth: usize,
|
|
718
|
+
keep_number_text: bool,
|
|
630
719
|
}
|
|
631
720
|
|
|
632
721
|
/// How deeply arrays and objects may nest before `parse` refuses. Each level is a recursive call,
|
|
@@ -634,13 +723,26 @@ struct Parser<'a> {
|
|
|
634
723
|
/// refusing. 128 is `serde_json`'s own recursion limit, so the host and this parser refuse alike.
|
|
635
724
|
const MAX_DEPTH: usize = 128;
|
|
636
725
|
|
|
637
|
-
impl
|
|
726
|
+
impl Parser<'_> {
|
|
638
727
|
fn skip_ws(&mut self) {
|
|
639
728
|
while matches!(self.chars.peek(), Some(c) if c.is_whitespace()) {
|
|
640
729
|
self.chars.next();
|
|
641
730
|
}
|
|
642
731
|
}
|
|
643
732
|
|
|
733
|
+
/// Appends the next char to `out` when it satisfies `pred`, reporting whether it did.
|
|
734
|
+
fn take_one(&mut self, out: &mut String, pred: impl Fn(char) -> bool) -> bool {
|
|
735
|
+
self.chars
|
|
736
|
+
.next_if(|c| pred(*c))
|
|
737
|
+
.map(|c| out.push(c))
|
|
738
|
+
.is_some()
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/// Appends every consecutive ASCII digit to `out`.
|
|
742
|
+
fn take_digits(&mut self, out: &mut String) {
|
|
743
|
+
while self.take_one(out, |c| c.is_ascii_digit()) {}
|
|
744
|
+
}
|
|
745
|
+
|
|
644
746
|
fn expect(&mut self, ch: char) -> Result<(), String> {
|
|
645
747
|
match self.chars.next() {
|
|
646
748
|
Some(c) if c == ch => Ok(()),
|
|
@@ -665,7 +767,7 @@ impl<'a> Parser<'a> {
|
|
|
665
767
|
Some('{') => self.parse_object(),
|
|
666
768
|
Some('[') => self.parse_array(),
|
|
667
769
|
Some('"') => self.parse_string().map(Json::Str),
|
|
668
|
-
Some('t'
|
|
770
|
+
Some('t' | 'f') => self.parse_bool(),
|
|
669
771
|
Some('n') => self.parse_null(),
|
|
670
772
|
Some(c) if *c == '-' || c.is_ascii_digit() => self.parse_number(),
|
|
671
773
|
other => Err(format!("unexpected {other:?} in JSON")),
|
|
@@ -673,7 +775,10 @@ impl<'a> Parser<'a> {
|
|
|
673
775
|
}
|
|
674
776
|
|
|
675
777
|
// Counts one level of nesting for the duration of `body`; refuses past `MAX_DEPTH`.
|
|
676
|
-
fn nested(
|
|
778
|
+
fn nested(
|
|
779
|
+
&mut self,
|
|
780
|
+
body: impl FnOnce(&mut Self) -> Result<Json, String>,
|
|
781
|
+
) -> Result<Json, String> {
|
|
677
782
|
if self.depth >= MAX_DEPTH {
|
|
678
783
|
return Err(format!("JSON nests deeper than {MAX_DEPTH} levels"));
|
|
679
784
|
}
|
|
@@ -708,7 +813,7 @@ impl<'a> Parser<'a> {
|
|
|
708
813
|
fields.push((key, value));
|
|
709
814
|
self.skip_ws();
|
|
710
815
|
match self.chars.next() {
|
|
711
|
-
Some(',') =>
|
|
816
|
+
Some(',') => {}
|
|
712
817
|
Some('}') => break,
|
|
713
818
|
other => return Err(format!("expected ',' or '}}' in object, got {other:?}")),
|
|
714
819
|
}
|
|
@@ -728,7 +833,7 @@ impl<'a> Parser<'a> {
|
|
|
728
833
|
items.push(self.parse_value()?);
|
|
729
834
|
self.skip_ws();
|
|
730
835
|
match self.chars.next() {
|
|
731
|
-
Some(',') =>
|
|
836
|
+
Some(',') => {}
|
|
732
837
|
Some(']') => break,
|
|
733
838
|
other => return Err(format!("expected ',' or ']' in array, got {other:?}")),
|
|
734
839
|
}
|
|
@@ -752,8 +857,10 @@ impl<'a> Parser<'a> {
|
|
|
752
857
|
Some('b') => out.push('\u{8}'),
|
|
753
858
|
Some('f') => out.push('\u{c}'),
|
|
754
859
|
Some('u') => {
|
|
755
|
-
let hex: String =
|
|
756
|
-
|
|
860
|
+
let hex: String =
|
|
861
|
+
(0..4).map(|_| self.chars.next().unwrap_or('0')).collect();
|
|
862
|
+
let code = u32::from_str_radix(&hex, 16)
|
|
863
|
+
.map_err(|_| format!("bad \\u escape {hex:?}"))?;
|
|
757
864
|
out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
|
|
758
865
|
}
|
|
759
866
|
other => return Err(format!("bad escape sequence \\{other:?}")),
|
|
@@ -785,26 +892,14 @@ impl<'a> Parser<'a> {
|
|
|
785
892
|
|
|
786
893
|
fn parse_number(&mut self) -> Result<Json, String> {
|
|
787
894
|
let mut s = String::new();
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
s.push(self.chars.next().unwrap());
|
|
793
|
-
}
|
|
794
|
-
if self.chars.peek() == Some(&'.') {
|
|
795
|
-
s.push(self.chars.next().unwrap());
|
|
796
|
-
while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
|
|
797
|
-
s.push(self.chars.next().unwrap());
|
|
798
|
-
}
|
|
895
|
+
self.take_one(&mut s, |c| c == '-');
|
|
896
|
+
self.take_digits(&mut s);
|
|
897
|
+
if self.take_one(&mut s, |c| c == '.') {
|
|
898
|
+
self.take_digits(&mut s);
|
|
799
899
|
}
|
|
800
|
-
if
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
s.push(self.chars.next().unwrap());
|
|
804
|
-
}
|
|
805
|
-
while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
|
|
806
|
-
s.push(self.chars.next().unwrap());
|
|
807
|
-
}
|
|
900
|
+
if self.take_one(&mut s, |c| c == 'e' || c == 'E') {
|
|
901
|
+
self.take_one(&mut s, |c| c == '+' || c == '-');
|
|
902
|
+
self.take_digits(&mut s);
|
|
808
903
|
}
|
|
809
904
|
let value: f64 = s.parse().map_err(|e| format!("bad number {s:?}: {e}"))?;
|
|
810
905
|
// A plain integer literal keeps its exact source digits (BUG#147) whenever `write`'s own
|
|
@@ -813,7 +908,8 @@ impl<'a> Parser<'a> {
|
|
|
813
908
|
// A literal with a `.`/exponent is a genuine `Float`, and Ruby's `Float` is the same
|
|
814
909
|
// `f64` Rust's is, so there is nothing to preserve there.
|
|
815
910
|
let is_plain_integer = !s.contains('.') && !s.contains(['e', 'E']);
|
|
816
|
-
let keeps_source =
|
|
911
|
+
let keeps_source =
|
|
912
|
+
self.keep_number_text || (is_plain_integer && canonical_integer_digits(value) != s);
|
|
817
913
|
// `1e999` parses to infinity with no source text kept, and `write` would print it as `inf`:
|
|
818
914
|
// not JSON, so refuse it. A plain integer keeps its digits, which `write` re-emits as-is.
|
|
819
915
|
if !value.is_finite() && !keeps_source {
|