Skip to main content

spikard_cli/codegen/protobuf/
mod.rs

1//! Protobuf Schema Definition Language (.proto) specification parsing and code generation
2//!
3//! This module provides parsing and code generation for Protocol Buffer (protobuf) specifications.
4//! Supports proto3 syntax only with message, service, and enum definitions.
5
6pub mod generators;
7pub mod spec_parser;
8
9// Re-export parser types and functions for public use
10pub use spec_parser::{
11    EnumDef, EnumValue, FieldDef, FieldLabel, MessageDef, MethodDef, ProtoType, ProtobufSchema, parse_proto_schema,
12    parse_proto_schema_string, parse_proto_schema_with_includes,
13};
14
15// Re-export generators trait
16pub use generators::{ProtobufGenerator, ProtobufTarget};
17
18use anyhow::Result;
19
20/// Generate Python Protobuf code from a schema
21///
22/// Parses the Protobuf schema and generates complete Python code with message
23/// definitions, service clients, and server stubs based on the target specification.
24///
25/// # Arguments
26///
27/// * `schema` - Parsed Protobuf schema
28/// * `target` - Generation target specifying what to generate:
29///   * `ProtobufTarget::All` - Complete code: messages, services, and utilities
30///   * `ProtobufTarget::Messages` - Message definitions only
31///   * `ProtobufTarget::Services` - Service clients and stubs only
32///
33/// # Returns
34///
35/// Generated Python code as a `String`, or an `anyhow::Error` if generation fails.
36pub fn generate_python_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
37    use generators::ProtobufGenerator;
38    use generators::python::PythonProtobufGenerator;
39
40    let generator = PythonProtobufGenerator;
41
42    match target {
43        ProtobufTarget::All => generator.generate_complete(schema),
44        ProtobufTarget::Messages => generator.generate_messages(schema),
45        ProtobufTarget::Services => generator.generate_services(schema),
46    }
47}
48
49/// Generate TypeScript Protobuf code from a schema
50///
51/// Parses the Protobuf schema and generates complete TypeScript code
52/// with message types, service clients, and server implementations based on
53/// the target specification.
54///
55/// # Arguments
56///
57/// * `schema` - Parsed Protobuf schema
58/// * `target` - Generation target: `ProtobufTarget::All` (complete), `ProtobufTarget::Messages` (messages only),
59///   or `ProtobufTarget::Services` (services only)
60///
61/// # Returns
62///
63/// Generated TypeScript code as a string, or an error if generation fails
64pub fn generate_typescript_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
65    use generators::ProtobufGenerator;
66    use generators::typescript::TypeScriptProtobufGenerator;
67
68    let generator = TypeScriptProtobufGenerator;
69
70    match target {
71        ProtobufTarget::All => generator.generate_complete(schema),
72        ProtobufTarget::Messages => generator.generate_messages(schema),
73        ProtobufTarget::Services => generator.generate_services(schema),
74    }
75}
76
77/// Generate Ruby Protobuf code from a schema
78///
79/// Parses the Protobuf schema and generates idiomatic Ruby code with message
80/// classes, service clients, and server implementations based on the target specification.
81///
82/// # Arguments
83///
84/// * `schema` - Parsed Protobuf schema
85/// * `target` - Generation target: `ProtobufTarget::All` (complete), `ProtobufTarget::Messages` (messages only),
86///   or `ProtobufTarget::Services` (services only)
87///
88/// # Returns
89///
90/// Generated Ruby code as a string, or an error if generation fails
91pub fn generate_ruby_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
92    use generators::ProtobufGenerator;
93    use generators::ruby::RubyProtobufGenerator;
94
95    let generator = RubyProtobufGenerator;
96
97    match target {
98        ProtobufTarget::All => generator.generate_complete(schema),
99        ProtobufTarget::Messages => generator.generate_messages(schema),
100        ProtobufTarget::Services => generator.generate_services(schema),
101    }
102}
103
104/// Generate PHP Protobuf code from a schema
105///
106/// Parses the Protobuf schema and generates complete PHP code with message
107/// type definitions, service clients, and server implementations based on the
108/// target specification. Generated code uses PSR-4 namespacing with PHP 8.1+
109/// typed properties and the google/protobuf library.
110///
111/// # Arguments
112///
113/// * `schema` - Parsed Protobuf schema
114/// * `target` - Generation target: `ProtobufTarget::All` (complete), `ProtobufTarget::Messages` (messages only),
115///   or `ProtobufTarget::Services` (services only)
116///
117/// # Returns
118///
119/// Generated PHP code as a string, or an error if generation fails
120pub fn generate_php_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
121    use generators::ProtobufGenerator;
122    use generators::php::PhpProtobufGenerator;
123
124    let generator = PhpProtobufGenerator;
125
126    match target {
127        ProtobufTarget::All => generator.generate_complete(schema),
128        ProtobufTarget::Messages => generator.generate_messages(schema),
129        ProtobufTarget::Services => generator.generate_services(schema),
130    }
131}
132
133/// Generate Rust Protobuf code from a schema
134pub fn generate_rust_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
135    use generators::ProtobufGenerator;
136    use generators::rust_lang::RustProtobufGenerator;
137
138    let generator = RustProtobufGenerator;
139
140    match target {
141        ProtobufTarget::All => generator.generate_complete(schema),
142        ProtobufTarget::Messages => generator.generate_messages(schema),
143        ProtobufTarget::Services => generator.generate_services(schema),
144    }
145}
146
147/// Generate Elixir Protobuf code from a schema.
148pub fn generate_elixir_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
149    use generators::ProtobufGenerator;
150    use generators::elixir::ElixirProtobufGenerator;
151
152    let generator = ElixirProtobufGenerator;
153
154    match target {
155        ProtobufTarget::All => generator.generate_complete(schema),
156        ProtobufTarget::Messages => generator.generate_messages(schema),
157        ProtobufTarget::Services => generator.generate_services(schema),
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::codegen::{TargetLanguage, quality::QualityValidator};
165    use std::path::Path;
166
167    #[test]
168    fn test_parse_and_generate_python_all() {
169        let proto = r#"syntax = "proto3";
170
171package example;
172
173message User {
174  string id = 1;
175  string name = 2;
176}
177"#;
178
179        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
180        let code = generate_python_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Python code");
181        assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
182        assert!(code.contains("from google.protobuf import message"));
183        assert!(code.contains("PROTOBUF_PACKAGE = \"example\""));
184    }
185
186    #[test]
187    fn test_parse_and_generate_python_all_validates() {
188        let proto = r#"syntax = "proto3";
189
190package example;
191
192message User {
193  string id = 1;
194  string name = 2;
195}
196"#;
197
198        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
199        let code = generate_python_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Python code");
200        let report = QualityValidator::new(TargetLanguage::Python)
201            .validate_all(&code)
202            .expect("python protobuf validation should run");
203
204        assert!(
205            report.is_valid(),
206            "generated Python Protobuf code should validate cleanly: {report}"
207        );
208    }
209
210    #[test]
211    fn test_parse_and_generate_typescript_messages() {
212        let proto = r#"syntax = "proto3";
213
214package example;
215
216message User {
217  string id = 1;
218}
219"#;
220
221        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
222        let code = generate_typescript_protobuf(&schema, &ProtobufTarget::Messages)
223            .expect("Failed to generate TypeScript code");
224        assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
225        assert!(code.contains("import * as $protobuf from \"protobufjs\""));
226        assert!(code.contains("// Package: example"));
227    }
228
229    #[test]
230    fn test_parse_and_generate_typescript_all_validates() {
231        let proto = r#"syntax = "proto3";
232
233package example.service;
234
235message User {
236  string id = 1;
237  repeated string tags = 2;
238}
239
240service UserService {
241  rpc GetUser (User) returns (User);
242}
243"#;
244
245        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
246        let code =
247            generate_typescript_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate TypeScript code");
248        let report = QualityValidator::new(TargetLanguage::TypeScript)
249            .validate_all(&code)
250            .expect("typescript protobuf validation should run");
251
252        assert!(
253            report.is_valid(),
254            "generated TypeScript Protobuf code should validate cleanly: {report}"
255        );
256    }
257
258    #[test]
259    fn test_reject_proto2_in_generation() {
260        let proto = r#"syntax = "proto2";
261
262package example;
263
264message User {
265  required string id = 1;
266}
267"#;
268
269        let result = parse_proto_schema_string(proto);
270        assert!(result.is_err());
271        let error_msg = format!("{}", result.unwrap_err());
272        assert!(error_msg.contains("Only proto3 syntax is supported"));
273    }
274
275    #[test]
276    fn test_generate_ruby_messages() {
277        let proto = r#"syntax = "proto3";
278
279package example;
280
281message User {
282  string id = 1;
283  string name = 2;
284}
285"#;
286
287        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
288        let code = generate_ruby_protobuf(&schema, &ProtobufTarget::Messages).expect("Failed to generate Ruby code");
289        assert!(code.contains("# frozen_string_literal: true"));
290        assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
291        assert!(code.contains("require 'google/protobuf'"));
292        assert!(code.contains("Package: example"));
293    }
294
295    #[test]
296    fn test_parse_and_generate_ruby_all_validates() {
297        let proto = r#"syntax = "proto3";
298
299package example.service;
300
301message User {
302  string id = 1;
303  repeated string tags = 2;
304}
305
306service UserService {
307  rpc GetUser (User) returns (User);
308}
309"#;
310
311        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
312        let code = generate_ruby_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Ruby code");
313        let report = QualityValidator::new(TargetLanguage::Ruby)
314            .validate_all(&code)
315            .expect("ruby protobuf validation should run");
316
317        assert!(
318            report.is_valid(),
319            "generated Ruby Protobuf code should validate cleanly: {report}"
320        );
321    }
322
323    #[test]
324    fn test_generate_php_all() {
325        let proto = r#"syntax = "proto3";
326
327package example.service;
328
329message Empty {}
330"#;
331
332        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
333        let code = generate_php_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate PHP code");
334        assert!(code.contains("<?php"));
335        assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
336        assert!(code.contains(r"namespace example\service"));
337    }
338
339    #[test]
340    fn test_parse_and_generate_php_all_validates() {
341        let proto = r#"syntax = "proto3";
342
343package example.service;
344
345message User {
346  string id = 1;
347  repeated string tags = 2;
348}
349
350service UserService {
351  rpc GetUser (User) returns (User);
352}
353"#;
354
355        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
356        let code = generate_php_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate PHP code");
357        let report = QualityValidator::new(TargetLanguage::Php)
358            .validate_all(&code)
359            .expect("php protobuf validation should run");
360
361        assert!(
362            report.is_valid(),
363            "generated PHP Protobuf code should validate cleanly: {report}"
364        );
365    }
366
367    #[test]
368    fn test_parse_and_generate_rust_all() {
369        let proto = r#"syntax = "proto3";
370
371package example;
372
373message User {
374  string id = 1;
375  string name = 2;
376}
377
378service UserService {
379  rpc GetUser (User) returns (User);
380}
381"#;
382
383        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
384        let code = generate_rust_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Rust code");
385        assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
386        assert!(code.contains("pub struct User"));
387        assert!(code.contains("pub trait UserService"));
388        assert!(code.contains("async fn get_user"));
389        assert_eq!(code.matches("DO NOT EDIT - Auto-generated by Spikard CLI").count(), 1);
390    }
391
392    #[test]
393    fn test_parse_and_generate_rust_example_validates() {
394        let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/schemas/user-service.proto");
395        let schema = parse_proto_schema(&fixture).expect("example proto schema should parse");
396        let code = generate_rust_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Rust code");
397        let report = QualityValidator::new(TargetLanguage::Rust)
398            .validate_all(&code)
399            .expect("rust protobuf validation should run");
400
401        assert!(
402            report.is_valid(),
403            "generated Rust protobuf code for the example schema should validate cleanly: {report}"
404        );
405        assert!(code.contains("pub enum UserStatus"));
406        assert!(code.contains("Unknown = 0"));
407        assert!(code.contains("Active = 1"));
408        assert!(!code.contains("UNKNOWN = 0"));
409    }
410
411    #[test]
412    fn test_parse_and_generate_elixir_all_validates() {
413        let proto = r#"syntax = "proto3";
414
415package example;
416
417message User {
418  string id = 1;
419  string name = 2;
420}
421
422service UserService {
423  rpc GetUser (User) returns (User);
424}
425"#;
426
427        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
428        let code = generate_elixir_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Elixir code");
429        assert!(code.contains("defmodule Example.User"));
430        assert!(code.contains("defstruct"));
431        assert!(code.contains("@callback get_user"));
432        assert!(code.contains("def registry(handler \\\\ Example.UserService.Server)"));
433        assert!(code.contains("Grpc.Service.register("));
434        assert!(code.contains("def service_name, do: \"example.UserService\""));
435
436        let report = QualityValidator::new(TargetLanguage::Elixir)
437            .validate_all(&code)
438            .expect("elixir protobuf validation should run");
439
440        assert!(
441            report.is_valid(),
442            "generated Elixir Protobuf code should validate cleanly: {report}"
443        );
444    }
445
446    #[test]
447    fn test_generate_elixir_services_emit_runtime_rpc_modes() {
448        let proto = r#"syntax = "proto3";
449
450package example;
451
452message User {
453  string id = 1;
454}
455
456service UserService {
457  rpc GetUser (User) returns (User);
458  rpc WatchUsers (User) returns (stream User);
459  rpc UploadUsers (stream User) returns (User);
460  rpc ChatUsers (stream User) returns (stream User);
461}
462"#;
463
464        let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
465        let code =
466            generate_elixir_protobuf(&schema, &ProtobufTarget::Services).expect("Failed to generate Elixir code");
467
468        assert!(code.contains("\"GetUser\" => :unary"));
469        assert!(code.contains("\"WatchUsers\" => :server_stream"));
470        assert!(code.contains("\"UploadUsers\" => :client_stream"));
471        assert!(code.contains("\"ChatUsers\" => :bidi_stream"));
472        assert!(code.contains("@callback get_user(Spikard.Grpc.Request.t())"));
473        assert!(code.contains("@callback upload_users("));
474    }
475}