hecks 1.3.0 → 1.5.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/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
require_relative "../projector"
|
|
2
|
+
require_relative "../runtime/refusal_wording"
|
|
3
|
+
require_relative "vocabulary"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Projections
|
|
7
|
+
# The language's closed sets, projected into the Rust kernel — one
|
|
8
|
+
# `rust/src/kernel/vocab/<table>.rs` per Vocabulary table, each a
|
|
9
|
+
# `pub enum` plus `match`-based accessors with no wildcard arm, so a
|
|
10
|
+
# row the chapter gains is a compile error at every exhaustive match
|
|
11
|
+
# over it rather than a silent gap.
|
|
12
|
+
#
|
|
13
|
+
# Projector.call(:rust_vocabulary, bluebook: <the Bluebook chapter>)
|
|
14
|
+
# # => { "vocab/mod.rs" => "...", "vocab/refusal_template.rs" => "...", ... }
|
|
15
|
+
#
|
|
16
|
+
# Paths are relative to rust/src/kernel/; bin/project_rust_vocabulary
|
|
17
|
+
# writes them. This generalizes the old bin/project_refusal_wording,
|
|
18
|
+
# which read Runtime::RefusalWording::TEMPLATES — a hand-typed copy of
|
|
19
|
+
# the RefusalTemplate rows. This reads the chapter itself.
|
|
20
|
+
#
|
|
21
|
+
# ## Only `TABLES` the kernel needs
|
|
22
|
+
#
|
|
23
|
+
# Each entry in `TABLES` names its enum and its generated extras; a
|
|
24
|
+
# table not listed is not projected. The enum names that differ from
|
|
25
|
+
# the table name keep existing Rust call sites (`RefusalSite`) and the
|
|
26
|
+
# dispatch-step names D1 consumes (`AggregateStep`, `EntityStep`).
|
|
27
|
+
#
|
|
28
|
+
# ## Typed refusal arguments
|
|
29
|
+
#
|
|
30
|
+
# The templates table also reads RefusalSiteArgument: every site gets a
|
|
31
|
+
# `<Variant>Args` struct whose fields are exactly the site's declared
|
|
32
|
+
# arguments (a `&str` per scalar, a `&[&str]` per list) and a
|
|
33
|
+
# `render_args` that formats each one by its row. `render` itself stays
|
|
34
|
+
# private to the generated module, so no call site can pass an argument
|
|
35
|
+
# list by hand — leaving one out does not compile. The generated test
|
|
36
|
+
# pins every site's `render_args` output (empty, single and multiple
|
|
37
|
+
# unsorted lists) against Runtime::RefusalWording.render_with, computed
|
|
38
|
+
# here in Ruby.
|
|
39
|
+
module RustVocabulary
|
|
40
|
+
extend Projector::Target
|
|
41
|
+
|
|
42
|
+
projects_as :rust_vocabulary, declares: "Vocabulary", emits: :files
|
|
43
|
+
|
|
44
|
+
GENERATOR = "bin/project_rust_vocabulary".freeze
|
|
45
|
+
SOURCE = "lib/hecks/language/bluebook/vocabulary.bluebook".freeze
|
|
46
|
+
|
|
47
|
+
# table name => enum name, variant-naming fields, file stem, kind.
|
|
48
|
+
# `:order` tables also get `ORDER` and `position`; `:templates`
|
|
49
|
+
# gets `render`, the typed `<Variant>Args` (read off the `arguments`
|
|
50
|
+
# table) and the placeholder and argument-rendering tests.
|
|
51
|
+
TABLES = {
|
|
52
|
+
"RefusalTemplate" => { enum: "RefusalSite", variant_from: %w[refusal site], file: "refusal_template",
|
|
53
|
+
kind: :templates, arguments: "RefusalSiteArgument" },
|
|
54
|
+
"QueryComparator" => { enum: "QueryComparator", variant_from: %w[name], file: "query_comparator", kind: :set },
|
|
55
|
+
"FieldHint" => { enum: "FieldHint", variant_from: %w[name], file: "field_hint", kind: :set },
|
|
56
|
+
"AggregateDispatchOrder" => { enum: "AggregateStep", variant_from: %w[step], file: "aggregate_dispatch_order",
|
|
57
|
+
kind: :order },
|
|
58
|
+
"EntityDispatchOrder" => { enum: "EntityStep", variant_from: %w[step], file: "entity_dispatch_order",
|
|
59
|
+
kind: :order }
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
RUST_KEYWORDS = %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop
|
|
63
|
+
match mod move mut pub ref return self static struct super trait true type unsafe use where
|
|
64
|
+
while abstract become box do final macro override priv typeof unsized virtual yield try].freeze
|
|
65
|
+
|
|
66
|
+
module_function
|
|
67
|
+
|
|
68
|
+
# The projector protocol.
|
|
69
|
+
#
|
|
70
|
+
# @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
|
|
71
|
+
# Vocabulary aggregate to project
|
|
72
|
+
# @param options [Hash] unused; accepted to satisfy the registry's call shape
|
|
73
|
+
# @return [Hash{String => String}] each generated file's path, relative to
|
|
74
|
+
# `rust/src/kernel/`, mapped to its full Rust source
|
|
75
|
+
def call(bluebook:, options: {}) = render(bluebook)
|
|
76
|
+
|
|
77
|
+
# Renders every file `TABLES` projects: `vocab/mod.rs` plus one `.rs` file
|
|
78
|
+
# per table, each an exhaustive enum with accessors and a pinned test module.
|
|
79
|
+
#
|
|
80
|
+
# @param bluebook [Bluebook::Behaviour::Chapter] the chapter declaring the
|
|
81
|
+
# Vocabulary aggregate to render
|
|
82
|
+
# @return [Hash{String => String}] each generated file's path, relative to
|
|
83
|
+
# `rust/src/kernel/`, mapped to its full Rust source
|
|
84
|
+
# @raise [ArgumentError] if `bluebook` does not declare a `TABLES`-named table,
|
|
85
|
+
# or the `:arguments` table a `:templates` entry names
|
|
86
|
+
def render(bluebook)
|
|
87
|
+
tables = Vocabulary.tables(bluebook)
|
|
88
|
+
files = TABLES.to_h do |table, spec|
|
|
89
|
+
rows = tables.fetch(table) { raise ArgumentError, "Vocabulary declares no #{table} table" }
|
|
90
|
+
arguments = spec[:arguments] && tables.fetch(spec[:arguments]) do
|
|
91
|
+
raise ArgumentError, "Vocabulary declares no #{spec[:arguments]} table"
|
|
92
|
+
end
|
|
93
|
+
["vocab/#{spec[:file]}.rs", table_file(table, spec, rows, arguments)]
|
|
94
|
+
end
|
|
95
|
+
{ "vocab/mod.rs" => mod_file }.merge(files)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The generated-file banner every projected file opens with.
|
|
99
|
+
#
|
|
100
|
+
# @param table [String] the Vocabulary table name being projected, such as
|
|
101
|
+
# `"RefusalTemplate"`
|
|
102
|
+
# @return [String] the banner, as Rust line comments
|
|
103
|
+
def header(table)
|
|
104
|
+
<<~RUST
|
|
105
|
+
// GENERATED by #{GENERATOR} (Hecks::Projections::RustVocabulary,
|
|
106
|
+
// lib/hecks/projections/rust_vocabulary.rb) from Vocabulary::#{table}
|
|
107
|
+
// (#{SOURCE}).
|
|
108
|
+
// Do not hand-edit — re-run #{GENERATOR} instead.
|
|
109
|
+
RUST
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Renders `vocab/mod.rs`, the entry point that re-exports every projected enum.
|
|
113
|
+
#
|
|
114
|
+
# @return [String] `vocab/mod.rs`'s full source: one `pub mod` and `pub use`
|
|
115
|
+
# per entry in `TABLES`
|
|
116
|
+
def mod_file
|
|
117
|
+
mods = TABLES.values.map { |spec| "pub mod #{spec[:file]};" }.join("\n")
|
|
118
|
+
exports = TABLES.values.map { |spec| "pub use #{spec[:file]}::#{spec[:enum]};" }.join("\n")
|
|
119
|
+
<<~RUST
|
|
120
|
+
// GENERATED by #{GENERATOR} (Hecks::Projections::RustVocabulary,
|
|
121
|
+
// lib/hecks/projections/rust_vocabulary.rb) from the Vocabulary
|
|
122
|
+
// aggregate (#{SOURCE}).
|
|
123
|
+
// Do not hand-edit — re-run #{GENERATOR} instead.
|
|
124
|
+
//
|
|
125
|
+
// One module per projected table. Every enum below is matched
|
|
126
|
+
// exhaustively by its own accessors — no wildcard arm — so a row the
|
|
127
|
+
// language gains fails to compile until it is handled.
|
|
128
|
+
|
|
129
|
+
#{mods}
|
|
130
|
+
|
|
131
|
+
#{exports}
|
|
132
|
+
RUST
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# One table's full `.rs` source: its exhaustive enum, per-field accessors,
|
|
136
|
+
# `from_name`, its `:order`/`:templates` extras, and its `#[cfg(test)]` module.
|
|
137
|
+
#
|
|
138
|
+
# @param table [String] the Vocabulary table name, such as `"RefusalTemplate"`
|
|
139
|
+
# @param spec [Hash] the table's `TABLES` entry (`:enum`, `:variant_from`,
|
|
140
|
+
# `:file`, `:kind`, and, for `:templates`, `:arguments`)
|
|
141
|
+
# @param rows [Array<Hash{String => String}>] the table's rows, as read off
|
|
142
|
+
# the chapter by `Vocabulary.tables`
|
|
143
|
+
# @param argument_rows [Array<Hash{String => String}>, nil] the
|
|
144
|
+
# `RefusalSiteArgument` rows, only when `spec[:kind] == :templates`
|
|
145
|
+
# @return [String] the table's full `.rs` file source
|
|
146
|
+
# @raise [ArgumentError] if two rows would generate the same enum variant name
|
|
147
|
+
def table_file(table, spec, rows, argument_rows = nil)
|
|
148
|
+
variants = rows.map { |row| variant_name(row, spec[:variant_from]) }
|
|
149
|
+
duplicates = variants.tally.select { |_, count| count > 1 }.keys
|
|
150
|
+
raise ArgumentError, "duplicate #{spec[:enum]} variant(s): #{duplicates.join(', ')}" if duplicates.any?
|
|
151
|
+
|
|
152
|
+
enum = spec[:enum]
|
|
153
|
+
fields = rows.first.keys
|
|
154
|
+
by_site = spec[:kind] == :templates ? site_arguments(rows, argument_rows) : nil
|
|
155
|
+
body = [
|
|
156
|
+
header(table),
|
|
157
|
+
"#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]",
|
|
158
|
+
"pub enum #{enum} {",
|
|
159
|
+
variants.map { |variant| " #{variant}," },
|
|
160
|
+
"}",
|
|
161
|
+
"",
|
|
162
|
+
"impl #{enum} {",
|
|
163
|
+
constants(enum, variants, spec[:kind]),
|
|
164
|
+
fields.map { |field| accessor(enum, field, variants, rows) },
|
|
165
|
+
from_name(enum, spec[:kind]),
|
|
166
|
+
extras(enum, variants, spec[:kind]),
|
|
167
|
+
"}",
|
|
168
|
+
"",
|
|
169
|
+
by_site ? argument_types(enum, variants, rows, by_site) : [],
|
|
170
|
+
tests(enum, spec[:kind], variants, rows, by_site)
|
|
171
|
+
]
|
|
172
|
+
"#{body.flatten.join("\n").rstrip}\n"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Renders the enum's declared-order constant: `ORDER` for `:order`, `ALL` otherwise.
|
|
176
|
+
#
|
|
177
|
+
# @param enum [String] the Rust enum name, such as `"RefusalSite"`
|
|
178
|
+
# @param variants [Array<String>] each row's generated variant name, in
|
|
179
|
+
# declared order
|
|
180
|
+
# @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
|
|
181
|
+
# @return [Array<String>] the lines declaring the enum's `ORDER` constant
|
|
182
|
+
# (`:order`) or `ALL` constant (otherwise)
|
|
183
|
+
def constants(enum, variants, kind)
|
|
184
|
+
list = variants.map { |variant| " #{enum}::#{variant}," }
|
|
185
|
+
if kind == :order
|
|
186
|
+
[" /// The declared step order, first to last.",
|
|
187
|
+
" pub const ORDER: [#{enum}; #{variants.size}] = [", list, " ];", ""]
|
|
188
|
+
else
|
|
189
|
+
[" /// Every row, in declared order.",
|
|
190
|
+
" pub const ALL: &'static [#{enum}] = &[", list, " ];", ""]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Renders one field's `pub fn` accessor, an exhaustive match over every variant.
|
|
195
|
+
#
|
|
196
|
+
# @param enum [String] the Rust enum name
|
|
197
|
+
# @param field [String] the row field this accessor reads, such as `"name"`
|
|
198
|
+
# @param variants [Array<String>] each row's generated variant name, in
|
|
199
|
+
# declared order, matching `rows`
|
|
200
|
+
# @param rows [Array<Hash{String => String}>] the table's rows, in the same
|
|
201
|
+
# order as `variants`
|
|
202
|
+
# @return [Array<String>] the lines for the field's `pub fn` accessor, one
|
|
203
|
+
# match arm per variant
|
|
204
|
+
def accessor(enum, field, variants, rows)
|
|
205
|
+
arms = variants.zip(rows).map { |variant, row| " #{enum}::#{variant} => #{rust_string(row.fetch(field))}," }
|
|
206
|
+
[" /// The row's declared `#{field}`.",
|
|
207
|
+
" pub fn #{accessor_name(field)}(&self) -> &'static str {",
|
|
208
|
+
" match self {",
|
|
209
|
+
arms,
|
|
210
|
+
" }",
|
|
211
|
+
" }",
|
|
212
|
+
""]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Built on the enum's own list and name accessor, not a string
|
|
216
|
+
# match, so it needs no wildcard arm either.
|
|
217
|
+
#
|
|
218
|
+
# @param enum [String] the Rust enum name
|
|
219
|
+
# @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
|
|
220
|
+
# @return [Array<String>] the lines for the enum's `from_name` associated function
|
|
221
|
+
def from_name(enum, kind)
|
|
222
|
+
list = kind == :order ? "ORDER" : "ALL"
|
|
223
|
+
field = kind == :templates ? "site" : name_field(kind)
|
|
224
|
+
[" /// The row whose `#{field}` is `name`, if any.",
|
|
225
|
+
" pub fn from_name(name: &str) -> Option<#{enum}> {",
|
|
226
|
+
" #{enum}::#{list}.iter().copied().find(|row| row.#{accessor_name(field)}() == name)",
|
|
227
|
+
" }",
|
|
228
|
+
""]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Renders the extra methods a table's `:kind` adds beyond the shared accessors.
|
|
232
|
+
#
|
|
233
|
+
# @param enum [String] the Rust enum name
|
|
234
|
+
# @param variants [Array<String>] each row's generated variant name, in
|
|
235
|
+
# declared order
|
|
236
|
+
# @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
|
|
237
|
+
# @return [Array<String>] `:order`'s `position` method, `:templates`'s private
|
|
238
|
+
# `render` method, or `[]` for `:set`
|
|
239
|
+
def extras(enum, variants, kind)
|
|
240
|
+
case kind
|
|
241
|
+
when :order
|
|
242
|
+
arms = variants.each_with_index.map { |variant, index| " #{enum}::#{variant} => #{index}," }
|
|
243
|
+
[" /// This step's index in `ORDER`.",
|
|
244
|
+
" pub fn position(&self) -> usize {",
|
|
245
|
+
" match self {",
|
|
246
|
+
arms,
|
|
247
|
+
" }",
|
|
248
|
+
" }"]
|
|
249
|
+
when :templates
|
|
250
|
+
[" /// `RefusalWording.substitute`: replace every `{key}` marker, in the",
|
|
251
|
+
" /// order given. A template is read, never evaluated. Private: call",
|
|
252
|
+
" /// sites go through a site's typed `<Variant>Args::render_args`,",
|
|
253
|
+
" /// which formats and supplies every declared argument.",
|
|
254
|
+
" fn render(&self, values: &[(&str, &str)]) -> String {",
|
|
255
|
+
" let mut text = self.template().to_string();",
|
|
256
|
+
" for (key, value) in values {",
|
|
257
|
+
" text = text.replace(&format!(\"{{{key}}}\"), value);",
|
|
258
|
+
" }",
|
|
259
|
+
" text",
|
|
260
|
+
" }"]
|
|
261
|
+
else
|
|
262
|
+
[]
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# [refusal, site] => that site's argument rows, in declared order —
|
|
267
|
+
# refused unless they name exactly the template's own placeholders,
|
|
268
|
+
# in the order each first appears, with only known formatting rules.
|
|
269
|
+
#
|
|
270
|
+
# @param rows [Array<Hash{String => String}>] the `RefusalTemplate` rows,
|
|
271
|
+
# in declared order
|
|
272
|
+
# @param argument_rows [Array<Hash{String => String}>] the `RefusalSiteArgument`
|
|
273
|
+
# rows for every site
|
|
274
|
+
# @return [Hash{Array<String> => Array<Hash{String => String}>}] each
|
|
275
|
+
# `[refusal, site]` pair mapped to its declared argument rows, in declared order
|
|
276
|
+
# @raise [ArgumentError] if an argument row names a `refusal`/`site` no
|
|
277
|
+
# `RefusalTemplate` row declares, if a site's declared arguments do not match
|
|
278
|
+
# its template's `{placeholder}`s in order, or if `check_rule!` rejects a
|
|
279
|
+
# row's `"shape"`, `"quoting"`, or `"sorted"` value
|
|
280
|
+
def site_arguments(rows, argument_rows)
|
|
281
|
+
grouped = argument_rows.group_by { |row| [row.fetch("refusal"), row.fetch("site")] }
|
|
282
|
+
orphans = grouped.keys - rows.map { |row| [row.fetch("refusal"), row.fetch("site")] }
|
|
283
|
+
raise ArgumentError, "RefusalSiteArgument rows name no RefusalTemplate: #{orphans.inspect}" if orphans.any?
|
|
284
|
+
|
|
285
|
+
rows.to_h do |row|
|
|
286
|
+
key = [row.fetch("refusal"), row.fetch("site")]
|
|
287
|
+
specs = grouped.fetch(key, [])
|
|
288
|
+
wants = row.fetch("template").scan(/\{(\w+)\}/).flatten.uniq
|
|
289
|
+
names = specs.map { |spec| spec.fetch("argument") }
|
|
290
|
+
unless names == wants
|
|
291
|
+
raise ArgumentError, "RefusalSiteArgument for #{key.join('/')} declares #{names.inspect}; " \
|
|
292
|
+
"its template's placeholders are #{wants.inspect}"
|
|
293
|
+
end
|
|
294
|
+
specs.each { |spec| check_rule!(key, spec) }
|
|
295
|
+
[key, specs]
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Refuses a `RefusalSiteArgument` row whose formatting fields are not
|
|
300
|
+
# recognized rules.
|
|
301
|
+
#
|
|
302
|
+
# @param key [Array<String>] the `[refusal, site]` pair `spec` belongs to,
|
|
303
|
+
# used in the raised message
|
|
304
|
+
# @param spec [Hash{String => String}] one `RefusalSiteArgument` row
|
|
305
|
+
# (`"shape"`, `"quoting"`, `"sorted"`)
|
|
306
|
+
# @return [void]
|
|
307
|
+
# @raise [ArgumentError] if `spec`'s `"shape"` is not in
|
|
308
|
+
# `Runtime::RefusalWording::SHAPES`, its `"quoting"` is not in
|
|
309
|
+
# `Runtime::RefusalWording::QUOTINGS`, or its `"sorted"` is not
|
|
310
|
+
# `"true"`/`"false"`
|
|
311
|
+
def check_rule!(key, spec)
|
|
312
|
+
where = "#{key.join('/')}.#{spec.fetch('argument')}"
|
|
313
|
+
unless Runtime::RefusalWording::SHAPES.include?(spec.fetch("shape"))
|
|
314
|
+
raise ArgumentError, "#{where}: unknown shape #{spec['shape'].inspect}"
|
|
315
|
+
end
|
|
316
|
+
unless Runtime::RefusalWording::QUOTINGS.include?(spec.fetch("quoting"))
|
|
317
|
+
raise ArgumentError, "#{where}: unknown quoting #{spec['quoting'].inspect}"
|
|
318
|
+
end
|
|
319
|
+
return if %w[true false].include?(spec.fetch("sorted"))
|
|
320
|
+
|
|
321
|
+
raise ArgumentError, "#{where}: sorted must be \"true\" or \"false\""
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Renders every `:templates` variant's typed argument struct, plus the two
|
|
325
|
+
# helper functions (`quoted`, `list`) they share.
|
|
326
|
+
#
|
|
327
|
+
# @param enum [String] the Rust enum name
|
|
328
|
+
# @param variants [Array<String>] each row's generated variant name, matching `rows`
|
|
329
|
+
# @param rows [Array<Hash{String => String}>] the `RefusalTemplate` rows, in
|
|
330
|
+
# the same order as `variants`
|
|
331
|
+
# @param by_site [Hash{Array<String> => Array<Hash{String => String}>}] each
|
|
332
|
+
# `[refusal, site]` pair's declared argument rows, as built by `site_arguments`
|
|
333
|
+
# @return [Array<String>] the shared `quoted`/`list` helper functions, followed
|
|
334
|
+
# by every variant's `<Variant>Args` struct and `render_args` implementation
|
|
335
|
+
def argument_types(enum, variants, rows, by_site)
|
|
336
|
+
support = [
|
|
337
|
+
"/// Ruby's `#inspect` of a name, as a refusal quotes it: `{:?}` on a",
|
|
338
|
+
"/// `&str`, the quoting every kernel call site used before",
|
|
339
|
+
"/// RefusalSiteArgument existed.",
|
|
340
|
+
"fn quoted(text: &str) -> String {",
|
|
341
|
+
" format!(\"{text:?}\")",
|
|
342
|
+
"}",
|
|
343
|
+
"",
|
|
344
|
+
"/// A list argument, written the way its RefusalSiteArgument row says:",
|
|
345
|
+
"/// sorted first (before quoting), each item quoted, then joined; an",
|
|
346
|
+
"/// empty list reads `when_empty`.",
|
|
347
|
+
"fn list(items: &[&str], sorted: bool, inspect: bool, separator: &str, when_empty: &str) -> String {",
|
|
348
|
+
" let mut items = items.to_vec();",
|
|
349
|
+
" if sorted {",
|
|
350
|
+
" items.sort_unstable();",
|
|
351
|
+
" }",
|
|
352
|
+
" if items.is_empty() {",
|
|
353
|
+
" return when_empty.to_string();",
|
|
354
|
+
" }",
|
|
355
|
+
" let write = |item: &&str| if inspect { quoted(item) } else { item.to_string() };",
|
|
356
|
+
" items.iter().map(write).collect::<Vec<_>>().join(separator)",
|
|
357
|
+
"}",
|
|
358
|
+
""
|
|
359
|
+
]
|
|
360
|
+
support + variants.zip(rows).flat_map do |variant, row|
|
|
361
|
+
args_struct(enum, variant, row, by_site.fetch([row.fetch("refusal"), row.fetch("site")]))
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Renders one variant's `<Variant>Args` struct and its `render_args` method.
|
|
366
|
+
#
|
|
367
|
+
# @param enum [String] the Rust enum name
|
|
368
|
+
# @param variant [String] the row's generated variant name
|
|
369
|
+
# @param row [Hash{String => String}] the `RefusalTemplate` row this variant projects
|
|
370
|
+
# @param specs [Array<Hash{String => String}>] the variant's declared
|
|
371
|
+
# `RefusalSiteArgument` rows, in declared order
|
|
372
|
+
# @return [Array<String>] the lines for the variant's `<Variant>Args` struct
|
|
373
|
+
# and its `render_args` method
|
|
374
|
+
def args_struct(enum, variant, row, specs)
|
|
375
|
+
fields = specs.flat_map do |spec|
|
|
376
|
+
type = spec.fetch("shape") == "list" ? "&'a [&'a str]" : "&'a str"
|
|
377
|
+
[" /// #{rule_reading(spec)}", " pub #{rust_field(spec.fetch('argument'))}: #{type},"]
|
|
378
|
+
end
|
|
379
|
+
locals = specs.filter_map { |spec| formatted_local(spec) }
|
|
380
|
+
pairs = specs.map do |spec|
|
|
381
|
+
name = spec.fetch("argument")
|
|
382
|
+
value = formatted_local(spec) ? "#{local_name(name)}.as_str()" : "self.#{rust_field(name)}"
|
|
383
|
+
" (#{rust_string(name)}, #{value}),"
|
|
384
|
+
end
|
|
385
|
+
["/// `#{enum}::#{variant}`'s arguments — `RefusalWording.render_site(" \
|
|
386
|
+
"#{rust_string(row.fetch('refusal'))}, #{rust_string(row.fetch('site'))}, ...)`.",
|
|
387
|
+
"#[derive(Debug, Clone, Copy)]",
|
|
388
|
+
"pub struct #{variant}Args<'a> {",
|
|
389
|
+
fields,
|
|
390
|
+
"}",
|
|
391
|
+
"",
|
|
392
|
+
"impl #{variant}Args<'_> {",
|
|
393
|
+
" /// The site's wording, every argument formatted by its declared row.",
|
|
394
|
+
" pub fn render_args(&self) -> String {",
|
|
395
|
+
locals.map { |line| " #{line}" },
|
|
396
|
+
" #{enum}::#{variant}.render(&[",
|
|
397
|
+
pairs,
|
|
398
|
+
" ])",
|
|
399
|
+
" }",
|
|
400
|
+
"}",
|
|
401
|
+
""]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# The `let` binding, if any, `args_struct` inserts before a variant's
|
|
405
|
+
# `render_args` computes its argument pairs.
|
|
406
|
+
#
|
|
407
|
+
# @param spec [Hash{String => String}] one `RefusalSiteArgument` row
|
|
408
|
+
# (`"argument"`, `"shape"`, `"quoting"`, and, for a list, `"sorted"`,
|
|
409
|
+
# `"separator"`, `"when_empty"`)
|
|
410
|
+
# @return [String, nil] the `let` line formatting the argument through
|
|
411
|
+
# `list`/`quoted`, or `nil` if the argument needs no formatting local
|
|
412
|
+
# (a scalar written as-is)
|
|
413
|
+
def formatted_local(spec)
|
|
414
|
+
name = spec.fetch("argument")
|
|
415
|
+
field = "self.#{rust_field(name)}"
|
|
416
|
+
if spec.fetch("shape") == "list"
|
|
417
|
+
"let #{local_name(name)} = list(#{field}, #{spec.fetch('sorted')}, #{spec.fetch('quoting') == 'inspect'}, " \
|
|
418
|
+
"#{rust_string(spec.fetch('separator'))}, #{rust_string(spec.fetch('when_empty'))});"
|
|
419
|
+
elsif spec.fetch("quoting") == "inspect"
|
|
420
|
+
"let #{local_name(name)} = quoted(#{field});"
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Describes one argument's formatting rule in English, for its struct field's doc comment.
|
|
425
|
+
#
|
|
426
|
+
# @param spec [Hash{String => String}] one `RefusalSiteArgument` row
|
|
427
|
+
# (`"shape"`, `"quoting"`, and, for a list, `"sorted"`, `"separator"`,
|
|
428
|
+
# `"when_empty"`)
|
|
429
|
+
# @return [String] a short English reading of the row's formatting rule,
|
|
430
|
+
# used as the struct field's own doc comment
|
|
431
|
+
def rule_reading(spec)
|
|
432
|
+
quoting = spec.fetch("quoting") == "inspect" ? ", quoted" : ""
|
|
433
|
+
return "scalar#{quoting}" unless spec.fetch("shape") == "list"
|
|
434
|
+
|
|
435
|
+
sorted = spec.fetch("sorted") == "true" ? ", sorted" : ""
|
|
436
|
+
"list#{sorted}#{quoting}, joined #{spec.fetch('separator').inspect}, empty reads #{spec.fetch('when_empty').inspect}"
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# The local variable name `formatted_local` binds an argument's formatted value to.
|
|
440
|
+
#
|
|
441
|
+
# @param argument [String] the `RefusalSiteArgument` row's `"argument"` name
|
|
442
|
+
# @return [String] the Rust local variable name `formatted_local` binds the
|
|
443
|
+
# formatted value to
|
|
444
|
+
def local_name(argument) = "#{argument}_text"
|
|
445
|
+
|
|
446
|
+
# An argument name, escaped for use as a Rust field/parameter identifier.
|
|
447
|
+
#
|
|
448
|
+
# @param argument [String] the `RefusalSiteArgument` row's `"argument"` name
|
|
449
|
+
# @return [String] `argument`, prefixed with `r#` if it collides with a Rust
|
|
450
|
+
# keyword in `RUST_KEYWORDS`
|
|
451
|
+
def rust_field(argument) = RUST_KEYWORDS.include?(argument) ? "r##{argument}" : argument
|
|
452
|
+
|
|
453
|
+
# Renders the table's `#[cfg(test)]` module: every kind's `from_name` round-trip
|
|
454
|
+
# test, plus `:order`'s and `:templates`' own tests.
|
|
455
|
+
#
|
|
456
|
+
# @param enum [String] the Rust enum name
|
|
457
|
+
# @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
|
|
458
|
+
# @param variants [Array<String>] each row's generated variant name, only
|
|
459
|
+
# read when `kind == :templates`
|
|
460
|
+
# @param rows [Array<Hash{String => String}>] the table's rows, only read
|
|
461
|
+
# when `kind == :templates`
|
|
462
|
+
# @param by_site [Hash{Array<String> => Array<Hash{String => String}>}, nil]
|
|
463
|
+
# each `[refusal, site]` pair's declared argument rows, only read when
|
|
464
|
+
# `kind == :templates`
|
|
465
|
+
# @return [Array<String>] the `#[cfg(test)]` module's full source lines
|
|
466
|
+
def tests(enum, kind, variants = [], rows = [], by_site = nil)
|
|
467
|
+
list = kind == :order ? "ORDER" : "ALL"
|
|
468
|
+
name = accessor_name(kind == :templates ? "site" : name_field(kind))
|
|
469
|
+
lines = ["#[cfg(test)]",
|
|
470
|
+
"mod tests {",
|
|
471
|
+
" use super::*;",
|
|
472
|
+
""]
|
|
473
|
+
unless kind == :templates
|
|
474
|
+
lines += [" #[test]",
|
|
475
|
+
" fn every_row_is_found_by_its_own_name() {",
|
|
476
|
+
" for row in #{enum}::#{list}.iter() {",
|
|
477
|
+
" assert_eq!(#{enum}::from_name(row.#{name}()), Some(*row));",
|
|
478
|
+
" }",
|
|
479
|
+
" }",
|
|
480
|
+
""]
|
|
481
|
+
end
|
|
482
|
+
lines += order_test(enum) if kind == :order
|
|
483
|
+
if kind == :templates
|
|
484
|
+
lines += placeholder_test(enum)
|
|
485
|
+
lines += [""] + render_args_test(variants, rows, by_site)
|
|
486
|
+
end
|
|
487
|
+
lines.pop while lines.last == ""
|
|
488
|
+
lines + ["}"]
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# The `:order` kind's own test, checked into every order table's test module.
|
|
492
|
+
#
|
|
493
|
+
# @param enum [String] the Rust enum name
|
|
494
|
+
# @return [Array<String>] the test asserting `ORDER`'s index matches each
|
|
495
|
+
# variant's `position`
|
|
496
|
+
def order_test(enum)
|
|
497
|
+
[" #[test]",
|
|
498
|
+
" fn position_is_the_index_in_order() {",
|
|
499
|
+
" for (index, step) in #{enum}::ORDER.iter().enumerate() {",
|
|
500
|
+
" assert_eq!(step.position(), index);",
|
|
501
|
+
" }",
|
|
502
|
+
" }",
|
|
503
|
+
""]
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Placeholders are read off each template's own text, never a second
|
|
507
|
+
# hand-kept list.
|
|
508
|
+
#
|
|
509
|
+
# @param enum [String] the Rust enum name
|
|
510
|
+
# @return [Array<String>] the test asserting every site's `render` leaves
|
|
511
|
+
# no `{...}` placeholder unfilled
|
|
512
|
+
def placeholder_test(enum)
|
|
513
|
+
[" #[test]",
|
|
514
|
+
" fn every_site_renders_with_no_leftover_placeholder() {",
|
|
515
|
+
" for site in #{enum}::ALL {",
|
|
516
|
+
" let template = site.template();",
|
|
517
|
+
" let mut keys: Vec<String> = Vec::new();",
|
|
518
|
+
" let mut current: Option<String> = None;",
|
|
519
|
+
" for ch in template.chars() {",
|
|
520
|
+
" match (ch, current.as_mut()) {",
|
|
521
|
+
" ('{', _) => current = Some(String::new()),",
|
|
522
|
+
" ('}', Some(key)) => {",
|
|
523
|
+
" keys.push(key.clone());",
|
|
524
|
+
" current = None;",
|
|
525
|
+
" }",
|
|
526
|
+
" (_, Some(key)) => key.push(ch),",
|
|
527
|
+
" (_, None) => {}",
|
|
528
|
+
" }",
|
|
529
|
+
" }",
|
|
530
|
+
" let dummies: Vec<(String, String)> = keys.iter().map(|k| (k.clone(), format!(\"<{k}>\"))).collect();",
|
|
531
|
+
" let pairs: Vec<(&str, &str)> = dummies.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();",
|
|
532
|
+
" let rendered = site.render(&pairs);",
|
|
533
|
+
" assert!(",
|
|
534
|
+
" !rendered.contains('{') && !rendered.contains('}'),",
|
|
535
|
+
" \"{site:?} left an unrendered placeholder behind: {rendered:?}\"",
|
|
536
|
+
" );",
|
|
537
|
+
" }",
|
|
538
|
+
" }"]
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# The Ruby oracle, pinned. Every site renders through its typed
|
|
542
|
+
# `render_args` for each edge case a list argument has — empty, one
|
|
543
|
+
# item, several out of order — and must equal what
|
|
544
|
+
# Runtime::RefusalWording.render_with answers for the same values,
|
|
545
|
+
# computed here at generation time. Scalars carry a `"` so quoting
|
|
546
|
+
# is compared too.
|
|
547
|
+
#
|
|
548
|
+
# @param variants [Array<String>] each row's generated variant name, matching `rows`
|
|
549
|
+
# @param rows [Array<Hash{String => String}>] the `RefusalTemplate` rows, in
|
|
550
|
+
# the same order as `variants`
|
|
551
|
+
# @param by_site [Hash{Array<String> => Array<Hash{String => String}>}] each
|
|
552
|
+
# `[refusal, site]` pair's declared argument rows, as built by `site_arguments`
|
|
553
|
+
# @return [Array<String>] the test asserting every variant's `render_args`
|
|
554
|
+
# output for each `argument_cases` case matches
|
|
555
|
+
# `Runtime::RefusalWording.render_with`, computed here in Ruby
|
|
556
|
+
def render_args_test(variants, rows, by_site)
|
|
557
|
+
asserts = variants.zip(rows).flat_map do |variant, row|
|
|
558
|
+
specs = by_site.fetch([row.fetch("refusal"), row.fetch("site")])
|
|
559
|
+
argument_cases(specs).map do |arguments|
|
|
560
|
+
expected = Runtime::RefusalWording.render_with(row.fetch("template"), specs, arguments)
|
|
561
|
+
fields = specs.map do |spec|
|
|
562
|
+
value = arguments.fetch(spec.fetch("argument").to_sym)
|
|
563
|
+
rust_value = value.is_a?(Array) ? "&[#{value.map { |item| rust_string(item) }.join(', ')}]" : rust_string(value)
|
|
564
|
+
"#{rust_field(spec.fetch('argument'))}: #{rust_value}"
|
|
565
|
+
end
|
|
566
|
+
[" assert_eq!(",
|
|
567
|
+
" #{variant}Args { #{fields.join(', ')} }.render_args(),",
|
|
568
|
+
" #{rust_string(expected)}",
|
|
569
|
+
" );"]
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
[" #[test]",
|
|
573
|
+
" fn render_args_matches_ruby_render_site() {",
|
|
574
|
+
asserts,
|
|
575
|
+
" }"].flatten
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
LIST_CASES = [[], ["only \"one\""], %w[zeta alpha mid]].freeze
|
|
579
|
+
|
|
580
|
+
# Builds the argument-value cases `render_args_test` checks a site's
|
|
581
|
+
# `render_args` against.
|
|
582
|
+
#
|
|
583
|
+
# @param specs [Array<Hash{String => String}>] one site's declared
|
|
584
|
+
# `RefusalSiteArgument` rows
|
|
585
|
+
# @return [Array<Hash{Symbol => Object}>] one argument Hash per test case —
|
|
586
|
+
# `LIST_CASES` (empty, one item, several unsorted) if any spec is
|
|
587
|
+
# list-shaped, else a single scalar case, each scalar value carrying a `"`
|
|
588
|
+
def argument_cases(specs)
|
|
589
|
+
cases = specs.any? { |spec| spec.fetch("shape") == "list" } ? LIST_CASES : [nil]
|
|
590
|
+
cases.map do |items|
|
|
591
|
+
specs.to_h do |spec|
|
|
592
|
+
name = spec.fetch("argument")
|
|
593
|
+
[name.to_sym, spec.fetch("shape") == "list" ? items : "#{name} \"x\""]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# The row field a table's `from_name`/round-trip test treats as its "name".
|
|
599
|
+
#
|
|
600
|
+
# @param kind [Symbol] the table's `:kind` (`:order`, `:templates`, or `:set`)
|
|
601
|
+
# @return [String] the row field used as this kind's "name" — `"step"` for
|
|
602
|
+
# `:order`, `"name"` otherwise
|
|
603
|
+
def name_field(kind) = kind == :order ? "step" : "name"
|
|
604
|
+
|
|
605
|
+
# `refusal` is a Rust-safe word but reads ambiguously beside the
|
|
606
|
+
# kernel's own `Refusal` enum; the accessor says what it answers.
|
|
607
|
+
#
|
|
608
|
+
# @param field [String] a row field name, such as `"name"` or `"refusal"`
|
|
609
|
+
# @return [String] the Rust accessor method name for `field` —
|
|
610
|
+
# `"refusal_class"` for `"refusal"`, `field` otherwise
|
|
611
|
+
def accessor_name(field) = field == "refusal" ? "refusal_class" : field
|
|
612
|
+
|
|
613
|
+
# Builds one row's Rust enum variant name from its declared `variant_from` fields.
|
|
614
|
+
#
|
|
615
|
+
# @param row [Hash{String => String}] the table row to name
|
|
616
|
+
# @param fields [Array<String>] the row fields, PascalCased and concatenated
|
|
617
|
+
# in order to build the variant name
|
|
618
|
+
# @return [String] the row's generated Rust enum variant name
|
|
619
|
+
# @raise [ArgumentError] if the built name is not a valid Rust variant
|
|
620
|
+
# identifier (uppercase first letter, alphanumeric only)
|
|
621
|
+
def variant_name(row, fields)
|
|
622
|
+
name = fields.map { |field| pascal(row.fetch(field)) }.join
|
|
623
|
+
raise ArgumentError, "#{name.inspect} is not a Rust variant name" unless name.match?(/\A[A-Z][A-Za-z0-9]*\z/)
|
|
624
|
+
|
|
625
|
+
name
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# PascalCases one field value for use inside a generated variant name.
|
|
629
|
+
#
|
|
630
|
+
# @param text [String, Symbol, nil] the value to PascalCase; each
|
|
631
|
+
# `_`-separated part's first letter is upcased
|
|
632
|
+
# @return [String] `text` converted to PascalCase, or `""` if `text` is `nil`
|
|
633
|
+
def pascal(text) = text.to_s.split("_").map { |part| part[0].to_s.upcase + part[1..].to_s }.join
|
|
634
|
+
|
|
635
|
+
# A Rust string literal: only `\`, `"` and newlines need escaping in
|
|
636
|
+
# the text any vocabulary row holds.
|
|
637
|
+
#
|
|
638
|
+
# @param text [String] the value to render as a Rust string literal
|
|
639
|
+
# @return [String] `text`, backslash/quote/newline-escaped and wrapped in `"..."`
|
|
640
|
+
def rust_string(text)
|
|
641
|
+
escaped = text.to_s.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub("\n") { "\\n" }
|
|
642
|
+
"\"#{escaped}\""
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
@@ -3,11 +3,11 @@ require_relative "../projector"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Projections
|
|
6
|
-
#
|
|
6
|
+
# The storage shape of one bluebook — the same structural form
|
|
7
7
|
# `StorageShape.mint_hash` hashes to name an era, which is what makes
|
|
8
8
|
# a bump/no-bump question answerable by diffing two of these.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# The retrofit, and the point of it: `Runtime::StorageShape.project`
|
|
11
11
|
# was written long before this framework existed, and it already
|
|
12
12
|
# takes exactly one bluebook, already returns a plain Hash, already
|
|
13
13
|
# touches no disk and no live runtime — it even already uses the
|
|
@@ -31,6 +31,14 @@ module Hecks
|
|
|
31
31
|
# is "every file here registers," not "every projection always
|
|
32
32
|
# succeeds") but refuses clearly if asked to run with the plugin
|
|
33
33
|
# unloaded, rather than raising on an undefined constant.
|
|
34
|
+
# Projects the storage shape of `bluebook` — the structural form an era
|
|
35
|
+
# bump/no-bump question is answered by diffing.
|
|
36
|
+
#
|
|
37
|
+
# @param bluebook [Bluebook::Chapter] the bluebook to reduce to its storage shape
|
|
38
|
+
# @param options [Hash] unused; accepted to satisfy the registry's call shape
|
|
39
|
+
# @return [Hash{String => Object}] `Runtime::StorageShape.project`'s Hash: `"name"`
|
|
40
|
+
# and a sorted `"aggregates"` array
|
|
41
|
+
# @raise [RuntimeError] if the era persistence plugin is not loaded
|
|
34
42
|
def call(bluebook:, options: {})
|
|
35
43
|
unless Ports::Persistence.plugin?(:era)
|
|
36
44
|
raise "the :shape projection needs the era persistence plugin loaded " \
|