domainforge 0.13.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.
- package/.cargo/config.toml +6 -0
- package/.claude/settings.local.json +18 -0
- package/.coderabbit.yml +43 -0
- package/.codex/skills/release-management/SKILL.md +151 -0
- package/.codex/skills/release-management/agents/openai.yaml +4 -0
- package/.github/actions/decrypt-secrets/action.yml +121 -0
- package/.github/agents/Coder.agent.md +97 -0
- package/.github/agents/DeepResearch.agent.md +61 -0
- package/.github/chatmodes/tdd.vibepro.chatmode.md +1183 -0
- package/.github/copilot-instructions.md +13 -0
- package/.github/dependabot.yml +68 -0
- package/.github/workflows/README.md +165 -0
- package/.github/workflows/ci.yml +335 -0
- package/.github/workflows/dependabot-automerge.yml +114 -0
- package/.github/workflows/dependency-review.yml +27 -0
- package/.github/workflows/deploy.yml +87 -0
- package/.github/workflows/prepare-release.yml +168 -0
- package/.github/workflows/release-crates.yml +42 -0
- package/.github/workflows/release-npm.yml +137 -0
- package/.github/workflows/release-please.yml +29 -0
- package/.github/workflows/release-pypi.yml +96 -0
- package/.gitkeep +1 -0
- package/.release-please-manifest.json +5 -0
- package/.sea-registry.toml +10 -0
- package/.serena/project.yml +133 -0
- package/.sops.yaml +10 -0
- package/AGENTS.md +216 -0
- package/CHANGELOG.md +400 -0
- package/CLAUDE.md +62 -0
- package/CONTRIBUTING.md +323 -0
- package/Cargo.lock +3612 -0
- package/Cargo.toml +12 -0
- package/LICENSE +201 -0
- package/README.md +660 -0
- package/README_PYTHON.md +256 -0
- package/README_TYPESCRIPT.md +305 -0
- package/README_WASM.md +329 -0
- package/RELEASE_NOTES.md +41 -0
- package/bun.lock +378 -0
- package/bunfig.toml +11 -0
- package/check_output.txt +83 -0
- package/clippy_output.txt +80 -0
- package/commitlint.config.cjs +8 -0
- package/deny.toml +42 -0
- package/devbox.json +14 -0
- package/devbox.lock +76 -0
- package/docs/RELEASE_PROCESS.md +360 -0
- package/docs/diagnostics.md +161 -0
- package/docs/doc_guidelines.md +53 -0
- package/docs/explanations/README.md +21 -0
- package/docs/explanations/architecture-overview.md +109 -0
- package/docs/explanations/cross-language-binding-strategy.md +68 -0
- package/docs/explanations/graph-store-design.md +47 -0
- package/docs/explanations/performance-benchmarks.md +63 -0
- package/docs/explanations/policy-evaluation-logic.md +106 -0
- package/docs/explanations/semantic-modeling-concepts.md +109 -0
- package/docs/explanations/three-valued-logic.md +66 -0
- package/docs/explanations/versioning-strategy.md +45 -0
- package/docs/governance.md +168 -0
- package/docs/how-tos/README.md +46 -0
- package/docs/how-tos/ci-cd-validation.md +93 -0
- package/docs/how-tos/create-custom-units.md +125 -0
- package/docs/how-tos/define-policies.md +119 -0
- package/docs/how-tos/export-to-calm.md +110 -0
- package/docs/how-tos/export-to-protobuf.md +312 -0
- package/docs/how-tos/extend-grammar.md +133 -0
- package/docs/how-tos/generate-rdf-turtle.md +106 -0
- package/docs/how-tos/import-from-calm.md +114 -0
- package/docs/how-tos/import-from-sbvr.md +249 -0
- package/docs/how-tos/install-cli.md +126 -0
- package/docs/how-tos/parse-sea-files.md +132 -0
- package/docs/how-tos/policy-evaluation-modes.md +30 -0
- package/docs/how-tos/run-cross-language-tests.md +115 -0
- package/docs/how-tos/troubleshoot-napi-builds.md +55 -0
- package/docs/how-tos/use-modules-imports.md +285 -0
- package/docs/index.md +13 -0
- package/docs/plans/canonical-normalizer.md +121 -0
- package/docs/plans/cd_improvement.md +112 -0
- package/docs/plans/cli-ast.md +29 -0
- package/docs/plans/expression-bindings-and-normalizer-integration.md +174 -0
- package/docs/plans/protobuf_advanced_features_plan.md +597 -0
- package/docs/plans/protobuf_plan.yml +525 -0
- package/docs/plans/refactor_dsl_architecture.md +131 -0
- package/docs/plans/release-plan.md +163 -0
- package/docs/plans/sea_fmt_implementation_plan.md +516 -0
- package/docs/playbooks/README.md +18 -0
- package/docs/playbooks/adding-new-primitive.md +68 -0
- package/docs/playbooks/debugging-parser-failures.md +42 -0
- package/docs/playbooks/local-release-preparation.md +139 -0
- package/docs/playbooks/migrating-schema-versions.md +43 -0
- package/docs/playbooks/onboarding-contributors.md +64 -0
- package/docs/playbooks/releasing-beta.md +86 -0
- package/docs/playbooks/secret-management.md +64 -0
- package/docs/reference/README.md +199 -0
- package/docs/reference/ast-json-api.md +427 -0
- package/docs/reference/calm-mapping.md +519 -0
- package/docs/reference/cli-commands.md +588 -0
- package/docs/reference/configuration.md +202 -0
- package/docs/reference/error-codes.md +664 -0
- package/docs/reference/generated-artifacts-policy.md +53 -0
- package/docs/reference/grammar-spec.md +255 -0
- package/docs/reference/primitives-api.md +317 -0
- package/docs/reference/protobuf-api.md +426 -0
- package/docs/reference/python-api.md +485 -0
- package/docs/reference/registry.md +50 -0
- package/docs/reference/sea-dsl-ai-cheatsheet.yaml +913 -0
- package/docs/reference/security-model.md +74 -0
- package/docs/reference/typescript-api.md +508 -0
- package/docs/reference/wasm-api.md +420 -0
- package/docs/semantic-pack-review.md +144 -0
- package/docs/semantic-pack-signing.md +234 -0
- package/docs/semantic-packs.md +284 -0
- package/docs/specs/ADR-001-sea-dsl-semantic-source-of-truth.md +33 -0
- package/docs/specs/ADR-002-projection-first-class-construct.md +50 -0
- package/docs/specs/ADR-003-protobuf-projection-target.md +51 -0
- package/docs/specs/ADR-004-projection-compatibility-semantics.md +57 -0
- package/docs/specs/ADR-005-multi-language-support-strategy.md +112 -0
- package/docs/specs/ADR-006-error-handling-strategy.md +115 -0
- package/docs/specs/ADR-007-policy-evaluation-engine.md +95 -0
- package/docs/specs/ADR-008-knowledge-graph-integration.md +90 -0
- package/docs/specs/ADR-009-module-resolution-strategy.md +115 -0
- package/docs/specs/ADR-010-unit-system.md +106 -0
- package/docs/specs/PRD-001-sea-projection-framework.md +155 -0
- package/docs/specs/PRD-002-sea-cli-tooling.md +169 -0
- package/docs/specs/PRD-003-dsl-core-capabilities.md +275 -0
- package/docs/specs/README.md +62 -0
- package/docs/specs/SDS-001-protobuf-projection-engine.md +451 -0
- package/docs/specs/SDS-002-sea-core-architecture.md +268 -0
- package/docs/specs/SDS-003-parser-semantic-graph.md +377 -0
- package/docs/specs/SDS-004-policy-engine-design.md +362 -0
- package/docs/specs/SDS-005-knowledge-graph-module.md +364 -0
- package/docs/specs/SDS-006-calm-integration.md +367 -0
- package/docs/specs/SDS-007-sbvr-import.md +347 -0
- package/docs/templates/template_explanation.md +14 -0
- package/docs/templates/template_howto.md +21 -0
- package/docs/templates/template_playbook.md +21 -0
- package/docs/templates/template_reference.md +17 -0
- package/docs/templates/template_tutorial.md +24 -0
- package/docs/tutorials/README.md +12 -0
- package/docs/tutorials/first-sea-model.md +85 -0
- package/docs/tutorials/getting-started.md +98 -0
- package/docs/tutorials/python-binding-quickstart.md +107 -0
- package/docs/tutorials/typescript-binding-quickstart.md +91 -0
- package/docs/tutorials/wasm-in-browser.md +75 -0
- package/domainforge-core/CHANGELOG.md +138 -0
- package/domainforge-core/Cargo.toml +101 -0
- package/domainforge-core/MIGRATING.md +32 -0
- package/domainforge-core/README.md +197 -0
- package/domainforge-core/benchmark_results.txt +51 -0
- package/domainforge-core/build.rs +6 -0
- package/domainforge-core/deny.toml +31 -0
- package/domainforge-core/docs/specs/projections/sbvr_kg_mapping.md +43 -0
- package/domainforge-core/examples/basic.sea +7 -0
- package/domainforge-core/examples/cli/import_export_workflow.sh +38 -0
- package/domainforge-core/examples/cli/validate_example.sh +30 -0
- package/domainforge-core/examples/evolution_semantics.sea +31 -0
- package/domainforge-core/examples/parser_demo.rs +203 -0
- package/domainforge-core/grammar/sea.pest +408 -0
- package/domainforge-core/schemas/calm-v1.schema.json +170 -0
- package/domainforge-core/schemas/shacl/sea_shapes.ttl +19 -0
- package/domainforge-core/src/authority/compiler.rs +309 -0
- package/domainforge-core/src/authority/environment.rs +203 -0
- package/domainforge-core/src/authority/error.rs +164 -0
- package/domainforge-core/src/authority/fact_resolver.rs +224 -0
- package/domainforge-core/src/authority/mod.rs +25 -0
- package/domainforge-core/src/authority/pack.rs +133 -0
- package/domainforge-core/src/authority/policy.rs +224 -0
- package/domainforge-core/src/authority/resolver.rs +446 -0
- package/domainforge-core/src/authority/trace.rs +217 -0
- package/domainforge-core/src/authority/transform.rs +168 -0
- package/domainforge-core/src/authority/types.rs +617 -0
- package/domainforge-core/src/bin/domainforge.rs +25 -0
- package/domainforge-core/src/calm/export.rs +538 -0
- package/domainforge-core/src/calm/import.rs +1220 -0
- package/domainforge-core/src/calm/mod.rs +9 -0
- package/domainforge-core/src/calm/models.rs +108 -0
- package/domainforge-core/src/calm/sbvr_import.rs +9 -0
- package/domainforge-core/src/cli/authority.rs +149 -0
- package/domainforge-core/src/cli/format.rs +85 -0
- package/domainforge-core/src/cli/import.rs +133 -0
- package/domainforge-core/src/cli/mod.rs +64 -0
- package/domainforge-core/src/cli/normalize.rs +180 -0
- package/domainforge-core/src/cli/pack.rs +904 -0
- package/domainforge-core/src/cli/parse.rs +112 -0
- package/domainforge-core/src/cli/project.rs +294 -0
- package/domainforge-core/src/cli/registry.rs +41 -0
- package/domainforge-core/src/cli/test.rs +12 -0
- package/domainforge-core/src/cli/validate.rs +195 -0
- package/domainforge-core/src/cli/validate_kg.rs +80 -0
- package/domainforge-core/src/concept_id.rs +89 -0
- package/domainforge-core/src/error/diagnostics.rs +426 -0
- package/domainforge-core/src/error/fuzzy.rs +253 -0
- package/domainforge-core/src/error/mod.rs +13 -0
- package/domainforge-core/src/formatter/comments.rs +223 -0
- package/domainforge-core/src/formatter/config.rs +114 -0
- package/domainforge-core/src/formatter/mod.rs +22 -0
- package/domainforge-core/src/formatter/printer.rs +906 -0
- package/domainforge-core/src/graph/mod.rs +858 -0
- package/domainforge-core/src/graph/to_ast.rs +66 -0
- package/domainforge-core/src/kg.rs +1476 -0
- package/domainforge-core/src/kg_import.rs +251 -0
- package/domainforge-core/src/lib.rs +203 -0
- package/domainforge-core/src/module/mod.rs +1 -0
- package/domainforge-core/src/module/resolver.rs +260 -0
- package/domainforge-core/src/parser/ast.rs +2919 -0
- package/domainforge-core/src/parser/ast_convert.rs +494 -0
- package/domainforge-core/src/parser/ast_schema.rs +491 -0
- package/domainforge-core/src/parser/error.rs +291 -0
- package/domainforge-core/src/parser/lint.rs +39 -0
- package/domainforge-core/src/parser/mod.rs +193 -0
- package/domainforge-core/src/parser/printer.rs +702 -0
- package/domainforge-core/src/parser/profiles.rs +71 -0
- package/domainforge-core/src/parser/string_utils.rs +138 -0
- package/domainforge-core/src/patterns.rs +68 -0
- package/domainforge-core/src/policy/core.rs +1148 -0
- package/domainforge-core/src/policy/expression.rs +399 -0
- package/domainforge-core/src/policy/mod.rs +18 -0
- package/domainforge-core/src/policy/normalize.rs +1028 -0
- package/domainforge-core/src/policy/quantifier.rs +940 -0
- package/domainforge-core/src/policy/three_valued.rs +140 -0
- package/domainforge-core/src/policy/three_valued_microbench.rs +104 -0
- package/domainforge-core/src/policy/type_inference.rs +67 -0
- package/domainforge-core/src/policy/violation.rs +36 -0
- package/domainforge-core/src/primitives/concept_change.rs +61 -0
- package/domainforge-core/src/primitives/entity.rs +224 -0
- package/domainforge-core/src/primitives/flow.rs +111 -0
- package/domainforge-core/src/primitives/instance.rs +93 -0
- package/domainforge-core/src/primitives/mapping_contract.rs +50 -0
- package/domainforge-core/src/primitives/metric.rs +79 -0
- package/domainforge-core/src/primitives/mod.rs +25 -0
- package/domainforge-core/src/primitives/projection_contract.rs +50 -0
- package/domainforge-core/src/primitives/quantity.rs +56 -0
- package/domainforge-core/src/primitives/relation.rs +68 -0
- package/domainforge-core/src/primitives/resource.rs +237 -0
- package/domainforge-core/src/primitives/resource_instance.rs +88 -0
- package/domainforge-core/src/primitives/role.rs +49 -0
- package/domainforge-core/src/projection/buf.rs +404 -0
- package/domainforge-core/src/projection/contracts.rs +22 -0
- package/domainforge-core/src/projection/engine.rs +19 -0
- package/domainforge-core/src/projection/mod.rs +16 -0
- package/domainforge-core/src/projection/protobuf.rs +3331 -0
- package/domainforge-core/src/projection/registry.rs +43 -0
- package/domainforge-core/src/python/authority.rs +253 -0
- package/domainforge-core/src/python/error.rs +227 -0
- package/domainforge-core/src/python/formatter.rs +86 -0
- package/domainforge-core/src/python/graph.rs +366 -0
- package/domainforge-core/src/python/mod.rs +9 -0
- package/domainforge-core/src/python/policy.rs +651 -0
- package/domainforge-core/src/python/primitives.rs +796 -0
- package/domainforge-core/src/python/registry.rs +98 -0
- package/domainforge-core/src/python/semantic_pack.rs +619 -0
- package/domainforge-core/src/python/units.rs +96 -0
- package/domainforge-core/src/registry/mod.rs +432 -0
- package/domainforge-core/src/registry/tests.rs +210 -0
- package/domainforge-core/src/sbvr.rs +744 -0
- package/domainforge-core/src/semantic_pack/builder.rs +470 -0
- package/domainforge-core/src/semantic_pack/canonical_json.rs +184 -0
- package/domainforge-core/src/semantic_pack/diagnostics.rs +214 -0
- package/domainforge-core/src/semantic_pack/diff.rs +216 -0
- package/domainforge-core/src/semantic_pack/mod.rs +31 -0
- package/domainforge-core/src/semantic_pack/pack_set.rs +240 -0
- package/domainforge-core/src/semantic_pack/resolver.rs +437 -0
- package/domainforge-core/src/semantic_pack/review.rs +125 -0
- package/domainforge-core/src/semantic_pack/schema.rs +342 -0
- package/domainforge-core/src/semantic_pack/signing.rs +105 -0
- package/domainforge-core/src/semantic_pack/validator.rs +368 -0
- package/domainforge-core/src/semantic_version.rs +140 -0
- package/domainforge-core/src/test_utils.rs +12 -0
- package/domainforge-core/src/typescript/authority.rs +184 -0
- package/domainforge-core/src/typescript/error.rs +146 -0
- package/domainforge-core/src/typescript/formatter.rs +76 -0
- package/domainforge-core/src/typescript/graph.rs +391 -0
- package/domainforge-core/src/typescript/mod.rs +9 -0
- package/domainforge-core/src/typescript/policy.rs +564 -0
- package/domainforge-core/src/typescript/primitives.rs +784 -0
- package/domainforge-core/src/typescript/registry.rs +88 -0
- package/domainforge-core/src/typescript/semantic_pack.rs +470 -0
- package/domainforge-core/src/typescript/units.rs +76 -0
- package/domainforge-core/src/units/mod.rs +462 -0
- package/domainforge-core/src/uuid_module.rs +42 -0
- package/domainforge-core/src/validation_error.rs +818 -0
- package/domainforge-core/src/validation_result.rs +30 -0
- package/domainforge-core/src/wasm/authority.rs +192 -0
- package/domainforge-core/src/wasm/error.rs +145 -0
- package/domainforge-core/src/wasm/formatter.rs +69 -0
- package/domainforge-core/src/wasm/graph.rs +471 -0
- package/domainforge-core/src/wasm/mod.rs +16 -0
- package/domainforge-core/src/wasm/policy.rs +607 -0
- package/domainforge-core/src/wasm/primitives.rs +295 -0
- package/domainforge-core/src/wasm/semantic_pack.rs +471 -0
- package/domainforge-core/src/wasm/units.rs +62 -0
- package/domainforge-core/std/aws.sea +6 -0
- package/domainforge-core/std/core.sea +6 -0
- package/domainforge-core/std/http.sea +27 -0
- package/domainforge-core/tests/aggregation_enhanced_tests.rs +162 -0
- package/domainforge-core/tests/aggregation_eval_tests.rs +248 -0
- package/domainforge-core/tests/aggregation_integration_tests.rs +379 -0
- package/domainforge-core/tests/aggregation_parser_tests.rs +92 -0
- package/domainforge-core/tests/aggregation_tests.rs +102 -0
- package/domainforge-core/tests/authority_conformance_tests.rs +1173 -0
- package/domainforge-core/tests/calm_round_trip_tests.rs +283 -0
- package/domainforge-core/tests/calm_schema_validation_tests.rs +137 -0
- package/domainforge-core/tests/cast_operator_tests.rs +85 -0
- package/domainforge-core/tests/cli_binary_check.rs +37 -0
- package/domainforge-core/tests/cli_import_tests.rs +291 -0
- package/domainforge-core/tests/cli_path_traversal_tests.rs +124 -0
- package/domainforge-core/tests/cli_tests.rs +63 -0
- package/domainforge-core/tests/diagnostics_tests.rs +203 -0
- package/domainforge-core/tests/dimension_unit_tests.rs +80 -0
- package/domainforge-core/tests/entity_tests.rs +69 -0
- package/domainforge-core/tests/evolution_semantics_tests.rs +157 -0
- package/domainforge-core/tests/flow_tests.rs +78 -0
- package/domainforge-core/tests/flow_unit_validation_tests.rs +31 -0
- package/domainforge-core/tests/graph_integration_tests.rs +218 -0
- package/domainforge-core/tests/graph_tests.rs +626 -0
- package/domainforge-core/tests/import_parsing_tests.rs +23 -0
- package/domainforge-core/tests/instance_integration_tests.rs +98 -0
- package/domainforge-core/tests/instance_parsing_tests.rs +58 -0
- package/domainforge-core/tests/instance_tests.rs +61 -0
- package/domainforge-core/tests/kg_uri_encoding_tests.rs +53 -0
- package/domainforge-core/tests/lint_tests.rs +19 -0
- package/domainforge-core/tests/metric_tests.rs +143 -0
- package/domainforge-core/tests/module_resolution_tests.rs +100 -0
- package/domainforge-core/tests/namespace_registry_tests.rs +247 -0
- package/domainforge-core/tests/null_handling_tests.rs +26 -0
- package/domainforge-core/tests/parser_ast_v3.rs +53 -0
- package/domainforge-core/tests/parser_dimension_registry_tests.rs +20 -0
- package/domainforge-core/tests/parser_integration_tests.rs +294 -0
- package/domainforge-core/tests/parser_metadata_tests.rs +97 -0
- package/domainforge-core/tests/parser_resource_domain_only_graph_test.rs +21 -0
- package/domainforge-core/tests/parser_resource_limits_tests.rs +122 -0
- package/domainforge-core/tests/parser_tests.rs +512 -0
- package/domainforge-core/tests/pattern_semantics_tests.rs +87 -0
- package/domainforge-core/tests/phase_14_determinism_tests.rs +166 -0
- package/domainforge-core/tests/phase_15_validation_error_tests.rs +136 -0
- package/domainforge-core/tests/phase_16_unicode_tests.rs +248 -0
- package/domainforge-core/tests/phase_17_export_tests.rs +285 -0
- package/domainforge-core/tests/phase_17_round_trip_tests.rs +264 -0
- package/domainforge-core/tests/policy_tests.rs +635 -0
- package/domainforge-core/tests/primitives_integration_tests.rs +151 -0
- package/domainforge-core/tests/print_rdf_xml.rs +14 -0
- package/domainforge-core/tests/printer_tests.rs +204 -0
- package/domainforge-core/tests/profile_tests.rs +35 -0
- package/domainforge-core/tests/projection_contracts_tests.rs +154 -0
- package/domainforge-core/tests/protobuf_projection_tests.rs +199 -0
- package/domainforge-core/tests/quantity_tests.rs +41 -0
- package/domainforge-core/tests/rdf_xml_typed_literal_tests.rs +105 -0
- package/domainforge-core/tests/registry_schema_tests.rs +33 -0
- package/domainforge-core/tests/resource_tests.rs +50 -0
- package/domainforge-core/tests/resource_unit_tests.rs +24 -0
- package/domainforge-core/tests/roles_relations_tests.rs +61 -0
- package/domainforge-core/tests/round_trip_tests.rs +34 -0
- package/domainforge-core/tests/runtime_toggle_tests.rs +70 -0
- package/domainforge-core/tests/sbvr_fact_schema_tests.rs +60 -0
- package/domainforge-core/tests/sbvr_flow_facts_tests.rs +55 -0
- package/domainforge-core/tests/sbvr_parsing_tests.rs +53 -0
- package/domainforge-core/tests/semantic_pack_alias_resolution.rs +197 -0
- package/domainforge-core/tests/semantic_pack_build.rs +302 -0
- package/domainforge-core/tests/semantic_pack_consumer_smoke.rs +150 -0
- package/domainforge-core/tests/semantic_pack_pack_set.rs +160 -0
- package/domainforge-core/tests/semantic_pack_signing.rs +157 -0
- package/domainforge-core/tests/semantic_pack_three_valued.rs +250 -0
- package/domainforge-core/tests/semantic_pack_validate.rs +196 -0
- package/domainforge-core/tests/std_lib_tests.rs +37 -0
- package/domainforge-core/tests/temporal_evaluation_tests.rs +159 -0
- package/domainforge-core/tests/temporal_semantics_tests.rs +214 -0
- package/domainforge-core/tests/three_valued_quantifiers_tests.rs +164 -0
- package/domainforge-core/tests/turtle_entity_export_tests.rs +38 -0
- package/domainforge-core/tests/turtle_escaping_tests.rs +53 -0
- package/domainforge-core/tests/turtle_resource_export_tests.rs +34 -0
- package/domainforge-core/tests/type_inference_tests.rs +40 -0
- package/domainforge-core/tests/unicode_validation_tests.rs +169 -0
- package/domainforge-core/tests/unit_tests.rs +81 -0
- package/domainforge-core/tests/validate_tests.rs +38 -0
- package/domainforge-core/tests/validation_unit_mismatch_tests.rs +83 -0
- package/domainforge-core/tests/wasm_tests.rs +229 -0
- package/domainforge-python/CHANGELOG-python.md +12 -0
- package/domainforge-python/MIGRATING.md +24 -0
- package/domainforge-python/README.md +256 -0
- package/domainforge-python/domainforge/__init__.py +95 -0
- package/domainforge-python/domainforge/domainforge.pyi +519 -0
- package/domainforge-python/pyproject.toml +36 -0
- package/domainforge-typescript/CHANGELOG-typescript.md +12 -0
- package/domainforge-typescript/LICENSE +201 -0
- package/domainforge-typescript/MIGRATING.md +24 -0
- package/domainforge-typescript/README.md +305 -0
- package/domainforge-typescript/index.d.ts +452 -0
- package/domainforge-typescript/index.js +361 -0
- package/domainforge-typescript/package.json +60 -0
- package/example.js +61 -0
- package/examples/browser.html +366 -0
- package/examples/namespaces/finance/cashflow.sea +5 -0
- package/examples/namespaces/logistics/core.sea +7 -0
- package/examples/observability_metrics.sea +38 -0
- package/fixtures/semantic_packs/acme_procurement/domain/entities.sea +39 -0
- package/fixtures/semantic_packs/acme_procurement/domain/metrics.sea +11 -0
- package/fixtures/semantic_packs/acme_procurement/domain/relations.sea +7 -0
- package/fixtures/semantic_packs/acme_procurement/domain/resources.sea +9 -0
- package/fixtures/semantic_packs/acme_procurement/review/acme.procurement.semantic-review.jsonl +7 -0
- package/fixtures/semantic_packs/acme_procurement/tests/ambiguous_vendor_alias.sea +8 -0
- package/fixtures/semantic_packs/acme_procurement/tests/deprecated_vendor_alias.sea +8 -0
- package/fixtures/semantic_packs/acme_procurement/tests/invalid_relation.sea +3 -0
- package/fixtures/semantic_packs/acme_procurement/tests/proposed_concept.sea +8 -0
- package/fixtures/semantic_packs/acme_procurement/tests/rejected_concept.sea +8 -0
- package/fixtures/semantic_packs/acme_procurement/tests/unit_mismatch.sea +7 -0
- package/fixtures/semantic_packs/acme_procurement/tests/unknown_vendor_policy.sea +8 -0
- package/fixtures/semantic_packs/acme_procurement/tests/valid_purchase_policy.sea +8 -0
- package/index.d.ts +2 -0
- package/index.js +8 -0
- package/justfile +200 -0
- package/lefthook.yml +13 -0
- package/lib/validate_native_exports.d.ts +4 -0
- package/lib/validate_native_exports.js +12 -0
- package/package.json +22 -0
- package/pytest.ini +5 -0
- package/python/tests/test_registry.py +75 -0
- package/python/tests/test_units.py +18 -0
- package/release-please-config.json +49 -0
- package/requirements-dev.txt +3 -0
- package/requirements.txt +3 -0
- package/rust-toolchain.toml +3 -0
- package/schemas/ast-v1.schema.json +72 -0
- package/schemas/ast-v2.schema.json +1200 -0
- package/schemas/ast-v3.schema.json +1200 -0
- package/schemas/sea-registry.schema.json +45 -0
- package/scripts/build-python.sh +37 -0
- package/scripts/build-release.sh +279 -0
- package/scripts/build-typescript.sh +13 -0
- package/scripts/build-wasm.sh +113 -0
- package/scripts/bump-version.sh +245 -0
- package/scripts/check_unused_test_imports.py +85 -0
- package/scripts/ci_tasks.py +379 -0
- package/scripts/clear_debug_test.sh +10 -0
- package/scripts/create-github-release.sh +262 -0
- package/scripts/create-tag.sh +203 -0
- package/scripts/find_and_link_test_binary.sh +70 -0
- package/scripts/generate-changelog.sh +271 -0
- package/scripts/generate-release-notes.sh +205 -0
- package/scripts/lint_release_security.py +96 -0
- package/scripts/lint_release_workflows.py +82 -0
- package/scripts/lint_workflow_gates.py +113 -0
- package/scripts/optimized-wasm-build.sh +61 -0
- package/scripts/patch_napi_types.py +62 -0
- package/scripts/pre-release-check.sh +289 -0
- package/scripts/prepare_rust_debug.sh +52 -0
- package/scripts/release.sh +373 -0
- package/scripts/resolve_rust_binary.py +230 -0
- package/scripts/run_commitlint.sh +29 -0
- package/scripts/test-all.sh +77 -0
- package/scripts/update_launch_program.py +93 -0
- package/secrets/README.md +27 -0
- package/secrets/secrets.yaml +21 -0
- package/test_integration.py +67 -0
- package/tests/test_authority.py +328 -0
- package/tests/test_ci_tasks.py +143 -0
- package/tests/test_expression.py +256 -0
- package/tests/test_golden_payment_flow.py +42 -0
- package/tests/test_graph.py +127 -0
- package/tests/test_instance.py +136 -0
- package/tests/test_parser.py +82 -0
- package/tests/test_primitives.py +68 -0
- package/tests/test_role_relation_parity.py +56 -0
- package/tests/test_runtime_toggle.py +156 -0
- package/tests/test_semantic_pack.py +639 -0
- package/tests/test_three_valued_eval.py +159 -0
- package/tsconfig.json +30 -0
- package/typescript-tests/advanced.test.ts +165 -0
- package/typescript-tests/authority.test.ts +216 -0
- package/typescript-tests/expression.test.ts +228 -0
- package/typescript-tests/golden-payment-flow.test.ts +51 -0
- package/typescript-tests/graph.test.ts +142 -0
- package/typescript-tests/native-binding.test.ts +20 -0
- package/typescript-tests/primitives.test.ts +88 -0
- package/typescript-tests/registry.test.ts +122 -0
- package/typescript-tests/role_relation.test.ts +63 -0
- package/typescript-tests/runtime_toggle.test.ts +141 -0
- package/typescript-tests/semantic-pack.test.ts +556 -0
- package/typescript-tests/three_valued_eval.test.ts +135 -0
- package/typescript-tests/units.test.ts +36 -0
- package/vitest.config.ts +13 -0
- package/wasm_demo.html +225 -0
|
@@ -0,0 +1,3331 @@
|
|
|
1
|
+
//! Protobuf Projection Engine
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides functionality to project SEA semantic graphs to Protocol Buffer
|
|
4
|
+
//! (`.proto`) files. It supports:
|
|
5
|
+
//!
|
|
6
|
+
//! - Entity and Resource projection to Protobuf messages
|
|
7
|
+
//! - Type mapping from SEA types to Protobuf scalar types
|
|
8
|
+
//! - Deterministic field numbering for schema stability
|
|
9
|
+
//! - Governance message generation
|
|
10
|
+
//!
|
|
11
|
+
//! # Example
|
|
12
|
+
//!
|
|
13
|
+
//! ```rust,ignore
|
|
14
|
+
//! use domainforge_core::projection::protobuf::ProtobufEngine;
|
|
15
|
+
//! use domainforge_core::graph::Graph;
|
|
16
|
+
//!
|
|
17
|
+
//! let graph = build_graph_from_model();
|
|
18
|
+
//! let proto_file = ProtobufEngine::project(&graph, "my_namespace", "my.package");
|
|
19
|
+
//! println!("{}", proto_file.to_proto_string());
|
|
20
|
+
//! ```
|
|
21
|
+
|
|
22
|
+
use crate::graph::Graph;
|
|
23
|
+
use crate::primitives::{Entity, Resource};
|
|
24
|
+
use serde::{Deserialize, Serialize};
|
|
25
|
+
use serde_json::Value;
|
|
26
|
+
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
27
|
+
use std::path::{Path, PathBuf};
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Protobuf IR Types
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/// Represents a complete `.proto` file.
|
|
34
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
35
|
+
pub struct ProtoFile {
|
|
36
|
+
/// The package name (e.g., "sea.example")
|
|
37
|
+
pub package: String,
|
|
38
|
+
/// Protobuf syntax version (always "proto3")
|
|
39
|
+
pub syntax: String,
|
|
40
|
+
/// Import statements
|
|
41
|
+
pub imports: Vec<String>,
|
|
42
|
+
/// File-level options
|
|
43
|
+
pub options: ProtoOptions,
|
|
44
|
+
/// Enum definitions
|
|
45
|
+
pub enums: Vec<ProtoEnum>,
|
|
46
|
+
/// Message definitions
|
|
47
|
+
pub messages: Vec<ProtoMessage>,
|
|
48
|
+
/// gRPC service definitions
|
|
49
|
+
pub services: Vec<ProtoService>,
|
|
50
|
+
/// Metadata about the projection
|
|
51
|
+
pub metadata: ProtoMetadata,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl ProtoFile {
|
|
55
|
+
/// Create a new ProtoFile with the given package name.
|
|
56
|
+
pub fn new(package: impl Into<String>) -> Self {
|
|
57
|
+
Self {
|
|
58
|
+
package: package.into(),
|
|
59
|
+
syntax: "proto3".to_string(),
|
|
60
|
+
imports: Vec::new(),
|
|
61
|
+
options: ProtoOptions::default(),
|
|
62
|
+
enums: Vec::new(),
|
|
63
|
+
messages: Vec::new(),
|
|
64
|
+
services: Vec::new(),
|
|
65
|
+
metadata: ProtoMetadata::default(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Serialize the ProtoFile to `.proto` text format.
|
|
70
|
+
pub fn to_proto_string(&self) -> String {
|
|
71
|
+
let mut out = String::new();
|
|
72
|
+
|
|
73
|
+
// Header comments
|
|
74
|
+
out.push_str("// Generated by SEA Projection Framework\n");
|
|
75
|
+
out.push_str(&format!(
|
|
76
|
+
"// Projection: {}\n",
|
|
77
|
+
self.metadata.projection_name
|
|
78
|
+
));
|
|
79
|
+
out.push_str(&format!(
|
|
80
|
+
"// Source Namespace: {}\n",
|
|
81
|
+
self.metadata.source_namespace
|
|
82
|
+
));
|
|
83
|
+
if let Some(ref version) = self.metadata.semantic_version {
|
|
84
|
+
out.push_str(&format!("// Version: {}\n", version));
|
|
85
|
+
}
|
|
86
|
+
out.push_str(&format!(
|
|
87
|
+
"// Generated At: {}\n",
|
|
88
|
+
self.metadata.generated_at
|
|
89
|
+
));
|
|
90
|
+
out.push_str("// DO NOT EDIT - This file is auto-generated\n\n");
|
|
91
|
+
|
|
92
|
+
// Syntax
|
|
93
|
+
out.push_str(&format!("syntax = \"{}\";\n\n", self.syntax));
|
|
94
|
+
|
|
95
|
+
// Package
|
|
96
|
+
out.push_str(&format!("package {};\n", self.package));
|
|
97
|
+
|
|
98
|
+
// Options
|
|
99
|
+
let options = &self.options;
|
|
100
|
+
|
|
101
|
+
if let Some(ref pkg) = options.java_package {
|
|
102
|
+
out.push_str(&format!("\noption java_package = \"{}\";", pkg));
|
|
103
|
+
}
|
|
104
|
+
if options.java_multiple_files {
|
|
105
|
+
out.push_str("\noption java_multiple_files = true;");
|
|
106
|
+
}
|
|
107
|
+
if let Some(ref pkg) = options.go_package {
|
|
108
|
+
out.push_str(&format!("\noption go_package = \"{}\";", pkg));
|
|
109
|
+
}
|
|
110
|
+
if let Some(ref ns) = options.csharp_namespace {
|
|
111
|
+
out.push_str(&format!("\noption csharp_namespace = \"{}\";", ns));
|
|
112
|
+
}
|
|
113
|
+
if let Some(ref ns) = options.php_namespace {
|
|
114
|
+
out.push_str(&format!("\noption php_namespace = \"{}\";", ns));
|
|
115
|
+
}
|
|
116
|
+
if let Some(ref pkg) = options.ruby_package {
|
|
117
|
+
out.push_str(&format!("\noption ruby_package = \"{}\";", pkg));
|
|
118
|
+
}
|
|
119
|
+
if let Some(ref prefix) = options.swift_prefix {
|
|
120
|
+
out.push_str(&format!("\noption swift_prefix = \"{}\";", prefix));
|
|
121
|
+
}
|
|
122
|
+
if let Some(ref prefix) = options.objc_class_prefix {
|
|
123
|
+
out.push_str(&format!("\noption objc_class_prefix = \"{}\";", prefix));
|
|
124
|
+
}
|
|
125
|
+
if let Some(ref opt) = options.optimize_for {
|
|
126
|
+
out.push_str(&format!("\noption optimize_for = {};", opt));
|
|
127
|
+
}
|
|
128
|
+
if options.deprecated {
|
|
129
|
+
out.push_str("\noption deprecated = true;");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Custom options
|
|
133
|
+
for custom in &options.custom_options {
|
|
134
|
+
out.push_str(&format!("\n{}", custom.to_proto_string()));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if options.java_package.is_some()
|
|
138
|
+
|| options.java_multiple_files
|
|
139
|
+
|| options.go_package.is_some()
|
|
140
|
+
|| options.csharp_namespace.is_some()
|
|
141
|
+
|| options.php_namespace.is_some()
|
|
142
|
+
|| options.ruby_package.is_some()
|
|
143
|
+
|| options.swift_prefix.is_some()
|
|
144
|
+
|| options.objc_class_prefix.is_some()
|
|
145
|
+
|| options.optimize_for.is_some()
|
|
146
|
+
|| options.deprecated
|
|
147
|
+
|| !options.custom_options.is_empty()
|
|
148
|
+
{
|
|
149
|
+
out.push('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Imports
|
|
153
|
+
if !self.imports.is_empty() {
|
|
154
|
+
out.push('\n');
|
|
155
|
+
for import in &self.imports {
|
|
156
|
+
out.push_str(&format!("import \"{}\";\n", import));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Enums
|
|
161
|
+
for e in &self.enums {
|
|
162
|
+
out.push('\n');
|
|
163
|
+
out.push_str(&e.to_proto_string());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Messages
|
|
167
|
+
for m in &self.messages {
|
|
168
|
+
out.push('\n');
|
|
169
|
+
out.push_str(&m.to_proto_string());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Services (gRPC)
|
|
173
|
+
for s in &self.services {
|
|
174
|
+
out.push('\n');
|
|
175
|
+
out.push_str(&s.to_proto_string());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
out
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Scan all messages and automatically add required Well-Known Type imports.
|
|
182
|
+
///
|
|
183
|
+
/// This method should be called after all messages are added to ensure
|
|
184
|
+
/// proper imports are included for any WKT fields.
|
|
185
|
+
pub fn add_wkt_imports(&mut self) {
|
|
186
|
+
use std::collections::HashSet;
|
|
187
|
+
let mut required_imports: HashSet<&'static str> = HashSet::new();
|
|
188
|
+
|
|
189
|
+
// Scan all message fields for WKT references
|
|
190
|
+
for msg in &self.messages {
|
|
191
|
+
Self::collect_wkt_imports_from_message(msg, &mut required_imports);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Add imports that aren't already present
|
|
195
|
+
for import in required_imports {
|
|
196
|
+
if !self.imports.contains(&import.to_string()) {
|
|
197
|
+
self.imports.push(import.to_string());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Sort imports for deterministic output
|
|
202
|
+
self.imports.sort();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn collect_wkt_imports_from_message(
|
|
206
|
+
msg: &ProtoMessage,
|
|
207
|
+
imports: &mut std::collections::HashSet<&'static str>,
|
|
208
|
+
) {
|
|
209
|
+
for field in &msg.fields {
|
|
210
|
+
if let ProtoType::Message(ref type_name) = field.proto_type {
|
|
211
|
+
if let Some(wkt) = WellKnownType::from_type_name(type_name) {
|
|
212
|
+
imports.insert(wkt.import_path());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Recurse into nested messages
|
|
218
|
+
for nested in &msg.nested_messages {
|
|
219
|
+
Self::collect_wkt_imports_from_message(nested, imports);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// File-level Protobuf options.
|
|
225
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
226
|
+
pub struct ProtoOptions {
|
|
227
|
+
/// Java package for generated code
|
|
228
|
+
pub java_package: Option<String>,
|
|
229
|
+
/// Generate separate files for each message in Java
|
|
230
|
+
pub java_multiple_files: bool,
|
|
231
|
+
/// Go package path
|
|
232
|
+
pub go_package: Option<String>,
|
|
233
|
+
/// C# namespace
|
|
234
|
+
pub csharp_namespace: Option<String>,
|
|
235
|
+
/// PHP namespace
|
|
236
|
+
pub php_namespace: Option<String>,
|
|
237
|
+
/// Ruby package
|
|
238
|
+
pub ruby_package: Option<String>,
|
|
239
|
+
/// Swift prefix
|
|
240
|
+
pub swift_prefix: Option<String>,
|
|
241
|
+
/// Objective-C class prefix
|
|
242
|
+
pub objc_class_prefix: Option<String>,
|
|
243
|
+
/// Optimize for: SPEED, CODE_SIZE, or LITE_RUNTIME
|
|
244
|
+
pub optimize_for: Option<String>,
|
|
245
|
+
/// Mark all messages as deprecated
|
|
246
|
+
pub deprecated: bool,
|
|
247
|
+
/// Custom options (user-defined or extension options)
|
|
248
|
+
pub custom_options: Vec<ProtoCustomOption>,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
impl ProtoOptions {
|
|
252
|
+
/// Set a standard option by name.
|
|
253
|
+
pub fn set_option(&mut self, name: &str, value: ProtoOptionValue) {
|
|
254
|
+
match name {
|
|
255
|
+
"java_package" => {
|
|
256
|
+
if let ProtoOptionValue::String(s) = value {
|
|
257
|
+
self.java_package = Some(s);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
"java_multiple_files" => {
|
|
261
|
+
if let ProtoOptionValue::Bool(b) = value {
|
|
262
|
+
self.java_multiple_files = b;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
"go_package" => {
|
|
266
|
+
if let ProtoOptionValue::String(s) = value {
|
|
267
|
+
self.go_package = Some(s);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
"csharp_namespace" => {
|
|
271
|
+
if let ProtoOptionValue::String(s) = value {
|
|
272
|
+
self.csharp_namespace = Some(s);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
"php_namespace" => {
|
|
276
|
+
if let ProtoOptionValue::String(s) = value {
|
|
277
|
+
self.php_namespace = Some(s);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
"ruby_package" => {
|
|
281
|
+
if let ProtoOptionValue::String(s) = value {
|
|
282
|
+
self.ruby_package = Some(s);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
"swift_prefix" => {
|
|
286
|
+
if let ProtoOptionValue::String(s) = value {
|
|
287
|
+
self.swift_prefix = Some(s);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
"objc_class_prefix" => {
|
|
291
|
+
if let ProtoOptionValue::String(s) = value {
|
|
292
|
+
self.objc_class_prefix = Some(s);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
"optimize_for" => {
|
|
296
|
+
if let ProtoOptionValue::String(s) = value {
|
|
297
|
+
self.optimize_for = Some(s);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
"deprecated" => {
|
|
301
|
+
if let ProtoOptionValue::Bool(b) = value {
|
|
302
|
+
self.deprecated = b;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
_ => {
|
|
306
|
+
// Unknown option, add as custom
|
|
307
|
+
self.custom_options.push(ProtoCustomOption {
|
|
308
|
+
name: name.to_string(),
|
|
309
|
+
value,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// A custom proto option (user-defined or extension).
|
|
317
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
318
|
+
pub struct ProtoCustomOption {
|
|
319
|
+
/// Option name (e.g., "java_package" or "(myopt).field")
|
|
320
|
+
pub name: String,
|
|
321
|
+
/// Option value
|
|
322
|
+
pub value: ProtoOptionValue,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
impl ProtoCustomOption {
|
|
326
|
+
/// Create a new custom option.
|
|
327
|
+
pub fn new(name: impl Into<String>, value: ProtoOptionValue) -> Self {
|
|
328
|
+
Self {
|
|
329
|
+
name: name.into(),
|
|
330
|
+
value,
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Serialize to proto option string.
|
|
335
|
+
pub fn to_proto_string(&self) -> String {
|
|
336
|
+
format!("option {} = {};", self.name, self.value.to_proto_string())
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Value for a proto option.
|
|
341
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
342
|
+
pub enum ProtoOptionValue {
|
|
343
|
+
/// String value
|
|
344
|
+
String(String),
|
|
345
|
+
/// Integer value
|
|
346
|
+
Int(i64),
|
|
347
|
+
/// Float value
|
|
348
|
+
Float(f64),
|
|
349
|
+
/// Boolean value
|
|
350
|
+
Bool(bool),
|
|
351
|
+
/// Identifier/enum value (unquoted)
|
|
352
|
+
Identifier(String),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
impl ProtoOptionValue {
|
|
356
|
+
/// Parse option value from JSON Value.
|
|
357
|
+
pub fn from_json(value: &serde_json::Value) -> Self {
|
|
358
|
+
match value {
|
|
359
|
+
serde_json::Value::String(s) => ProtoOptionValue::String(s.clone()),
|
|
360
|
+
serde_json::Value::Bool(b) => ProtoOptionValue::Bool(*b),
|
|
361
|
+
serde_json::Value::Number(n) => {
|
|
362
|
+
if let Some(i) = n.as_i64() {
|
|
363
|
+
ProtoOptionValue::Int(i)
|
|
364
|
+
} else if let Some(f) = n.as_f64() {
|
|
365
|
+
ProtoOptionValue::Float(f)
|
|
366
|
+
} else {
|
|
367
|
+
ProtoOptionValue::String(n.to_string())
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
_ => ProtoOptionValue::String(value.to_string()),
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// Serialize to proto option value string.
|
|
375
|
+
pub fn to_proto_string(&self) -> String {
|
|
376
|
+
match self {
|
|
377
|
+
ProtoOptionValue::String(s) => {
|
|
378
|
+
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
|
|
379
|
+
}
|
|
380
|
+
ProtoOptionValue::Int(i) => i.to_string(),
|
|
381
|
+
ProtoOptionValue::Float(f) => f.to_string(),
|
|
382
|
+
ProtoOptionValue::Bool(b) => b.to_string(),
|
|
383
|
+
ProtoOptionValue::Identifier(s) => s.clone(),
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Metadata about the projection source.
|
|
389
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
390
|
+
pub struct ProtoMetadata {
|
|
391
|
+
/// Name of the projection that generated this file
|
|
392
|
+
pub projection_name: String,
|
|
393
|
+
/// Semantic version if available
|
|
394
|
+
pub semantic_version: Option<String>,
|
|
395
|
+
/// Source namespace from the SEA model
|
|
396
|
+
pub source_namespace: String,
|
|
397
|
+
/// Timestamp of generation
|
|
398
|
+
pub generated_at: String,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ============================================================================
|
|
402
|
+
// gRPC Service Types
|
|
403
|
+
// ============================================================================
|
|
404
|
+
|
|
405
|
+
/// Represents a gRPC service definition.
|
|
406
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
407
|
+
pub struct ProtoService {
|
|
408
|
+
/// Service name (e.g., "PaymentProcessorService")
|
|
409
|
+
pub name: String,
|
|
410
|
+
/// RPC methods in this service
|
|
411
|
+
pub methods: Vec<ProtoRpcMethod>,
|
|
412
|
+
/// Documentation comments
|
|
413
|
+
pub comments: Vec<String>,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
impl ProtoService {
|
|
417
|
+
/// Create a new ProtoService with the given name.
|
|
418
|
+
pub fn new(name: impl Into<String>) -> Self {
|
|
419
|
+
Self {
|
|
420
|
+
name: name.into(),
|
|
421
|
+
methods: Vec::new(),
|
|
422
|
+
comments: Vec::new(),
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// Serialize the service to `.proto` text format.
|
|
427
|
+
pub fn to_proto_string(&self) -> String {
|
|
428
|
+
let mut out = String::new();
|
|
429
|
+
|
|
430
|
+
// Comments
|
|
431
|
+
for comment in &self.comments {
|
|
432
|
+
out.push_str(&format!("// {}\n", comment));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
out.push_str(&format!("service {} {{\n", self.name));
|
|
436
|
+
|
|
437
|
+
for method in &self.methods {
|
|
438
|
+
out.push_str(&format!(" {}\n", method.to_proto_string()));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
out.push_str("}\n");
|
|
442
|
+
out
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// Represents an RPC method in a gRPC service.
|
|
447
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
448
|
+
pub struct ProtoRpcMethod {
|
|
449
|
+
/// Method name (e.g., "ProcessPayment")
|
|
450
|
+
pub name: String,
|
|
451
|
+
/// Request message type
|
|
452
|
+
pub request_type: String,
|
|
453
|
+
/// Response message type
|
|
454
|
+
pub response_type: String,
|
|
455
|
+
/// Streaming mode
|
|
456
|
+
pub streaming: StreamingMode,
|
|
457
|
+
/// Documentation comments
|
|
458
|
+
pub comments: Vec<String>,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
impl ProtoRpcMethod {
|
|
462
|
+
/// Create a new unary RPC method.
|
|
463
|
+
pub fn new(
|
|
464
|
+
name: impl Into<String>,
|
|
465
|
+
request_type: impl Into<String>,
|
|
466
|
+
response_type: impl Into<String>,
|
|
467
|
+
) -> Self {
|
|
468
|
+
Self {
|
|
469
|
+
name: name.into(),
|
|
470
|
+
request_type: request_type.into(),
|
|
471
|
+
response_type: response_type.into(),
|
|
472
|
+
streaming: StreamingMode::Unary,
|
|
473
|
+
comments: Vec::new(),
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/// Serialize the method to `.proto` text format.
|
|
478
|
+
pub fn to_proto_string(&self) -> String {
|
|
479
|
+
let request = match self.streaming {
|
|
480
|
+
StreamingMode::ClientStreaming | StreamingMode::Bidirectional => {
|
|
481
|
+
format!("stream {}", self.request_type)
|
|
482
|
+
}
|
|
483
|
+
_ => self.request_type.clone(),
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
let response = match self.streaming {
|
|
487
|
+
StreamingMode::ServerStreaming | StreamingMode::Bidirectional => {
|
|
488
|
+
format!("stream {}", self.response_type)
|
|
489
|
+
}
|
|
490
|
+
_ => self.response_type.clone(),
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
format!("rpc {}({}) returns ({});", self.name, request, response)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// gRPC streaming mode for RPC methods.
|
|
498
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
499
|
+
pub enum StreamingMode {
|
|
500
|
+
/// Unary RPC (default): single request, single response
|
|
501
|
+
#[default]
|
|
502
|
+
Unary,
|
|
503
|
+
/// Server streaming: single request, stream of responses
|
|
504
|
+
ServerStreaming,
|
|
505
|
+
/// Client streaming: stream of requests, single response
|
|
506
|
+
ClientStreaming,
|
|
507
|
+
/// Bidirectional streaming: stream of requests, stream of responses
|
|
508
|
+
Bidirectional,
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
impl StreamingMode {
|
|
512
|
+
/// Parse streaming mode from a string (e.g., from Flow attributes).
|
|
513
|
+
pub fn parse(s: &str) -> Self {
|
|
514
|
+
match s.to_lowercase().as_str() {
|
|
515
|
+
"streaming" | "server_streaming" | "serverstreaming" => StreamingMode::ServerStreaming,
|
|
516
|
+
"client_streaming" | "clientstreaming" => StreamingMode::ClientStreaming,
|
|
517
|
+
"bidirectional" | "bidi" | "duplex" => StreamingMode::Bidirectional,
|
|
518
|
+
_ => StreamingMode::Unary,
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
impl std::fmt::Display for StreamingMode {
|
|
524
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
525
|
+
match self {
|
|
526
|
+
StreamingMode::Unary => write!(f, "unary"),
|
|
527
|
+
StreamingMode::ServerStreaming => write!(f, "server_streaming"),
|
|
528
|
+
StreamingMode::ClientStreaming => write!(f, "client_streaming"),
|
|
529
|
+
StreamingMode::Bidirectional => write!(f, "bidirectional"),
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/// Represents a Protobuf message definition.
|
|
535
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
536
|
+
pub struct ProtoMessage {
|
|
537
|
+
/// Message name (PascalCase)
|
|
538
|
+
pub name: String,
|
|
539
|
+
/// Field definitions
|
|
540
|
+
pub fields: Vec<ProtoField>,
|
|
541
|
+
/// Nested message definitions
|
|
542
|
+
pub nested_messages: Vec<ProtoMessage>,
|
|
543
|
+
/// Nested enum definitions
|
|
544
|
+
pub nested_enums: Vec<ProtoEnum>,
|
|
545
|
+
/// Reserved field numbers
|
|
546
|
+
pub reserved_numbers: Vec<u32>,
|
|
547
|
+
/// Reserved field names
|
|
548
|
+
pub reserved_names: Vec<String>,
|
|
549
|
+
/// Documentation comments
|
|
550
|
+
pub comments: Vec<String>,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
impl ProtoMessage {
|
|
554
|
+
/// Create a new empty ProtoMessage.
|
|
555
|
+
pub fn new(name: impl Into<String>) -> Self {
|
|
556
|
+
Self {
|
|
557
|
+
name: name.into(),
|
|
558
|
+
fields: Vec::new(),
|
|
559
|
+
nested_messages: Vec::new(),
|
|
560
|
+
nested_enums: Vec::new(),
|
|
561
|
+
reserved_numbers: Vec::new(),
|
|
562
|
+
reserved_names: Vec::new(),
|
|
563
|
+
comments: Vec::new(),
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/// Serialize the message to `.proto` text format.
|
|
568
|
+
pub fn to_proto_string(&self) -> String {
|
|
569
|
+
let mut out = String::new();
|
|
570
|
+
|
|
571
|
+
// Comments
|
|
572
|
+
for comment in &self.comments {
|
|
573
|
+
out.push_str(&format!("// {}\n", comment));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
out.push_str(&format!("message {} {{\n", self.name));
|
|
577
|
+
|
|
578
|
+
// Reserved fields
|
|
579
|
+
if !self.reserved_numbers.is_empty() {
|
|
580
|
+
let nums: Vec<String> = self
|
|
581
|
+
.reserved_numbers
|
|
582
|
+
.iter()
|
|
583
|
+
.map(|n| n.to_string())
|
|
584
|
+
.collect();
|
|
585
|
+
out.push_str(&format!(" reserved {};\n", nums.join(", ")));
|
|
586
|
+
}
|
|
587
|
+
if !self.reserved_names.is_empty() {
|
|
588
|
+
let names: Vec<String> = self
|
|
589
|
+
.reserved_names
|
|
590
|
+
.iter()
|
|
591
|
+
.map(|n| format!("\"{}\"", n))
|
|
592
|
+
.collect();
|
|
593
|
+
out.push_str(&format!(" reserved {};\n", names.join(", ")));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Nested enums
|
|
597
|
+
for e in &self.nested_enums {
|
|
598
|
+
for line in e.to_proto_string().lines() {
|
|
599
|
+
out.push_str(&format!(" {}\n", line));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Nested messages
|
|
604
|
+
for m in &self.nested_messages {
|
|
605
|
+
for line in m.to_proto_string().lines() {
|
|
606
|
+
out.push_str(&format!(" {}\n", line));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Fields
|
|
611
|
+
for field in &self.fields {
|
|
612
|
+
out.push_str(&format!(" {}\n", field.to_proto_string()));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
out.push_str("}\n");
|
|
616
|
+
out
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/// Represents a Protobuf field definition.
|
|
621
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
622
|
+
pub struct ProtoField {
|
|
623
|
+
/// Field name (snake_case)
|
|
624
|
+
pub name: String,
|
|
625
|
+
/// Field number (must be unique within message)
|
|
626
|
+
pub number: u32,
|
|
627
|
+
/// Field type
|
|
628
|
+
pub proto_type: ProtoType,
|
|
629
|
+
/// Whether this is a repeated field
|
|
630
|
+
pub repeated: bool,
|
|
631
|
+
/// Whether this field is optional (proto3 optional)
|
|
632
|
+
pub optional: bool,
|
|
633
|
+
/// Documentation comments
|
|
634
|
+
pub comments: Vec<String>,
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
impl ProtoField {
|
|
638
|
+
/// Serialize the field to `.proto` text format.
|
|
639
|
+
pub fn to_proto_string(&self) -> String {
|
|
640
|
+
let mut parts = Vec::new();
|
|
641
|
+
|
|
642
|
+
// Comments as inline
|
|
643
|
+
if !self.comments.is_empty() {
|
|
644
|
+
// We'll add comment at the end
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if self.optional {
|
|
648
|
+
parts.push("optional".to_string());
|
|
649
|
+
}
|
|
650
|
+
if self.repeated {
|
|
651
|
+
parts.push("repeated".to_string());
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
parts.push(self.proto_type.to_proto_string());
|
|
655
|
+
parts.push(self.name.clone());
|
|
656
|
+
|
|
657
|
+
let mut line = format!("{} = {};", parts.join(" "), self.number);
|
|
658
|
+
|
|
659
|
+
if !self.comments.is_empty() {
|
|
660
|
+
line.push_str(&format!(" // {}", self.comments.join("; ")));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
line
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/// Represents a Protobuf type reference.
|
|
668
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
669
|
+
pub enum ProtoType {
|
|
670
|
+
/// Scalar types (int32, string, etc.)
|
|
671
|
+
Scalar(ScalarType),
|
|
672
|
+
/// Reference to another message type
|
|
673
|
+
Message(String),
|
|
674
|
+
/// Reference to an enum type
|
|
675
|
+
Enum(String),
|
|
676
|
+
/// Map type (map<key, value>)
|
|
677
|
+
Map {
|
|
678
|
+
key: Box<ProtoType>,
|
|
679
|
+
value: Box<ProtoType>,
|
|
680
|
+
},
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
impl ProtoType {
|
|
684
|
+
/// Serialize the type to `.proto` text format.
|
|
685
|
+
pub fn to_proto_string(&self) -> String {
|
|
686
|
+
match self {
|
|
687
|
+
ProtoType::Scalar(s) => s.to_proto_string(),
|
|
688
|
+
ProtoType::Message(name) => name.clone(),
|
|
689
|
+
ProtoType::Enum(name) => name.clone(),
|
|
690
|
+
ProtoType::Map { key, value } => {
|
|
691
|
+
format!(
|
|
692
|
+
"map<{}, {}>",
|
|
693
|
+
key.to_proto_string(),
|
|
694
|
+
value.to_proto_string()
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/// Protobuf scalar types.
|
|
702
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
703
|
+
pub enum ScalarType {
|
|
704
|
+
Double,
|
|
705
|
+
Float,
|
|
706
|
+
Int32,
|
|
707
|
+
Int64,
|
|
708
|
+
Uint32,
|
|
709
|
+
Uint64,
|
|
710
|
+
Sint32,
|
|
711
|
+
Sint64,
|
|
712
|
+
Fixed32,
|
|
713
|
+
Fixed64,
|
|
714
|
+
Sfixed32,
|
|
715
|
+
Sfixed64,
|
|
716
|
+
Bool,
|
|
717
|
+
String,
|
|
718
|
+
Bytes,
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
impl ScalarType {
|
|
722
|
+
/// Serialize the scalar type to `.proto` text format.
|
|
723
|
+
pub fn to_proto_string(&self) -> String {
|
|
724
|
+
match self {
|
|
725
|
+
ScalarType::Double => "double",
|
|
726
|
+
ScalarType::Float => "float",
|
|
727
|
+
ScalarType::Int32 => "int32",
|
|
728
|
+
ScalarType::Int64 => "int64",
|
|
729
|
+
ScalarType::Uint32 => "uint32",
|
|
730
|
+
ScalarType::Uint64 => "uint64",
|
|
731
|
+
ScalarType::Sint32 => "sint32",
|
|
732
|
+
ScalarType::Sint64 => "sint64",
|
|
733
|
+
ScalarType::Fixed32 => "fixed32",
|
|
734
|
+
ScalarType::Fixed64 => "fixed64",
|
|
735
|
+
ScalarType::Sfixed32 => "sfixed32",
|
|
736
|
+
ScalarType::Sfixed64 => "sfixed64",
|
|
737
|
+
ScalarType::Bool => "bool",
|
|
738
|
+
ScalarType::String => "string",
|
|
739
|
+
ScalarType::Bytes => "bytes",
|
|
740
|
+
}
|
|
741
|
+
.to_string()
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/// Represents a Protobuf enum definition.
|
|
746
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
747
|
+
pub struct ProtoEnum {
|
|
748
|
+
/// Enum name (PascalCase)
|
|
749
|
+
pub name: String,
|
|
750
|
+
/// Enum values
|
|
751
|
+
pub values: Vec<ProtoEnumValue>,
|
|
752
|
+
/// Documentation comments
|
|
753
|
+
pub comments: Vec<String>,
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
impl ProtoEnum {
|
|
757
|
+
/// Create a new ProtoEnum with default UNSPECIFIED value.
|
|
758
|
+
pub fn new(name: impl Into<String>) -> Self {
|
|
759
|
+
let name = name.into();
|
|
760
|
+
Self {
|
|
761
|
+
values: vec![ProtoEnumValue {
|
|
762
|
+
name: format!("{}_UNSPECIFIED", to_screaming_snake_case(&name)),
|
|
763
|
+
number: 0,
|
|
764
|
+
}],
|
|
765
|
+
name,
|
|
766
|
+
comments: Vec::new(),
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/// Add a value to the enum.
|
|
771
|
+
pub fn add_value(&mut self, name: impl Into<String>) {
|
|
772
|
+
let number = self.values.len() as i32;
|
|
773
|
+
self.values.push(ProtoEnumValue {
|
|
774
|
+
name: name.into(),
|
|
775
|
+
number,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/// Serialize the enum to `.proto` text format.
|
|
780
|
+
pub fn to_proto_string(&self) -> String {
|
|
781
|
+
let mut out = String::new();
|
|
782
|
+
|
|
783
|
+
for comment in &self.comments {
|
|
784
|
+
out.push_str(&format!("// {}\n", comment));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
out.push_str(&format!("enum {} {{\n", self.name));
|
|
788
|
+
|
|
789
|
+
for value in &self.values {
|
|
790
|
+
out.push_str(&format!(" {} = {};\n", value.name, value.number));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
out.push_str("}\n");
|
|
794
|
+
out
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/// Represents a Protobuf enum value.
|
|
799
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
800
|
+
pub struct ProtoEnumValue {
|
|
801
|
+
/// Value name (SCREAMING_SNAKE_CASE)
|
|
802
|
+
pub name: String,
|
|
803
|
+
/// Numeric value
|
|
804
|
+
pub number: i32,
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ============================================================================
|
|
808
|
+
// Well-Known Types
|
|
809
|
+
// ============================================================================
|
|
810
|
+
|
|
811
|
+
/// Google Protobuf Well-Known Types.
|
|
812
|
+
///
|
|
813
|
+
/// These are standard types provided by Google that have special handling
|
|
814
|
+
/// in most Protobuf implementations.
|
|
815
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
816
|
+
pub enum WellKnownType {
|
|
817
|
+
/// google.protobuf.Timestamp - for date/time values
|
|
818
|
+
Timestamp,
|
|
819
|
+
/// google.protobuf.Duration - for time spans
|
|
820
|
+
Duration,
|
|
821
|
+
/// google.protobuf.Any - for dynamic typing
|
|
822
|
+
Any,
|
|
823
|
+
/// google.protobuf.Struct - for JSON-like structures
|
|
824
|
+
Struct,
|
|
825
|
+
/// google.protobuf.Value - for dynamic JSON values
|
|
826
|
+
Value,
|
|
827
|
+
/// google.protobuf.ListValue - for JSON arrays
|
|
828
|
+
ListValue,
|
|
829
|
+
/// google.protobuf.Empty - for empty messages
|
|
830
|
+
Empty,
|
|
831
|
+
/// Wrapper types for nullable primitives
|
|
832
|
+
Int32Value,
|
|
833
|
+
Int64Value,
|
|
834
|
+
UInt32Value,
|
|
835
|
+
UInt64Value,
|
|
836
|
+
FloatValue,
|
|
837
|
+
DoubleValue,
|
|
838
|
+
BoolValue,
|
|
839
|
+
StringValue,
|
|
840
|
+
BytesValue,
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
impl WellKnownType {
|
|
844
|
+
/// Get the fully qualified type name.
|
|
845
|
+
pub fn type_name(&self) -> &'static str {
|
|
846
|
+
match self {
|
|
847
|
+
WellKnownType::Timestamp => "google.protobuf.Timestamp",
|
|
848
|
+
WellKnownType::Duration => "google.protobuf.Duration",
|
|
849
|
+
WellKnownType::Any => "google.protobuf.Any",
|
|
850
|
+
WellKnownType::Struct => "google.protobuf.Struct",
|
|
851
|
+
WellKnownType::Value => "google.protobuf.Value",
|
|
852
|
+
WellKnownType::ListValue => "google.protobuf.ListValue",
|
|
853
|
+
WellKnownType::Empty => "google.protobuf.Empty",
|
|
854
|
+
WellKnownType::Int32Value => "google.protobuf.Int32Value",
|
|
855
|
+
WellKnownType::Int64Value => "google.protobuf.Int64Value",
|
|
856
|
+
WellKnownType::UInt32Value => "google.protobuf.UInt32Value",
|
|
857
|
+
WellKnownType::UInt64Value => "google.protobuf.UInt64Value",
|
|
858
|
+
WellKnownType::FloatValue => "google.protobuf.FloatValue",
|
|
859
|
+
WellKnownType::DoubleValue => "google.protobuf.DoubleValue",
|
|
860
|
+
WellKnownType::BoolValue => "google.protobuf.BoolValue",
|
|
861
|
+
WellKnownType::StringValue => "google.protobuf.StringValue",
|
|
862
|
+
WellKnownType::BytesValue => "google.protobuf.BytesValue",
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/// Get the import path for this type.
|
|
867
|
+
pub fn import_path(&self) -> &'static str {
|
|
868
|
+
match self {
|
|
869
|
+
WellKnownType::Timestamp => "google/protobuf/timestamp.proto",
|
|
870
|
+
WellKnownType::Duration => "google/protobuf/duration.proto",
|
|
871
|
+
WellKnownType::Any => "google/protobuf/any.proto",
|
|
872
|
+
WellKnownType::Struct | WellKnownType::Value | WellKnownType::ListValue => {
|
|
873
|
+
"google/protobuf/struct.proto"
|
|
874
|
+
}
|
|
875
|
+
WellKnownType::Empty => "google/protobuf/empty.proto",
|
|
876
|
+
WellKnownType::Int32Value
|
|
877
|
+
| WellKnownType::Int64Value
|
|
878
|
+
| WellKnownType::UInt32Value
|
|
879
|
+
| WellKnownType::UInt64Value
|
|
880
|
+
| WellKnownType::FloatValue
|
|
881
|
+
| WellKnownType::DoubleValue
|
|
882
|
+
| WellKnownType::BoolValue
|
|
883
|
+
| WellKnownType::StringValue
|
|
884
|
+
| WellKnownType::BytesValue => "google/protobuf/wrappers.proto",
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/// Try to parse a type name into a WellKnownType.
|
|
889
|
+
pub fn from_type_name(name: &str) -> Option<Self> {
|
|
890
|
+
match name {
|
|
891
|
+
"google.protobuf.Timestamp" => Some(WellKnownType::Timestamp),
|
|
892
|
+
"google.protobuf.Duration" => Some(WellKnownType::Duration),
|
|
893
|
+
"google.protobuf.Any" => Some(WellKnownType::Any),
|
|
894
|
+
"google.protobuf.Struct" => Some(WellKnownType::Struct),
|
|
895
|
+
"google.protobuf.Value" => Some(WellKnownType::Value),
|
|
896
|
+
"google.protobuf.ListValue" => Some(WellKnownType::ListValue),
|
|
897
|
+
"google.protobuf.Empty" => Some(WellKnownType::Empty),
|
|
898
|
+
"google.protobuf.Int32Value" => Some(WellKnownType::Int32Value),
|
|
899
|
+
"google.protobuf.Int64Value" => Some(WellKnownType::Int64Value),
|
|
900
|
+
"google.protobuf.UInt32Value" => Some(WellKnownType::UInt32Value),
|
|
901
|
+
"google.protobuf.UInt64Value" => Some(WellKnownType::UInt64Value),
|
|
902
|
+
"google.protobuf.FloatValue" => Some(WellKnownType::FloatValue),
|
|
903
|
+
"google.protobuf.DoubleValue" => Some(WellKnownType::DoubleValue),
|
|
904
|
+
"google.protobuf.BoolValue" => Some(WellKnownType::BoolValue),
|
|
905
|
+
"google.protobuf.StringValue" => Some(WellKnownType::StringValue),
|
|
906
|
+
"google.protobuf.BytesValue" => Some(WellKnownType::BytesValue),
|
|
907
|
+
_ => None,
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ============================================================================
|
|
913
|
+
// Type Mapping
|
|
914
|
+
// ============================================================================
|
|
915
|
+
|
|
916
|
+
/// Map SEA type strings to Protobuf types.
|
|
917
|
+
///
|
|
918
|
+
/// This function handles the common type names used in SEA attributes,
|
|
919
|
+
/// including mapping to Google Well-Known Types where appropriate.
|
|
920
|
+
pub fn map_sea_type_to_proto(sea_type: &str) -> ProtoType {
|
|
921
|
+
match sea_type.to_lowercase().as_str() {
|
|
922
|
+
// Scalar types
|
|
923
|
+
"string" | "text" | "varchar" => ProtoType::Scalar(ScalarType::String),
|
|
924
|
+
"int" | "integer" | "int64" | "long" => ProtoType::Scalar(ScalarType::Int64),
|
|
925
|
+
"int32" | "short" => ProtoType::Scalar(ScalarType::Int32),
|
|
926
|
+
"uint32" => ProtoType::Scalar(ScalarType::Uint32),
|
|
927
|
+
"uint64" | "ulong" => ProtoType::Scalar(ScalarType::Uint64),
|
|
928
|
+
"float" | "double" | "decimal" | "number" => ProtoType::Scalar(ScalarType::Double),
|
|
929
|
+
"float32" => ProtoType::Scalar(ScalarType::Float),
|
|
930
|
+
"bool" | "boolean" => ProtoType::Scalar(ScalarType::Bool),
|
|
931
|
+
"bytes" | "binary" | "blob" => ProtoType::Scalar(ScalarType::Bytes),
|
|
932
|
+
"uuid" | "guid" => ProtoType::Scalar(ScalarType::String),
|
|
933
|
+
|
|
934
|
+
// Well-Known Types
|
|
935
|
+
"date" | "datetime" | "timestamp" => {
|
|
936
|
+
ProtoType::Message(WellKnownType::Timestamp.type_name().to_string())
|
|
937
|
+
}
|
|
938
|
+
"duration" | "timespan" | "interval" => {
|
|
939
|
+
ProtoType::Message(WellKnownType::Duration.type_name().to_string())
|
|
940
|
+
}
|
|
941
|
+
"any" | "dynamic" | "object" => {
|
|
942
|
+
ProtoType::Message(WellKnownType::Any.type_name().to_string())
|
|
943
|
+
}
|
|
944
|
+
"struct" | "json" | "jsonobject" => {
|
|
945
|
+
ProtoType::Message(WellKnownType::Struct.type_name().to_string())
|
|
946
|
+
}
|
|
947
|
+
"value" | "jsonvalue" => ProtoType::Message(WellKnownType::Value.type_name().to_string()),
|
|
948
|
+
"empty" | "void" | "unit" => {
|
|
949
|
+
ProtoType::Message(WellKnownType::Empty.type_name().to_string())
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Nullable wrapper types
|
|
953
|
+
"optional_int" | "nullable_int" | "int?" => {
|
|
954
|
+
ProtoType::Message(WellKnownType::Int64Value.type_name().to_string())
|
|
955
|
+
}
|
|
956
|
+
"optional_int32" | "nullable_int32" | "int32?" => {
|
|
957
|
+
ProtoType::Message(WellKnownType::Int32Value.type_name().to_string())
|
|
958
|
+
}
|
|
959
|
+
"optional_string" | "nullable_string" | "string?" => {
|
|
960
|
+
ProtoType::Message(WellKnownType::StringValue.type_name().to_string())
|
|
961
|
+
}
|
|
962
|
+
"optional_bool" | "nullable_bool" | "bool?" => {
|
|
963
|
+
ProtoType::Message(WellKnownType::BoolValue.type_name().to_string())
|
|
964
|
+
}
|
|
965
|
+
"optional_double" | "nullable_double" | "double?" => {
|
|
966
|
+
ProtoType::Message(WellKnownType::DoubleValue.type_name().to_string())
|
|
967
|
+
}
|
|
968
|
+
"optional_float" | "nullable_float" | "float?" => {
|
|
969
|
+
ProtoType::Message(WellKnownType::FloatValue.type_name().to_string())
|
|
970
|
+
}
|
|
971
|
+
"optional_bytes" | "nullable_bytes" | "bytes?" => {
|
|
972
|
+
ProtoType::Message(WellKnownType::BytesValue.type_name().to_string())
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Custom types become messages
|
|
976
|
+
_ => ProtoType::Message(sanitize_proto_ident(sea_type)),
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/// Infer a ProtoType from a serde_json::Value.
|
|
981
|
+
pub fn infer_proto_type_from_value(value: &Value) -> ProtoType {
|
|
982
|
+
match value {
|
|
983
|
+
Value::Null => ProtoType::Scalar(ScalarType::String),
|
|
984
|
+
Value::Bool(_) => ProtoType::Scalar(ScalarType::Bool),
|
|
985
|
+
Value::Number(n) => {
|
|
986
|
+
if n.is_f64() {
|
|
987
|
+
ProtoType::Scalar(ScalarType::Double)
|
|
988
|
+
} else {
|
|
989
|
+
ProtoType::Scalar(ScalarType::Int64)
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
Value::String(_) => ProtoType::Scalar(ScalarType::String),
|
|
993
|
+
Value::Array(_) => ProtoType::Scalar(ScalarType::String), // Arrays need special handling
|
|
994
|
+
Value::Object(_) => ProtoType::Scalar(ScalarType::String), // Objects need special handling
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ============================================================================
|
|
999
|
+
// Protobuf Engine
|
|
1000
|
+
// ============================================================================
|
|
1001
|
+
|
|
1002
|
+
/// The Protobuf projection engine.
|
|
1003
|
+
///
|
|
1004
|
+
/// This struct provides methods to convert a SEA Graph into Protobuf IR
|
|
1005
|
+
/// which can then be serialized to `.proto` text format.
|
|
1006
|
+
pub struct ProtobufEngine;
|
|
1007
|
+
|
|
1008
|
+
impl ProtobufEngine {
|
|
1009
|
+
/// Project a SEA Graph to a ProtoFile.
|
|
1010
|
+
///
|
|
1011
|
+
/// # Arguments
|
|
1012
|
+
///
|
|
1013
|
+
/// * `graph` - The semantic graph to project
|
|
1014
|
+
/// * `namespace` - Filter to only include entities/resources from this namespace (empty = all)
|
|
1015
|
+
/// * `package` - The Protobuf package name to use
|
|
1016
|
+
///
|
|
1017
|
+
/// # Returns
|
|
1018
|
+
///
|
|
1019
|
+
/// A `ProtoFile` containing the generated Protobuf IR.
|
|
1020
|
+
pub fn project(graph: &Graph, namespace: &str, package: &str) -> ProtoFile {
|
|
1021
|
+
let mut proto = ProtoFile::new(package);
|
|
1022
|
+
proto.metadata.source_namespace = namespace.to_string();
|
|
1023
|
+
proto.metadata.generated_at = std::env::var("SOURCE_DATE_EPOCH")
|
|
1024
|
+
.ok()
|
|
1025
|
+
.and_then(|epoch| epoch.parse::<i64>().ok())
|
|
1026
|
+
.and_then(|epoch| chrono::DateTime::from_timestamp(epoch, 0))
|
|
1027
|
+
.map(|dt| dt.to_rfc3339())
|
|
1028
|
+
.unwrap_or_default();
|
|
1029
|
+
|
|
1030
|
+
// Convert entities to messages
|
|
1031
|
+
for entity in graph.all_entities() {
|
|
1032
|
+
if namespace.is_empty() || entity.namespace() == namespace {
|
|
1033
|
+
proto.messages.push(Self::entity_to_message(entity));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Convert resources to messages
|
|
1038
|
+
for resource in graph.all_resources() {
|
|
1039
|
+
if namespace.is_empty() || resource.namespace() == namespace {
|
|
1040
|
+
proto.messages.push(Self::resource_to_message(resource));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Sort messages by name for deterministic output
|
|
1045
|
+
proto.messages.sort_by(|a, b| a.name.cmp(&b.name));
|
|
1046
|
+
|
|
1047
|
+
// Auto-detect and add Well-Known Type imports
|
|
1048
|
+
proto.add_wkt_imports();
|
|
1049
|
+
|
|
1050
|
+
proto
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/// Project with options for a projection contract.
|
|
1054
|
+
pub fn project_with_options(
|
|
1055
|
+
graph: &Graph,
|
|
1056
|
+
namespace: &str,
|
|
1057
|
+
package: &str,
|
|
1058
|
+
projection_name: &str,
|
|
1059
|
+
include_governance: bool,
|
|
1060
|
+
) -> ProtoFile {
|
|
1061
|
+
Self::project_with_full_options(
|
|
1062
|
+
graph,
|
|
1063
|
+
namespace,
|
|
1064
|
+
package,
|
|
1065
|
+
projection_name,
|
|
1066
|
+
include_governance,
|
|
1067
|
+
false, // include_services
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/// Project with all options including gRPC service generation.
|
|
1072
|
+
pub fn project_with_full_options(
|
|
1073
|
+
graph: &Graph,
|
|
1074
|
+
namespace: &str,
|
|
1075
|
+
package: &str,
|
|
1076
|
+
projection_name: &str,
|
|
1077
|
+
include_governance: bool,
|
|
1078
|
+
include_services: bool,
|
|
1079
|
+
) -> ProtoFile {
|
|
1080
|
+
let mut proto = Self::project(graph, namespace, package);
|
|
1081
|
+
proto.metadata.projection_name = projection_name.to_string();
|
|
1082
|
+
|
|
1083
|
+
if include_governance {
|
|
1084
|
+
proto.messages.extend(Self::generate_governance_messages());
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if include_services {
|
|
1088
|
+
proto.services = Self::flows_to_services(graph, namespace);
|
|
1089
|
+
let response_msgs = Self::collect_response_messages(&proto.services);
|
|
1090
|
+
proto.messages.extend(response_msgs);
|
|
1091
|
+
proto.messages.sort_by(|a, b| a.name.cmp(&b.name));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Re-add WKT imports in case governance messages need them
|
|
1095
|
+
proto.add_wkt_imports();
|
|
1096
|
+
|
|
1097
|
+
proto
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/// Project a SEA Graph to multiple ProtoFiles (one per namespace).
|
|
1101
|
+
///
|
|
1102
|
+
/// This method partitions the graph by namespace, creating a separate `.proto` file
|
|
1103
|
+
/// for each. Cross-namespace references are automatically resolved by adding imports
|
|
1104
|
+
/// and fully qualifying type names.
|
|
1105
|
+
///
|
|
1106
|
+
/// # Arguments
|
|
1107
|
+
///
|
|
1108
|
+
/// * `graph` - The semantic graph to project
|
|
1109
|
+
/// * `base_package` - The root package name (e.g. "sea.generated")
|
|
1110
|
+
/// * `include_governance` - Whether to include governance messages
|
|
1111
|
+
/// * `include_services` - Whether to generate gRPC services
|
|
1112
|
+
///
|
|
1113
|
+
/// # Returns
|
|
1114
|
+
///
|
|
1115
|
+
/// A map of relative file paths to ProtoFile definitions.
|
|
1116
|
+
pub fn project_multi_file(
|
|
1117
|
+
graph: &Graph,
|
|
1118
|
+
base_package: &str,
|
|
1119
|
+
include_governance: bool,
|
|
1120
|
+
include_services: bool,
|
|
1121
|
+
) -> BTreeMap<PathBuf, ProtoFile> {
|
|
1122
|
+
let mut files: BTreeMap<String, ProtoFile> = BTreeMap::new();
|
|
1123
|
+
// Index: TypeName -> (Namespace, FullPackage)
|
|
1124
|
+
let mut type_index: HashMap<String, (String, String)> = HashMap::new();
|
|
1125
|
+
|
|
1126
|
+
// 1. Collect all namespaces and entities
|
|
1127
|
+
for entity in graph.all_entities() {
|
|
1128
|
+
let ns = entity.namespace();
|
|
1129
|
+
let package = if ns.is_empty() {
|
|
1130
|
+
base_package.to_string()
|
|
1131
|
+
} else {
|
|
1132
|
+
format!("{}.{}", base_package, ns)
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
type_index.insert(
|
|
1136
|
+
sanitize_proto_ident(entity.name()),
|
|
1137
|
+
(ns.to_string(), package.clone()),
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
files
|
|
1141
|
+
.entry(ns.to_string())
|
|
1142
|
+
.or_insert_with(|| ProtoFile::new(package))
|
|
1143
|
+
.messages
|
|
1144
|
+
.push(Self::entity_to_message(entity));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
for resource in graph.all_resources() {
|
|
1148
|
+
let ns = resource.namespace();
|
|
1149
|
+
let package = if ns.is_empty() {
|
|
1150
|
+
base_package.to_string()
|
|
1151
|
+
} else {
|
|
1152
|
+
format!("{}.{}", base_package, ns)
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
type_index.insert(
|
|
1156
|
+
sanitize_proto_ident(resource.name()),
|
|
1157
|
+
(ns.to_string(), package.clone()),
|
|
1158
|
+
);
|
|
1159
|
+
|
|
1160
|
+
files
|
|
1161
|
+
.entry(ns.to_string())
|
|
1162
|
+
.or_insert_with(|| ProtoFile::new(package))
|
|
1163
|
+
.messages
|
|
1164
|
+
.push(Self::resource_to_message(resource));
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// 2. Add Services
|
|
1168
|
+
if include_services {
|
|
1169
|
+
let services = Self::flows_to_services(graph, "");
|
|
1170
|
+
let response_msgs = Self::collect_response_messages(&services);
|
|
1171
|
+
for service in services {
|
|
1172
|
+
let entity_name = service
|
|
1173
|
+
.name
|
|
1174
|
+
.strip_suffix("Service")
|
|
1175
|
+
.unwrap_or(&service.name);
|
|
1176
|
+
|
|
1177
|
+
if let Some((ns, package)) = type_index.get(entity_name) {
|
|
1178
|
+
files
|
|
1179
|
+
.entry(ns.clone())
|
|
1180
|
+
.or_insert_with(|| ProtoFile::new(package.clone()))
|
|
1181
|
+
.services
|
|
1182
|
+
.push(service);
|
|
1183
|
+
} else {
|
|
1184
|
+
let package = base_package.to_string();
|
|
1185
|
+
files
|
|
1186
|
+
.entry("".to_string())
|
|
1187
|
+
.or_insert_with(|| ProtoFile::new(package))
|
|
1188
|
+
.services
|
|
1189
|
+
.push(service);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
for resp in response_msgs {
|
|
1193
|
+
type_index.insert(
|
|
1194
|
+
resp.name.clone(),
|
|
1195
|
+
("".to_string(), base_package.to_string()),
|
|
1196
|
+
);
|
|
1197
|
+
files
|
|
1198
|
+
.entry("".to_string())
|
|
1199
|
+
.or_insert_with(|| ProtoFile::new(base_package.to_string()))
|
|
1200
|
+
.messages
|
|
1201
|
+
.push(resp);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// 3. Add Governance (in root namespace usually)
|
|
1206
|
+
if include_governance {
|
|
1207
|
+
let root_file = files
|
|
1208
|
+
.entry("".to_string())
|
|
1209
|
+
.or_insert_with(|| ProtoFile::new(base_package.to_string()));
|
|
1210
|
+
|
|
1211
|
+
let governance_msgs = Self::generate_governance_messages();
|
|
1212
|
+
for msg in &governance_msgs {
|
|
1213
|
+
type_index.insert(msg.name.clone(), ("".to_string(), base_package.to_string()));
|
|
1214
|
+
}
|
|
1215
|
+
root_file.messages.extend(governance_msgs);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// 4. Resolve Imports and Finalize
|
|
1219
|
+
for (ns, file) in files.iter_mut() {
|
|
1220
|
+
let mut imports_to_add = HashSet::new();
|
|
1221
|
+
|
|
1222
|
+
// Check messages for cross-references
|
|
1223
|
+
for msg in &mut file.messages {
|
|
1224
|
+
Self::resolve_imports_in_message(msg, ns, &type_index, &mut imports_to_add);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Check services
|
|
1228
|
+
for svc in &mut file.services {
|
|
1229
|
+
for method in &mut svc.methods {
|
|
1230
|
+
// Check request types
|
|
1231
|
+
if let Some((target_ns, target_pkg)) = type_index.get(&method.request_type) {
|
|
1232
|
+
if target_ns != ns {
|
|
1233
|
+
imports_to_add.insert(target_ns.clone());
|
|
1234
|
+
method.request_type = format!("{}.{}", target_pkg, method.request_type);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
// Check response types
|
|
1238
|
+
if let Some((target_ns, target_pkg)) = type_index.get(&method.response_type) {
|
|
1239
|
+
if target_ns != ns {
|
|
1240
|
+
imports_to_add.insert(target_ns.clone());
|
|
1241
|
+
method.response_type =
|
|
1242
|
+
format!("{}.{}", target_pkg, method.response_type);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Add collected imports
|
|
1249
|
+
for target_ns in imports_to_add {
|
|
1250
|
+
// self-import is prevented by check above
|
|
1251
|
+
// root namespace usually doesn't need path prefix if files in same dir,
|
|
1252
|
+
// but assume we strictly follow directory structure.
|
|
1253
|
+
let import_path = if target_ns.is_empty() {
|
|
1254
|
+
"projection.proto".to_string()
|
|
1255
|
+
} else {
|
|
1256
|
+
format!("{}.proto", target_ns.replace('.', "/"))
|
|
1257
|
+
};
|
|
1258
|
+
file.imports.push(import_path);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Sort and deduplicate imports (including WKTs)
|
|
1262
|
+
file.add_wkt_imports();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Convert map to PathBuf keys
|
|
1266
|
+
let mut results = BTreeMap::new();
|
|
1267
|
+
for (ns, file) in files {
|
|
1268
|
+
let path = if ns.is_empty() {
|
|
1269
|
+
PathBuf::from("projection.proto")
|
|
1270
|
+
} else {
|
|
1271
|
+
PathBuf::from(format!("{}.proto", ns.replace('.', "/")))
|
|
1272
|
+
};
|
|
1273
|
+
results.insert(path, file);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
results
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/// Helper to resolve cross-namespace imports within a message.
|
|
1280
|
+
fn resolve_imports_in_message(
|
|
1281
|
+
msg: &mut ProtoMessage,
|
|
1282
|
+
current_ns: &str,
|
|
1283
|
+
index: &HashMap<String, (String, String)>,
|
|
1284
|
+
imports: &mut HashSet<String>,
|
|
1285
|
+
) {
|
|
1286
|
+
for field in &mut msg.fields {
|
|
1287
|
+
match &mut field.proto_type {
|
|
1288
|
+
ProtoType::Message(name) | ProtoType::Enum(name) => {
|
|
1289
|
+
// Ignore WKTs
|
|
1290
|
+
if WellKnownType::from_type_name(name).is_some() {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if let Some((target_ns, target_pkg)) = index.get(name.as_str()) {
|
|
1295
|
+
if target_ns != current_ns {
|
|
1296
|
+
imports.insert(target_ns.clone());
|
|
1297
|
+
*name = format!("{}.{}", target_pkg, name);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
ProtoType::Map { key: _, value } => {
|
|
1302
|
+
// Check recursively (simplified for now assuming only value can be message)
|
|
1303
|
+
if let ProtoType::Message(name) = &mut **value {
|
|
1304
|
+
if WellKnownType::from_type_name(name).is_some() {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
if let Some((target_ns, target_pkg)) = index.get(name.as_str()) {
|
|
1308
|
+
if target_ns != current_ns {
|
|
1309
|
+
imports.insert(target_ns.clone());
|
|
1310
|
+
*name = format!("{}.{}", target_pkg, name);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
_ => {}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Recurse into nested messages
|
|
1320
|
+
for nested in &mut msg.nested_messages {
|
|
1321
|
+
Self::resolve_imports_in_message(nested, current_ns, index, imports);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
///
|
|
1325
|
+
/// This method groups flows by their destination entity (service provider)
|
|
1326
|
+
/// and creates RPC methods for each flow. The naming convention follows
|
|
1327
|
+
/// gRPC best practices: `{DestinationEntity}Service`.
|
|
1328
|
+
///
|
|
1329
|
+
/// # Example
|
|
1330
|
+
///
|
|
1331
|
+
/// A flow `Customer -> PaymentProcessor of PaymentRequest` generates:
|
|
1332
|
+
/// ```protobuf
|
|
1333
|
+
/// service PaymentProcessorService {
|
|
1334
|
+
/// rpc ProcessPaymentRequest(PaymentRequest) returns (PaymentRequestResponse);
|
|
1335
|
+
/// }
|
|
1336
|
+
/// ```
|
|
1337
|
+
pub fn flows_to_services(graph: &Graph, namespace: &str) -> Vec<ProtoService> {
|
|
1338
|
+
let mut services: BTreeMap<String, ProtoService> = BTreeMap::new();
|
|
1339
|
+
|
|
1340
|
+
for flow in graph.all_flows() {
|
|
1341
|
+
if !namespace.is_empty() && flow.namespace() != namespace {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
let to_entity = match graph.get_entity(flow.to_id()) {
|
|
1346
|
+
Some(e) => e,
|
|
1347
|
+
None => continue,
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
let resource = match graph.get_resource(flow.resource_id()) {
|
|
1351
|
+
Some(r) => r,
|
|
1352
|
+
None => continue,
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
let service_name = format!("{}Service", sanitize_proto_ident(to_entity.name()));
|
|
1356
|
+
let method_name = format!("Process{}", sanitize_proto_ident(resource.name()));
|
|
1357
|
+
let request_type = sanitize_proto_ident(resource.name());
|
|
1358
|
+
let response_type = format!("{}Response", sanitize_proto_ident(resource.name()));
|
|
1359
|
+
|
|
1360
|
+
let streaming = flow
|
|
1361
|
+
.get_attribute("streaming")
|
|
1362
|
+
.and_then(|v| v.as_str())
|
|
1363
|
+
.map(StreamingMode::parse)
|
|
1364
|
+
.unwrap_or(StreamingMode::Unary);
|
|
1365
|
+
|
|
1366
|
+
let mut method = ProtoRpcMethod::new(&method_name, &request_type, &response_type);
|
|
1367
|
+
method.streaming = streaming;
|
|
1368
|
+
|
|
1369
|
+
if let Some(from_entity) = graph.get_entity(flow.from_id()) {
|
|
1370
|
+
method.comments.push(format!(
|
|
1371
|
+
"Flow: {} -> {} of {}",
|
|
1372
|
+
from_entity.name(),
|
|
1373
|
+
to_entity.name(),
|
|
1374
|
+
resource.name()
|
|
1375
|
+
));
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
services
|
|
1379
|
+
.entry(service_name.clone())
|
|
1380
|
+
.or_insert_with(|| {
|
|
1381
|
+
let mut svc = ProtoService::new(&service_name);
|
|
1382
|
+
svc.comments
|
|
1383
|
+
.push(format!("gRPC service for {}", to_entity.name()));
|
|
1384
|
+
svc
|
|
1385
|
+
})
|
|
1386
|
+
.methods
|
|
1387
|
+
.push(method);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
services.into_values().collect()
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
pub fn collect_response_messages(services: &[ProtoService]) -> Vec<ProtoMessage> {
|
|
1394
|
+
let mut seen: HashSet<String> = HashSet::new();
|
|
1395
|
+
let mut messages = Vec::new();
|
|
1396
|
+
for service in services {
|
|
1397
|
+
for method in &service.methods {
|
|
1398
|
+
if !seen.contains(&method.response_type) {
|
|
1399
|
+
seen.insert(method.response_type.clone());
|
|
1400
|
+
let mut msg = ProtoMessage::new(&method.response_type);
|
|
1401
|
+
msg.comments
|
|
1402
|
+
.push(format!("Response message for {}", method.name));
|
|
1403
|
+
msg.fields.push(ProtoField {
|
|
1404
|
+
name: "success".to_string(),
|
|
1405
|
+
number: 1,
|
|
1406
|
+
proto_type: ProtoType::Scalar(ScalarType::Bool),
|
|
1407
|
+
repeated: false,
|
|
1408
|
+
optional: false,
|
|
1409
|
+
comments: vec![],
|
|
1410
|
+
});
|
|
1411
|
+
msg.fields.push(ProtoField {
|
|
1412
|
+
name: "message".to_string(),
|
|
1413
|
+
number: 2,
|
|
1414
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1415
|
+
repeated: false,
|
|
1416
|
+
optional: true,
|
|
1417
|
+
comments: vec![],
|
|
1418
|
+
});
|
|
1419
|
+
messages.push(msg);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
messages
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/// Convert an Entity to a ProtoMessage.
|
|
1427
|
+
fn entity_to_message(entity: &Entity) -> ProtoMessage {
|
|
1428
|
+
let mut msg = ProtoMessage::new(sanitize_proto_ident(entity.name()));
|
|
1429
|
+
msg.comments.push(format!("SEA Entity: {}", entity.name()));
|
|
1430
|
+
msg.comments
|
|
1431
|
+
.push(format!("Namespace: {}", entity.namespace()));
|
|
1432
|
+
|
|
1433
|
+
let mut field_number = 1u32;
|
|
1434
|
+
|
|
1435
|
+
// Add id field (always first)
|
|
1436
|
+
msg.fields.push(ProtoField {
|
|
1437
|
+
name: "id".to_string(),
|
|
1438
|
+
number: field_number,
|
|
1439
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1440
|
+
repeated: false,
|
|
1441
|
+
optional: false,
|
|
1442
|
+
comments: vec!["Unique identifier".to_string()],
|
|
1443
|
+
});
|
|
1444
|
+
field_number += 1;
|
|
1445
|
+
|
|
1446
|
+
// Add name field
|
|
1447
|
+
msg.fields.push(ProtoField {
|
|
1448
|
+
name: "name".to_string(),
|
|
1449
|
+
number: field_number,
|
|
1450
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1451
|
+
repeated: false,
|
|
1452
|
+
optional: false,
|
|
1453
|
+
comments: vec!["Entity name".to_string()],
|
|
1454
|
+
});
|
|
1455
|
+
field_number += 1;
|
|
1456
|
+
|
|
1457
|
+
// Add attributes sorted alphabetically for deterministic output
|
|
1458
|
+
let mut sorted_attrs: BTreeMap<&String, &Value> = BTreeMap::new();
|
|
1459
|
+
for (key, value) in entity.attributes() {
|
|
1460
|
+
sorted_attrs.insert(key, value);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
for (key, value) in sorted_attrs {
|
|
1464
|
+
msg.fields.push(ProtoField {
|
|
1465
|
+
name: to_snake_case(key),
|
|
1466
|
+
number: field_number,
|
|
1467
|
+
proto_type: infer_proto_type_from_value(value),
|
|
1468
|
+
repeated: matches!(value, Value::Array(_)),
|
|
1469
|
+
optional: true,
|
|
1470
|
+
comments: vec![],
|
|
1471
|
+
});
|
|
1472
|
+
field_number += 1;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
msg
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/// Convert a Resource to a ProtoMessage.
|
|
1479
|
+
fn resource_to_message(resource: &Resource) -> ProtoMessage {
|
|
1480
|
+
let mut msg = ProtoMessage::new(sanitize_proto_ident(resource.name()));
|
|
1481
|
+
msg.comments
|
|
1482
|
+
.push(format!("SEA Resource: {}", resource.name()));
|
|
1483
|
+
msg.comments
|
|
1484
|
+
.push(format!("Unit: {}", resource.unit().symbol()));
|
|
1485
|
+
|
|
1486
|
+
let mut field_number = 1u32;
|
|
1487
|
+
|
|
1488
|
+
// Add id field
|
|
1489
|
+
msg.fields.push(ProtoField {
|
|
1490
|
+
name: "id".to_string(),
|
|
1491
|
+
number: field_number,
|
|
1492
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1493
|
+
repeated: false,
|
|
1494
|
+
optional: false,
|
|
1495
|
+
comments: vec!["Unique identifier".to_string()],
|
|
1496
|
+
});
|
|
1497
|
+
field_number += 1;
|
|
1498
|
+
|
|
1499
|
+
// Add name field
|
|
1500
|
+
msg.fields.push(ProtoField {
|
|
1501
|
+
name: "name".to_string(),
|
|
1502
|
+
number: field_number,
|
|
1503
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1504
|
+
repeated: false,
|
|
1505
|
+
optional: false,
|
|
1506
|
+
comments: vec!["Resource name".to_string()],
|
|
1507
|
+
});
|
|
1508
|
+
field_number += 1;
|
|
1509
|
+
|
|
1510
|
+
// Add quantity field with unit comment
|
|
1511
|
+
msg.fields.push(ProtoField {
|
|
1512
|
+
name: "quantity".to_string(),
|
|
1513
|
+
number: field_number,
|
|
1514
|
+
proto_type: ProtoType::Scalar(ScalarType::Double),
|
|
1515
|
+
repeated: false,
|
|
1516
|
+
optional: true,
|
|
1517
|
+
comments: vec![format!("Quantity in {}", resource.unit().symbol())],
|
|
1518
|
+
});
|
|
1519
|
+
field_number += 1;
|
|
1520
|
+
|
|
1521
|
+
// Add unit field
|
|
1522
|
+
msg.fields.push(ProtoField {
|
|
1523
|
+
name: "unit".to_string(),
|
|
1524
|
+
number: field_number,
|
|
1525
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1526
|
+
repeated: false,
|
|
1527
|
+
optional: false,
|
|
1528
|
+
comments: vec!["Unit of measurement".to_string()],
|
|
1529
|
+
});
|
|
1530
|
+
field_number += 1;
|
|
1531
|
+
|
|
1532
|
+
// Add attributes
|
|
1533
|
+
let mut sorted_attrs: BTreeMap<&String, &Value> = BTreeMap::new();
|
|
1534
|
+
for (key, value) in resource.attributes() {
|
|
1535
|
+
sorted_attrs.insert(key, value);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
for (key, value) in sorted_attrs {
|
|
1539
|
+
msg.fields.push(ProtoField {
|
|
1540
|
+
name: to_snake_case(key),
|
|
1541
|
+
number: field_number,
|
|
1542
|
+
proto_type: infer_proto_type_from_value(value),
|
|
1543
|
+
repeated: matches!(value, Value::Array(_)),
|
|
1544
|
+
optional: true,
|
|
1545
|
+
comments: vec![],
|
|
1546
|
+
});
|
|
1547
|
+
field_number += 1;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
msg
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/// Generate standard governance messages.
|
|
1554
|
+
fn generate_governance_messages() -> Vec<ProtoMessage> {
|
|
1555
|
+
let mut messages = Vec::new();
|
|
1556
|
+
|
|
1557
|
+
// PolicyViolation message
|
|
1558
|
+
let mut violation = ProtoMessage::new("PolicyViolation");
|
|
1559
|
+
violation
|
|
1560
|
+
.comments
|
|
1561
|
+
.push("Represents a policy violation event".to_string());
|
|
1562
|
+
violation.fields = vec![
|
|
1563
|
+
ProtoField {
|
|
1564
|
+
name: "policy_name".to_string(),
|
|
1565
|
+
number: 1,
|
|
1566
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1567
|
+
repeated: false,
|
|
1568
|
+
optional: false,
|
|
1569
|
+
comments: vec!["Name of the violated policy".to_string()],
|
|
1570
|
+
},
|
|
1571
|
+
ProtoField {
|
|
1572
|
+
name: "entity_id".to_string(),
|
|
1573
|
+
number: 2,
|
|
1574
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1575
|
+
repeated: false,
|
|
1576
|
+
optional: false,
|
|
1577
|
+
comments: vec!["ID of the entity that violated the policy".to_string()],
|
|
1578
|
+
},
|
|
1579
|
+
ProtoField {
|
|
1580
|
+
name: "severity".to_string(),
|
|
1581
|
+
number: 3,
|
|
1582
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1583
|
+
repeated: false,
|
|
1584
|
+
optional: false,
|
|
1585
|
+
comments: vec!["Severity level (error, warn, info)".to_string()],
|
|
1586
|
+
},
|
|
1587
|
+
ProtoField {
|
|
1588
|
+
name: "message".to_string(),
|
|
1589
|
+
number: 4,
|
|
1590
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1591
|
+
repeated: false,
|
|
1592
|
+
optional: false,
|
|
1593
|
+
comments: vec!["Human-readable violation message".to_string()],
|
|
1594
|
+
},
|
|
1595
|
+
ProtoField {
|
|
1596
|
+
name: "timestamp".to_string(),
|
|
1597
|
+
number: 5,
|
|
1598
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1599
|
+
repeated: false,
|
|
1600
|
+
optional: false,
|
|
1601
|
+
comments: vec!["When the violation occurred".to_string()],
|
|
1602
|
+
},
|
|
1603
|
+
];
|
|
1604
|
+
messages.push(violation);
|
|
1605
|
+
|
|
1606
|
+
// MetricEvent message
|
|
1607
|
+
let mut metric = ProtoMessage::new("MetricEvent");
|
|
1608
|
+
metric
|
|
1609
|
+
.comments
|
|
1610
|
+
.push("Represents a metric measurement event".to_string());
|
|
1611
|
+
metric.fields = vec![
|
|
1612
|
+
ProtoField {
|
|
1613
|
+
name: "metric_name".to_string(),
|
|
1614
|
+
number: 1,
|
|
1615
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1616
|
+
repeated: false,
|
|
1617
|
+
optional: false,
|
|
1618
|
+
comments: vec!["Name of the metric".to_string()],
|
|
1619
|
+
},
|
|
1620
|
+
ProtoField {
|
|
1621
|
+
name: "value".to_string(),
|
|
1622
|
+
number: 2,
|
|
1623
|
+
proto_type: ProtoType::Scalar(ScalarType::Double),
|
|
1624
|
+
repeated: false,
|
|
1625
|
+
optional: false,
|
|
1626
|
+
comments: vec!["Measured value".to_string()],
|
|
1627
|
+
},
|
|
1628
|
+
ProtoField {
|
|
1629
|
+
name: "unit".to_string(),
|
|
1630
|
+
number: 3,
|
|
1631
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1632
|
+
repeated: false,
|
|
1633
|
+
optional: true,
|
|
1634
|
+
comments: vec!["Unit of measurement".to_string()],
|
|
1635
|
+
},
|
|
1636
|
+
ProtoField {
|
|
1637
|
+
name: "timestamp".to_string(),
|
|
1638
|
+
number: 4,
|
|
1639
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
1640
|
+
repeated: false,
|
|
1641
|
+
optional: false,
|
|
1642
|
+
comments: vec!["When the measurement was taken".to_string()],
|
|
1643
|
+
},
|
|
1644
|
+
];
|
|
1645
|
+
messages.push(metric);
|
|
1646
|
+
|
|
1647
|
+
messages
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// ============================================================================
|
|
1652
|
+
// Helper Functions
|
|
1653
|
+
// ============================================================================
|
|
1654
|
+
|
|
1655
|
+
/// Convert a string to PascalCase.
|
|
1656
|
+
///
|
|
1657
|
+
/// Preserves original casing of characters after the first letter of each word,
|
|
1658
|
+
/// so "PaymentProcessor" stays "PaymentProcessor" (not "Paymentprocessor").
|
|
1659
|
+
fn to_pascal_case(s: &str) -> String {
|
|
1660
|
+
s.split(|c: char| !c.is_alphanumeric())
|
|
1661
|
+
.filter(|part| !part.is_empty())
|
|
1662
|
+
.map(|part| {
|
|
1663
|
+
let mut chars = part.chars();
|
|
1664
|
+
match chars.next() {
|
|
1665
|
+
Some(first) => first.to_uppercase().to_string() + chars.as_str(),
|
|
1666
|
+
None => String::new(),
|
|
1667
|
+
}
|
|
1668
|
+
})
|
|
1669
|
+
.collect()
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
fn sanitize_proto_ident(raw: &str) -> String {
|
|
1673
|
+
let pascal = to_pascal_case(raw);
|
|
1674
|
+
if pascal.is_empty() {
|
|
1675
|
+
return "SeaUnnamed".to_string();
|
|
1676
|
+
}
|
|
1677
|
+
let mut result = String::new();
|
|
1678
|
+
for (i, c) in pascal.chars().enumerate() {
|
|
1679
|
+
if i == 0 && !c.is_ascii_alphabetic() && c != '_' {
|
|
1680
|
+
result.push_str("Sea");
|
|
1681
|
+
}
|
|
1682
|
+
if c.is_ascii_alphanumeric() || c == '_' {
|
|
1683
|
+
result.push(c);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if result.is_empty() {
|
|
1687
|
+
return "SeaUnnamed".to_string();
|
|
1688
|
+
}
|
|
1689
|
+
let reserved = [
|
|
1690
|
+
"syntax", "package", "message", "service", "rpc", "enum", "import", "option", "returns",
|
|
1691
|
+
"stream", "reserved", "bool", "string", "bytes", "double", "float", "int32", "int64",
|
|
1692
|
+
"uint32", "uint64", "true", "false",
|
|
1693
|
+
];
|
|
1694
|
+
if reserved.contains(&result.to_lowercase().as_str()) {
|
|
1695
|
+
result = format!("Sea{}", result);
|
|
1696
|
+
}
|
|
1697
|
+
result
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/// Convert a string to snake_case.
|
|
1701
|
+
fn to_snake_case(s: &str) -> String {
|
|
1702
|
+
let mut result = String::new();
|
|
1703
|
+
let mut prev_is_uppercase = false;
|
|
1704
|
+
|
|
1705
|
+
for (i, c) in s.chars().enumerate() {
|
|
1706
|
+
if c.is_uppercase() {
|
|
1707
|
+
if i > 0 && !prev_is_uppercase {
|
|
1708
|
+
result.push('_');
|
|
1709
|
+
}
|
|
1710
|
+
result.push(c.to_lowercase().next().unwrap_or(c));
|
|
1711
|
+
prev_is_uppercase = true;
|
|
1712
|
+
} else if c.is_alphanumeric() {
|
|
1713
|
+
result.push(c);
|
|
1714
|
+
prev_is_uppercase = false;
|
|
1715
|
+
} else {
|
|
1716
|
+
if !result.is_empty() && !result.ends_with('_') {
|
|
1717
|
+
result.push('_');
|
|
1718
|
+
}
|
|
1719
|
+
prev_is_uppercase = false;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
result.trim_matches('_').to_string()
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/// Convert a string to SCREAMING_SNAKE_CASE.
|
|
1727
|
+
fn to_screaming_snake_case(s: &str) -> String {
|
|
1728
|
+
to_snake_case(s).to_uppercase()
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
pub fn validate_proto_package_namespace(ns: &str) -> Result<(), String> {
|
|
1732
|
+
if ns.is_empty() {
|
|
1733
|
+
return Ok(());
|
|
1734
|
+
}
|
|
1735
|
+
if ns.contains('/')
|
|
1736
|
+
|| ns.contains('\\')
|
|
1737
|
+
|| ns.contains("..")
|
|
1738
|
+
|| ns.starts_with('.')
|
|
1739
|
+
|| ns.starts_with('/')
|
|
1740
|
+
{
|
|
1741
|
+
return Err(format!(
|
|
1742
|
+
"Invalid protobuf namespace '{}': must not contain path separators or traversal",
|
|
1743
|
+
ns
|
|
1744
|
+
));
|
|
1745
|
+
}
|
|
1746
|
+
let mut chars = ns.chars().peekable();
|
|
1747
|
+
while let Some(first) = chars.next() {
|
|
1748
|
+
if !first.is_ascii_alphabetic() && first != '_' {
|
|
1749
|
+
return Err(format!(
|
|
1750
|
+
"Invalid protobuf namespace '{}': must be dotted identifiers (e.g., com.example.api)",
|
|
1751
|
+
ns
|
|
1752
|
+
));
|
|
1753
|
+
}
|
|
1754
|
+
loop {
|
|
1755
|
+
match chars.peek() {
|
|
1756
|
+
Some('.') => {
|
|
1757
|
+
chars.next();
|
|
1758
|
+
break;
|
|
1759
|
+
}
|
|
1760
|
+
Some(c) if c.is_ascii_alphanumeric() || *c == '_' => {
|
|
1761
|
+
chars.next();
|
|
1762
|
+
}
|
|
1763
|
+
Some(_) => {
|
|
1764
|
+
return Err(format!(
|
|
1765
|
+
"Invalid protobuf namespace '{}': must be dotted identifiers (e.g., com.example.api)",
|
|
1766
|
+
ns
|
|
1767
|
+
));
|
|
1768
|
+
}
|
|
1769
|
+
None => break,
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
Ok(())
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
pub fn validate_output_path(output_root: &Path, target: &Path) -> Result<(), String> {
|
|
1777
|
+
let canonical_output = output_root.canonicalize().map_err(|e| {
|
|
1778
|
+
format!(
|
|
1779
|
+
"Cannot resolve output root '{}': {}",
|
|
1780
|
+
output_root.display(),
|
|
1781
|
+
e
|
|
1782
|
+
)
|
|
1783
|
+
})?;
|
|
1784
|
+
|
|
1785
|
+
let rel = target
|
|
1786
|
+
.strip_prefix(output_root)
|
|
1787
|
+
.or_else(|_| target.strip_prefix(&canonical_output))
|
|
1788
|
+
.map_err(|_| {
|
|
1789
|
+
format!(
|
|
1790
|
+
"Security: output path '{}' escapes output directory",
|
|
1791
|
+
target.display()
|
|
1792
|
+
)
|
|
1793
|
+
})?;
|
|
1794
|
+
|
|
1795
|
+
// Lexically reject traversal even when the file does not exist yet.
|
|
1796
|
+
if rel.components().any(|c| {
|
|
1797
|
+
matches!(
|
|
1798
|
+
c,
|
|
1799
|
+
std::path::Component::ParentDir
|
|
1800
|
+
| std::path::Component::RootDir
|
|
1801
|
+
| std::path::Component::Prefix(_)
|
|
1802
|
+
)
|
|
1803
|
+
}) {
|
|
1804
|
+
return Err(format!(
|
|
1805
|
+
"Security: output path '{}' escapes output directory",
|
|
1806
|
+
target.display()
|
|
1807
|
+
));
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
let canonical_target_base = canonical_output.join(rel);
|
|
1811
|
+
let target_for_canonical_checks = if target.is_absolute() {
|
|
1812
|
+
target
|
|
1813
|
+
} else {
|
|
1814
|
+
canonical_target_base.as_path()
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
if let Some(canonical_parent) = canonical_target_base
|
|
1818
|
+
.parent()
|
|
1819
|
+
.filter(|p| p.exists())
|
|
1820
|
+
.and_then(|p| p.canonicalize().ok())
|
|
1821
|
+
{
|
|
1822
|
+
if !canonical_parent.starts_with(&canonical_output) {
|
|
1823
|
+
return Err(format!(
|
|
1824
|
+
"Security: output path '{}' escapes output directory",
|
|
1825
|
+
target.display()
|
|
1826
|
+
));
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
if let Some(canonical_parent) = target_for_canonical_checks
|
|
1831
|
+
.parent()
|
|
1832
|
+
.filter(|p| p.exists())
|
|
1833
|
+
.and_then(|p| p.canonicalize().ok())
|
|
1834
|
+
{
|
|
1835
|
+
if !canonical_parent.starts_with(&canonical_output) {
|
|
1836
|
+
return Err(format!(
|
|
1837
|
+
"Security: output path '{}' escapes output directory",
|
|
1838
|
+
target.display()
|
|
1839
|
+
));
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Best-effort canonicalization for existing paths catches symlink escapes.
|
|
1844
|
+
if let Ok(canonical_target) = target_for_canonical_checks.canonicalize() {
|
|
1845
|
+
if !canonical_target.starts_with(&canonical_output) {
|
|
1846
|
+
return Err(format!(
|
|
1847
|
+
"Security: output path '{}' escapes output directory",
|
|
1848
|
+
target.display()
|
|
1849
|
+
));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
Ok(())
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// ============================================================================
|
|
1857
|
+
// Compatibility Enforcement
|
|
1858
|
+
// ============================================================================
|
|
1859
|
+
|
|
1860
|
+
/// Compatibility mode for schema evolution.
|
|
1861
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
1862
|
+
pub enum CompatibilityMode {
|
|
1863
|
+
/// Only additions allowed - strictest mode for public APIs
|
|
1864
|
+
Additive,
|
|
1865
|
+
/// Removals become reserved fields - default for internal APIs
|
|
1866
|
+
#[default]
|
|
1867
|
+
Backward,
|
|
1868
|
+
/// All changes allowed - for breaking releases
|
|
1869
|
+
Breaking,
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
impl std::fmt::Display for CompatibilityMode {
|
|
1873
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
1874
|
+
match self {
|
|
1875
|
+
CompatibilityMode::Additive => write!(f, "additive"),
|
|
1876
|
+
CompatibilityMode::Backward => write!(f, "backward"),
|
|
1877
|
+
CompatibilityMode::Breaking => write!(f, "breaking"),
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
impl std::str::FromStr for CompatibilityMode {
|
|
1883
|
+
type Err = String;
|
|
1884
|
+
|
|
1885
|
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
1886
|
+
match s.to_lowercase().as_str() {
|
|
1887
|
+
"additive" | "strict" => Ok(CompatibilityMode::Additive),
|
|
1888
|
+
"backward" | "backwards" | "default" => Ok(CompatibilityMode::Backward),
|
|
1889
|
+
"breaking" | "none" => Ok(CompatibilityMode::Breaking),
|
|
1890
|
+
_ => Err(format!("Unknown compatibility mode: {}", s)),
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/// A compatibility violation found during schema comparison.
|
|
1896
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
1897
|
+
pub struct CompatibilityViolation {
|
|
1898
|
+
/// The message name where the violation occurred
|
|
1899
|
+
pub message_name: String,
|
|
1900
|
+
/// The field name involved (if applicable)
|
|
1901
|
+
pub field_name: Option<String>,
|
|
1902
|
+
/// The field number involved (if applicable)
|
|
1903
|
+
pub field_number: Option<u32>,
|
|
1904
|
+
/// Type of violation
|
|
1905
|
+
pub violation_type: ViolationType,
|
|
1906
|
+
/// Human-readable description
|
|
1907
|
+
pub description: String,
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/// Types of compatibility violations.
|
|
1911
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
1912
|
+
pub enum ViolationType {
|
|
1913
|
+
/// A field was removed
|
|
1914
|
+
FieldRemoved,
|
|
1915
|
+
/// A field number was reused with a different name/type
|
|
1916
|
+
FieldNumberReused,
|
|
1917
|
+
/// A field type was changed
|
|
1918
|
+
FieldTypeChanged,
|
|
1919
|
+
/// A field was renamed (same number, different name)
|
|
1920
|
+
FieldRenamed,
|
|
1921
|
+
/// A message was removed
|
|
1922
|
+
MessageRemoved,
|
|
1923
|
+
/// A required field was added (breaking in proto3)
|
|
1924
|
+
RequiredFieldAdded,
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
impl std::fmt::Display for ViolationType {
|
|
1928
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
1929
|
+
match self {
|
|
1930
|
+
ViolationType::FieldRemoved => write!(f, "field_removed"),
|
|
1931
|
+
ViolationType::FieldNumberReused => write!(f, "field_number_reused"),
|
|
1932
|
+
ViolationType::FieldTypeChanged => write!(f, "field_type_changed"),
|
|
1933
|
+
ViolationType::FieldRenamed => write!(f, "field_renamed"),
|
|
1934
|
+
ViolationType::MessageRemoved => write!(f, "message_removed"),
|
|
1935
|
+
ViolationType::RequiredFieldAdded => write!(f, "required_field_added"),
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/// Result of a compatibility check.
|
|
1941
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
1942
|
+
pub struct CompatibilityResult {
|
|
1943
|
+
/// Whether the schemas are compatible under the given mode
|
|
1944
|
+
pub is_compatible: bool,
|
|
1945
|
+
/// The mode used for checking
|
|
1946
|
+
pub mode: CompatibilityMode,
|
|
1947
|
+
/// List of violations found
|
|
1948
|
+
pub violations: Vec<CompatibilityViolation>,
|
|
1949
|
+
/// Suggested fixes (reserved fields to add)
|
|
1950
|
+
pub suggested_reserved_numbers: BTreeMap<String, Vec<u32>>,
|
|
1951
|
+
/// Suggested reserved names
|
|
1952
|
+
pub suggested_reserved_names: BTreeMap<String, Vec<String>>,
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
impl CompatibilityResult {
|
|
1956
|
+
/// Create a compatible (empty) result.
|
|
1957
|
+
pub fn compatible(mode: CompatibilityMode) -> Self {
|
|
1958
|
+
Self {
|
|
1959
|
+
is_compatible: true,
|
|
1960
|
+
mode,
|
|
1961
|
+
violations: Vec::new(),
|
|
1962
|
+
suggested_reserved_numbers: BTreeMap::new(),
|
|
1963
|
+
suggested_reserved_names: BTreeMap::new(),
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/// Check if there are any violations.
|
|
1968
|
+
pub fn has_violations(&self) -> bool {
|
|
1969
|
+
!self.violations.is_empty()
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/// Format violations as a human-readable report.
|
|
1973
|
+
pub fn to_report(&self) -> String {
|
|
1974
|
+
let mut out = String::new();
|
|
1975
|
+
out.push_str(&format!("Compatibility Check (mode: {})\n", self.mode));
|
|
1976
|
+
out.push_str(&format!(
|
|
1977
|
+
"Result: {}\n",
|
|
1978
|
+
if self.is_compatible { "PASS" } else { "FAIL" }
|
|
1979
|
+
));
|
|
1980
|
+
|
|
1981
|
+
if !self.violations.is_empty() {
|
|
1982
|
+
out.push_str(&format!("\nViolations ({}):\n", self.violations.len()));
|
|
1983
|
+
for v in &self.violations {
|
|
1984
|
+
out.push_str(&format!(
|
|
1985
|
+
" - [{}] {}: {}\n",
|
|
1986
|
+
v.violation_type, v.message_name, v.description
|
|
1987
|
+
));
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if !self.suggested_reserved_numbers.is_empty() {
|
|
1992
|
+
out.push_str("\nSuggested Reserved Fields:\n");
|
|
1993
|
+
for (msg, nums) in &self.suggested_reserved_numbers {
|
|
1994
|
+
let nums_str: Vec<String> = nums.iter().map(|n| n.to_string()).collect();
|
|
1995
|
+
out.push_str(&format!(
|
|
1996
|
+
" message {}: reserved {};\n",
|
|
1997
|
+
msg,
|
|
1998
|
+
nums_str.join(", ")
|
|
1999
|
+
));
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
out
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/// Schema compatibility checker.
|
|
2008
|
+
pub struct CompatibilityChecker;
|
|
2009
|
+
|
|
2010
|
+
impl CompatibilityChecker {
|
|
2011
|
+
/// Check compatibility between an old and new ProtoFile.
|
|
2012
|
+
///
|
|
2013
|
+
/// # Arguments
|
|
2014
|
+
/// * `old` - The previous schema version
|
|
2015
|
+
/// * `new` - The new schema version
|
|
2016
|
+
/// * `mode` - The compatibility mode to enforce
|
|
2017
|
+
///
|
|
2018
|
+
/// # Returns
|
|
2019
|
+
/// A CompatibilityResult with violations and suggested fixes.
|
|
2020
|
+
pub fn check(old: &ProtoFile, new: &ProtoFile, mode: CompatibilityMode) -> CompatibilityResult {
|
|
2021
|
+
let mut result = CompatibilityResult::compatible(mode);
|
|
2022
|
+
|
|
2023
|
+
// Build lookup maps for old schema
|
|
2024
|
+
let old_messages: BTreeMap<&str, &ProtoMessage> =
|
|
2025
|
+
old.messages.iter().map(|m| (m.name.as_str(), m)).collect();
|
|
2026
|
+
|
|
2027
|
+
let new_messages: BTreeMap<&str, &ProtoMessage> =
|
|
2028
|
+
new.messages.iter().map(|m| (m.name.as_str(), m)).collect();
|
|
2029
|
+
|
|
2030
|
+
// Check for removed messages
|
|
2031
|
+
for name in old_messages.keys() {
|
|
2032
|
+
if !new_messages.contains_key(name) {
|
|
2033
|
+
result.violations.push(CompatibilityViolation {
|
|
2034
|
+
message_name: name.to_string(),
|
|
2035
|
+
field_name: None,
|
|
2036
|
+
field_number: None,
|
|
2037
|
+
violation_type: ViolationType::MessageRemoved,
|
|
2038
|
+
description: format!("Message '{}' was removed", name),
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// Check each message that exists in both
|
|
2044
|
+
for (name, old_msg) in &old_messages {
|
|
2045
|
+
if let Some(new_msg) = new_messages.get(name) {
|
|
2046
|
+
Self::check_message(old_msg, new_msg, &mut result);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Determine if compatible based on mode
|
|
2051
|
+
result.is_compatible = match mode {
|
|
2052
|
+
CompatibilityMode::Breaking => true, // Always compatible in breaking mode
|
|
2053
|
+
CompatibilityMode::Backward => {
|
|
2054
|
+
// Compatible if no field number reuse or type changes
|
|
2055
|
+
!result.violations.iter().any(|v| {
|
|
2056
|
+
matches!(
|
|
2057
|
+
v.violation_type,
|
|
2058
|
+
ViolationType::FieldNumberReused | ViolationType::FieldTypeChanged
|
|
2059
|
+
)
|
|
2060
|
+
})
|
|
2061
|
+
}
|
|
2062
|
+
CompatibilityMode::Additive => {
|
|
2063
|
+
// Any removal or change is incompatible
|
|
2064
|
+
result.violations.is_empty()
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
result
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/// Check compatibility between two messages.
|
|
2072
|
+
fn check_message(old: &ProtoMessage, new: &ProtoMessage, result: &mut CompatibilityResult) {
|
|
2073
|
+
// Build field maps by number and by name
|
|
2074
|
+
let old_by_number: BTreeMap<u32, &ProtoField> =
|
|
2075
|
+
old.fields.iter().map(|f| (f.number, f)).collect();
|
|
2076
|
+
|
|
2077
|
+
let new_by_number: BTreeMap<u32, &ProtoField> =
|
|
2078
|
+
new.fields.iter().map(|f| (f.number, f)).collect();
|
|
2079
|
+
|
|
2080
|
+
let old_by_name: BTreeMap<&str, &ProtoField> =
|
|
2081
|
+
old.fields.iter().map(|f| (f.name.as_str(), f)).collect();
|
|
2082
|
+
|
|
2083
|
+
// Check for removed fields
|
|
2084
|
+
for (number, old_field) in &old_by_number {
|
|
2085
|
+
if !new_by_number.contains_key(number) {
|
|
2086
|
+
result.violations.push(CompatibilityViolation {
|
|
2087
|
+
message_name: old.name.clone(),
|
|
2088
|
+
field_name: Some(old_field.name.clone()),
|
|
2089
|
+
field_number: Some(*number),
|
|
2090
|
+
violation_type: ViolationType::FieldRemoved,
|
|
2091
|
+
description: format!(
|
|
2092
|
+
"Field '{}' (number {}) was removed",
|
|
2093
|
+
old_field.name, number
|
|
2094
|
+
),
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
// Suggest reserving this field number
|
|
2098
|
+
result
|
|
2099
|
+
.suggested_reserved_numbers
|
|
2100
|
+
.entry(old.name.clone())
|
|
2101
|
+
.or_default()
|
|
2102
|
+
.push(*number);
|
|
2103
|
+
|
|
2104
|
+
result
|
|
2105
|
+
.suggested_reserved_names
|
|
2106
|
+
.entry(old.name.clone())
|
|
2107
|
+
.or_default()
|
|
2108
|
+
.push(old_field.name.clone());
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Check for field number reuse with different name/type
|
|
2113
|
+
for (number, new_field) in &new_by_number {
|
|
2114
|
+
if let Some(old_field) = old_by_number.get(number) {
|
|
2115
|
+
// Check name change
|
|
2116
|
+
if old_field.name != new_field.name {
|
|
2117
|
+
result.violations.push(CompatibilityViolation {
|
|
2118
|
+
message_name: old.name.clone(),
|
|
2119
|
+
field_name: Some(new_field.name.clone()),
|
|
2120
|
+
field_number: Some(*number),
|
|
2121
|
+
violation_type: ViolationType::FieldRenamed,
|
|
2122
|
+
description: format!(
|
|
2123
|
+
"Field number {} renamed from '{}' to '{}'",
|
|
2124
|
+
number, old_field.name, new_field.name
|
|
2125
|
+
),
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// Check type change
|
|
2130
|
+
if old_field.proto_type != new_field.proto_type {
|
|
2131
|
+
result.violations.push(CompatibilityViolation {
|
|
2132
|
+
message_name: old.name.clone(),
|
|
2133
|
+
field_name: Some(new_field.name.clone()),
|
|
2134
|
+
field_number: Some(*number),
|
|
2135
|
+
violation_type: ViolationType::FieldTypeChanged,
|
|
2136
|
+
description: format!(
|
|
2137
|
+
"Field '{}' type changed from {} to {}",
|
|
2138
|
+
new_field.name,
|
|
2139
|
+
old_field.proto_type.to_proto_string(),
|
|
2140
|
+
new_field.proto_type.to_proto_string()
|
|
2141
|
+
),
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
} else {
|
|
2145
|
+
// New field - check if it reuses a previously removed name
|
|
2146
|
+
if old_by_name.contains_key(new_field.name.as_str()) {
|
|
2147
|
+
let old_field = old_by_name[new_field.name.as_str()];
|
|
2148
|
+
if old_field.number != *number {
|
|
2149
|
+
result.violations.push(CompatibilityViolation {
|
|
2150
|
+
message_name: old.name.clone(),
|
|
2151
|
+
field_name: Some(new_field.name.clone()),
|
|
2152
|
+
field_number: Some(*number),
|
|
2153
|
+
violation_type: ViolationType::FieldNumberReused,
|
|
2154
|
+
description: format!(
|
|
2155
|
+
"Field '{}' changed number from {} to {}",
|
|
2156
|
+
new_field.name, old_field.number, number
|
|
2157
|
+
),
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/// Apply compatibility fixes to a new ProtoFile based on an old one.
|
|
2166
|
+
///
|
|
2167
|
+
/// This adds reserved field numbers and names for removed fields.
|
|
2168
|
+
pub fn apply_backward_compatibility(old: &ProtoFile, new: &mut ProtoFile) {
|
|
2169
|
+
let result = Self::check(old, new, CompatibilityMode::Backward);
|
|
2170
|
+
|
|
2171
|
+
// Apply suggested reserved numbers
|
|
2172
|
+
for msg in &mut new.messages {
|
|
2173
|
+
if let Some(reserved_nums) = result.suggested_reserved_numbers.get(&msg.name) {
|
|
2174
|
+
for num in reserved_nums {
|
|
2175
|
+
if !msg.reserved_numbers.contains(num) {
|
|
2176
|
+
msg.reserved_numbers.push(*num);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
msg.reserved_numbers.sort();
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
if let Some(reserved_names) = result.suggested_reserved_names.get(&msg.name) {
|
|
2183
|
+
for name in reserved_names {
|
|
2184
|
+
if !msg.reserved_names.contains(name) {
|
|
2185
|
+
msg.reserved_names.push(name.clone());
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
msg.reserved_names.sort();
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/// File-based schema history storage.
|
|
2195
|
+
pub struct SchemaHistory {
|
|
2196
|
+
/// Directory where schema history is stored
|
|
2197
|
+
history_dir: std::path::PathBuf,
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
impl SchemaHistory {
|
|
2201
|
+
/// Create a new SchemaHistory with the given directory.
|
|
2202
|
+
pub fn new(history_dir: impl Into<std::path::PathBuf>) -> Self {
|
|
2203
|
+
Self {
|
|
2204
|
+
history_dir: history_dir.into(),
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/// Get the path for a schema file.
|
|
2209
|
+
fn schema_path(&self, package: &str) -> std::path::PathBuf {
|
|
2210
|
+
let filename = format!("{}.json", package.replace('.', "_"));
|
|
2211
|
+
self.history_dir.join(filename)
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/// Load the previous schema for a package.
|
|
2215
|
+
pub fn load(&self, package: &str) -> Result<Option<ProtoFile>, String> {
|
|
2216
|
+
let path = self.schema_path(package);
|
|
2217
|
+
if !path.exists() {
|
|
2218
|
+
return Ok(None);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
let content = std::fs::read_to_string(&path)
|
|
2222
|
+
.map_err(|e| format!("Failed to read schema history: {}", e))?;
|
|
2223
|
+
|
|
2224
|
+
let proto: ProtoFile = serde_json::from_str(&content)
|
|
2225
|
+
.map_err(|e| format!("Failed to parse schema history: {}", e))?;
|
|
2226
|
+
|
|
2227
|
+
Ok(Some(proto))
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/// Save a schema to history.
|
|
2231
|
+
pub fn save(&self, proto: &ProtoFile) -> Result<(), String> {
|
|
2232
|
+
// Ensure directory exists
|
|
2233
|
+
std::fs::create_dir_all(&self.history_dir)
|
|
2234
|
+
.map_err(|e| format!("Failed to create history directory: {}", e))?;
|
|
2235
|
+
|
|
2236
|
+
let path = self.schema_path(&proto.package);
|
|
2237
|
+
let content = serde_json::to_string_pretty(proto)
|
|
2238
|
+
.map_err(|e| format!("Failed to serialize schema: {}", e))?;
|
|
2239
|
+
|
|
2240
|
+
std::fs::write(&path, content)
|
|
2241
|
+
.map_err(|e| format!("Failed to write schema history: {}", e))?;
|
|
2242
|
+
|
|
2243
|
+
Ok(())
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
/// Check compatibility and optionally apply fixes.
|
|
2247
|
+
pub fn check_and_update(
|
|
2248
|
+
&self,
|
|
2249
|
+
new: &mut ProtoFile,
|
|
2250
|
+
mode: CompatibilityMode,
|
|
2251
|
+
apply_fixes: bool,
|
|
2252
|
+
) -> Result<CompatibilityResult, String> {
|
|
2253
|
+
let old = self.load(&new.package)?;
|
|
2254
|
+
|
|
2255
|
+
let result = match old {
|
|
2256
|
+
Some(ref old_proto) => {
|
|
2257
|
+
if apply_fixes && mode == CompatibilityMode::Backward {
|
|
2258
|
+
CompatibilityChecker::apply_backward_compatibility(old_proto, new);
|
|
2259
|
+
}
|
|
2260
|
+
CompatibilityChecker::check(old_proto, new, mode)
|
|
2261
|
+
}
|
|
2262
|
+
None => CompatibilityResult::compatible(mode),
|
|
2263
|
+
};
|
|
2264
|
+
|
|
2265
|
+
// Save the new schema if compatible (or in breaking mode)
|
|
2266
|
+
if result.is_compatible || mode == CompatibilityMode::Breaking {
|
|
2267
|
+
self.save(new)?;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
Ok(result)
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// ============================================================================
|
|
2275
|
+
// Tests
|
|
2276
|
+
// ============================================================================
|
|
2277
|
+
|
|
2278
|
+
#[cfg(test)]
|
|
2279
|
+
mod tests {
|
|
2280
|
+
use super::*;
|
|
2281
|
+
|
|
2282
|
+
#[test]
|
|
2283
|
+
fn test_to_pascal_case() {
|
|
2284
|
+
assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
|
|
2285
|
+
assert_eq!(to_pascal_case("my-entity"), "MyEntity");
|
|
2286
|
+
assert_eq!(to_pascal_case("already PascalCase"), "AlreadyPascalCase");
|
|
2287
|
+
assert_eq!(to_pascal_case("UPPERCASE"), "UPPERCASE");
|
|
2288
|
+
assert_eq!(to_pascal_case("PaymentProcessor"), "PaymentProcessor");
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
#[test]
|
|
2292
|
+
fn test_sanitize_proto_ident() {
|
|
2293
|
+
assert_eq!(sanitize_proto_ident("PaymentProcessor"), "PaymentProcessor");
|
|
2294
|
+
assert_eq!(sanitize_proto_ident("123Invalid"), "Sea123Invalid");
|
|
2295
|
+
assert_eq!(sanitize_proto_ident("message"), "SeaMessage");
|
|
2296
|
+
assert_eq!(sanitize_proto_ident("String"), "SeaString");
|
|
2297
|
+
assert_eq!(sanitize_proto_ident("hello_world"), "HelloWorld");
|
|
2298
|
+
assert_eq!(sanitize_proto_ident(""), "SeaUnnamed");
|
|
2299
|
+
assert_eq!(sanitize_proto_ident("rpc"), "SeaRpc");
|
|
2300
|
+
assert_eq!(sanitize_proto_ident("service"), "SeaService");
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
#[cfg(unix)]
|
|
2304
|
+
#[test]
|
|
2305
|
+
fn test_validate_output_path_accepts_symlinked_output_root() {
|
|
2306
|
+
let tmp = tempfile::tempdir().unwrap();
|
|
2307
|
+
let real_output = tmp.path().join("real-output");
|
|
2308
|
+
let linked_output = tmp.path().join("linked-output");
|
|
2309
|
+
std::fs::create_dir_all(&real_output).unwrap();
|
|
2310
|
+
std::os::unix::fs::symlink(&real_output, &linked_output).unwrap();
|
|
2311
|
+
|
|
2312
|
+
let target = linked_output.join("generated.proto");
|
|
2313
|
+
|
|
2314
|
+
assert!(validate_output_path(&linked_output, &target).is_ok());
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
#[test]
|
|
2318
|
+
fn test_validate_output_path_rejects_traversal() {
|
|
2319
|
+
let tmp = tempfile::tempdir().unwrap();
|
|
2320
|
+
let output = tmp.path().join("out");
|
|
2321
|
+
std::fs::create_dir_all(&output).unwrap();
|
|
2322
|
+
let target = output.join("..").join("escape.proto");
|
|
2323
|
+
|
|
2324
|
+
assert!(validate_output_path(&output, &target).is_err());
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
#[test]
|
|
2328
|
+
fn test_to_snake_case() {
|
|
2329
|
+
assert_eq!(to_snake_case("HelloWorld"), "hello_world");
|
|
2330
|
+
assert_eq!(to_snake_case("myEntity"), "my_entity");
|
|
2331
|
+
assert_eq!(to_snake_case("already_snake"), "already_snake");
|
|
2332
|
+
// Consecutive uppercase chars become lowercase without underscore separation
|
|
2333
|
+
assert_eq!(to_snake_case("XMLParser"), "xmlparser");
|
|
2334
|
+
assert_eq!(to_snake_case("PaymentID"), "payment_id");
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
#[test]
|
|
2338
|
+
fn test_to_screaming_snake_case() {
|
|
2339
|
+
assert_eq!(to_screaming_snake_case("MyEnum"), "MY_ENUM");
|
|
2340
|
+
assert_eq!(to_screaming_snake_case("StatusCode"), "STATUS_CODE");
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
#[test]
|
|
2344
|
+
fn test_map_sea_type_to_proto() {
|
|
2345
|
+
assert_eq!(
|
|
2346
|
+
map_sea_type_to_proto("string"),
|
|
2347
|
+
ProtoType::Scalar(ScalarType::String)
|
|
2348
|
+
);
|
|
2349
|
+
assert_eq!(
|
|
2350
|
+
map_sea_type_to_proto("int"),
|
|
2351
|
+
ProtoType::Scalar(ScalarType::Int64)
|
|
2352
|
+
);
|
|
2353
|
+
assert_eq!(
|
|
2354
|
+
map_sea_type_to_proto("boolean"),
|
|
2355
|
+
ProtoType::Scalar(ScalarType::Bool)
|
|
2356
|
+
);
|
|
2357
|
+
assert_eq!(
|
|
2358
|
+
map_sea_type_to_proto("timestamp"),
|
|
2359
|
+
ProtoType::Message("google.protobuf.Timestamp".to_string())
|
|
2360
|
+
);
|
|
2361
|
+
assert_eq!(
|
|
2362
|
+
map_sea_type_to_proto("CustomType"),
|
|
2363
|
+
ProtoType::Message("CustomType".to_string())
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
#[test]
|
|
2368
|
+
fn test_proto_field_to_string() {
|
|
2369
|
+
let field = ProtoField {
|
|
2370
|
+
name: "my_field".to_string(),
|
|
2371
|
+
number: 1,
|
|
2372
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
2373
|
+
repeated: false,
|
|
2374
|
+
optional: false,
|
|
2375
|
+
comments: vec![],
|
|
2376
|
+
};
|
|
2377
|
+
assert_eq!(field.to_proto_string(), "string my_field = 1;");
|
|
2378
|
+
|
|
2379
|
+
let optional_field = ProtoField {
|
|
2380
|
+
name: "optional_field".to_string(),
|
|
2381
|
+
number: 2,
|
|
2382
|
+
proto_type: ProtoType::Scalar(ScalarType::Int64),
|
|
2383
|
+
repeated: false,
|
|
2384
|
+
optional: true,
|
|
2385
|
+
comments: vec![],
|
|
2386
|
+
};
|
|
2387
|
+
assert_eq!(
|
|
2388
|
+
optional_field.to_proto_string(),
|
|
2389
|
+
"optional int64 optional_field = 2;"
|
|
2390
|
+
);
|
|
2391
|
+
|
|
2392
|
+
let repeated_field = ProtoField {
|
|
2393
|
+
name: "items".to_string(),
|
|
2394
|
+
number: 3,
|
|
2395
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
2396
|
+
repeated: true,
|
|
2397
|
+
optional: false,
|
|
2398
|
+
comments: vec![],
|
|
2399
|
+
};
|
|
2400
|
+
assert_eq!(
|
|
2401
|
+
repeated_field.to_proto_string(),
|
|
2402
|
+
"repeated string items = 3;"
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
#[test]
|
|
2407
|
+
fn test_proto_enum_to_string() {
|
|
2408
|
+
let mut e = ProtoEnum::new("Status");
|
|
2409
|
+
e.add_value("STATUS_ACTIVE");
|
|
2410
|
+
e.add_value("STATUS_INACTIVE");
|
|
2411
|
+
|
|
2412
|
+
let output = e.to_proto_string();
|
|
2413
|
+
assert!(output.contains("enum Status {"));
|
|
2414
|
+
assert!(output.contains("STATUS_UNSPECIFIED = 0;"));
|
|
2415
|
+
assert!(output.contains("STATUS_ACTIVE = 1;"));
|
|
2416
|
+
assert!(output.contains("STATUS_INACTIVE = 2;"));
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
#[test]
|
|
2420
|
+
fn test_proto_message_to_string() {
|
|
2421
|
+
let mut msg = ProtoMessage::new("Person");
|
|
2422
|
+
msg.fields.push(ProtoField {
|
|
2423
|
+
name: "name".to_string(),
|
|
2424
|
+
number: 1,
|
|
2425
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
2426
|
+
repeated: false,
|
|
2427
|
+
optional: false,
|
|
2428
|
+
comments: vec![],
|
|
2429
|
+
});
|
|
2430
|
+
msg.fields.push(ProtoField {
|
|
2431
|
+
name: "age".to_string(),
|
|
2432
|
+
number: 2,
|
|
2433
|
+
proto_type: ProtoType::Scalar(ScalarType::Int32),
|
|
2434
|
+
repeated: false,
|
|
2435
|
+
optional: true,
|
|
2436
|
+
comments: vec![],
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
let output = msg.to_proto_string();
|
|
2440
|
+
assert!(output.contains("message Person {"));
|
|
2441
|
+
assert!(output.contains("string name = 1;"));
|
|
2442
|
+
assert!(output.contains("optional int32 age = 2;"));
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
#[test]
|
|
2446
|
+
fn test_proto_file_to_string() {
|
|
2447
|
+
let mut proto = ProtoFile::new("test.package");
|
|
2448
|
+
proto.metadata.projection_name = "TestProjection".to_string();
|
|
2449
|
+
proto.metadata.source_namespace = "test".to_string();
|
|
2450
|
+
|
|
2451
|
+
let mut msg = ProtoMessage::new("TestMessage");
|
|
2452
|
+
msg.fields.push(ProtoField {
|
|
2453
|
+
name: "id".to_string(),
|
|
2454
|
+
number: 1,
|
|
2455
|
+
proto_type: ProtoType::Scalar(ScalarType::String),
|
|
2456
|
+
repeated: false,
|
|
2457
|
+
optional: false,
|
|
2458
|
+
comments: vec![],
|
|
2459
|
+
});
|
|
2460
|
+
proto.messages.push(msg);
|
|
2461
|
+
|
|
2462
|
+
let output = proto.to_proto_string();
|
|
2463
|
+
assert!(output.contains("syntax = \"proto3\";"));
|
|
2464
|
+
assert!(output.contains("package test.package;"));
|
|
2465
|
+
assert!(output.contains("message TestMessage {"));
|
|
2466
|
+
assert!(output.contains("string id = 1;"));
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
#[test]
|
|
2470
|
+
fn test_entity_to_message() {
|
|
2471
|
+
use serde_json::json;
|
|
2472
|
+
|
|
2473
|
+
let mut entity = Entity::new_with_namespace("Warehouse", "logistics");
|
|
2474
|
+
entity.set_attribute("capacity", json!(5000));
|
|
2475
|
+
entity.set_attribute("location", json!("Building A"));
|
|
2476
|
+
|
|
2477
|
+
let msg = ProtobufEngine::entity_to_message(&entity);
|
|
2478
|
+
|
|
2479
|
+
assert_eq!(msg.name, "Warehouse");
|
|
2480
|
+
assert!(msg.fields.iter().any(|f| f.name == "id"));
|
|
2481
|
+
assert!(msg.fields.iter().any(|f| f.name == "name"));
|
|
2482
|
+
assert!(msg.fields.iter().any(|f| f.name == "capacity"));
|
|
2483
|
+
assert!(msg.fields.iter().any(|f| f.name == "location"));
|
|
2484
|
+
|
|
2485
|
+
// Check field numbers are sequential
|
|
2486
|
+
let numbers: Vec<u32> = msg.fields.iter().map(|f| f.number).collect();
|
|
2487
|
+
assert_eq!(numbers, vec![1, 2, 3, 4]); // id, name, capacity, location (sorted)
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
#[test]
|
|
2491
|
+
fn test_governance_messages() {
|
|
2492
|
+
let messages = ProtobufEngine::generate_governance_messages();
|
|
2493
|
+
assert_eq!(messages.len(), 2);
|
|
2494
|
+
|
|
2495
|
+
let violation = messages.iter().find(|m| m.name == "PolicyViolation");
|
|
2496
|
+
assert!(violation.is_some());
|
|
2497
|
+
|
|
2498
|
+
let metric = messages.iter().find(|m| m.name == "MetricEvent");
|
|
2499
|
+
assert!(metric.is_some());
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// ========================================================================
|
|
2503
|
+
// Well-Known Type Tests
|
|
2504
|
+
// ========================================================================
|
|
2505
|
+
|
|
2506
|
+
#[test]
|
|
2507
|
+
fn test_wkt_type_name() {
|
|
2508
|
+
assert_eq!(
|
|
2509
|
+
WellKnownType::Timestamp.type_name(),
|
|
2510
|
+
"google.protobuf.Timestamp"
|
|
2511
|
+
);
|
|
2512
|
+
assert_eq!(
|
|
2513
|
+
WellKnownType::Duration.type_name(),
|
|
2514
|
+
"google.protobuf.Duration"
|
|
2515
|
+
);
|
|
2516
|
+
assert_eq!(WellKnownType::Any.type_name(), "google.protobuf.Any");
|
|
2517
|
+
assert_eq!(WellKnownType::Struct.type_name(), "google.protobuf.Struct");
|
|
2518
|
+
assert_eq!(WellKnownType::Empty.type_name(), "google.protobuf.Empty");
|
|
2519
|
+
assert_eq!(
|
|
2520
|
+
WellKnownType::Int64Value.type_name(),
|
|
2521
|
+
"google.protobuf.Int64Value"
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
#[test]
|
|
2526
|
+
fn test_wkt_import_path() {
|
|
2527
|
+
assert_eq!(
|
|
2528
|
+
WellKnownType::Timestamp.import_path(),
|
|
2529
|
+
"google/protobuf/timestamp.proto"
|
|
2530
|
+
);
|
|
2531
|
+
assert_eq!(
|
|
2532
|
+
WellKnownType::Duration.import_path(),
|
|
2533
|
+
"google/protobuf/duration.proto"
|
|
2534
|
+
);
|
|
2535
|
+
assert_eq!(
|
|
2536
|
+
WellKnownType::Any.import_path(),
|
|
2537
|
+
"google/protobuf/any.proto"
|
|
2538
|
+
);
|
|
2539
|
+
assert_eq!(
|
|
2540
|
+
WellKnownType::Struct.import_path(),
|
|
2541
|
+
"google/protobuf/struct.proto"
|
|
2542
|
+
);
|
|
2543
|
+
assert_eq!(
|
|
2544
|
+
WellKnownType::Value.import_path(),
|
|
2545
|
+
"google/protobuf/struct.proto"
|
|
2546
|
+
);
|
|
2547
|
+
assert_eq!(
|
|
2548
|
+
WellKnownType::Empty.import_path(),
|
|
2549
|
+
"google/protobuf/empty.proto"
|
|
2550
|
+
);
|
|
2551
|
+
assert_eq!(
|
|
2552
|
+
WellKnownType::Int64Value.import_path(),
|
|
2553
|
+
"google/protobuf/wrappers.proto"
|
|
2554
|
+
);
|
|
2555
|
+
assert_eq!(
|
|
2556
|
+
WellKnownType::StringValue.import_path(),
|
|
2557
|
+
"google/protobuf/wrappers.proto"
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
#[test]
|
|
2562
|
+
fn test_wkt_from_type_name() {
|
|
2563
|
+
assert_eq!(
|
|
2564
|
+
WellKnownType::from_type_name("google.protobuf.Timestamp"),
|
|
2565
|
+
Some(WellKnownType::Timestamp)
|
|
2566
|
+
);
|
|
2567
|
+
assert_eq!(
|
|
2568
|
+
WellKnownType::from_type_name("google.protobuf.Duration"),
|
|
2569
|
+
Some(WellKnownType::Duration)
|
|
2570
|
+
);
|
|
2571
|
+
assert_eq!(
|
|
2572
|
+
WellKnownType::from_type_name("google.protobuf.Any"),
|
|
2573
|
+
Some(WellKnownType::Any)
|
|
2574
|
+
);
|
|
2575
|
+
assert_eq!(
|
|
2576
|
+
WellKnownType::from_type_name("google.protobuf.StringValue"),
|
|
2577
|
+
Some(WellKnownType::StringValue)
|
|
2578
|
+
);
|
|
2579
|
+
assert_eq!(WellKnownType::from_type_name("SomeOtherType"), None);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
#[test]
|
|
2583
|
+
fn test_map_sea_type_to_wkt() {
|
|
2584
|
+
// Timestamp types
|
|
2585
|
+
assert_eq!(
|
|
2586
|
+
map_sea_type_to_proto("timestamp"),
|
|
2587
|
+
ProtoType::Message("google.protobuf.Timestamp".to_string())
|
|
2588
|
+
);
|
|
2589
|
+
assert_eq!(
|
|
2590
|
+
map_sea_type_to_proto("datetime"),
|
|
2591
|
+
ProtoType::Message("google.protobuf.Timestamp".to_string())
|
|
2592
|
+
);
|
|
2593
|
+
|
|
2594
|
+
// Duration types
|
|
2595
|
+
assert_eq!(
|
|
2596
|
+
map_sea_type_to_proto("duration"),
|
|
2597
|
+
ProtoType::Message("google.protobuf.Duration".to_string())
|
|
2598
|
+
);
|
|
2599
|
+
assert_eq!(
|
|
2600
|
+
map_sea_type_to_proto("timespan"),
|
|
2601
|
+
ProtoType::Message("google.protobuf.Duration".to_string())
|
|
2602
|
+
);
|
|
2603
|
+
|
|
2604
|
+
// Dynamic types
|
|
2605
|
+
assert_eq!(
|
|
2606
|
+
map_sea_type_to_proto("any"),
|
|
2607
|
+
ProtoType::Message("google.protobuf.Any".to_string())
|
|
2608
|
+
);
|
|
2609
|
+
assert_eq!(
|
|
2610
|
+
map_sea_type_to_proto("json"),
|
|
2611
|
+
ProtoType::Message("google.protobuf.Struct".to_string())
|
|
2612
|
+
);
|
|
2613
|
+
|
|
2614
|
+
// Empty type
|
|
2615
|
+
assert_eq!(
|
|
2616
|
+
map_sea_type_to_proto("void"),
|
|
2617
|
+
ProtoType::Message("google.protobuf.Empty".to_string())
|
|
2618
|
+
);
|
|
2619
|
+
|
|
2620
|
+
// Nullable/optional types
|
|
2621
|
+
assert_eq!(
|
|
2622
|
+
map_sea_type_to_proto("optional_string"),
|
|
2623
|
+
ProtoType::Message("google.protobuf.StringValue".to_string())
|
|
2624
|
+
);
|
|
2625
|
+
assert_eq!(
|
|
2626
|
+
map_sea_type_to_proto("nullable_int"),
|
|
2627
|
+
ProtoType::Message("google.protobuf.Int64Value".to_string())
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
#[test]
|
|
2632
|
+
fn test_add_wkt_imports() {
|
|
2633
|
+
let mut proto = ProtoFile::new("test");
|
|
2634
|
+
|
|
2635
|
+
// Add a message with a Timestamp field
|
|
2636
|
+
let mut msg = ProtoMessage::new("Event");
|
|
2637
|
+
msg.fields.push(ProtoField {
|
|
2638
|
+
name: "created_at".to_string(),
|
|
2639
|
+
number: 1,
|
|
2640
|
+
proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
|
|
2641
|
+
repeated: false,
|
|
2642
|
+
optional: false,
|
|
2643
|
+
comments: vec![],
|
|
2644
|
+
});
|
|
2645
|
+
msg.fields.push(ProtoField {
|
|
2646
|
+
name: "duration".to_string(),
|
|
2647
|
+
number: 2,
|
|
2648
|
+
proto_type: ProtoType::Message("google.protobuf.Duration".to_string()),
|
|
2649
|
+
repeated: false,
|
|
2650
|
+
optional: false,
|
|
2651
|
+
comments: vec![],
|
|
2652
|
+
});
|
|
2653
|
+
proto.messages.push(msg);
|
|
2654
|
+
|
|
2655
|
+
proto.add_wkt_imports();
|
|
2656
|
+
|
|
2657
|
+
assert!(proto
|
|
2658
|
+
.imports
|
|
2659
|
+
.contains(&"google/protobuf/timestamp.proto".to_string()));
|
|
2660
|
+
assert!(proto
|
|
2661
|
+
.imports
|
|
2662
|
+
.contains(&"google/protobuf/duration.proto".to_string()));
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
#[test]
|
|
2666
|
+
fn test_resolve_imports_in_message() {
|
|
2667
|
+
// Setup index with a target message
|
|
2668
|
+
let mut index = HashMap::new();
|
|
2669
|
+
index.insert(
|
|
2670
|
+
"TargetType".to_string(),
|
|
2671
|
+
("other.ns".to_string(), "base.other.ns".to_string()),
|
|
2672
|
+
);
|
|
2673
|
+
|
|
2674
|
+
let mut msg = ProtoMessage::new("SourceMessage");
|
|
2675
|
+
msg.fields.push(ProtoField {
|
|
2676
|
+
name: "field1".to_string(),
|
|
2677
|
+
number: 1,
|
|
2678
|
+
proto_type: ProtoType::Message("TargetType".to_string()),
|
|
2679
|
+
repeated: false,
|
|
2680
|
+
optional: false,
|
|
2681
|
+
comments: vec![],
|
|
2682
|
+
});
|
|
2683
|
+
|
|
2684
|
+
let mut imports = HashSet::new();
|
|
2685
|
+
|
|
2686
|
+
// Resolve imports
|
|
2687
|
+
ProtobufEngine::resolve_imports_in_message(&mut msg, "current.ns", &index, &mut imports);
|
|
2688
|
+
|
|
2689
|
+
// Should find import for other.ns
|
|
2690
|
+
assert!(imports.contains("other.ns"));
|
|
2691
|
+
|
|
2692
|
+
// Should update type name to fully qualified
|
|
2693
|
+
let field_type = if let ProtoType::Message(ref name) = msg.fields[0].proto_type {
|
|
2694
|
+
name.clone()
|
|
2695
|
+
} else {
|
|
2696
|
+
panic!("Wrong type");
|
|
2697
|
+
};
|
|
2698
|
+
assert_eq!(field_type, "base.other.ns.TargetType");
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
#[test]
|
|
2702
|
+
fn test_resolve_imports_in_nested_message() {
|
|
2703
|
+
let mut index = HashMap::new();
|
|
2704
|
+
index.insert(
|
|
2705
|
+
"NestedTarget".to_string(),
|
|
2706
|
+
("other.ns".to_string(), "base.other.ns".to_string()),
|
|
2707
|
+
);
|
|
2708
|
+
|
|
2709
|
+
let mut msg = ProtoMessage::new("Outer");
|
|
2710
|
+
let mut inner = ProtoMessage::new("Inner");
|
|
2711
|
+
inner.fields.push(ProtoField {
|
|
2712
|
+
name: "field".to_string(),
|
|
2713
|
+
number: 1,
|
|
2714
|
+
proto_type: ProtoType::Message("NestedTarget".to_string()),
|
|
2715
|
+
repeated: false,
|
|
2716
|
+
optional: false,
|
|
2717
|
+
comments: vec![],
|
|
2718
|
+
});
|
|
2719
|
+
msg.nested_messages.push(inner);
|
|
2720
|
+
|
|
2721
|
+
let mut imports = HashSet::new();
|
|
2722
|
+
ProtobufEngine::resolve_imports_in_message(&mut msg, "current.ns", &index, &mut imports);
|
|
2723
|
+
|
|
2724
|
+
assert!(imports.contains("other.ns"));
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
#[test]
|
|
2728
|
+
fn test_wkt_imports_in_proto_string() {
|
|
2729
|
+
let mut proto = ProtoFile::new("test.wkt");
|
|
2730
|
+
|
|
2731
|
+
let mut msg = ProtoMessage::new("AuditLog");
|
|
2732
|
+
msg.fields.push(ProtoField {
|
|
2733
|
+
name: "timestamp".to_string(),
|
|
2734
|
+
number: 1,
|
|
2735
|
+
proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
|
|
2736
|
+
repeated: false,
|
|
2737
|
+
optional: false,
|
|
2738
|
+
comments: vec![],
|
|
2739
|
+
});
|
|
2740
|
+
proto.messages.push(msg);
|
|
2741
|
+
proto.add_wkt_imports();
|
|
2742
|
+
|
|
2743
|
+
let output = proto.to_proto_string();
|
|
2744
|
+
assert!(output.contains("import \"google/protobuf/timestamp.proto\";"));
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
#[test]
|
|
2748
|
+
fn test_wkt_no_duplicate_imports() {
|
|
2749
|
+
let mut proto = ProtoFile::new("test");
|
|
2750
|
+
|
|
2751
|
+
// Add multiple messages using the same WKT
|
|
2752
|
+
let mut msg1 = ProtoMessage::new("Event1");
|
|
2753
|
+
msg1.fields.push(ProtoField {
|
|
2754
|
+
name: "time1".to_string(),
|
|
2755
|
+
number: 1,
|
|
2756
|
+
proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
|
|
2757
|
+
repeated: false,
|
|
2758
|
+
optional: false,
|
|
2759
|
+
comments: vec![],
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2762
|
+
let mut msg2 = ProtoMessage::new("Event2");
|
|
2763
|
+
msg2.fields.push(ProtoField {
|
|
2764
|
+
name: "time2".to_string(),
|
|
2765
|
+
number: 1,
|
|
2766
|
+
proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
|
|
2767
|
+
repeated: false,
|
|
2768
|
+
optional: false,
|
|
2769
|
+
comments: vec![],
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
proto.messages.push(msg1);
|
|
2773
|
+
proto.messages.push(msg2);
|
|
2774
|
+
proto.add_wkt_imports();
|
|
2775
|
+
|
|
2776
|
+
// Should only have one timestamp import
|
|
2777
|
+
let timestamp_count = proto
|
|
2778
|
+
.imports
|
|
2779
|
+
.iter()
|
|
2780
|
+
.filter(|i| i.contains("timestamp"))
|
|
2781
|
+
.count();
|
|
2782
|
+
assert_eq!(timestamp_count, 1);
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// ========================================================================
|
|
2786
|
+
// Custom Options Tests
|
|
2787
|
+
// ========================================================================
|
|
2788
|
+
|
|
2789
|
+
#[test]
|
|
2790
|
+
fn test_proto_option_value_string() {
|
|
2791
|
+
let val = ProtoOptionValue::String("com.example.api".to_string());
|
|
2792
|
+
assert_eq!(val.to_proto_string(), "\"com.example.api\"");
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
#[test]
|
|
2796
|
+
fn test_proto_option_value_string_escaping() {
|
|
2797
|
+
let val = ProtoOptionValue::String("path\\to\\file".to_string());
|
|
2798
|
+
assert_eq!(val.to_proto_string(), "\"path\\\\to\\\\file\"");
|
|
2799
|
+
|
|
2800
|
+
let val2 = ProtoOptionValue::String("say \"hello\"".to_string());
|
|
2801
|
+
assert_eq!(val2.to_proto_string(), "\"say \\\"hello\\\"\"");
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
#[test]
|
|
2805
|
+
fn test_proto_option_value_int() {
|
|
2806
|
+
let val = ProtoOptionValue::Int(42);
|
|
2807
|
+
assert_eq!(val.to_proto_string(), "42");
|
|
2808
|
+
|
|
2809
|
+
let neg = ProtoOptionValue::Int(-100);
|
|
2810
|
+
assert_eq!(neg.to_proto_string(), "-100");
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
#[test]
|
|
2814
|
+
fn test_proto_option_value_float() {
|
|
2815
|
+
let val = ProtoOptionValue::Float(3.15);
|
|
2816
|
+
assert_eq!(val.to_proto_string(), "3.15");
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
#[test]
|
|
2820
|
+
fn test_proto_option_value_bool() {
|
|
2821
|
+
assert_eq!(ProtoOptionValue::Bool(true).to_proto_string(), "true");
|
|
2822
|
+
assert_eq!(ProtoOptionValue::Bool(false).to_proto_string(), "false");
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
#[test]
|
|
2826
|
+
fn test_proto_option_value_identifier() {
|
|
2827
|
+
let val = ProtoOptionValue::Identifier("SPEED".to_string());
|
|
2828
|
+
assert_eq!(val.to_proto_string(), "SPEED");
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
#[test]
|
|
2832
|
+
fn test_proto_custom_option_to_string() {
|
|
2833
|
+
let opt = ProtoCustomOption::new(
|
|
2834
|
+
"java_package",
|
|
2835
|
+
ProtoOptionValue::String("com.example".to_string()),
|
|
2836
|
+
);
|
|
2837
|
+
assert_eq!(
|
|
2838
|
+
opt.to_proto_string(),
|
|
2839
|
+
"option java_package = \"com.example\";"
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
#[test]
|
|
2844
|
+
fn test_proto_custom_option_extension() {
|
|
2845
|
+
// Extension option with parentheses
|
|
2846
|
+
let opt = ProtoCustomOption::new("(mycompany.api_version)", ProtoOptionValue::Int(2));
|
|
2847
|
+
assert_eq!(opt.to_proto_string(), "option (mycompany.api_version) = 2;");
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
#[test]
|
|
2851
|
+
fn test_proto_options_set_standard_options() {
|
|
2852
|
+
let mut opts = ProtoOptions::default();
|
|
2853
|
+
|
|
2854
|
+
opts.set_option(
|
|
2855
|
+
"java_package",
|
|
2856
|
+
ProtoOptionValue::String("com.example".to_string()),
|
|
2857
|
+
);
|
|
2858
|
+
opts.set_option("java_multiple_files", ProtoOptionValue::Bool(true));
|
|
2859
|
+
opts.set_option(
|
|
2860
|
+
"go_package",
|
|
2861
|
+
ProtoOptionValue::String("github.com/example".to_string()),
|
|
2862
|
+
);
|
|
2863
|
+
opts.set_option(
|
|
2864
|
+
"csharp_namespace",
|
|
2865
|
+
ProtoOptionValue::String("Example.Api".to_string()),
|
|
2866
|
+
);
|
|
2867
|
+
opts.set_option("deprecated", ProtoOptionValue::Bool(true));
|
|
2868
|
+
|
|
2869
|
+
assert_eq!(opts.java_package, Some("com.example".to_string()));
|
|
2870
|
+
assert!(opts.java_multiple_files);
|
|
2871
|
+
assert_eq!(opts.go_package, Some("github.com/example".to_string()));
|
|
2872
|
+
assert_eq!(opts.csharp_namespace, Some("Example.Api".to_string()));
|
|
2873
|
+
assert!(opts.deprecated);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
#[test]
|
|
2877
|
+
fn test_proto_options_set_custom_option() {
|
|
2878
|
+
let mut opts = ProtoOptions::default();
|
|
2879
|
+
|
|
2880
|
+
opts.set_option(
|
|
2881
|
+
"my_custom_option",
|
|
2882
|
+
ProtoOptionValue::String("custom_value".to_string()),
|
|
2883
|
+
);
|
|
2884
|
+
|
|
2885
|
+
assert_eq!(opts.custom_options.len(), 1);
|
|
2886
|
+
assert_eq!(opts.custom_options[0].name, "my_custom_option");
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
#[test]
|
|
2890
|
+
fn test_proto_file_with_all_options() {
|
|
2891
|
+
let mut proto = ProtoFile::new("test.api");
|
|
2892
|
+
proto.options.java_package = Some("com.example.api".to_string());
|
|
2893
|
+
proto.options.java_multiple_files = true;
|
|
2894
|
+
proto.options.go_package = Some("github.com/example/api".to_string());
|
|
2895
|
+
proto.options.csharp_namespace = Some("Example.Api".to_string());
|
|
2896
|
+
proto.options.optimize_for = Some("SPEED".to_string());
|
|
2897
|
+
proto.options.custom_options.push(ProtoCustomOption::new(
|
|
2898
|
+
"(api.version)",
|
|
2899
|
+
ProtoOptionValue::Int(1),
|
|
2900
|
+
));
|
|
2901
|
+
|
|
2902
|
+
let output = proto.to_proto_string();
|
|
2903
|
+
assert!(output.contains("option java_package = \"com.example.api\";"));
|
|
2904
|
+
assert!(output.contains("option java_multiple_files = true;"));
|
|
2905
|
+
assert!(output.contains("option go_package = \"github.com/example/api\";"));
|
|
2906
|
+
assert!(output.contains("option csharp_namespace = \"Example.Api\";"));
|
|
2907
|
+
assert!(output.contains("option optimize_for = SPEED;"));
|
|
2908
|
+
assert!(output.contains("option (api.version) = 1;"));
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
#[test]
|
|
2912
|
+
fn test_proto_option_value_from_json() {
|
|
2913
|
+
use serde_json::json;
|
|
2914
|
+
|
|
2915
|
+
assert_eq!(
|
|
2916
|
+
ProtoOptionValue::from_json(&json!("hello")),
|
|
2917
|
+
ProtoOptionValue::String("hello".to_string())
|
|
2918
|
+
);
|
|
2919
|
+
assert_eq!(
|
|
2920
|
+
ProtoOptionValue::from_json(&json!(true)),
|
|
2921
|
+
ProtoOptionValue::Bool(true)
|
|
2922
|
+
);
|
|
2923
|
+
assert_eq!(
|
|
2924
|
+
ProtoOptionValue::from_json(&json!(42)),
|
|
2925
|
+
ProtoOptionValue::Int(42)
|
|
2926
|
+
);
|
|
2927
|
+
assert_eq!(
|
|
2928
|
+
ProtoOptionValue::from_json(&json!(3.15)),
|
|
2929
|
+
ProtoOptionValue::Float(3.15)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// ========================================================================
|
|
2934
|
+
// gRPC Service Tests
|
|
2935
|
+
// ========================================================================
|
|
2936
|
+
|
|
2937
|
+
#[test]
|
|
2938
|
+
fn test_proto_service_to_string() {
|
|
2939
|
+
let mut service = ProtoService::new("PaymentService");
|
|
2940
|
+
service
|
|
2941
|
+
.comments
|
|
2942
|
+
.push("Payment processing service".to_string());
|
|
2943
|
+
|
|
2944
|
+
service.methods.push(ProtoRpcMethod::new(
|
|
2945
|
+
"ProcessPayment",
|
|
2946
|
+
"PaymentRequest",
|
|
2947
|
+
"PaymentResponse",
|
|
2948
|
+
));
|
|
2949
|
+
|
|
2950
|
+
let output = service.to_proto_string();
|
|
2951
|
+
assert!(output.contains("service PaymentService {"));
|
|
2952
|
+
assert!(output.contains("rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);"));
|
|
2953
|
+
assert!(output.contains("// Payment processing service"));
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
#[test]
|
|
2957
|
+
fn test_proto_rpc_method_unary() {
|
|
2958
|
+
let method = ProtoRpcMethod::new("GetUser", "GetUserRequest", "User");
|
|
2959
|
+
let output = method.to_proto_string();
|
|
2960
|
+
assert_eq!(output, "rpc GetUser(GetUserRequest) returns (User);");
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
#[test]
|
|
2964
|
+
fn test_proto_rpc_method_server_streaming() {
|
|
2965
|
+
let mut method = ProtoRpcMethod::new("ListEvents", "ListEventsRequest", "Event");
|
|
2966
|
+
method.streaming = StreamingMode::ServerStreaming;
|
|
2967
|
+
let output = method.to_proto_string();
|
|
2968
|
+
assert_eq!(
|
|
2969
|
+
output,
|
|
2970
|
+
"rpc ListEvents(ListEventsRequest) returns (stream Event);"
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
#[test]
|
|
2975
|
+
fn test_proto_rpc_method_client_streaming() {
|
|
2976
|
+
let mut method = ProtoRpcMethod::new("UploadChunks", "DataChunk", "UploadResult");
|
|
2977
|
+
method.streaming = StreamingMode::ClientStreaming;
|
|
2978
|
+
let output = method.to_proto_string();
|
|
2979
|
+
assert_eq!(
|
|
2980
|
+
output,
|
|
2981
|
+
"rpc UploadChunks(stream DataChunk) returns (UploadResult);"
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
#[test]
|
|
2986
|
+
fn test_proto_rpc_method_bidirectional() {
|
|
2987
|
+
let mut method = ProtoRpcMethod::new("Chat", "ChatMessage", "ChatMessage");
|
|
2988
|
+
method.streaming = StreamingMode::Bidirectional;
|
|
2989
|
+
let output = method.to_proto_string();
|
|
2990
|
+
assert_eq!(
|
|
2991
|
+
output,
|
|
2992
|
+
"rpc Chat(stream ChatMessage) returns (stream ChatMessage);"
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
#[test]
|
|
2997
|
+
fn test_streaming_mode_from_str() {
|
|
2998
|
+
assert_eq!(
|
|
2999
|
+
StreamingMode::parse("streaming"),
|
|
3000
|
+
StreamingMode::ServerStreaming
|
|
3001
|
+
);
|
|
3002
|
+
assert_eq!(
|
|
3003
|
+
StreamingMode::parse("server_streaming"),
|
|
3004
|
+
StreamingMode::ServerStreaming
|
|
3005
|
+
);
|
|
3006
|
+
assert_eq!(
|
|
3007
|
+
StreamingMode::parse("client_streaming"),
|
|
3008
|
+
StreamingMode::ClientStreaming
|
|
3009
|
+
);
|
|
3010
|
+
assert_eq!(
|
|
3011
|
+
StreamingMode::parse("bidirectional"),
|
|
3012
|
+
StreamingMode::Bidirectional
|
|
3013
|
+
);
|
|
3014
|
+
assert_eq!(StreamingMode::parse("bidi"), StreamingMode::Bidirectional);
|
|
3015
|
+
assert_eq!(StreamingMode::parse("duplex"), StreamingMode::Bidirectional);
|
|
3016
|
+
assert_eq!(StreamingMode::parse("unary"), StreamingMode::Unary);
|
|
3017
|
+
assert_eq!(StreamingMode::parse(""), StreamingMode::Unary);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
#[test]
|
|
3021
|
+
fn test_streaming_mode_display() {
|
|
3022
|
+
assert_eq!(format!("{}", StreamingMode::Unary), "unary");
|
|
3023
|
+
assert_eq!(
|
|
3024
|
+
format!("{}", StreamingMode::ServerStreaming),
|
|
3025
|
+
"server_streaming"
|
|
3026
|
+
);
|
|
3027
|
+
assert_eq!(
|
|
3028
|
+
format!("{}", StreamingMode::ClientStreaming),
|
|
3029
|
+
"client_streaming"
|
|
3030
|
+
);
|
|
3031
|
+
assert_eq!(format!("{}", StreamingMode::Bidirectional), "bidirectional");
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
#[test]
|
|
3035
|
+
fn test_proto_file_with_services() {
|
|
3036
|
+
let mut proto = ProtoFile::new("test.api");
|
|
3037
|
+
|
|
3038
|
+
let mut service = ProtoService::new("GreeterService");
|
|
3039
|
+
service.methods.push(ProtoRpcMethod::new(
|
|
3040
|
+
"SayHello",
|
|
3041
|
+
"HelloRequest",
|
|
3042
|
+
"HelloResponse",
|
|
3043
|
+
));
|
|
3044
|
+
proto.services.push(service);
|
|
3045
|
+
|
|
3046
|
+
let output = proto.to_proto_string();
|
|
3047
|
+
assert!(output.contains("service GreeterService {"));
|
|
3048
|
+
assert!(output.contains("rpc SayHello(HelloRequest) returns (HelloResponse);"));
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// ========================================================================
|
|
3052
|
+
// Compatibility Tests
|
|
3053
|
+
// ========================================================================
|
|
3054
|
+
|
|
3055
|
+
fn make_test_proto(messages: Vec<ProtoMessage>) -> ProtoFile {
|
|
3056
|
+
ProtoFile {
|
|
3057
|
+
package: "test.package".to_string(),
|
|
3058
|
+
syntax: "proto3".to_string(),
|
|
3059
|
+
imports: vec![],
|
|
3060
|
+
options: ProtoOptions::default(),
|
|
3061
|
+
enums: vec![],
|
|
3062
|
+
messages,
|
|
3063
|
+
services: vec![],
|
|
3064
|
+
metadata: ProtoMetadata::default(),
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
fn make_test_message(name: &str, fields: Vec<ProtoField>) -> ProtoMessage {
|
|
3069
|
+
ProtoMessage {
|
|
3070
|
+
name: name.to_string(),
|
|
3071
|
+
fields,
|
|
3072
|
+
nested_messages: vec![],
|
|
3073
|
+
nested_enums: vec![],
|
|
3074
|
+
reserved_numbers: vec![],
|
|
3075
|
+
reserved_names: vec![],
|
|
3076
|
+
comments: vec![],
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
fn make_test_field(name: &str, number: u32, proto_type: ProtoType) -> ProtoField {
|
|
3081
|
+
ProtoField {
|
|
3082
|
+
name: name.to_string(),
|
|
3083
|
+
number,
|
|
3084
|
+
proto_type,
|
|
3085
|
+
repeated: false,
|
|
3086
|
+
optional: false,
|
|
3087
|
+
comments: vec![],
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
#[test]
|
|
3092
|
+
fn test_compatibility_no_changes() {
|
|
3093
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3094
|
+
"Person",
|
|
3095
|
+
vec![
|
|
3096
|
+
make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
|
|
3097
|
+
make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
|
|
3098
|
+
],
|
|
3099
|
+
)]);
|
|
3100
|
+
|
|
3101
|
+
let new = old.clone();
|
|
3102
|
+
|
|
3103
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
|
|
3104
|
+
assert!(result.is_compatible);
|
|
3105
|
+
assert!(result.violations.is_empty());
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
#[test]
|
|
3109
|
+
fn test_compatibility_field_added() {
|
|
3110
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3111
|
+
"Person",
|
|
3112
|
+
vec![make_test_field(
|
|
3113
|
+
"id",
|
|
3114
|
+
1,
|
|
3115
|
+
ProtoType::Scalar(ScalarType::String),
|
|
3116
|
+
)],
|
|
3117
|
+
)]);
|
|
3118
|
+
|
|
3119
|
+
let new = make_test_proto(vec![make_test_message(
|
|
3120
|
+
"Person",
|
|
3121
|
+
vec![
|
|
3122
|
+
make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
|
|
3123
|
+
make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
|
|
3124
|
+
],
|
|
3125
|
+
)]);
|
|
3126
|
+
|
|
3127
|
+
// Adding fields is compatible in all modes
|
|
3128
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
|
|
3129
|
+
assert!(result.is_compatible);
|
|
3130
|
+
assert!(result.violations.is_empty());
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
#[test]
|
|
3134
|
+
fn test_compatibility_field_removed_additive() {
|
|
3135
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3136
|
+
"Person",
|
|
3137
|
+
vec![
|
|
3138
|
+
make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
|
|
3139
|
+
make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
|
|
3140
|
+
],
|
|
3141
|
+
)]);
|
|
3142
|
+
|
|
3143
|
+
let new = make_test_proto(vec![make_test_message(
|
|
3144
|
+
"Person",
|
|
3145
|
+
vec![make_test_field(
|
|
3146
|
+
"id",
|
|
3147
|
+
1,
|
|
3148
|
+
ProtoType::Scalar(ScalarType::String),
|
|
3149
|
+
)],
|
|
3150
|
+
)]);
|
|
3151
|
+
|
|
3152
|
+
// Removing fields is NOT compatible in additive mode
|
|
3153
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
|
|
3154
|
+
assert!(!result.is_compatible);
|
|
3155
|
+
assert_eq!(result.violations.len(), 1);
|
|
3156
|
+
assert_eq!(
|
|
3157
|
+
result.violations[0].violation_type,
|
|
3158
|
+
ViolationType::FieldRemoved
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
#[test]
|
|
3163
|
+
fn test_compatibility_field_removed_backward() {
|
|
3164
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3165
|
+
"Person",
|
|
3166
|
+
vec![
|
|
3167
|
+
make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
|
|
3168
|
+
make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
|
|
3169
|
+
],
|
|
3170
|
+
)]);
|
|
3171
|
+
|
|
3172
|
+
let new = make_test_proto(vec![make_test_message(
|
|
3173
|
+
"Person",
|
|
3174
|
+
vec![make_test_field(
|
|
3175
|
+
"id",
|
|
3176
|
+
1,
|
|
3177
|
+
ProtoType::Scalar(ScalarType::String),
|
|
3178
|
+
)],
|
|
3179
|
+
)]);
|
|
3180
|
+
|
|
3181
|
+
// Removing fields IS compatible in backward mode (with warnings)
|
|
3182
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
|
|
3183
|
+
assert!(result.is_compatible); // Still compatible, just has violations
|
|
3184
|
+
assert!(!result.violations.is_empty());
|
|
3185
|
+
|
|
3186
|
+
// Should suggest reserving the field
|
|
3187
|
+
assert!(result.suggested_reserved_numbers.contains_key("Person"));
|
|
3188
|
+
assert!(result.suggested_reserved_numbers["Person"].contains(&2));
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
#[test]
|
|
3192
|
+
fn test_compatibility_type_change() {
|
|
3193
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3194
|
+
"Person",
|
|
3195
|
+
vec![make_test_field(
|
|
3196
|
+
"age",
|
|
3197
|
+
1,
|
|
3198
|
+
ProtoType::Scalar(ScalarType::Int32),
|
|
3199
|
+
)],
|
|
3200
|
+
)]);
|
|
3201
|
+
|
|
3202
|
+
let new = make_test_proto(vec![make_test_message(
|
|
3203
|
+
"Person",
|
|
3204
|
+
vec![make_test_field(
|
|
3205
|
+
"age",
|
|
3206
|
+
1,
|
|
3207
|
+
ProtoType::Scalar(ScalarType::String),
|
|
3208
|
+
)],
|
|
3209
|
+
)]);
|
|
3210
|
+
|
|
3211
|
+
// Type changes are NOT compatible in backward mode
|
|
3212
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
|
|
3213
|
+
assert!(!result.is_compatible);
|
|
3214
|
+
assert!(result
|
|
3215
|
+
.violations
|
|
3216
|
+
.iter()
|
|
3217
|
+
.any(|v| v.violation_type == ViolationType::FieldTypeChanged));
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
#[test]
|
|
3221
|
+
fn test_compatibility_breaking_mode() {
|
|
3222
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3223
|
+
"Person",
|
|
3224
|
+
vec![make_test_field(
|
|
3225
|
+
"id",
|
|
3226
|
+
1,
|
|
3227
|
+
ProtoType::Scalar(ScalarType::String),
|
|
3228
|
+
)],
|
|
3229
|
+
)]);
|
|
3230
|
+
|
|
3231
|
+
let new = make_test_proto(vec![make_test_message(
|
|
3232
|
+
"Person",
|
|
3233
|
+
vec![make_test_field(
|
|
3234
|
+
"uuid",
|
|
3235
|
+
1,
|
|
3236
|
+
ProtoType::Scalar(ScalarType::Int64),
|
|
3237
|
+
)],
|
|
3238
|
+
)]);
|
|
3239
|
+
|
|
3240
|
+
// Breaking mode allows everything
|
|
3241
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Breaking);
|
|
3242
|
+
assert!(result.is_compatible);
|
|
3243
|
+
// Violations are still reported for informational purposes
|
|
3244
|
+
assert!(!result.violations.is_empty());
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
#[test]
|
|
3248
|
+
fn test_apply_backward_compatibility() {
|
|
3249
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3250
|
+
"Person",
|
|
3251
|
+
vec![
|
|
3252
|
+
make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
|
|
3253
|
+
make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
|
|
3254
|
+
make_test_field("email", 3, ProtoType::Scalar(ScalarType::String)),
|
|
3255
|
+
],
|
|
3256
|
+
)]);
|
|
3257
|
+
|
|
3258
|
+
let mut new = make_test_proto(vec![make_test_message(
|
|
3259
|
+
"Person",
|
|
3260
|
+
vec![
|
|
3261
|
+
make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
|
|
3262
|
+
// name (2) removed
|
|
3263
|
+
// email (3) removed
|
|
3264
|
+
make_test_field("phone", 4, ProtoType::Scalar(ScalarType::String)),
|
|
3265
|
+
],
|
|
3266
|
+
)]);
|
|
3267
|
+
|
|
3268
|
+
CompatibilityChecker::apply_backward_compatibility(&old, &mut new);
|
|
3269
|
+
|
|
3270
|
+
// Should have added reserved numbers
|
|
3271
|
+
let person = &new.messages[0];
|
|
3272
|
+
assert!(person.reserved_numbers.contains(&2));
|
|
3273
|
+
assert!(person.reserved_numbers.contains(&3));
|
|
3274
|
+
assert!(person.reserved_names.contains(&"name".to_string()));
|
|
3275
|
+
assert!(person.reserved_names.contains(&"email".to_string()));
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
#[test]
|
|
3279
|
+
fn test_compatibility_message_removed() {
|
|
3280
|
+
let old = make_test_proto(vec![
|
|
3281
|
+
make_test_message("Person", vec![]),
|
|
3282
|
+
make_test_message("Address", vec![]),
|
|
3283
|
+
]);
|
|
3284
|
+
|
|
3285
|
+
let new = make_test_proto(vec![make_test_message("Person", vec![])]);
|
|
3286
|
+
|
|
3287
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
|
|
3288
|
+
assert!(!result.is_compatible);
|
|
3289
|
+
assert!(result.violations.iter().any(|v| {
|
|
3290
|
+
v.violation_type == ViolationType::MessageRemoved && v.message_name == "Address"
|
|
3291
|
+
}));
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
#[test]
|
|
3295
|
+
fn test_compatibility_result_report() {
|
|
3296
|
+
let old = make_test_proto(vec![make_test_message(
|
|
3297
|
+
"Person",
|
|
3298
|
+
vec![make_test_field(
|
|
3299
|
+
"name",
|
|
3300
|
+
1,
|
|
3301
|
+
ProtoType::Scalar(ScalarType::String),
|
|
3302
|
+
)],
|
|
3303
|
+
)]);
|
|
3304
|
+
|
|
3305
|
+
let new = make_test_proto(vec![make_test_message("Person", vec![])]);
|
|
3306
|
+
|
|
3307
|
+
let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
|
|
3308
|
+
let report = result.to_report();
|
|
3309
|
+
|
|
3310
|
+
assert!(report.contains("Compatibility Check"));
|
|
3311
|
+
assert!(report.contains("field_removed"));
|
|
3312
|
+
assert!(report.contains("Person"));
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
#[test]
|
|
3316
|
+
fn test_compatibility_mode_parsing() {
|
|
3317
|
+
assert_eq!(
|
|
3318
|
+
"additive".parse::<CompatibilityMode>().unwrap(),
|
|
3319
|
+
CompatibilityMode::Additive
|
|
3320
|
+
);
|
|
3321
|
+
assert_eq!(
|
|
3322
|
+
"backward".parse::<CompatibilityMode>().unwrap(),
|
|
3323
|
+
CompatibilityMode::Backward
|
|
3324
|
+
);
|
|
3325
|
+
assert_eq!(
|
|
3326
|
+
"breaking".parse::<CompatibilityMode>().unwrap(),
|
|
3327
|
+
CompatibilityMode::Breaking
|
|
3328
|
+
);
|
|
3329
|
+
assert!("invalid".parse::<CompatibilityMode>().is_err());
|
|
3330
|
+
}
|
|
3331
|
+
}
|