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.
Files changed (481) hide show
  1. package/.cargo/config.toml +6 -0
  2. package/.claude/settings.local.json +18 -0
  3. package/.coderabbit.yml +43 -0
  4. package/.codex/skills/release-management/SKILL.md +151 -0
  5. package/.codex/skills/release-management/agents/openai.yaml +4 -0
  6. package/.github/actions/decrypt-secrets/action.yml +121 -0
  7. package/.github/agents/Coder.agent.md +97 -0
  8. package/.github/agents/DeepResearch.agent.md +61 -0
  9. package/.github/chatmodes/tdd.vibepro.chatmode.md +1183 -0
  10. package/.github/copilot-instructions.md +13 -0
  11. package/.github/dependabot.yml +68 -0
  12. package/.github/workflows/README.md +165 -0
  13. package/.github/workflows/ci.yml +335 -0
  14. package/.github/workflows/dependabot-automerge.yml +114 -0
  15. package/.github/workflows/dependency-review.yml +27 -0
  16. package/.github/workflows/deploy.yml +87 -0
  17. package/.github/workflows/prepare-release.yml +168 -0
  18. package/.github/workflows/release-crates.yml +42 -0
  19. package/.github/workflows/release-npm.yml +137 -0
  20. package/.github/workflows/release-please.yml +29 -0
  21. package/.github/workflows/release-pypi.yml +96 -0
  22. package/.gitkeep +1 -0
  23. package/.release-please-manifest.json +5 -0
  24. package/.sea-registry.toml +10 -0
  25. package/.serena/project.yml +133 -0
  26. package/.sops.yaml +10 -0
  27. package/AGENTS.md +216 -0
  28. package/CHANGELOG.md +400 -0
  29. package/CLAUDE.md +62 -0
  30. package/CONTRIBUTING.md +323 -0
  31. package/Cargo.lock +3612 -0
  32. package/Cargo.toml +12 -0
  33. package/LICENSE +201 -0
  34. package/README.md +660 -0
  35. package/README_PYTHON.md +256 -0
  36. package/README_TYPESCRIPT.md +305 -0
  37. package/README_WASM.md +329 -0
  38. package/RELEASE_NOTES.md +41 -0
  39. package/bun.lock +378 -0
  40. package/bunfig.toml +11 -0
  41. package/check_output.txt +83 -0
  42. package/clippy_output.txt +80 -0
  43. package/commitlint.config.cjs +8 -0
  44. package/deny.toml +42 -0
  45. package/devbox.json +14 -0
  46. package/devbox.lock +76 -0
  47. package/docs/RELEASE_PROCESS.md +360 -0
  48. package/docs/diagnostics.md +161 -0
  49. package/docs/doc_guidelines.md +53 -0
  50. package/docs/explanations/README.md +21 -0
  51. package/docs/explanations/architecture-overview.md +109 -0
  52. package/docs/explanations/cross-language-binding-strategy.md +68 -0
  53. package/docs/explanations/graph-store-design.md +47 -0
  54. package/docs/explanations/performance-benchmarks.md +63 -0
  55. package/docs/explanations/policy-evaluation-logic.md +106 -0
  56. package/docs/explanations/semantic-modeling-concepts.md +109 -0
  57. package/docs/explanations/three-valued-logic.md +66 -0
  58. package/docs/explanations/versioning-strategy.md +45 -0
  59. package/docs/governance.md +168 -0
  60. package/docs/how-tos/README.md +46 -0
  61. package/docs/how-tos/ci-cd-validation.md +93 -0
  62. package/docs/how-tos/create-custom-units.md +125 -0
  63. package/docs/how-tos/define-policies.md +119 -0
  64. package/docs/how-tos/export-to-calm.md +110 -0
  65. package/docs/how-tos/export-to-protobuf.md +312 -0
  66. package/docs/how-tos/extend-grammar.md +133 -0
  67. package/docs/how-tos/generate-rdf-turtle.md +106 -0
  68. package/docs/how-tos/import-from-calm.md +114 -0
  69. package/docs/how-tos/import-from-sbvr.md +249 -0
  70. package/docs/how-tos/install-cli.md +126 -0
  71. package/docs/how-tos/parse-sea-files.md +132 -0
  72. package/docs/how-tos/policy-evaluation-modes.md +30 -0
  73. package/docs/how-tos/run-cross-language-tests.md +115 -0
  74. package/docs/how-tos/troubleshoot-napi-builds.md +55 -0
  75. package/docs/how-tos/use-modules-imports.md +285 -0
  76. package/docs/index.md +13 -0
  77. package/docs/plans/canonical-normalizer.md +121 -0
  78. package/docs/plans/cd_improvement.md +112 -0
  79. package/docs/plans/cli-ast.md +29 -0
  80. package/docs/plans/expression-bindings-and-normalizer-integration.md +174 -0
  81. package/docs/plans/protobuf_advanced_features_plan.md +597 -0
  82. package/docs/plans/protobuf_plan.yml +525 -0
  83. package/docs/plans/refactor_dsl_architecture.md +131 -0
  84. package/docs/plans/release-plan.md +163 -0
  85. package/docs/plans/sea_fmt_implementation_plan.md +516 -0
  86. package/docs/playbooks/README.md +18 -0
  87. package/docs/playbooks/adding-new-primitive.md +68 -0
  88. package/docs/playbooks/debugging-parser-failures.md +42 -0
  89. package/docs/playbooks/local-release-preparation.md +139 -0
  90. package/docs/playbooks/migrating-schema-versions.md +43 -0
  91. package/docs/playbooks/onboarding-contributors.md +64 -0
  92. package/docs/playbooks/releasing-beta.md +86 -0
  93. package/docs/playbooks/secret-management.md +64 -0
  94. package/docs/reference/README.md +199 -0
  95. package/docs/reference/ast-json-api.md +427 -0
  96. package/docs/reference/calm-mapping.md +519 -0
  97. package/docs/reference/cli-commands.md +588 -0
  98. package/docs/reference/configuration.md +202 -0
  99. package/docs/reference/error-codes.md +664 -0
  100. package/docs/reference/generated-artifacts-policy.md +53 -0
  101. package/docs/reference/grammar-spec.md +255 -0
  102. package/docs/reference/primitives-api.md +317 -0
  103. package/docs/reference/protobuf-api.md +426 -0
  104. package/docs/reference/python-api.md +485 -0
  105. package/docs/reference/registry.md +50 -0
  106. package/docs/reference/sea-dsl-ai-cheatsheet.yaml +913 -0
  107. package/docs/reference/security-model.md +74 -0
  108. package/docs/reference/typescript-api.md +508 -0
  109. package/docs/reference/wasm-api.md +420 -0
  110. package/docs/semantic-pack-review.md +144 -0
  111. package/docs/semantic-pack-signing.md +234 -0
  112. package/docs/semantic-packs.md +284 -0
  113. package/docs/specs/ADR-001-sea-dsl-semantic-source-of-truth.md +33 -0
  114. package/docs/specs/ADR-002-projection-first-class-construct.md +50 -0
  115. package/docs/specs/ADR-003-protobuf-projection-target.md +51 -0
  116. package/docs/specs/ADR-004-projection-compatibility-semantics.md +57 -0
  117. package/docs/specs/ADR-005-multi-language-support-strategy.md +112 -0
  118. package/docs/specs/ADR-006-error-handling-strategy.md +115 -0
  119. package/docs/specs/ADR-007-policy-evaluation-engine.md +95 -0
  120. package/docs/specs/ADR-008-knowledge-graph-integration.md +90 -0
  121. package/docs/specs/ADR-009-module-resolution-strategy.md +115 -0
  122. package/docs/specs/ADR-010-unit-system.md +106 -0
  123. package/docs/specs/PRD-001-sea-projection-framework.md +155 -0
  124. package/docs/specs/PRD-002-sea-cli-tooling.md +169 -0
  125. package/docs/specs/PRD-003-dsl-core-capabilities.md +275 -0
  126. package/docs/specs/README.md +62 -0
  127. package/docs/specs/SDS-001-protobuf-projection-engine.md +451 -0
  128. package/docs/specs/SDS-002-sea-core-architecture.md +268 -0
  129. package/docs/specs/SDS-003-parser-semantic-graph.md +377 -0
  130. package/docs/specs/SDS-004-policy-engine-design.md +362 -0
  131. package/docs/specs/SDS-005-knowledge-graph-module.md +364 -0
  132. package/docs/specs/SDS-006-calm-integration.md +367 -0
  133. package/docs/specs/SDS-007-sbvr-import.md +347 -0
  134. package/docs/templates/template_explanation.md +14 -0
  135. package/docs/templates/template_howto.md +21 -0
  136. package/docs/templates/template_playbook.md +21 -0
  137. package/docs/templates/template_reference.md +17 -0
  138. package/docs/templates/template_tutorial.md +24 -0
  139. package/docs/tutorials/README.md +12 -0
  140. package/docs/tutorials/first-sea-model.md +85 -0
  141. package/docs/tutorials/getting-started.md +98 -0
  142. package/docs/tutorials/python-binding-quickstart.md +107 -0
  143. package/docs/tutorials/typescript-binding-quickstart.md +91 -0
  144. package/docs/tutorials/wasm-in-browser.md +75 -0
  145. package/domainforge-core/CHANGELOG.md +138 -0
  146. package/domainforge-core/Cargo.toml +101 -0
  147. package/domainforge-core/MIGRATING.md +32 -0
  148. package/domainforge-core/README.md +197 -0
  149. package/domainforge-core/benchmark_results.txt +51 -0
  150. package/domainforge-core/build.rs +6 -0
  151. package/domainforge-core/deny.toml +31 -0
  152. package/domainforge-core/docs/specs/projections/sbvr_kg_mapping.md +43 -0
  153. package/domainforge-core/examples/basic.sea +7 -0
  154. package/domainforge-core/examples/cli/import_export_workflow.sh +38 -0
  155. package/domainforge-core/examples/cli/validate_example.sh +30 -0
  156. package/domainforge-core/examples/evolution_semantics.sea +31 -0
  157. package/domainforge-core/examples/parser_demo.rs +203 -0
  158. package/domainforge-core/grammar/sea.pest +408 -0
  159. package/domainforge-core/schemas/calm-v1.schema.json +170 -0
  160. package/domainforge-core/schemas/shacl/sea_shapes.ttl +19 -0
  161. package/domainforge-core/src/authority/compiler.rs +309 -0
  162. package/domainforge-core/src/authority/environment.rs +203 -0
  163. package/domainforge-core/src/authority/error.rs +164 -0
  164. package/domainforge-core/src/authority/fact_resolver.rs +224 -0
  165. package/domainforge-core/src/authority/mod.rs +25 -0
  166. package/domainforge-core/src/authority/pack.rs +133 -0
  167. package/domainforge-core/src/authority/policy.rs +224 -0
  168. package/domainforge-core/src/authority/resolver.rs +446 -0
  169. package/domainforge-core/src/authority/trace.rs +217 -0
  170. package/domainforge-core/src/authority/transform.rs +168 -0
  171. package/domainforge-core/src/authority/types.rs +617 -0
  172. package/domainforge-core/src/bin/domainforge.rs +25 -0
  173. package/domainforge-core/src/calm/export.rs +538 -0
  174. package/domainforge-core/src/calm/import.rs +1220 -0
  175. package/domainforge-core/src/calm/mod.rs +9 -0
  176. package/domainforge-core/src/calm/models.rs +108 -0
  177. package/domainforge-core/src/calm/sbvr_import.rs +9 -0
  178. package/domainforge-core/src/cli/authority.rs +149 -0
  179. package/domainforge-core/src/cli/format.rs +85 -0
  180. package/domainforge-core/src/cli/import.rs +133 -0
  181. package/domainforge-core/src/cli/mod.rs +64 -0
  182. package/domainforge-core/src/cli/normalize.rs +180 -0
  183. package/domainforge-core/src/cli/pack.rs +904 -0
  184. package/domainforge-core/src/cli/parse.rs +112 -0
  185. package/domainforge-core/src/cli/project.rs +294 -0
  186. package/domainforge-core/src/cli/registry.rs +41 -0
  187. package/domainforge-core/src/cli/test.rs +12 -0
  188. package/domainforge-core/src/cli/validate.rs +195 -0
  189. package/domainforge-core/src/cli/validate_kg.rs +80 -0
  190. package/domainforge-core/src/concept_id.rs +89 -0
  191. package/domainforge-core/src/error/diagnostics.rs +426 -0
  192. package/domainforge-core/src/error/fuzzy.rs +253 -0
  193. package/domainforge-core/src/error/mod.rs +13 -0
  194. package/domainforge-core/src/formatter/comments.rs +223 -0
  195. package/domainforge-core/src/formatter/config.rs +114 -0
  196. package/domainforge-core/src/formatter/mod.rs +22 -0
  197. package/domainforge-core/src/formatter/printer.rs +906 -0
  198. package/domainforge-core/src/graph/mod.rs +858 -0
  199. package/domainforge-core/src/graph/to_ast.rs +66 -0
  200. package/domainforge-core/src/kg.rs +1476 -0
  201. package/domainforge-core/src/kg_import.rs +251 -0
  202. package/domainforge-core/src/lib.rs +203 -0
  203. package/domainforge-core/src/module/mod.rs +1 -0
  204. package/domainforge-core/src/module/resolver.rs +260 -0
  205. package/domainforge-core/src/parser/ast.rs +2919 -0
  206. package/domainforge-core/src/parser/ast_convert.rs +494 -0
  207. package/domainforge-core/src/parser/ast_schema.rs +491 -0
  208. package/domainforge-core/src/parser/error.rs +291 -0
  209. package/domainforge-core/src/parser/lint.rs +39 -0
  210. package/domainforge-core/src/parser/mod.rs +193 -0
  211. package/domainforge-core/src/parser/printer.rs +702 -0
  212. package/domainforge-core/src/parser/profiles.rs +71 -0
  213. package/domainforge-core/src/parser/string_utils.rs +138 -0
  214. package/domainforge-core/src/patterns.rs +68 -0
  215. package/domainforge-core/src/policy/core.rs +1148 -0
  216. package/domainforge-core/src/policy/expression.rs +399 -0
  217. package/domainforge-core/src/policy/mod.rs +18 -0
  218. package/domainforge-core/src/policy/normalize.rs +1028 -0
  219. package/domainforge-core/src/policy/quantifier.rs +940 -0
  220. package/domainforge-core/src/policy/three_valued.rs +140 -0
  221. package/domainforge-core/src/policy/three_valued_microbench.rs +104 -0
  222. package/domainforge-core/src/policy/type_inference.rs +67 -0
  223. package/domainforge-core/src/policy/violation.rs +36 -0
  224. package/domainforge-core/src/primitives/concept_change.rs +61 -0
  225. package/domainforge-core/src/primitives/entity.rs +224 -0
  226. package/domainforge-core/src/primitives/flow.rs +111 -0
  227. package/domainforge-core/src/primitives/instance.rs +93 -0
  228. package/domainforge-core/src/primitives/mapping_contract.rs +50 -0
  229. package/domainforge-core/src/primitives/metric.rs +79 -0
  230. package/domainforge-core/src/primitives/mod.rs +25 -0
  231. package/domainforge-core/src/primitives/projection_contract.rs +50 -0
  232. package/domainforge-core/src/primitives/quantity.rs +56 -0
  233. package/domainforge-core/src/primitives/relation.rs +68 -0
  234. package/domainforge-core/src/primitives/resource.rs +237 -0
  235. package/domainforge-core/src/primitives/resource_instance.rs +88 -0
  236. package/domainforge-core/src/primitives/role.rs +49 -0
  237. package/domainforge-core/src/projection/buf.rs +404 -0
  238. package/domainforge-core/src/projection/contracts.rs +22 -0
  239. package/domainforge-core/src/projection/engine.rs +19 -0
  240. package/domainforge-core/src/projection/mod.rs +16 -0
  241. package/domainforge-core/src/projection/protobuf.rs +3331 -0
  242. package/domainforge-core/src/projection/registry.rs +43 -0
  243. package/domainforge-core/src/python/authority.rs +253 -0
  244. package/domainforge-core/src/python/error.rs +227 -0
  245. package/domainforge-core/src/python/formatter.rs +86 -0
  246. package/domainforge-core/src/python/graph.rs +366 -0
  247. package/domainforge-core/src/python/mod.rs +9 -0
  248. package/domainforge-core/src/python/policy.rs +651 -0
  249. package/domainforge-core/src/python/primitives.rs +796 -0
  250. package/domainforge-core/src/python/registry.rs +98 -0
  251. package/domainforge-core/src/python/semantic_pack.rs +619 -0
  252. package/domainforge-core/src/python/units.rs +96 -0
  253. package/domainforge-core/src/registry/mod.rs +432 -0
  254. package/domainforge-core/src/registry/tests.rs +210 -0
  255. package/domainforge-core/src/sbvr.rs +744 -0
  256. package/domainforge-core/src/semantic_pack/builder.rs +470 -0
  257. package/domainforge-core/src/semantic_pack/canonical_json.rs +184 -0
  258. package/domainforge-core/src/semantic_pack/diagnostics.rs +214 -0
  259. package/domainforge-core/src/semantic_pack/diff.rs +216 -0
  260. package/domainforge-core/src/semantic_pack/mod.rs +31 -0
  261. package/domainforge-core/src/semantic_pack/pack_set.rs +240 -0
  262. package/domainforge-core/src/semantic_pack/resolver.rs +437 -0
  263. package/domainforge-core/src/semantic_pack/review.rs +125 -0
  264. package/domainforge-core/src/semantic_pack/schema.rs +342 -0
  265. package/domainforge-core/src/semantic_pack/signing.rs +105 -0
  266. package/domainforge-core/src/semantic_pack/validator.rs +368 -0
  267. package/domainforge-core/src/semantic_version.rs +140 -0
  268. package/domainforge-core/src/test_utils.rs +12 -0
  269. package/domainforge-core/src/typescript/authority.rs +184 -0
  270. package/domainforge-core/src/typescript/error.rs +146 -0
  271. package/domainforge-core/src/typescript/formatter.rs +76 -0
  272. package/domainforge-core/src/typescript/graph.rs +391 -0
  273. package/domainforge-core/src/typescript/mod.rs +9 -0
  274. package/domainforge-core/src/typescript/policy.rs +564 -0
  275. package/domainforge-core/src/typescript/primitives.rs +784 -0
  276. package/domainforge-core/src/typescript/registry.rs +88 -0
  277. package/domainforge-core/src/typescript/semantic_pack.rs +470 -0
  278. package/domainforge-core/src/typescript/units.rs +76 -0
  279. package/domainforge-core/src/units/mod.rs +462 -0
  280. package/domainforge-core/src/uuid_module.rs +42 -0
  281. package/domainforge-core/src/validation_error.rs +818 -0
  282. package/domainforge-core/src/validation_result.rs +30 -0
  283. package/domainforge-core/src/wasm/authority.rs +192 -0
  284. package/domainforge-core/src/wasm/error.rs +145 -0
  285. package/domainforge-core/src/wasm/formatter.rs +69 -0
  286. package/domainforge-core/src/wasm/graph.rs +471 -0
  287. package/domainforge-core/src/wasm/mod.rs +16 -0
  288. package/domainforge-core/src/wasm/policy.rs +607 -0
  289. package/domainforge-core/src/wasm/primitives.rs +295 -0
  290. package/domainforge-core/src/wasm/semantic_pack.rs +471 -0
  291. package/domainforge-core/src/wasm/units.rs +62 -0
  292. package/domainforge-core/std/aws.sea +6 -0
  293. package/domainforge-core/std/core.sea +6 -0
  294. package/domainforge-core/std/http.sea +27 -0
  295. package/domainforge-core/tests/aggregation_enhanced_tests.rs +162 -0
  296. package/domainforge-core/tests/aggregation_eval_tests.rs +248 -0
  297. package/domainforge-core/tests/aggregation_integration_tests.rs +379 -0
  298. package/domainforge-core/tests/aggregation_parser_tests.rs +92 -0
  299. package/domainforge-core/tests/aggregation_tests.rs +102 -0
  300. package/domainforge-core/tests/authority_conformance_tests.rs +1173 -0
  301. package/domainforge-core/tests/calm_round_trip_tests.rs +283 -0
  302. package/domainforge-core/tests/calm_schema_validation_tests.rs +137 -0
  303. package/domainforge-core/tests/cast_operator_tests.rs +85 -0
  304. package/domainforge-core/tests/cli_binary_check.rs +37 -0
  305. package/domainforge-core/tests/cli_import_tests.rs +291 -0
  306. package/domainforge-core/tests/cli_path_traversal_tests.rs +124 -0
  307. package/domainforge-core/tests/cli_tests.rs +63 -0
  308. package/domainforge-core/tests/diagnostics_tests.rs +203 -0
  309. package/domainforge-core/tests/dimension_unit_tests.rs +80 -0
  310. package/domainforge-core/tests/entity_tests.rs +69 -0
  311. package/domainforge-core/tests/evolution_semantics_tests.rs +157 -0
  312. package/domainforge-core/tests/flow_tests.rs +78 -0
  313. package/domainforge-core/tests/flow_unit_validation_tests.rs +31 -0
  314. package/domainforge-core/tests/graph_integration_tests.rs +218 -0
  315. package/domainforge-core/tests/graph_tests.rs +626 -0
  316. package/domainforge-core/tests/import_parsing_tests.rs +23 -0
  317. package/domainforge-core/tests/instance_integration_tests.rs +98 -0
  318. package/domainforge-core/tests/instance_parsing_tests.rs +58 -0
  319. package/domainforge-core/tests/instance_tests.rs +61 -0
  320. package/domainforge-core/tests/kg_uri_encoding_tests.rs +53 -0
  321. package/domainforge-core/tests/lint_tests.rs +19 -0
  322. package/domainforge-core/tests/metric_tests.rs +143 -0
  323. package/domainforge-core/tests/module_resolution_tests.rs +100 -0
  324. package/domainforge-core/tests/namespace_registry_tests.rs +247 -0
  325. package/domainforge-core/tests/null_handling_tests.rs +26 -0
  326. package/domainforge-core/tests/parser_ast_v3.rs +53 -0
  327. package/domainforge-core/tests/parser_dimension_registry_tests.rs +20 -0
  328. package/domainforge-core/tests/parser_integration_tests.rs +294 -0
  329. package/domainforge-core/tests/parser_metadata_tests.rs +97 -0
  330. package/domainforge-core/tests/parser_resource_domain_only_graph_test.rs +21 -0
  331. package/domainforge-core/tests/parser_resource_limits_tests.rs +122 -0
  332. package/domainforge-core/tests/parser_tests.rs +512 -0
  333. package/domainforge-core/tests/pattern_semantics_tests.rs +87 -0
  334. package/domainforge-core/tests/phase_14_determinism_tests.rs +166 -0
  335. package/domainforge-core/tests/phase_15_validation_error_tests.rs +136 -0
  336. package/domainforge-core/tests/phase_16_unicode_tests.rs +248 -0
  337. package/domainforge-core/tests/phase_17_export_tests.rs +285 -0
  338. package/domainforge-core/tests/phase_17_round_trip_tests.rs +264 -0
  339. package/domainforge-core/tests/policy_tests.rs +635 -0
  340. package/domainforge-core/tests/primitives_integration_tests.rs +151 -0
  341. package/domainforge-core/tests/print_rdf_xml.rs +14 -0
  342. package/domainforge-core/tests/printer_tests.rs +204 -0
  343. package/domainforge-core/tests/profile_tests.rs +35 -0
  344. package/domainforge-core/tests/projection_contracts_tests.rs +154 -0
  345. package/domainforge-core/tests/protobuf_projection_tests.rs +199 -0
  346. package/domainforge-core/tests/quantity_tests.rs +41 -0
  347. package/domainforge-core/tests/rdf_xml_typed_literal_tests.rs +105 -0
  348. package/domainforge-core/tests/registry_schema_tests.rs +33 -0
  349. package/domainforge-core/tests/resource_tests.rs +50 -0
  350. package/domainforge-core/tests/resource_unit_tests.rs +24 -0
  351. package/domainforge-core/tests/roles_relations_tests.rs +61 -0
  352. package/domainforge-core/tests/round_trip_tests.rs +34 -0
  353. package/domainforge-core/tests/runtime_toggle_tests.rs +70 -0
  354. package/domainforge-core/tests/sbvr_fact_schema_tests.rs +60 -0
  355. package/domainforge-core/tests/sbvr_flow_facts_tests.rs +55 -0
  356. package/domainforge-core/tests/sbvr_parsing_tests.rs +53 -0
  357. package/domainforge-core/tests/semantic_pack_alias_resolution.rs +197 -0
  358. package/domainforge-core/tests/semantic_pack_build.rs +302 -0
  359. package/domainforge-core/tests/semantic_pack_consumer_smoke.rs +150 -0
  360. package/domainforge-core/tests/semantic_pack_pack_set.rs +160 -0
  361. package/domainforge-core/tests/semantic_pack_signing.rs +157 -0
  362. package/domainforge-core/tests/semantic_pack_three_valued.rs +250 -0
  363. package/domainforge-core/tests/semantic_pack_validate.rs +196 -0
  364. package/domainforge-core/tests/std_lib_tests.rs +37 -0
  365. package/domainforge-core/tests/temporal_evaluation_tests.rs +159 -0
  366. package/domainforge-core/tests/temporal_semantics_tests.rs +214 -0
  367. package/domainforge-core/tests/three_valued_quantifiers_tests.rs +164 -0
  368. package/domainforge-core/tests/turtle_entity_export_tests.rs +38 -0
  369. package/domainforge-core/tests/turtle_escaping_tests.rs +53 -0
  370. package/domainforge-core/tests/turtle_resource_export_tests.rs +34 -0
  371. package/domainforge-core/tests/type_inference_tests.rs +40 -0
  372. package/domainforge-core/tests/unicode_validation_tests.rs +169 -0
  373. package/domainforge-core/tests/unit_tests.rs +81 -0
  374. package/domainforge-core/tests/validate_tests.rs +38 -0
  375. package/domainforge-core/tests/validation_unit_mismatch_tests.rs +83 -0
  376. package/domainforge-core/tests/wasm_tests.rs +229 -0
  377. package/domainforge-python/CHANGELOG-python.md +12 -0
  378. package/domainforge-python/MIGRATING.md +24 -0
  379. package/domainforge-python/README.md +256 -0
  380. package/domainforge-python/domainforge/__init__.py +95 -0
  381. package/domainforge-python/domainforge/domainforge.pyi +519 -0
  382. package/domainforge-python/pyproject.toml +36 -0
  383. package/domainforge-typescript/CHANGELOG-typescript.md +12 -0
  384. package/domainforge-typescript/LICENSE +201 -0
  385. package/domainforge-typescript/MIGRATING.md +24 -0
  386. package/domainforge-typescript/README.md +305 -0
  387. package/domainforge-typescript/index.d.ts +452 -0
  388. package/domainforge-typescript/index.js +361 -0
  389. package/domainforge-typescript/package.json +60 -0
  390. package/example.js +61 -0
  391. package/examples/browser.html +366 -0
  392. package/examples/namespaces/finance/cashflow.sea +5 -0
  393. package/examples/namespaces/logistics/core.sea +7 -0
  394. package/examples/observability_metrics.sea +38 -0
  395. package/fixtures/semantic_packs/acme_procurement/domain/entities.sea +39 -0
  396. package/fixtures/semantic_packs/acme_procurement/domain/metrics.sea +11 -0
  397. package/fixtures/semantic_packs/acme_procurement/domain/relations.sea +7 -0
  398. package/fixtures/semantic_packs/acme_procurement/domain/resources.sea +9 -0
  399. package/fixtures/semantic_packs/acme_procurement/review/acme.procurement.semantic-review.jsonl +7 -0
  400. package/fixtures/semantic_packs/acme_procurement/tests/ambiguous_vendor_alias.sea +8 -0
  401. package/fixtures/semantic_packs/acme_procurement/tests/deprecated_vendor_alias.sea +8 -0
  402. package/fixtures/semantic_packs/acme_procurement/tests/invalid_relation.sea +3 -0
  403. package/fixtures/semantic_packs/acme_procurement/tests/proposed_concept.sea +8 -0
  404. package/fixtures/semantic_packs/acme_procurement/tests/rejected_concept.sea +8 -0
  405. package/fixtures/semantic_packs/acme_procurement/tests/unit_mismatch.sea +7 -0
  406. package/fixtures/semantic_packs/acme_procurement/tests/unknown_vendor_policy.sea +8 -0
  407. package/fixtures/semantic_packs/acme_procurement/tests/valid_purchase_policy.sea +8 -0
  408. package/index.d.ts +2 -0
  409. package/index.js +8 -0
  410. package/justfile +200 -0
  411. package/lefthook.yml +13 -0
  412. package/lib/validate_native_exports.d.ts +4 -0
  413. package/lib/validate_native_exports.js +12 -0
  414. package/package.json +22 -0
  415. package/pytest.ini +5 -0
  416. package/python/tests/test_registry.py +75 -0
  417. package/python/tests/test_units.py +18 -0
  418. package/release-please-config.json +49 -0
  419. package/requirements-dev.txt +3 -0
  420. package/requirements.txt +3 -0
  421. package/rust-toolchain.toml +3 -0
  422. package/schemas/ast-v1.schema.json +72 -0
  423. package/schemas/ast-v2.schema.json +1200 -0
  424. package/schemas/ast-v3.schema.json +1200 -0
  425. package/schemas/sea-registry.schema.json +45 -0
  426. package/scripts/build-python.sh +37 -0
  427. package/scripts/build-release.sh +279 -0
  428. package/scripts/build-typescript.sh +13 -0
  429. package/scripts/build-wasm.sh +113 -0
  430. package/scripts/bump-version.sh +245 -0
  431. package/scripts/check_unused_test_imports.py +85 -0
  432. package/scripts/ci_tasks.py +379 -0
  433. package/scripts/clear_debug_test.sh +10 -0
  434. package/scripts/create-github-release.sh +262 -0
  435. package/scripts/create-tag.sh +203 -0
  436. package/scripts/find_and_link_test_binary.sh +70 -0
  437. package/scripts/generate-changelog.sh +271 -0
  438. package/scripts/generate-release-notes.sh +205 -0
  439. package/scripts/lint_release_security.py +96 -0
  440. package/scripts/lint_release_workflows.py +82 -0
  441. package/scripts/lint_workflow_gates.py +113 -0
  442. package/scripts/optimized-wasm-build.sh +61 -0
  443. package/scripts/patch_napi_types.py +62 -0
  444. package/scripts/pre-release-check.sh +289 -0
  445. package/scripts/prepare_rust_debug.sh +52 -0
  446. package/scripts/release.sh +373 -0
  447. package/scripts/resolve_rust_binary.py +230 -0
  448. package/scripts/run_commitlint.sh +29 -0
  449. package/scripts/test-all.sh +77 -0
  450. package/scripts/update_launch_program.py +93 -0
  451. package/secrets/README.md +27 -0
  452. package/secrets/secrets.yaml +21 -0
  453. package/test_integration.py +67 -0
  454. package/tests/test_authority.py +328 -0
  455. package/tests/test_ci_tasks.py +143 -0
  456. package/tests/test_expression.py +256 -0
  457. package/tests/test_golden_payment_flow.py +42 -0
  458. package/tests/test_graph.py +127 -0
  459. package/tests/test_instance.py +136 -0
  460. package/tests/test_parser.py +82 -0
  461. package/tests/test_primitives.py +68 -0
  462. package/tests/test_role_relation_parity.py +56 -0
  463. package/tests/test_runtime_toggle.py +156 -0
  464. package/tests/test_semantic_pack.py +639 -0
  465. package/tests/test_three_valued_eval.py +159 -0
  466. package/tsconfig.json +30 -0
  467. package/typescript-tests/advanced.test.ts +165 -0
  468. package/typescript-tests/authority.test.ts +216 -0
  469. package/typescript-tests/expression.test.ts +228 -0
  470. package/typescript-tests/golden-payment-flow.test.ts +51 -0
  471. package/typescript-tests/graph.test.ts +142 -0
  472. package/typescript-tests/native-binding.test.ts +20 -0
  473. package/typescript-tests/primitives.test.ts +88 -0
  474. package/typescript-tests/registry.test.ts +122 -0
  475. package/typescript-tests/role_relation.test.ts +63 -0
  476. package/typescript-tests/runtime_toggle.test.ts +141 -0
  477. package/typescript-tests/semantic-pack.test.ts +556 -0
  478. package/typescript-tests/three_valued_eval.test.ts +135 -0
  479. package/typescript-tests/units.test.ts +36 -0
  480. package/vitest.config.ts +13 -0
  481. 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
+ }