spikard_cli/codegen/protobuf/
mod.rs1pub mod generators;
7pub mod spec_parser;
8
9pub 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
15pub use generators::{ProtobufGenerator, ProtobufTarget};
17
18use anyhow::Result;
19
20pub 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
49pub 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
77pub 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
104pub 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
133pub 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
147pub 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}