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/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,16 @@ 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
|
-
let mut parser = Parser {
|
|
373
|
+
pub fn parse(input: &str) -> Result<Self, String> {
|
|
374
|
+
let mut parser = Parser {
|
|
375
|
+
chars: input.chars().peekable(),
|
|
376
|
+
depth: 0,
|
|
377
|
+
};
|
|
350
378
|
let value = parser.parse_value()?;
|
|
351
379
|
parser.skip_ws();
|
|
352
380
|
Ok(value)
|
|
@@ -360,7 +388,7 @@ impl Json {
|
|
|
360
388
|
|
|
361
389
|
fn write(&self, out: &mut String) {
|
|
362
390
|
match self {
|
|
363
|
-
|
|
391
|
+
Self::Object(fields) => {
|
|
364
392
|
out.push('{');
|
|
365
393
|
for (i, (k, v)) in fields.iter().enumerate() {
|
|
366
394
|
if i > 0 {
|
|
@@ -372,7 +400,7 @@ impl Json {
|
|
|
372
400
|
}
|
|
373
401
|
out.push('}');
|
|
374
402
|
}
|
|
375
|
-
|
|
403
|
+
Self::Array(items) => {
|
|
376
404
|
out.push('[');
|
|
377
405
|
for (i, v) in items.iter().enumerate() {
|
|
378
406
|
if i > 0 {
|
|
@@ -382,21 +410,21 @@ impl Json {
|
|
|
382
410
|
}
|
|
383
411
|
out.push(']');
|
|
384
412
|
}
|
|
385
|
-
|
|
413
|
+
Self::Str(s) => write_escaped_string(s, out),
|
|
386
414
|
// The exact source text wins when there is one (BUG#147): re-deriving digits from
|
|
387
415
|
// `n` would re-round a value the `f64` approximation already lost precision on.
|
|
388
|
-
|
|
389
|
-
|
|
416
|
+
Self::Num(_, Some(raw)) => out.push_str(raw),
|
|
417
|
+
Self::Num(n, None) => out.push_str(&canonical_integer_digits(*n)),
|
|
390
418
|
// Always a decimal point (`10.0`), as Ruby's Float#to_json; `Num` must not grow one.
|
|
391
|
-
|
|
419
|
+
Self::Float(n) => {
|
|
392
420
|
let rendered = n.to_string();
|
|
393
421
|
out.push_str(&rendered);
|
|
394
422
|
if !rendered.contains('.') && !rendered.contains('e') && !rendered.contains('E') {
|
|
395
423
|
out.push_str(".0");
|
|
396
424
|
}
|
|
397
425
|
}
|
|
398
|
-
|
|
399
|
-
|
|
426
|
+
Self::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
|
427
|
+
Self::Null => out.push_str("null"),
|
|
400
428
|
}
|
|
401
429
|
}
|
|
402
430
|
}
|
|
@@ -419,8 +447,9 @@ fn integral_i64(n: f64) -> Option<i64> {
|
|
|
419
447
|
/// wrong `i64` via `integral_i64`'s `f64` path alone. Parsing the original digits sidesteps that
|
|
420
448
|
/// gap; `raw` is `None` for anything short enough that the gap never opens, so this is the same
|
|
421
449
|
/// answer `integral_i64` already gave for every value the old, one-field `Num` could represent.
|
|
422
|
-
fn exact_i64(n: f64, raw:
|
|
423
|
-
raw.
|
|
450
|
+
fn exact_i64(n: f64, raw: Option<&str>) -> Option<i64> {
|
|
451
|
+
raw.and_then(|r| r.parse::<i64>().ok())
|
|
452
|
+
.or_else(|| integral_i64(n))
|
|
424
453
|
}
|
|
425
454
|
|
|
426
455
|
/// The digits `write` prints for a `Num` with no exact source text: whichever of the two
|
|
@@ -456,16 +485,24 @@ mod integral_i64_tests {
|
|
|
456
485
|
fn accepts_i64_boundaries() {
|
|
457
486
|
assert_eq!(integral_i64(i64::MIN as f64), Some(i64::MIN));
|
|
458
487
|
// Largest f64 below 2^63; `i64::MAX` itself rounds up to 2^63 in f64.
|
|
459
|
-
let largest_safe =
|
|
488
|
+
let largest_safe = 9_223_372_036_854_774_784.0_f64;
|
|
460
489
|
assert_eq!(integral_i64(largest_safe), Some(largest_safe as i64));
|
|
461
490
|
}
|
|
462
491
|
|
|
463
492
|
#[test]
|
|
464
493
|
fn accepts_i64_saturation_boundary() {
|
|
465
494
|
// `as i64` would saturate on either of these.
|
|
466
|
-
assert_eq!(
|
|
495
|
+
assert_eq!(
|
|
496
|
+
integral_i64(2f64.powi(63)),
|
|
497
|
+
None,
|
|
498
|
+
"2^63 must not saturate to i64::MAX"
|
|
499
|
+
);
|
|
467
500
|
// Near 2^63 a small nudge rounds back to the same float, so use 2^64.
|
|
468
|
-
assert_eq!(
|
|
501
|
+
assert_eq!(
|
|
502
|
+
integral_i64(-(2f64.powi(64))),
|
|
503
|
+
None,
|
|
504
|
+
"well below i64::MIN must not saturate to i64::MIN"
|
|
505
|
+
);
|
|
469
506
|
}
|
|
470
507
|
|
|
471
508
|
#[test]
|
|
@@ -478,18 +515,34 @@ mod integral_i64_tests {
|
|
|
478
515
|
#[test]
|
|
479
516
|
fn to_id_component_reflects_true_magnitude_instead_of_a_clamped_one() {
|
|
480
517
|
let huge = Json::Num(1e30, None);
|
|
481
|
-
let id = huge
|
|
482
|
-
|
|
483
|
-
|
|
518
|
+
let id = huge
|
|
519
|
+
.to_id_component()
|
|
520
|
+
.expect("numbers are always usable as an id component");
|
|
521
|
+
assert_ne!(
|
|
522
|
+
id,
|
|
523
|
+
i64::MAX.to_string(),
|
|
524
|
+
"must not silently clamp to i64::MAX"
|
|
525
|
+
);
|
|
526
|
+
assert!(
|
|
527
|
+
id.starts_with('1'),
|
|
528
|
+
"should reflect the real magnitude, got {id:?}"
|
|
529
|
+
);
|
|
484
530
|
}
|
|
485
531
|
|
|
486
532
|
#[test]
|
|
487
533
|
fn parse_preserves_a_huge_integers_exact_digits_past_f64_precision() {
|
|
488
534
|
// BUG#147: 2^100 (2**100, the fuzzer's own INTEGER_EDGE_CASES bignum), and the
|
|
489
535
|
// signed 31-digit repro from Roster::Roster.AddSeat's routing_key mutation.
|
|
490
|
-
for literal in [
|
|
536
|
+
for literal in [
|
|
537
|
+
"-1267650600228229401496703205376",
|
|
538
|
+
"1267650600228229401496703205376",
|
|
539
|
+
] {
|
|
491
540
|
let parsed = Json::parse(literal).expect("a bare JSON integer parses");
|
|
492
|
-
assert_eq!(
|
|
541
|
+
assert_eq!(
|
|
542
|
+
parsed.to_json_string(),
|
|
543
|
+
literal,
|
|
544
|
+
"re-emitted JSON must not lose a single digit"
|
|
545
|
+
);
|
|
493
546
|
assert_eq!(
|
|
494
547
|
parsed.to_id_component().unwrap(),
|
|
495
548
|
literal,
|
|
@@ -523,8 +576,14 @@ mod integral_i64_tests {
|
|
|
523
576
|
fn a_bignum_and_an_ordinary_number_are_still_comparable_and_never_falsely_equal() {
|
|
524
577
|
let huge = Json::parse("1267650600228229401496703205376").unwrap();
|
|
525
578
|
let other_huge = Json::parse("1267650600228229401496703205377").unwrap();
|
|
526
|
-
assert_ne!(
|
|
527
|
-
|
|
579
|
+
assert_ne!(
|
|
580
|
+
huge, other_huge,
|
|
581
|
+
"different bignums must not collide via f64 rounding"
|
|
582
|
+
);
|
|
583
|
+
assert_eq!(
|
|
584
|
+
huge,
|
|
585
|
+
Json::parse("1267650600228229401496703205376").unwrap()
|
|
586
|
+
);
|
|
528
587
|
assert_ne!(huge, Json::Num(5.0, None));
|
|
529
588
|
}
|
|
530
589
|
|
|
@@ -532,7 +591,10 @@ mod integral_i64_tests {
|
|
|
532
591
|
fn to_id_component_refuses_an_empty_string() {
|
|
533
592
|
// Ruby treats a blank identity part as absent; a blank id must not be accepted.
|
|
534
593
|
let blank = Json::Str(String::new());
|
|
535
|
-
assert!(
|
|
594
|
+
assert!(
|
|
595
|
+
blank.to_id_component().is_err(),
|
|
596
|
+
"an empty string must not be usable as an identity component"
|
|
597
|
+
);
|
|
536
598
|
}
|
|
537
599
|
|
|
538
600
|
#[test]
|
|
@@ -545,14 +607,21 @@ mod integral_i64_tests {
|
|
|
545
607
|
fn to_id_component_lenient_accepts_an_empty_string() {
|
|
546
608
|
// A blank key is truthy in Ruby and flows on as a non-matching value.
|
|
547
609
|
let blank = Json::Str(String::new());
|
|
548
|
-
assert_eq!(
|
|
610
|
+
assert_eq!(
|
|
611
|
+
blank.to_id_component_lenient().unwrap(),
|
|
612
|
+
"",
|
|
613
|
+
"a present, blank string must be a valid (non-matching) component"
|
|
614
|
+
);
|
|
549
615
|
}
|
|
550
616
|
|
|
551
617
|
#[test]
|
|
552
618
|
fn to_id_component_lenient_still_refuses_a_non_scalar() {
|
|
553
619
|
// Only the blank-string case differs from `to_id_component`.
|
|
554
620
|
let list = Json::Array(vec![]);
|
|
555
|
-
assert!(
|
|
621
|
+
assert!(
|
|
622
|
+
list.to_id_component_lenient().is_err(),
|
|
623
|
+
"a non-scalar must still refuse, same as to_id_component"
|
|
624
|
+
);
|
|
556
625
|
}
|
|
557
626
|
}
|
|
558
627
|
|
|
@@ -570,7 +639,9 @@ mod value_object_shape_tests {
|
|
|
570
639
|
#[test]
|
|
571
640
|
fn an_object_passes_through_untouched() {
|
|
572
641
|
let object = Json::obj(vec![("given", Json::Str("Ada".into()))]);
|
|
573
|
-
assert!(object
|
|
642
|
+
assert!(object
|
|
643
|
+
.expect_value_object_shape("name", "PersonName")
|
|
644
|
+
.is_ok());
|
|
574
645
|
}
|
|
575
646
|
|
|
576
647
|
#[test]
|
|
@@ -579,7 +650,10 @@ mod value_object_shape_tests {
|
|
|
579
650
|
refusal_text(&Json::Str("Ada".into())),
|
|
580
651
|
"name is a PersonName — pass its fields as an object, not \"Ada\""
|
|
581
652
|
);
|
|
582
|
-
assert_eq!(
|
|
653
|
+
assert_eq!(
|
|
654
|
+
refusal_text(&Json::Num(7.0, None)),
|
|
655
|
+
"name is a PersonName — pass its fields as an object, not 7"
|
|
656
|
+
);
|
|
583
657
|
}
|
|
584
658
|
|
|
585
659
|
#[test]
|
|
@@ -616,7 +690,9 @@ fn write_escaped_string(s: &str, out: &mut String) {
|
|
|
616
690
|
// Ruby's `JSON.generate` writes these two short, not as `\u0008` / `\u000c`.
|
|
617
691
|
'\u{8}' => out.push_str("\\b"),
|
|
618
692
|
'\u{c}' => out.push_str("\\f"),
|
|
619
|
-
c if (c as u32) < 0x20 =>
|
|
693
|
+
c if (c as u32) < 0x20 => {
|
|
694
|
+
let _ = write!(out, "\\u{:04x}", c as u32);
|
|
695
|
+
}
|
|
620
696
|
c => out.push(c),
|
|
621
697
|
}
|
|
622
698
|
}
|
|
@@ -634,13 +710,26 @@ struct Parser<'a> {
|
|
|
634
710
|
/// refusing. 128 is `serde_json`'s own recursion limit, so the host and this parser refuse alike.
|
|
635
711
|
const MAX_DEPTH: usize = 128;
|
|
636
712
|
|
|
637
|
-
impl
|
|
713
|
+
impl Parser<'_> {
|
|
638
714
|
fn skip_ws(&mut self) {
|
|
639
715
|
while matches!(self.chars.peek(), Some(c) if c.is_whitespace()) {
|
|
640
716
|
self.chars.next();
|
|
641
717
|
}
|
|
642
718
|
}
|
|
643
719
|
|
|
720
|
+
/// Appends the next char to `out` when it satisfies `pred`, reporting whether it did.
|
|
721
|
+
fn take_one(&mut self, out: &mut String, pred: impl Fn(char) -> bool) -> bool {
|
|
722
|
+
self.chars
|
|
723
|
+
.next_if(|c| pred(*c))
|
|
724
|
+
.map(|c| out.push(c))
|
|
725
|
+
.is_some()
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/// Appends every consecutive ASCII digit to `out`.
|
|
729
|
+
fn take_digits(&mut self, out: &mut String) {
|
|
730
|
+
while self.take_one(out, |c| c.is_ascii_digit()) {}
|
|
731
|
+
}
|
|
732
|
+
|
|
644
733
|
fn expect(&mut self, ch: char) -> Result<(), String> {
|
|
645
734
|
match self.chars.next() {
|
|
646
735
|
Some(c) if c == ch => Ok(()),
|
|
@@ -665,7 +754,7 @@ impl<'a> Parser<'a> {
|
|
|
665
754
|
Some('{') => self.parse_object(),
|
|
666
755
|
Some('[') => self.parse_array(),
|
|
667
756
|
Some('"') => self.parse_string().map(Json::Str),
|
|
668
|
-
Some('t'
|
|
757
|
+
Some('t' | 'f') => self.parse_bool(),
|
|
669
758
|
Some('n') => self.parse_null(),
|
|
670
759
|
Some(c) if *c == '-' || c.is_ascii_digit() => self.parse_number(),
|
|
671
760
|
other => Err(format!("unexpected {other:?} in JSON")),
|
|
@@ -673,7 +762,10 @@ impl<'a> Parser<'a> {
|
|
|
673
762
|
}
|
|
674
763
|
|
|
675
764
|
// Counts one level of nesting for the duration of `body`; refuses past `MAX_DEPTH`.
|
|
676
|
-
fn nested(
|
|
765
|
+
fn nested(
|
|
766
|
+
&mut self,
|
|
767
|
+
body: impl FnOnce(&mut Self) -> Result<Json, String>,
|
|
768
|
+
) -> Result<Json, String> {
|
|
677
769
|
if self.depth >= MAX_DEPTH {
|
|
678
770
|
return Err(format!("JSON nests deeper than {MAX_DEPTH} levels"));
|
|
679
771
|
}
|
|
@@ -708,7 +800,7 @@ impl<'a> Parser<'a> {
|
|
|
708
800
|
fields.push((key, value));
|
|
709
801
|
self.skip_ws();
|
|
710
802
|
match self.chars.next() {
|
|
711
|
-
Some(',') =>
|
|
803
|
+
Some(',') => {}
|
|
712
804
|
Some('}') => break,
|
|
713
805
|
other => return Err(format!("expected ',' or '}}' in object, got {other:?}")),
|
|
714
806
|
}
|
|
@@ -728,7 +820,7 @@ impl<'a> Parser<'a> {
|
|
|
728
820
|
items.push(self.parse_value()?);
|
|
729
821
|
self.skip_ws();
|
|
730
822
|
match self.chars.next() {
|
|
731
|
-
Some(',') =>
|
|
823
|
+
Some(',') => {}
|
|
732
824
|
Some(']') => break,
|
|
733
825
|
other => return Err(format!("expected ',' or ']' in array, got {other:?}")),
|
|
734
826
|
}
|
|
@@ -752,8 +844,10 @@ impl<'a> Parser<'a> {
|
|
|
752
844
|
Some('b') => out.push('\u{8}'),
|
|
753
845
|
Some('f') => out.push('\u{c}'),
|
|
754
846
|
Some('u') => {
|
|
755
|
-
let hex: String =
|
|
756
|
-
|
|
847
|
+
let hex: String =
|
|
848
|
+
(0..4).map(|_| self.chars.next().unwrap_or('0')).collect();
|
|
849
|
+
let code = u32::from_str_radix(&hex, 16)
|
|
850
|
+
.map_err(|_| format!("bad \\u escape {hex:?}"))?;
|
|
757
851
|
out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
|
|
758
852
|
}
|
|
759
853
|
other => return Err(format!("bad escape sequence \\{other:?}")),
|
|
@@ -785,26 +879,14 @@ impl<'a> Parser<'a> {
|
|
|
785
879
|
|
|
786
880
|
fn parse_number(&mut self) -> Result<Json, String> {
|
|
787
881
|
let mut s = String::new();
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
while matches!(self.chars.peek(), Some(c) if c.is_ascii_digit()) {
|
|
797
|
-
s.push(self.chars.next().unwrap());
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
if matches!(self.chars.peek(), Some('e') | Some('E')) {
|
|
801
|
-
s.push(self.chars.next().unwrap());
|
|
802
|
-
if matches!(self.chars.peek(), Some('+') | Some('-')) {
|
|
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
|
-
}
|
|
882
|
+
self.take_one(&mut s, |c| c == '-');
|
|
883
|
+
self.take_digits(&mut s);
|
|
884
|
+
if self.take_one(&mut s, |c| c == '.') {
|
|
885
|
+
self.take_digits(&mut s);
|
|
886
|
+
}
|
|
887
|
+
if self.take_one(&mut s, |c| c == 'e' || c == 'E') {
|
|
888
|
+
self.take_one(&mut s, |c| c == '+' || c == '-');
|
|
889
|
+
self.take_digits(&mut s);
|
|
808
890
|
}
|
|
809
891
|
let value: f64 = s.parse().map_err(|e| format!("bad number {s:?}: {e}"))?;
|
|
810
892
|
// A plain integer literal keeps its exact source digits (BUG#147) whenever `write`'s own
|