spikard_cli/codegen/protobuf/
mod.rs1pub 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
18pub 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
47pub 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
75pub 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
102pub 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
131pub 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
145pub 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}