545 lines
17 KiB
Rust
545 lines
17 KiB
Rust
//! 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<String, HashMap<String, MethodConfig>>,
|
|
}
|
|
|
|
// The configuration for each method
|
|
#[derive(Serialize, Clone, Debug)]
|
|
struct MethodConfig {
|
|
tags: Vec<String>,
|
|
description: String,
|
|
#[serde(rename = "requestBody")]
|
|
request_body: RequestBody,
|
|
responses: HashMap<String, Response>,
|
|
}
|
|
|
|
// 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<String, Property>,
|
|
}
|
|
|
|
// 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<ArrayItems>,
|
|
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<dyn Error>> {
|
|
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<String, Box<dyn Error>> {
|
|
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::<LitStr>()?.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>, String), Box<dyn Error>> {
|
|
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<Vec<String>, Box<dyn Error>> {
|
|
// 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<dyn Error>> {
|
|
// 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<String> {
|
|
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<HashMap<String, Response>, Box<dyn Error>> {
|
|
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<HashMap<String, Property>, Box<dyn Error>> {
|
|
// 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)
|
|
}
|