use std::{error::Error, fs::File, io::Write};
use indexmap::IndexMap;
use quote::ToTokens;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Serialize;
use syn::LitStr;
use zebra_rpc::methods::{trees::GetTreestate, *};
const SERVER: &str = "http://localhost:8232";
#[derive(Serialize, Debug)]
struct Methods {
paths: IndexMap<String, IndexMap<String, MethodConfig>>,
}
#[derive(Serialize, Clone, Debug)]
struct MethodConfig {
tags: Vec<String>,
description: String,
#[serde(rename = "requestBody")]
request_body: RequestBody,
responses: IndexMap<String, Response>,
}
#[derive(Serialize, Clone, Debug)]
struct RequestBody {
required: bool,
content: Content,
}
#[derive(Serialize, Clone, Debug)]
struct Content {
#[serde(rename = "application/json")]
application_json: Application,
}
#[derive(Serialize, Clone, Debug)]
struct Application {
schema: Schema,
}
#[derive(Serialize, Clone, Debug)]
struct Schema {
#[serde(rename = "type")]
type_: String,
properties: IndexMap<String, Property>,
}
#[derive(Serialize, Clone, Debug)]
struct Property {
#[serde(rename = "type")]
type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
items: Option<ArrayItems>,
default: String,
}
#[derive(Serialize, Clone, Debug)]
struct Response {
description: String,
content: Content,
}
#[derive(Serialize, Clone, Debug)]
struct ArrayItems {}
fn main() -> Result<(), Box<dyn Error>> {
let current_path = env!("CARGO_MANIFEST_DIR");
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",
),
];
let mut methods = IndexMap::new();
for zebra_rpc_methods_path in paths {
let source_code = std::fs::read_to_string(zebra_rpc_methods_path.0)?;
let syn_file = syn::parse_file(&source_code)?;
let mut methods_config = IndexMap::new();
for item in &syn_file.items {
if let syn::Item::Trait(trait_item) = item {
if trait_item.ident == zebra_rpc_methods_path.1 {
for trait_item in &trait_item.items {
let method_name = method_name(trait_item)?;
let (method_doc, mut description) = method_doc(trait_item)?;
let request_type = "post".to_string();
let tags = tags(&method_doc)?;
let mut parameters_example = "[]".to_string();
if let Ok((params_description, params_example)) = get_params(&method_doc) {
description =
add_params_to_description(&description, ¶ms_description);
parameters_example = params_example;
}
let request_body = create_request_body(&method_name, ¶meters_example);
let mut have_parameters = true;
if parameters_example == "[]" {
have_parameters = false;
}
let responses = create_responses(&method_name, have_parameters)?;
methods_config.insert(
request_type,
MethodConfig {
tags,
description,
request_body,
responses,
},
);
methods.insert(format!("/{}", method_name), methods_config.clone());
}
}
}
}
}
let all_methods = Methods { paths: methods };
let yml_string = serde_yml::to_string(&all_methods)?;
let mut w = File::create("openapi.yaml")?;
w.write_all(format!("{}{}", create_yaml(), yml_string).as_bytes())?;
Ok(())
}
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)
}
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();
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)
}
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 {
let doc_attrs: Vec<_> = method
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
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());
}
});
}
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.trim_end_matches("\"]").to_string()))
}
fn tags(method_doc: &[String]) -> Result<Vec<String>, Box<dyn Error>> {
let tags_line = method_doc
.iter()
.find(|line| line.contains("tags:"))
.ok_or("Tags not found in method documentation")?;
let mut tags = Vec::new();
let tags_str = tags_line
.split(':')
.nth(1)
.ok_or("Invalid tags line")?
.trim();
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)
}
fn get_params(method_doc: &[String]) -> Result<(String, String), Box<dyn Error>> {
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 let (Some((params_index, _)), Some((notes_index, _))) =
(params_start_index, notes_start_index)
{
let params = &method_doc[params_index + 2..notes_index - 1];
let mut param_descriptions = Vec::new();
let mut param_examples = Vec::new();
for param_line in params {
if param_line.trim().starts_with("# [doc = \" -") {
if let Some((name, description)) = extract_param_info(param_line) {
param_descriptions.push(format!("- `{}` - {}", name, description));
if let Some(example) = extract_param_example(param_line) {
param_examples.push(example);
}
}
}
}
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())
}
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))
}
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
}
}
fn create_request_body(method_name: &str, parameters_example: &str) -> RequestBody {
let method_name_prop = Property {
type_: "string".to_string(),
items: None,
default: method_name.to_string(),
};
let rand_string: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(10)
.map(char::from)
.collect();
let request_id_prop = Property {
type_: "string".to_string(),
items: None,
default: rand_string,
};
let mut schema = IndexMap::new();
schema.insert("method".to_string(), method_name_prop);
schema.insert("id".to_string(), request_id_prop);
let default = parameters_example.replace('\\', "");
schema.insert(
"params".to_string(),
Property {
type_: "array".to_string(),
items: Some(ArrayItems {}),
default,
},
);
let content = Content {
application_json: Application {
schema: Schema {
type_: "object".to_string(),
properties: schema,
},
},
};
RequestBody {
required: true,
content,
}
}
fn create_responses(
method_name: &str,
have_parameters: bool,
) -> Result<IndexMap<String, Response>, Box<dyn Error>> {
let mut responses = IndexMap::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 = IndexMap::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)
}
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
}
fn default_property<T: serde::Serialize>(
type_: &str,
items: Option<ArrayItems>,
default_value: T,
) -> Result<Property, Box<dyn Error>> {
Ok(Property {
type_: type_.to_string(),
items,
default: serde_json::to_string(&default_value)?,
})
}
fn get_default_properties(method_name: &str) -> Result<IndexMap<String, Property>, Box<dyn Error>> {
let type_ = "object";
let items = None;
let mut props = IndexMap::new();
let default_result = match method_name {
"getnetworkhashps" => default_property(type_, items.clone(), u64::default())?,
"getblocksubsidy" => default_property(
type_,
items.clone(),
get_block_template_rpcs::types::subsidy::BlockSubsidy::default(),
)?,
"getmininginfo" => default_property(
type_,
items.clone(),
get_block_template_rpcs::types::get_mining_info::Response::default(),
)?,
"getnetworksolps" => default_property(type_, items.clone(), u64::default())?,
"submitblock" => default_property(
type_,
items.clone(),
get_block_template_rpcs::types::submit_block::Response::default(),
)?,
"validateaddress" => default_property(
type_,
items.clone(),
get_block_template_rpcs::types::validate_address::Response::default(),
)?,
"z_validateaddress" => default_property(
type_,
items.clone(),
get_block_template_rpcs::types::z_validate_address::Response::default(),
)?,
"getaddressbalance" => default_property(type_, items.clone(), AddressBalance::default())?,
"getaddressutxos" => default_property(type_, items.clone(), GetAddressUtxos::default())?,
"getaddresstxids" => default_property(type_, items.clone(), Vec::<String>::default())?,
"getpeerinfo" => default_property(
type_,
items.clone(),
get_block_template_rpcs::types::peer_info::PeerInfo::default(),
)?,
"getdifficulty" => default_property(type_, items.clone(), f64::default())?,
"getblockchaininfo" => {
default_property(type_, items.clone(), GetBlockChainInfo::default())?
}
"getrawmempool" => default_property(type_, items.clone(), Vec::<String>::default())?,
"getblockhash" => default_property(type_, items.clone(), GetBlockHash::default())?,
"z_getsubtreesbyindex" => {
default_property(type_, items.clone(), trees::GetSubtrees::default())?
}
"z_gettreestate" => default_property(type_, items.clone(), GetTreestate::default())?,
"getblockcount" => default_property(type_, items.clone(), u32::default())?,
"getbestblockhash" => default_property(type_, items.clone(), GetBlockHash::default())?,
"getblock" => default_property(type_, items.clone(), GetBlock::default())?,
"z_listunifiedreceivers" => default_property(
type_,
items.clone(),
get_block_template_rpcs::types::unified_address::Response::default(),
)?,
"getinfo" => default_property(type_, items.clone(), GetInfo::default())?,
"stop" => default_property(type_, items.clone(), ())?,
"sendrawtransaction" => {
default_property(type_, items.clone(), SentTransactionHash::default())?
}
"getrawtransaction" => {
default_property(type_, items.clone(), GetRawTransaction::default())?
}
_ => Property {
type_: type_.to_string(),
items: None,
default: "{}".to_string(),
},
};
props.insert("result".to_string(), default_result);
Ok(props)
}