1use std::{error::Error, fs::File, io::Write};
4
5use indexmap::IndexMap;
6use quote::ToTokens;
7use rand::{distributions::Alphanumeric, thread_rng, Rng};
8use serde::Serialize;
9use syn::LitStr;
10
11use zebra_rpc::methods::{trees::GetTreestate, *};
12
13use types::{get_mining_info, submit_block, subsidy, validate_address, z_validate_address};
14
15const SERVER: &str = "http://localhost:8232";
17
18#[derive(Serialize, Debug)]
20struct Methods {
21 paths: IndexMap<String, IndexMap<String, MethodConfig>>,
22}
23
24#[derive(Serialize, Clone, Debug)]
26struct MethodConfig {
27 tags: Vec<String>,
28 description: String,
29 #[serde(rename = "requestBody")]
30 request_body: RequestBody,
31 responses: IndexMap<String, Response>,
32}
33
34#[derive(Serialize, Clone, Debug)]
36struct RequestBody {
37 required: bool,
38 content: Content,
39}
40
41#[derive(Serialize, Clone, Debug)]
43struct Content {
44 #[serde(rename = "application/json")]
45 application_json: Application,
46}
47
48#[derive(Serialize, Clone, Debug)]
50struct Application {
51 schema: Schema,
52}
53
54#[derive(Serialize, Clone, Debug)]
56struct Schema {
57 #[serde(rename = "type")]
58 type_: String,
59 properties: IndexMap<String, Property>,
60}
61
62#[derive(Serialize, Clone, Debug)]
64struct Property {
65 #[serde(rename = "type")]
66 type_: String,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 items: Option<ArrayItems>,
69 default: String,
70}
71
72#[derive(Serialize, Clone, Debug)]
74struct Response {
75 description: String,
76 content: Content,
77}
78
79#[derive(Serialize, Clone, Debug)]
81struct ArrayItems {}
82
83fn main() -> Result<(), Box<dyn Error>> {
84 let current_path = env!("CARGO_MANIFEST_DIR");
85
86 let paths = vec![(
88 format!("{}/../zebra-rpc/src/methods.rs", current_path),
89 "Rpc",
90 )];
91
92 let mut methods = IndexMap::new();
94
95 for zebra_rpc_methods_path in paths {
96 let source_code = std::fs::read_to_string(zebra_rpc_methods_path.0)?;
98
99 let syn_file = syn::parse_file(&source_code)?;
101
102 let mut methods_config = IndexMap::new();
104
105 for item in &syn_file.items {
107 if let syn::Item::Trait(trait_item) = item {
108 if trait_item.ident == zebra_rpc_methods_path.1 {
110 for trait_item in &trait_item.items {
112 let method_name = method_name(trait_item)?;
114
115 let (method_doc, mut description) = method_doc(trait_item)?;
117
118 let request_type = "post".to_string();
120
121 let tags = tags(&method_doc)?;
123
124 let mut parameters_example = "[]".to_string();
126 if let Ok((params_description, params_example)) = get_params(&method_doc) {
127 description =
129 add_params_to_description(&description, ¶ms_description);
130 parameters_example = params_example;
133 }
134
135 let request_body = create_request_body(&method_name, ¶meters_example);
137
138 let mut have_parameters = true;
140 if parameters_example == "[]" {
141 have_parameters = false;
142 }
143
144 let responses = create_responses(&method_name, have_parameters)?;
146
147 methods_config.insert(
149 request_type,
150 MethodConfig {
151 tags,
152 description,
153 request_body,
154 responses,
155 },
156 );
157
158 methods.insert(format!("/{}", method_name), methods_config.clone());
160 }
161 }
162 }
163 }
164 }
165
166 let all_methods = Methods { paths: methods };
168
169 let yml_string = serde_yml::to_string(&all_methods)?;
171 let mut w = File::create("openapi.yaml")?;
172 w.write_all(format!("{}{}", create_yaml(), yml_string).as_bytes())?;
173
174 Ok(())
175}
176
177fn create_yaml() -> String {
179 format!("openapi: 3.0.3
180info:
181 title: Swagger Zebra API - OpenAPI 3.0
182 version: 0.0.1
183 description: |-
184 This is the Zebra API. It is a JSON-RPC 2.0 API that allows you to interact with the Zebra node.
185
186 Useful links:
187 - [The Zebra repository](https://github.com/ZcashFoundation/zebra)
188 - [The latests API spec](https://github.com/ZcashFoundation/zebra/blob/main/openapi.yaml)
189servers:
190 - url: {}
191", SERVER)
192}
193
194fn method_name(trait_item: &syn::TraitItem) -> Result<String, Box<dyn Error>> {
196 let mut method_name = "".to_string();
197 if let syn::TraitItem::Fn(method) = trait_item {
198 method_name = method.sig.ident.to_string();
199
200 method.attrs.iter().for_each(|attr| {
202 if attr.path().is_ident("rpc") {
203 let _ = attr.parse_nested_meta(|meta| {
204 method_name = meta.value()?.parse::<LitStr>()?.value();
205 Ok(())
206 });
207 }
208 });
209 }
210 Ok(method_name)
211}
212
213fn method_doc(method: &syn::TraitItem) -> Result<(Vec<String>, String), Box<dyn Error>> {
215 let mut method_doc = vec![];
216 if let syn::TraitItem::Fn(method) = method {
217 let doc_attrs: Vec<_> = method
219 .attrs
220 .iter()
221 .filter(|attr| attr.path().is_ident("doc"))
222 .collect();
223
224 if doc_attrs.is_empty() {
226 return Err("No documentation attribute found for the method".into());
227 }
228
229 method.attrs.iter().for_each(|attr| {
230 if attr.path().is_ident("doc") {
231 method_doc.push(attr.to_token_stream().to_string());
232 }
233 });
234 }
235
236 let description = match method_doc[0].split_once('"') {
238 Some((_, desc)) => desc.trim().to_string().replace('\'', "''"),
239 None => return Err("Description not found in method documentation".into()),
240 };
241
242 Ok((method_doc, description.trim_end_matches("\"]").to_string()))
243}
244
245fn tags(method_doc: &[String]) -> Result<Vec<String>, Box<dyn Error>> {
247 let tags_line = method_doc
249 .iter()
250 .find(|line| line.contains("tags:"))
251 .ok_or("Tags not found in method documentation")?;
252
253 let mut tags = Vec::new();
255 let tags_str = tags_line
256 .split(':')
257 .nth(1)
258 .ok_or("Invalid tags line")?
259 .trim();
260
261 for tag in tags_str.split(',') {
263 let trimmed_tag = tag.trim_matches(|c: char| !c.is_alphanumeric());
264 if !trimmed_tag.is_empty() {
265 tags.push(trimmed_tag.to_string());
266 }
267 }
268
269 Ok(tags)
270}
271
272fn get_params(method_doc: &[String]) -> Result<(String, String), Box<dyn Error>> {
274 let params_start_index = method_doc
276 .iter()
277 .enumerate()
278 .find(|(_, line)| line.contains("# Parameters"));
279 let notes_start_index = method_doc
280 .iter()
281 .enumerate()
282 .find(|(_, line)| line.contains("# Notes"));
283
284 if let (Some((params_index, _)), Some((notes_index, _))) =
286 (params_start_index, notes_start_index)
287 {
288 let params = &method_doc[params_index + 2..notes_index - 1];
289
290 let mut param_descriptions = Vec::new();
292 let mut param_examples = Vec::new();
293
294 for param_line in params {
296 if param_line.trim().starts_with("# [doc = \" -") {
298 if let Some((name, description)) = extract_param_info(param_line) {
300 param_descriptions.push(format!("- `{}` - {}", name, description));
301
302 if let Some(example) = extract_param_example(param_line) {
304 param_examples.push(example);
305 }
306 }
307 }
308 }
309
310 let params_formatted = format!("[{}]", param_examples.join(", "));
312 let params_description = param_descriptions.join("\n");
313
314 return Ok((params_description, params_formatted));
315 }
316
317 Err("No parameters found".into())
318}
319
320fn extract_param_info(param_line: &str) -> Option<(String, String)> {
322 let start_idx = param_line.find('`')?;
323 let end_idx = param_line.rfind('`')?;
324 let name = param_line[start_idx + 1..end_idx].trim().to_string();
325
326 let description_starts = param_line.find(") ")?;
327 let description_ends = param_line.rfind("\"]")?;
328 let description = param_line[description_starts + 2..description_ends]
329 .trim()
330 .to_string();
331
332 Some((name, description))
333}
334
335fn extract_param_example(param_line: &str) -> Option<String> {
337 if let Some(example_start) = param_line.find("example=") {
338 let example_ends = param_line.rfind(')')?;
339 let example = param_line[example_start + 8..example_ends].trim();
340 Some(example.to_string())
341 } else {
342 None
343 }
344}
345
346fn create_request_body(method_name: &str, parameters_example: &str) -> RequestBody {
348 let method_name_prop = Property {
350 type_: "string".to_string(),
351 items: None,
352 default: method_name.to_string(),
353 };
354
355 let rand_string: String = thread_rng()
357 .sample_iter(&Alphanumeric)
358 .take(10)
359 .map(char::from)
360 .collect();
361 let request_id_prop = Property {
362 type_: "string".to_string(),
363 items: None,
364 default: rand_string,
365 };
366
367 let mut schema = IndexMap::new();
369 schema.insert("method".to_string(), method_name_prop);
370 schema.insert("id".to_string(), request_id_prop);
371
372 let default = parameters_example.replace('\\', "");
374 schema.insert(
375 "params".to_string(),
376 Property {
377 type_: "array".to_string(),
378 items: Some(ArrayItems {}),
379 default,
380 },
381 );
382
383 let content = Content {
385 application_json: Application {
386 schema: Schema {
387 type_: "object".to_string(),
388 properties: schema,
389 },
390 },
391 };
392
393 RequestBody {
394 required: true,
395 content,
396 }
397}
398
399fn create_responses(
401 method_name: &str,
402 have_parameters: bool,
403) -> Result<IndexMap<String, Response>, Box<dyn Error>> {
404 let mut responses = IndexMap::new();
405
406 let properties = get_default_properties(method_name)?;
407
408 let res_ok = Response {
409 description: "OK".to_string(),
410 content: Content {
411 application_json: Application {
412 schema: Schema {
413 type_: "object".to_string(),
414 properties,
415 },
416 },
417 },
418 };
419 responses.insert("200".to_string(), res_ok);
420
421 let mut properties = IndexMap::new();
422 if have_parameters {
423 properties.insert(
424 "error".to_string(),
425 Property {
426 type_: "string".to_string(),
427 items: None,
428 default: "Invalid parameters".to_string(),
429 },
430 );
431 let res_bad_request = Response {
432 description: "Bad request".to_string(),
433 content: Content {
434 application_json: Application {
435 schema: Schema {
436 type_: "object".to_string(),
437 properties,
438 },
439 },
440 },
441 };
442 responses.insert("400".to_string(), res_bad_request);
443 }
444
445 Ok(responses)
446}
447
448fn add_params_to_description(description: &str, params_description: &str) -> String {
450 let mut new_description = description.to_string();
451 new_description.push_str("\n\n**Request body `params` arguments:**\n\n");
452 new_description.push_str(params_description);
453 new_description
454}
455
456fn default_property<T: serde::Serialize>(
457 type_: &str,
458 items: Option<ArrayItems>,
459 default_value: T,
460) -> Result<Property, Box<dyn Error>> {
461 Ok(Property {
462 type_: type_.to_string(),
463 items,
464 default: serde_json::to_string(&default_value)?,
465 })
466}
467
468fn get_default_properties(method_name: &str) -> Result<IndexMap<String, Property>, Box<dyn Error>> {
471 let type_ = "object";
472 let items = None;
473 let mut props = IndexMap::new();
474
475 let default_result = match method_name {
477 "getnetworkhashps" => default_property(type_, items.clone(), u64::default())?,
480 "getblocksubsidy" => {
481 default_property(type_, items.clone(), subsidy::BlockSubsidy::default())?
482 }
483 "getmininginfo" => {
484 default_property(type_, items.clone(), get_mining_info::Response::default())?
485 }
486 "getnetworksolps" => default_property(type_, items.clone(), u64::default())?,
487 "submitblock" => default_property(type_, items.clone(), submit_block::Response::default())?,
488 "validateaddress" => {
490 default_property(type_, items.clone(), validate_address::Response::default())?
491 }
492 "z_validateaddress" => default_property(
493 type_,
494 items.clone(),
495 z_validate_address::Response::default(),
496 )?,
497 "getaddressbalance" => default_property(type_, items.clone(), AddressBalance::default())?,
499 "getaddressutxos" => default_property(type_, items.clone(), GetAddressUtxos::default())?,
500 "getaddresstxids" => default_property(type_, items.clone(), Vec::<String>::default())?,
501 "getpeerinfo" => {
503 default_property(type_, items.clone(), types::peer_info::PeerInfo::default())?
504 }
505 "getdifficulty" => default_property(type_, items.clone(), f64::default())?,
507 "getblockchaininfo" => {
508 default_property(type_, items.clone(), GetBlockChainInfo::default())?
509 }
510 "getrawmempool" => default_property(type_, items.clone(), Vec::<String>::default())?,
511 "getblockhash" => default_property(type_, items.clone(), GetBlockHash::default())?,
512 "z_getsubtreesbyindex" => {
513 default_property(type_, items.clone(), trees::GetSubtrees::default())?
514 }
515 "z_gettreestate" => default_property(type_, items.clone(), GetTreestate::default())?,
516 "getblockcount" => default_property(type_, items.clone(), u32::default())?,
517 "getbestblockhash" => default_property(type_, items.clone(), GetBlockHash::default())?,
518 "getblock" => default_property(type_, items.clone(), GetBlock::default())?,
519 "z_listunifiedreceivers" => default_property(
521 type_,
522 items.clone(),
523 types::unified_address::Response::default(),
524 )?,
525 "getinfo" => default_property(type_, items.clone(), GetInfo::default())?,
527 "stop" => default_property(type_, items.clone(), ())?,
528 "sendrawtransaction" => {
530 default_property(type_, items.clone(), SentTransactionHash::default())?
531 }
532 "getrawtransaction" => {
533 default_property(type_, items.clone(), GetRawTransaction::default())?
534 }
535 _ => Property {
537 type_: type_.to_string(),
538 items: None,
539 default: "{}".to_string(),
540 },
541 };
542
543 props.insert("result".to_string(), default_result);
544 Ok(props)
545}