pub mod generators;
pub mod spec_parser;
pub use spec_parser::{
EnumDef, EnumValue, FieldDef, FieldLabel, MessageDef, MethodDef, ProtoType, ProtobufSchema, parse_proto_schema,
parse_proto_schema_string, parse_proto_schema_with_includes,
};
pub use generators::{ProtobufGenerator, ProtobufTarget};
use anyhow::Result;
pub fn generate_python_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
use generators::ProtobufGenerator;
use generators::python::PythonProtobufGenerator;
let generator = PythonProtobufGenerator;
match target {
ProtobufTarget::All => generator.generate_complete(schema),
ProtobufTarget::Messages => generator.generate_messages(schema),
ProtobufTarget::Services => generator.generate_services(schema),
}
}
pub fn generate_typescript_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
use generators::ProtobufGenerator;
use generators::typescript::TypeScriptProtobufGenerator;
let generator = TypeScriptProtobufGenerator;
match target {
ProtobufTarget::All => generator.generate_complete(schema),
ProtobufTarget::Messages => generator.generate_messages(schema),
ProtobufTarget::Services => generator.generate_services(schema),
}
}
pub fn generate_ruby_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
use generators::ProtobufGenerator;
use generators::ruby::RubyProtobufGenerator;
let generator = RubyProtobufGenerator;
match target {
ProtobufTarget::All => generator.generate_complete(schema),
ProtobufTarget::Messages => generator.generate_messages(schema),
ProtobufTarget::Services => generator.generate_services(schema),
}
}
pub fn generate_php_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
use generators::ProtobufGenerator;
use generators::php::PhpProtobufGenerator;
let generator = PhpProtobufGenerator;
match target {
ProtobufTarget::All => generator.generate_complete(schema),
ProtobufTarget::Messages => generator.generate_messages(schema),
ProtobufTarget::Services => generator.generate_services(schema),
}
}
pub fn generate_rust_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
use generators::ProtobufGenerator;
use generators::rust_lang::RustProtobufGenerator;
let generator = RustProtobufGenerator;
match target {
ProtobufTarget::All => generator.generate_complete(schema),
ProtobufTarget::Messages => generator.generate_messages(schema),
ProtobufTarget::Services => generator.generate_services(schema),
}
}
pub fn generate_elixir_protobuf(schema: &ProtobufSchema, target: &ProtobufTarget) -> Result<String> {
use generators::ProtobufGenerator;
use generators::elixir::ElixirProtobufGenerator;
let generator = ElixirProtobufGenerator;
match target {
ProtobufTarget::All => generator.generate_complete(schema),
ProtobufTarget::Messages => generator.generate_messages(schema),
ProtobufTarget::Services => generator.generate_services(schema),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::{TargetLanguage, quality::QualityValidator};
use std::path::Path;
#[test]
fn test_parse_and_generate_python_all() {
let proto = r#"syntax = "proto3";
package example;
message User {
string id = 1;
string name = 2;
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_python_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Python code");
assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
assert!(code.contains("from google.protobuf import message"));
assert!(code.contains("PROTOBUF_PACKAGE = \"example\""));
}
#[test]
fn test_parse_and_generate_python_all_validates() {
let proto = r#"syntax = "proto3";
package example;
message User {
string id = 1;
string name = 2;
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_python_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Python code");
let report = QualityValidator::new(TargetLanguage::Python)
.validate_all(&code)
.expect("python protobuf validation should run");
assert!(
report.is_valid(),
"generated Python Protobuf code should validate cleanly: {report}"
);
}
#[test]
fn test_parse_and_generate_typescript_messages() {
let proto = r#"syntax = "proto3";
package example;
message User {
string id = 1;
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_typescript_protobuf(&schema, &ProtobufTarget::Messages)
.expect("Failed to generate TypeScript code");
assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
assert!(code.contains("import * as $protobuf from \"protobufjs\""));
assert!(code.contains("// Package: example"));
}
#[test]
fn test_parse_and_generate_typescript_all_validates() {
let proto = r#"syntax = "proto3";
package example.service;
message User {
string id = 1;
repeated string tags = 2;
}
service UserService {
rpc GetUser (User) returns (User);
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code =
generate_typescript_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate TypeScript code");
let report = QualityValidator::new(TargetLanguage::TypeScript)
.validate_all(&code)
.expect("typescript protobuf validation should run");
assert!(
report.is_valid(),
"generated TypeScript Protobuf code should validate cleanly: {report}"
);
}
#[test]
fn test_reject_proto2_in_generation() {
let proto = r#"syntax = "proto2";
package example;
message User {
required string id = 1;
}
"#;
let result = parse_proto_schema_string(proto);
assert!(result.is_err());
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("Only proto3 syntax is supported"));
}
#[test]
fn test_generate_ruby_messages() {
let proto = r#"syntax = "proto3";
package example;
message User {
string id = 1;
string name = 2;
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_ruby_protobuf(&schema, &ProtobufTarget::Messages).expect("Failed to generate Ruby code");
assert!(code.contains("# frozen_string_literal: true"));
assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
assert!(code.contains("require 'google/protobuf'"));
assert!(code.contains("Package: example"));
}
#[test]
fn test_parse_and_generate_ruby_all_validates() {
let proto = r#"syntax = "proto3";
package example.service;
message User {
string id = 1;
repeated string tags = 2;
}
service UserService {
rpc GetUser (User) returns (User);
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_ruby_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Ruby code");
let report = QualityValidator::new(TargetLanguage::Ruby)
.validate_all(&code)
.expect("ruby protobuf validation should run");
assert!(
report.is_valid(),
"generated Ruby Protobuf code should validate cleanly: {report}"
);
}
#[test]
fn test_generate_php_all() {
let proto = r#"syntax = "proto3";
package example.service;
message Empty {}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_php_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate PHP code");
assert!(code.contains("<?php"));
assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
assert!(code.contains(r"namespace example\service"));
}
#[test]
fn test_parse_and_generate_php_all_validates() {
let proto = r#"syntax = "proto3";
package example.service;
message User {
string id = 1;
repeated string tags = 2;
}
service UserService {
rpc GetUser (User) returns (User);
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_php_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate PHP code");
let report = QualityValidator::new(TargetLanguage::Php)
.validate_all(&code)
.expect("php protobuf validation should run");
assert!(
report.is_valid(),
"generated PHP Protobuf code should validate cleanly: {report}"
);
}
#[test]
fn test_parse_and_generate_rust_all() {
let proto = r#"syntax = "proto3";
package example;
message User {
string id = 1;
string name = 2;
}
service UserService {
rpc GetUser (User) returns (User);
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_rust_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Rust code");
assert!(code.contains("DO NOT EDIT - Auto-generated by Spikard CLI"));
assert!(code.contains("pub struct User"));
assert!(code.contains("pub trait UserService"));
assert!(code.contains("async fn get_user"));
assert_eq!(code.matches("DO NOT EDIT - Auto-generated by Spikard CLI").count(), 1);
}
#[test]
fn test_parse_and_generate_rust_example_validates() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/schemas/user-service.proto");
let schema = parse_proto_schema(&fixture).expect("example proto schema should parse");
let code = generate_rust_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Rust code");
let report = QualityValidator::new(TargetLanguage::Rust)
.validate_all(&code)
.expect("rust protobuf validation should run");
assert!(
report.is_valid(),
"generated Rust protobuf code for the example schema should validate cleanly: {report}"
);
assert!(code.contains("pub enum UserStatus"));
assert!(code.contains("Unknown = 0"));
assert!(code.contains("Active = 1"));
assert!(!code.contains("UNKNOWN = 0"));
}
#[test]
fn test_parse_and_generate_elixir_all_validates() {
let proto = r#"syntax = "proto3";
package example;
message User {
string id = 1;
string name = 2;
}
service UserService {
rpc GetUser (User) returns (User);
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code = generate_elixir_protobuf(&schema, &ProtobufTarget::All).expect("Failed to generate Elixir code");
assert!(code.contains("defmodule Example.User"));
assert!(code.contains("defstruct"));
assert!(code.contains("@callback get_user"));
assert!(code.contains("def registry(handler \\\\ Example.UserService.Server)"));
assert!(code.contains("Grpc.Service.register("));
assert!(code.contains("def service_name, do: \"example.UserService\""));
let report = QualityValidator::new(TargetLanguage::Elixir)
.validate_all(&code)
.expect("elixir protobuf validation should run");
assert!(
report.is_valid(),
"generated Elixir Protobuf code should validate cleanly: {report}"
);
}
#[test]
fn test_generate_elixir_services_emit_runtime_rpc_modes() {
let proto = r#"syntax = "proto3";
package example;
message User {
string id = 1;
}
service UserService {
rpc GetUser (User) returns (User);
rpc WatchUsers (User) returns (stream User);
rpc UploadUsers (stream User) returns (User);
rpc ChatUsers (stream User) returns (stream User);
}
"#;
let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
let code =
generate_elixir_protobuf(&schema, &ProtobufTarget::Services).expect("Failed to generate Elixir code");
assert!(code.contains("\"GetUser\" => :unary"));
assert!(code.contains("\"WatchUsers\" => :server_stream"));
assert!(code.contains("\"UploadUsers\" => :client_stream"));
assert!(code.contains("\"ChatUsers\" => :bidi_stream"));
assert!(code.contains("@callback get_user(Spikard.Grpc.Request.t())"));
assert!(code.contains("@callback upload_users("));
}
}