synapse-codegen 0.0.2

Code generation from protobuf service definitions for Synapse
Documentation
//! Synapse codegen - Generate server traits and client stubs from protobuf services

use anyhow::Result;
use heck::ToSnakeCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};

pub mod build_helper;
mod client_gen;
pub mod parser;
mod server_gen;

pub use build_helper::{CodegenConfig, generate_synapse_code};
pub use client_gen::generate_client_stub;
pub use parser::{parse_proto_file, parse_proto_file_with_includes};
pub use server_gen::generate_server_trait;

/// Service definition parsed from proto
#[derive(Debug, Clone)]
pub struct ServiceDef {
    pub package: String,
    pub service_name: String,
    pub methods: Vec<MethodDef>,
}

/// Method definition
#[derive(Debug, Clone)]
pub struct MethodDef {
    pub name: String,
    pub input_type: String,
    pub output_type: String,
    pub comment: Option<String>,
}

impl ServiceDef {
    /// Get the full qualified name (package.ServiceName)
    pub fn full_name(&self) -> String {
        format!("{}.{}", self.package, self.service_name)
    }

    /// Get the Rust module path (package parts as snake_case)
    pub fn module_path(&self) -> String {
        self.package.replace('.', "::")
    }

    /// Get interface ID calculation
    pub fn interface_id_expr(&self) -> TokenStream {
        let full_name = self.full_name();
        quote! {
            synapse_primitives::InterfaceId::from_name(#full_name)
        }
    }
}

impl MethodDef {
    /// Get snake_case method name for Rust
    pub fn method_name_snake(&self) -> String {
        self.name.to_snake_case()
    }

    /// Get method ID calculation
    pub fn method_id_expr(&self) -> TokenStream {
        let name = &self.name;
        quote! {
            synapse_primitives::MethodId::from_name(#name)
        }
    }

    /// Parse input type to Rust path
    pub fn input_type_path(&self, package: &str) -> TokenStream {
        type_name_to_path(&self.input_type, package)
    }

    /// Parse output type to Rust path
    pub fn output_type_path(&self, package: &str) -> TokenStream {
        type_name_to_path(&self.output_type, package)
    }
}

/// Convert proto type name to Rust path
fn type_name_to_path(type_name: &str, package: &str) -> TokenStream {
    // Remove leading dot if present
    let type_name = type_name.strip_prefix('.').unwrap_or(type_name);

    // Split into parts
    let parts: Vec<&str> = type_name.split('.').collect();

    if parts.is_empty() {
        return quote! { () };
    }

    // Handle well-known package mappings (synapse -> synapse_proto)
    if type_name.starts_with("synapse.") {
        let relative_parts: Vec<_> = parts[1..].to_vec();
        let idents: Vec<_> = relative_parts
            .iter()
            .map(|p| format_ident!("{}", p))
            .collect();
        return quote! { ::synapse_proto::#(#idents)::* };
    }

    // If it starts with our package, use relative path
    if type_name.starts_with(package) {
        let relative_parts: Vec<_> = parts[package.split('.').count()..].to_vec();
        let idents: Vec<_> = relative_parts
            .iter()
            .map(|p| format_ident!("{}", p))
            .collect();
        quote! { #(#idents)::* }
    } else {
        // Use absolute path
        let idents: Vec<_> = parts.iter().map(|p| format_ident!("{}", p)).collect();
        quote! { #(#idents)::* }
    }
}

/// Generate complete code for a service (server + client)
pub fn generate_service_code(service: &ServiceDef) -> Result<String> {
    let server_trait = server_gen::generate_server_trait(service)?;
    let client_stub = client_gen::generate_client_stub(service)?;
    let router_impl = server_gen::generate_router_impl(service)?;

    let combined = quote! {
        #server_trait

        #client_stub

        #router_impl
    };

    // Format the generated code
    let syntax_tree = syn::parse2(combined)?;
    let formatted = prettyplease::unparse(&syntax_tree);

    Ok(formatted)
}