//! Generate an openapi.yaml file from the Zebra RPC methods use std::{collections::HashMap, error::Error, fs::File, io::Write}; use quote::ToTokens; use serde::Serialize; use syn::LitStr; use zebra_rpc::methods::*; // The API server const SERVER: &str = "http://localhost:8232"; // The API methods #[derive(Serialize, Debug)] struct Methods { paths: HashMap>, } // The configuration for each method #[derive(Serialize, Clone, Debug)] struct MethodConfig { tags: Vec, description: String, #[serde(rename = "requestBody")] request_body: RequestBody, responses: HashMap, } // The request body #[derive(Serialize, Clone, Debug)] struct RequestBody { required: bool, content: Content, } // The content of the request body #[derive(Serialize, Clone, Debug)] struct Content { #[serde(rename = "application/json")] application_json: Application, } // The application of the request body #[derive(Serialize, Clone, Debug)] struct Application { schema: Schema, } // The schema of the request body #[derive(Serialize, Clone, Debug)] struct Schema { #[serde(rename = "type")] type_: String, properties: HashMap, } // The properties of the request body #[derive(Serialize, Clone, Debug)] struct Property { #[serde(rename = "type")] type_: String, #[serde(skip_serializing_if = "Option::is_none")] items: Option, default: String, } // The response #[derive(Serialize, Clone, Debug)] struct Response { description: String, content: Content, } // The array items #[derive(Serialize, Clone, Debug)] struct ArrayItems {} fn main() -> Result<(), Box> { let current_path = env!("CARGO_MANIFEST_DIR"); // Define the paths to the Zebra RPC methods let paths = vec![ ( format!("{}/../zebra-rpc/src/methods.rs", current_path), "Rpc", ), ( format!( "{}/../zebra-rpc/src/methods/get_block_template_rpcs.rs", current_path ), "GetBlockTemplateRpc", ), ]; // Create a hashmap to store the method names and configuration let mut methods = HashMap::new(); for zebra_rpc_methods_path in paths { // Read the source code from the file let source_code = std::fs::read_to_string(zebra_rpc_methods_path.0)?; // Parse the source code into a syn AST let syn_file = syn::parse_file(&source_code)?; // Create a hashmap to store the methods configuration let mut methods_config = HashMap::new(); // Iterate over items in the file looking for traits for item in &syn_file.items { if let syn::Item::Trait(trait_item) = item { // Check if this trait is the one we're interested in if trait_item.ident == zebra_rpc_methods_path.1 { // Iterate over the trait items looking for methods for trait_item in &trait_item.items { // Extract method name let method_name = method_name(trait_item)?; // Extract method documentation and description let (method_doc, mut description) = method_doc(trait_item)?; // Request type. TODO: All methods are POST so we just hardcode it let request_type = "post".to_string(); // Tags. TODO: We are assuming 1 tag per call for now let tags = tags(&method_doc)?; // Parameters let mut parameters_example = "[]".to_string(); if let Ok((params_description, params_example)) = get_params(&method_doc) { // Add parameters to method description: description = add_params_to_description(&description, ¶ms_description); // The Zebra API uses a `params` array to pass arguments to the RPC methods, // so we need to add this to the OpenAPI spec instead of `parameters` parameters_example = params_example; } // Create the request body let request_body = create_request_body(&method_name, ¶meters_example); // Check if we have parameters let mut have_parameters = true; if parameters_example == "[]" { have_parameters = false; } // Create the responses let responses = create_responses(&method_name, have_parameters)?; // Add the method configuration to the hashmap methods_config.insert( request_type, MethodConfig { tags, description, request_body, responses, }, ); // Add the method name and configuration to the hashmap methods.insert(format!("/{}", method_name), methods_config.clone()); } } } } } // Create a struct to hold all the methods let all_methods = Methods { paths: methods }; // Add openapi header and write to file let yaml_string = serde_yaml::to_string(&all_methods)?; let mut w = File::create("openapi.yaml")?; w.write_all(format!("{}{}", create_yaml(), yaml_string).as_bytes())?; Ok(()) } // Create the openapi.yaml header fn create_yaml() -> String { format!("openapi: 3.0.3 info: title: Swagger Zebra API - OpenAPI 3.0 version: 0.0.1 description: |- This is the Zebra API. It is a JSON-RPC 2.0 API that allows you to interact with the Zebra node. Useful links: - [The Zebra repository](https://github.com/ZcashFoundation/zebra) - [The latests API spec](https://github.com/ZcashFoundation/zebra/blob/main/openapi.yaml) servers: - url: {} ", SERVER) } // Extract the method name from the trait item fn method_name(trait_item: &syn::TraitItem) -> Result> { let mut method_name = "".to_string(); if let syn::TraitItem::Fn(method) = trait_item { method_name = method.sig.ident.to_string(); // Refine name if needed method.attrs.iter().for_each(|attr| { if attr.path().is_ident("rpc") { let _ = attr.parse_nested_meta(|meta| { method_name = meta.value()?.parse::()?.value(); Ok(()) }); } }); } Ok(method_name) } // Return the method docs array and the description of the method fn method_doc(method: &syn::TraitItem) -> Result<(Vec, String), Box> { let mut method_doc = vec![]; if let syn::TraitItem::Fn(method) = method { // Filter only doc attributes let doc_attrs: Vec<_> = method .attrs .iter() .filter(|attr| attr.path().is_ident("doc")) .collect(); // If no doc attributes found, return an error if doc_attrs.is_empty() { return Err("No documentation attribute found for the method".into()); } method.attrs.iter().for_each(|attr| { if attr.path().is_ident("doc") { method_doc.push(attr.to_token_stream().to_string()); } }); } // Extract the description from the first line of documentation let description = match method_doc[0].split_once('"') { Some((_, desc)) => desc.trim().to_string().replace('\'', "''"), None => return Err("Description not found in method documentation".into()), }; Ok((method_doc, description)) } // Extract the tags from the method documentation. TODO: Assuming 1 tag per method for now fn tags(method_doc: &[String]) -> Result, Box> { // Find the line containing tags information let tags_line = method_doc .iter() .find(|line| line.contains("tags:")) .ok_or("Tags not found in method documentation")?; // Extract tags from the tags line let mut tags = Vec::new(); let tags_str = tags_line .split(':') .nth(1) .ok_or("Invalid tags line")? .trim(); // Split the tags string into individual tags for tag in tags_str.split(',') { let trimmed_tag = tag.trim_matches(|c: char| !c.is_alphanumeric()); if !trimmed_tag.is_empty() { tags.push(trimmed_tag.to_string()); } } Ok(tags) } // Extract the parameters from the method documentation fn get_params(method_doc: &[String]) -> Result<(String, String), Box> { // Find the start and end index of the parameters let params_start_index = method_doc .iter() .enumerate() .find(|(_, line)| line.contains("# Parameters")); let notes_start_index = method_doc .iter() .enumerate() .find(|(_, line)| line.contains("# Notes")); // If start and end indices of parameters are found, extract them if let (Some((params_index, _)), Some((notes_index, _))) = (params_start_index, notes_start_index) { let params = &method_doc[params_index + 2..notes_index - 1]; // Initialize variables to store parameter descriptions and examples let mut param_descriptions = Vec::new(); let mut param_examples = Vec::new(); // Iterate over the parameters and extract information for param_line in params { // Check if the line starts with the expected format if param_line.trim().starts_with("# [doc = \" -") { // Extract parameter name and description if let Some((name, description)) = extract_param_info(param_line) { param_descriptions.push(format!("- `{}` - {}", name, description)); // Extract parameter example if available if let Some(example) = extract_param_example(param_line) { param_examples.push(example); } } } } // Format parameters and examples let params_formatted = format!("[{}]", param_examples.join(", ")); let params_description = param_descriptions.join("\n"); return Ok((params_description, params_formatted)); } Err("No parameters found".into()) } // Extract parameter name and description fn extract_param_info(param_line: &str) -> Option<(String, String)> { let start_idx = param_line.find('`')?; let end_idx = param_line.rfind('`')?; let name = param_line[start_idx + 1..end_idx].trim().to_string(); let description_starts = param_line.find(") ")?; let description_ends = param_line.rfind("\"]")?; let description = param_line[description_starts + 2..description_ends] .trim() .to_string(); Some((name, description)) } // Extract parameter example if available fn extract_param_example(param_line: &str) -> Option { if let Some(example_start) = param_line.find("example=") { let example_ends = param_line.rfind(')')?; let example = param_line[example_start + 8..example_ends].trim(); Some(example.to_string()) } else { None } } // Create the request body fn create_request_body(method_name: &str, parameters_example: &str) -> RequestBody { // Add the method name to the request body let method_name_prop = Property { type_: "string".to_string(), items: None, default: method_name.to_string(), }; // Add a hardcoded request_id to the request body let request_id_prop = Property { type_: "number".to_string(), items: None, default: "123".to_string(), }; // Create the schema and add the first 2 properties let mut schema = HashMap::new(); schema.insert("method".to_string(), method_name_prop); schema.insert("id".to_string(), request_id_prop); // Add the parameters with the extracted examples let default = parameters_example.replace('\\', ""); schema.insert( "params".to_string(), Property { type_: "array".to_string(), items: Some(ArrayItems {}), default, }, ); // Create the request body let content = Content { application_json: Application { schema: Schema { type_: "object".to_string(), properties: schema, }, }, }; RequestBody { required: true, content, } } // Create the responses fn create_responses( method_name: &str, have_parameters: bool, ) -> Result, Box> { let mut responses = HashMap::new(); let properties = get_default_properties(method_name)?; let res_ok = Response { description: "OK".to_string(), content: Content { application_json: Application { schema: Schema { type_: "object".to_string(), properties, }, }, }, }; responses.insert("200".to_string(), res_ok); let mut properties = HashMap::new(); if have_parameters { properties.insert( "error".to_string(), Property { type_: "string".to_string(), items: None, default: "Invalid parameters".to_string(), }, ); let res_bad_request = Response { description: "Bad request".to_string(), content: Content { application_json: Application { schema: Schema { type_: "object".to_string(), properties, }, }, }, }; responses.insert("400".to_string(), res_bad_request); } Ok(responses) } // Add the parameters to the method description fn add_params_to_description(description: &str, params_description: &str) -> String { let mut new_description = description.to_string(); new_description.push_str("\n\n**Request body `params` arguments:**\n\n"); new_description.push_str(params_description); new_description } // Get requests examples by using defaults from the Zebra RPC methods fn get_default_properties(method_name: &str) -> Result, Box> { // TODO: Complete the list of methods let type_ = "object".to_string(); let items = None; let mut props = HashMap::new(); let properties = match method_name { "getinfo" => { props.insert( "result".to_string(), Property { type_, items, default: serde_json::to_string(&GetInfo::default())?, }, ); props } "getbestblockhash" => { props.insert( "result".to_string(), Property { type_, items, default: serde_json::to_string(&GetBlockHash::default())?, }, ); props } "getblockchaininfo" => { props.insert( "result".to_string(), Property { type_, items, default: serde_json::to_string(&GetBlockChainInfo::default())?, }, ); props } "getblock" => { props.insert( "result".to_string(), Property { type_, items, default: serde_json::to_string(&GetBlock::default())?, }, ); props } "getblockhash" => { props.insert( "result".to_string(), Property { type_, items, default: serde_json::to_string(&GetBlockHash::default())?, }, ); props } "z_gettreestate" => { props.insert( "result".to_string(), Property { type_, items, default: serde_json::to_string(&GetTreestate::default())?, }, ); props } _ => { props.insert( "result".to_string(), Property { type_, items: None, default: "{}".to_string(), }, ); props } }; Ok(properties) }