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