pulse-schema-derive 0.1.0-alpha.1

Rust proc-macro attributes for generating Pulse Schema Language (PSL) from native Rust types
Documentation
use syn::{DeriveInput, ItemTrait, Data, Fields, Type};
use std::fs::OpenOptions;
use std::io::Write;

fn psl_file() -> std::fs::File {
    OpenOptions::new()
        .create(true)
        .append(true)
        .open("schema.psl")
        .unwrap()
}

fn convert_type(ty: &Type) -> String {
    match ty {
        Type::Path(p) => {
            let last = p.path.segments.last().unwrap();
            let ident = last.ident.to_string();
            
            if ident == "Option" {
                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
                    if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
                        return format!("{}?", convert_type(inner));
                    }
                }
            } else if ident == "Vec" {
                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
                    if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
                        let inner_ty = convert_type(inner);
                        if inner_ty == "u8" {
                            return "bytes".to_string();
                        }
                        return format!("{}[]", inner_ty);
                    }
                }
            } else if ident == "HashMap" || ident == "BTreeMap" {
                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
                    let mut iter = args.args.iter();
                    let key_ty = if let Some(syn::GenericArgument::Type(t)) = iter.next() {
                        convert_type(t)
                    } else {
                        "any".to_string()
                    };
                    let val_ty = if let Some(syn::GenericArgument::Type(t)) = iter.next() {
                        convert_type(t)
                    } else {
                        "any".to_string()
                    };
                    return format!("map<{}, {}>", key_ty, val_ty);
                }
            } else if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
                // Generic types like Page<T> → Page<T>
                let inner_types: Vec<String> = args.args.iter()
                    .filter_map(|a| {
                        if let syn::GenericArgument::Type(t) = a {
                            Some(convert_type(t))
                        } else {
                            None
                        }
                    })
                    .collect();
                if !inner_types.is_empty() {
                    return format!("{}<{}>", ident, inner_types.join(", "));
                }
            }

            match ident.as_str() {
                "String" => "string".to_string(),
                "i32" | "i64" | "u32" | "u64" => "int".to_string(),
                "f32" | "f64" => "float".to_string(),
                "bool" => "boolean".to_string(),
                "SystemTime" => "timestamp".to_string(),
                "u8" => "u8".to_string(),
                other => other.to_string(),
            }
        },
        _ => "any".to_string(),
    }
}

pub fn write_psl_type(input: &DeriveInput) {
    let mut file = psl_file();
    let name = &input.ident;
    
    // Naive decorators check
    let mut decorators = Vec::new();
    for attr in &input.attrs {
        if attr.path.is_ident("collection") {
            decorators.push("@collection");
        }
    }

    for dec in decorators {
        writeln!(file, "{}", dec).unwrap();
    }
    
    writeln!(file, "message {} {{", name).unwrap();
    
    if let Data::Struct(data) = &input.data {
        if let Fields::Named(fields) = &data.fields {
            for f in &fields.named {
                let fname = f.ident.as_ref().unwrap();
                let ftype = convert_type(&f.ty);
                writeln!(file, "  {}: {};", fname, ftype).unwrap();
            }
        }
    }
    
    writeln!(file, "}}\n").unwrap();
}

pub fn write_psl_enum(input: &DeriveInput) {
    let mut file = psl_file();
    let name = &input.ident;
    writeln!(file, "enum {} {{", name).unwrap();
    
    if let Data::Enum(data) = &input.data {
        for variant in &data.variants {
            let vname = &variant.ident;
            match &variant.fields {
                Fields::Unit => {
                    writeln!(file, "  {},", vname).unwrap();
                }
                Fields::Named(fields) => {
                    writeln!(file, "  {} {{", vname).unwrap();
                    for f in &fields.named {
                        let fname = f.ident.as_ref().unwrap();
                        let ftype = convert_type(&f.ty);
                        writeln!(file, "    {}: {};", fname, ftype).unwrap();
                    }
                    writeln!(file, "  }},").unwrap();
                }
                Fields::Unnamed(fields) => {
                    if fields.unnamed.len() == 1 {
                        let ftype = convert_type(&fields.unnamed.first().unwrap().ty);
                        writeln!(file, "  {}({}),", vname, ftype).unwrap();
                    }
                }
            }
        }
    }
    writeln!(file, "}}\n").unwrap();
}

pub fn write_psl_error(input: &DeriveInput) {
    let mut file = psl_file();
    let name = &input.ident;
    writeln!(file, "error {} {{", name).unwrap();
    
    if let Data::Enum(data) = &input.data {
        for variant in &data.variants {
            let vname = &variant.ident;
            match &variant.fields {
                Fields::Unit => {
                    writeln!(file, "  {},", vname).unwrap();
                }
                Fields::Named(fields) => {
                    writeln!(file, "  {} {{", vname).unwrap();
                    for f in &fields.named {
                        let fname = f.ident.as_ref().unwrap();
                        let ftype = convert_type(&f.ty);
                        writeln!(file, "    {}: {};", fname, ftype).unwrap();
                    }
                    writeln!(file, "  }},").unwrap();
                }
                Fields::Unnamed(fields) => {
                    if fields.unnamed.len() == 1 {
                        let ftype = convert_type(&fields.unnamed.first().unwrap().ty);
                        writeln!(file, "  {}({}),", vname, ftype).unwrap();
                    }
                }
            }
        }
    }
    writeln!(file, "}}\n").unwrap();
}

pub fn write_psl_service(input: &ItemTrait) {
    let mut file = psl_file();
    let name = &input.ident;
    writeln!(file, "service {} {{", name).unwrap();
    
    for item in &input.items {
        if let syn::TraitItem::Method(method) = item {
            let mname = &method.sig.ident;
            
            // Check for #[auth(...)] attribute
            for attr in &method.attrs {
                if attr.path.is_ident("auth") {
                    if let Ok(syn::Meta::List(meta_list)) = attr.parse_meta() {
                        if let Some(syn::NestedMeta::Lit(syn::Lit::Str(lit))) = meta_list.nested.first() {
                            writeln!(file, "  @auth(\"{}\")", lit.value()).unwrap();
                        }
                    }
                }
            }
            
            // Extract arguments (skip &self)
            let mut args = Vec::new();
            for arg in &method.sig.inputs {
                if let syn::FnArg::Typed(pat_type) = arg {
                    if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
                        let arg_name = pat_ident.ident.to_string();
                        let arg_type = convert_type(&pat_type.ty);
                        args.push(format!("{}: {}", arg_name, arg_type));
                    }
                }
            }
            let args_str = args.join(", ");
            
            // Extract return type
            let ret_str = match &method.sig.output {
                syn::ReturnType::Default => String::new(),
                syn::ReturnType::Type(_, ty) => {
                    // Check for Result<T, E>
                    if let Type::Path(p) = ty.as_ref() {
                        let last = p.path.segments.last().unwrap();
                        if last.ident == "Result" {
                            if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
                                let mut iter = args.args.iter();
                                let ok_type = if let Some(syn::GenericArgument::Type(t)) = iter.next() {
                                    convert_type(t)
                                } else {
                                    "void".to_string()
                                };
                                let err_type = if let Some(syn::GenericArgument::Type(t)) = iter.next() {
                                    convert_type(t)
                                } else {
                                    String::new()
                                };
                                if err_type.is_empty() {
                                    format!(" -> {}", ok_type)
                                } else {
                                    format!(" -> {} ! {}", ok_type, err_type)
                                }
                            } else {
                                String::new()
                            }
                        } else {
                            format!(" -> {}", convert_type(ty))
                        }
                    } else {
                        format!(" -> {}", convert_type(ty))
                    }
                }
            };
            
            writeln!(file, "  rpc {}({}){};", mname, args_str, ret_str).unwrap();
        }
    }
    writeln!(file, "}}\n").unwrap();
}