//! Generate Rust clients from OpenRPC documents with typify's Draft 7 converter. use quote::quote; use serde_json::{Value, json}; /// Generate a formatted private client module for a provider's document. pub fn client_rs(name: &str, doc: &Value) -> Result { fn local_refs(value: &mut Value) { match value { Value::Object(map) => { if let Some(Value::String(r)) = map.get_mut("$ref") { *r = r.replace("#/components/schemas/", "#/definitions/"); } for v in map.values_mut() { local_refs(v); } } Value::Array(a) => { for v in a { local_refs(v); } } _ => {} } } let module = syn::parse_str::(&name.replace('-', "_")) .or_else(|_| syn::parse_str::(&format!("r#{}", name.replace('-', "_")))) .map_err(|e| e.to_string())?; let mut space = typify::TypeSpace::default(); let mut defs = doc["components"]["schemas"] .as_object() .cloned() .unwrap_or_default(); for schema in defs.values_mut() { local_refs(schema); } let defs: Vec<(String, schemars08::schema::Schema)> = defs .into_iter() .map(|(n, v)| serde_json::from_value(v).map(|v| (n, v))) .collect::>() .map_err(|e| e.to_string())?; space.add_ref_types(defs).map_err(|e| e.to_string())?; fn methods( space: &mut typify::TypeSpace, provider: &str, methods: &Value, endpoint: bool, ) -> Result, String> { let mut result = vec![]; for method in methods.as_array().into_iter().flatten() { let wire = method["name"].as_str().ok_or("method name missing")?; let ident = syn::parse_str::(wire) .or_else(|_| syn::parse_str::(&format!("r#{wire}"))) .map_err(|e| e.to_string())?; let mut rs = method["result"]["schema"].clone(); local_refs(&mut rs); let empty = method["params"].as_array().is_none_or(Vec::is_empty); let p = if empty { None } else { let mut properties = serde_json::Map::new(); let mut required = vec![]; for param in method["params"].as_array().into_iter().flatten() { let name = param["name"].as_str().ok_or("parameter name missing")?; properties.insert(name.into(), param["schema"].clone()); if param["required"] == true { required.push(name); } } let mut ps = method.get("x-sausage-params").cloned().unwrap_or_else(|| { let title = wire.split('_').filter(|s| !s.is_empty()).map(|s| { let mut chars = s.chars(); chars.next().unwrap().to_uppercase().to_string() + chars.as_str() }).collect::(); json!({"title":format!("{title}Params"),"type":"object","properties":properties,"required":required}) }); local_refs(&mut ps); Some( space .add_type(&serde_json::from_value(ps).map_err(|e| e.to_string())?) .map_err(|e| e.to_string())?, ) }; let r = space .add_type(&serde_json::from_value(rs).map_err(|e| e.to_string())?) .map_err(|e| e.to_string())?; let p = p .map(|p| { space .get_type(&p) .map(|p| p.ident()) .map_err(|e| e.to_string()) }) .transpose()?; let r = space.get_type(&r).map_err(|e| e.to_string())?.ident(); let docs = method .get("description") .or_else(|| method.get("summary")) .and_then(Value::as_str) .unwrap_or(""); let arg = if empty { quote!() } else { quote!(params:#p) }; let params = if empty { quote!(::sausage_plugin::serde_json::json!({})) } else { quote!(params) }; let target = format!("{provider}.{wire}"); if endpoint { let arg = if empty { quote!() } else { quote!(, #arg) }; result.push(quote!(#[doc=#docs] #[warn(dead_code)] pub fn #ident(&self #arg)->::sausage_plugin::EndpointCall<#r>{self.0.call(#wire,#params)})); } else { result.push(quote!(#[doc=#docs] #[warn(dead_code)] pub fn #ident(#arg)->::sausage_plugin::TypedCall<#r>{::sausage_plugin::TypedCall::new(::sausage_plugin::call(#target,#params))})); } } Ok(result) } let functions = methods(&mut space, name, &doc["methods"], false)?; let mut endpoints = vec![]; for (name, description) in doc["x-sausage-endpoints"].as_object().into_iter().flatten() { let ident = syn::parse_str::(name) .or_else(|_| syn::parse_str::(&format!("r#{name}"))) .map_err(|e| e.to_string())?; let functions = methods(&mut space, "", &description["methods"], true)?; endpoints.push(quote!{pub mod #ident {use super::*;pub struct Client(::sausage_plugin::Endpoint);pub async fn connect(path:impl AsRef<::std::path::Path>)->::sausage_plugin::Result{let endpoint=::sausage_plugin::connect(path).await?;endpoint.require_interface(#name)?;Ok(Client(endpoint))} impl Client {#(#functions)*}}}); } let types = space.to_stream().to_string(); let mut file = syn::parse_file(&types).map_err(|e| e.to_string())?; fn serde_attrs(items: &mut [syn::Item]) { for item in items { match item { syn::Item::Struct(s) => s .attrs .push(syn::parse_quote!(#[serde(crate="::sausage_plugin::serde")])), syn::Item::Enum(e) => e .attrs .push(syn::parse_quote!(#[serde(crate="::sausage_plugin::serde")])), syn::Item::Mod(m) => { if let Some((_, items)) = &mut m.content { serde_attrs(items) } } _ => {} } } } // Only data types have serde derives. ConversionError is a support type. for item in &mut file.items { if let syn::Item::Mod(m) = item && m.ident == "error" { continue; } serde_attrs(std::slice::from_mut(item)); } let uses_error = file .items .iter() .filter(|item| !matches!(item, syn::Item::Mod(m) if m.ident == "error")) .any(|item| { quote!(#item) .to_string() .contains("error :: ConversionError") }); if !uses_error { file.items .retain(|item| !matches!(item, syn::Item::Mod(m) if m.ident == "error")); } let types = &file.items; let mut file = syn::parse2::(quote!(mod #module {#(#types)* #(#functions)* #(#endpoints)*})) .map_err(|e| e.to_string())?; struct Reexports; impl syn::visit_mut::VisitMut for Reexports { fn visit_path_mut(&mut self, path: &mut syn::Path) { syn::visit_mut::visit_path_mut(self, path); if path.leading_colon.is_some() && path .segments .first() .is_some_and(|s| s.ident == "serde" || s.ident == "serde_json") { path.segments.insert(0, syn::parse_quote!(sausage_plugin)); } } fn visit_attribute_mut(&mut self, attr: &mut syn::Attribute) { if attr.path().is_ident("derive") { let mut paths = attr .parse_args_with( syn::punctuated::Punctuated::::parse_terminated, ) .expect("derive paths"); for path in &mut paths { self.visit_path_mut(path); } *attr = syn::parse_quote!(#[derive(#paths)]); } else { syn::visit_mut::visit_attribute_mut(self, attr); } } } syn::visit_mut::VisitMut::visit_file_mut(&mut Reexports, &mut file); Ok(prettyplease::unparse(&file)) }