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::client::*;
12
13const SERVER: &str = "http://localhost:8232";
15
16#[derive(Serialize, Debug)]
18struct Methods {
19 paths: IndexMap<String, IndexMap<String, MethodConfig>>,
20}
21
22#[derive(Serialize, Clone, Debug)]
24struct MethodConfig {
25 tags: Vec<String>,
26 description: String,
27 #[serde(rename = "requestBody")]
28 request_body: RequestBody,
29 responses: IndexMap<String, Response>,
30}
31
32#[derive(Serialize, Clone, Debug)]
34struct RequestBody {
35 required: bool,
36 content: Content,
37}
38
39#[derive(Serialize, Clone, Debug)]
41struct Content {
42 #[serde(rename = "application/json")]
43 application_json: Application,
44}
45
46#[derive(Serialize, Clone, Debug)]
48struct Application {
49 schema: Schema,
50}
51
52#[derive(Serialize, Clone, Debug)]
54struct Schema {
55 #[serde(rename = "type")]
56 type_: String,
57 properties: IndexMap<String, Property>,
58}
59
60#[derive(Serialize, Clone, Debug)]
62struct Property {
63 #[serde(rename = "type")]
64 type_: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 items: Option<ArrayItems>,
67 default: String,
68}
69
70#[derive(Serialize, Clone, Debug)]
72struct Response {
73 description: String,
74 content: Content,
75}
76
77#[derive(Serialize, Clone, Debug)]
79struct ArrayItems {}
80
81fn main() -> Result<(), Box<dyn Error>> {
82 let current_path = env!("CARGO_MANIFEST_DIR");
83
84 let paths = vec![(format!("{current_path}/../zebra-rpc/src/methods.rs"), "Rpc")];
86
87 let mut methods = IndexMap::new();
89
90 for zebra_rpc_methods_path in paths {
91 let source_code = std::fs::read_to_string(zebra_rpc_methods_path.0)?;
93
94 let syn_file = syn::parse_file(&source_code)?;
96
97 let mut methods_config = IndexMap::new();
99
100 for item in &syn_file.items {
102 if let syn::Item::Trait(trait_item) = item {
103 if trait_item.ident == zebra_rpc_methods_path.1 {
105 for trait_item in &trait_item.items {
107 let method_name = method_name(trait_item)?;
109
110 let (method_doc, mut description) = method_doc(trait_item)?;
112
113 let request_type = "post".to_string();
115
116 let tags = tags(&method_doc)?;
118
119 let mut parameters_example = "[]".to_string();
121 if let Ok((params_description, params_example)) = get_params(&method_doc) {
122 description =
124 add_params_to_description(&description, ¶ms_description);
125 parameters_example = params_example;
128 }
129
130 let request_body = create_request_body(&method_name, ¶meters_example);
132
133 let mut have_parameters = true;
135 if parameters_example == "[]" {
136 have_parameters = false;
137 }
138
139 let responses = create_responses(&method_name, have_parameters)?;
141
142 methods_config.insert(
144 request_type,
145 MethodConfig {
146 tags,
147 description,
148 request_body,
149 responses,
150 },
151 );
152
153 methods.insert(format!("/{method_name}"), methods_config.clone());
155 }
156 }
157 }
158 }
159 }
160
161 let all_methods = Methods { paths: methods };
163
164 let yml_string = serde_yml::to_string(&all_methods)?;
166 let mut w = File::create("openapi.yaml")?;
167 w.write_all(format!("{}{}", create_yaml(), yml_string).as_bytes())?;
168
169 Ok(())
170}
171
172fn create_yaml() -> String {
174 format!("openapi: 3.0.3
175info:
176 title: Swagger Zebra API - OpenAPI 3.0
177 version: 0.0.1
178 description: |-
179 This is the Zebra API. It is a JSON-RPC 2.0 API that allows you to interact with the Zebra node.
180
181 Useful links:
182 - [The Zebra repository](https://github.com/ZcashFoundation/zebra)
183 - [The latests API spec](https://github.com/ZcashFoundation/zebra/blob/main/openapi.yaml)
184servers:
185 - url: {SERVER}
186")
187}
188
189fn method_name(trait_item: &syn::TraitItem) -> Result<String, Box<dyn Error>> {
191 let mut method_name = "".to_string();
192 if let syn::TraitItem::Fn(method) = trait_item {
193 method_name = method.sig.ident.to_string();
194
195 method.attrs.iter().for_each(|attr| {
197 if attr.path().is_ident("rpc") {
198 let _ = attr.parse_nested_meta(|meta| {
199 method_name = meta.value()?.parse::<LitStr>()?.value();
200 Ok(())
201 });
202 }
203 });
204 }
205 Ok(method_name)
206}
207
208fn method_doc(method: &syn::TraitItem) -> Result<(Vec<String>, String), Box<dyn Error>> {
210 let mut method_doc = vec![];
211 if let syn::TraitItem::Fn(method) = method {
212 let doc_attrs: Vec<_> = method
214 .attrs
215 .iter()
216 .filter(|attr| attr.path().is_ident("doc"))
217 .collect();
218
219 if doc_attrs.is_empty() {
221 return Err("No documentation attribute found for the method".into());
222 }
223
224 method.attrs.iter().for_each(|attr| {
225 if attr.path().is_ident("doc") {
226 method_doc.push(attr.to_token_stream().to_string());
227 }
228 });
229 }
230
231 let description = match method_doc[0].split_once('"') {
233 Some((_, desc)) => desc.trim().to_string().replace('\'', "''"),
234 None => return Err("Description not found in method documentation".into()),
235 };
236
237 Ok((method_doc, description.trim_end_matches("\"]").to_string()))
238}
239
240fn tags(method_doc: &[String]) -> Result<Vec<String>, Box<dyn Error>> {
242 let tags_line = method_doc
244 .iter()
245 .find(|line| line.contains("tags:"))
246 .ok_or("Tags not found in method documentation")?;
247
248 let mut tags = Vec::new();
250 let tags_str = tags_line
251 .split(':')
252 .nth(1)
253 .ok_or("Invalid tags line")?
254 .trim();
255
256 for tag in tags_str.split(',') {
258 let trimmed_tag = tag.trim_matches(|c: char| !c.is_alphanumeric());
259 if !trimmed_tag.is_empty() {
260 tags.push(trimmed_tag.to_string());
261 }
262 }
263
264 Ok(tags)
265}
266
267fn get_params(method_doc: &[String]) -> Result<(String, String), Box<dyn Error>> {
269 let params_start_index = method_doc
271 .iter()
272 .enumerate()
273 .find(|(_, line)| line.contains("# Parameters"));
274 let notes_start_index = method_doc
275 .iter()
276 .enumerate()
277 .find(|(_, line)| line.contains("# Notes"));
278
279 if let (Some((params_index, _)), Some((notes_index, _))) =
281 (params_start_index, notes_start_index)
282 {
283 let params = &method_doc[params_index + 2..notes_index - 1];
284
285 let mut param_descriptions = Vec::new();
287 let mut param_examples = Vec::new();
288
289 for param_line in params {
291 if param_line.trim().starts_with("# [doc = \" -") {
293 if let Some((name, description)) = extract_param_info(param_line) {
295 param_descriptions.push(format!("- `{name}` - {description}"));
296
297 if let Some(example) = extract_param_example(param_line) {
299 param_examples.push(example);
300 }
301 }
302 }
303 }
304
305 let params_formatted = format!("[{}]", param_examples.join(", "));
307 let params_description = param_descriptions.join("\n");
308
309 return Ok((params_description, params_formatted));
310 }
311
312 Err("No parameters found".into())
313}
314
315fn extract_param_info(param_line: &str) -> Option<(String, String)> {
317 let start_idx = param_line.find('`')?;
318 let end_idx = param_line.rfind('`')?;
319 let name = param_line[start_idx + 1..end_idx].trim().to_string();
320
321 let description_starts = param_line.find(") ")?;
322 let description_ends = param_line.rfind("\"]")?;
323 let description = param_line[description_starts + 2..description_ends]
324 .trim()
325 .to_string();
326
327 Some((name, description))
328}
329
330fn extract_param_example(param_line: &str) -> Option<String> {
332 if let Some(example_start) = param_line.find("example=") {
333 let example_ends = param_line.rfind(')')?;
334 let example = param_line[example_start + 8..example_ends].trim();
335 Some(example.to_string())
336 } else {
337 None
338 }
339}
340
341fn create_request_body(method_name: &str, parameters_example: &str) -> RequestBody {
343 let method_name_prop = Property {
345 type_: "string".to_string(),
346 items: None,
347 default: method_name.to_string(),
348 };
349
350 let rand_string: String = thread_rng()
352 .sample_iter(&Alphanumeric)
353 .take(10)
354 .map(char::from)
355 .collect();
356 let request_id_prop = Property {
357 type_: "string".to_string(),
358 items: None,
359 default: rand_string,
360 };
361
362 let mut schema = IndexMap::new();
364 schema.insert("method".to_string(), method_name_prop);
365 schema.insert("id".to_string(), request_id_prop);
366
367 let default = parameters_example.replace('\\', "");
369 schema.insert(
370 "params".to_string(),
371 Property {
372 type_: "array".to_string(),
373 items: Some(ArrayItems {}),
374 default,
375 },
376 );
377
378 let content = Content {
380 application_json: Application {
381 schema: Schema {
382 type_: "object".to_string(),
383 properties: schema,
384 },
385 },
386 };
387
388 RequestBody {
389 required: true,
390 content,
391 }
392}
393
394fn create_responses(
396 method_name: &str,
397 have_parameters: bool,
398) -> Result<IndexMap<String, Response>, Box<dyn Error>> {
399 let mut responses = IndexMap::new();
400
401 let properties = get_default_properties(method_name)?;
402
403 let res_ok = Response {
404 description: "OK".to_string(),
405 content: Content {
406 application_json: Application {
407 schema: Schema {
408 type_: "object".to_string(),
409 properties,
410 },
411 },
412 },
413 };
414 responses.insert("200".to_string(), res_ok);
415
416 let mut properties = IndexMap::new();
417 if have_parameters {
418 properties.insert(
419 "error".to_string(),
420 Property {
421 type_: "string".to_string(),
422 items: None,
423 default: "Invalid parameters".to_string(),
424 },
425 );
426 let res_bad_request = Response {
427 description: "Bad request".to_string(),
428 content: Content {
429 application_json: Application {
430 schema: Schema {
431 type_: "object".to_string(),
432 properties,
433 },
434 },
435 },
436 };
437 responses.insert("400".to_string(), res_bad_request);
438 }
439
440 Ok(responses)
441}
442
443fn add_params_to_description(description: &str, params_description: &str) -> String {
445 let mut new_description = description.to_string();
446 new_description.push_str("\n\n**Request body `params` arguments:**\n\n");
447 new_description.push_str(params_description);
448 new_description
449}
450
451fn default_property<T: serde::Serialize>(
452 type_: &str,
453 items: Option<ArrayItems>,
454 default_value: T,
455) -> Result<Property, Box<dyn Error>> {
456 Ok(Property {
457 type_: type_.to_string(),
458 items,
459 default: serde_json::to_string(&default_value)?,
460 })
461}
462
463fn get_default_properties(method_name: &str) -> Result<IndexMap<String, Property>, Box<dyn Error>> {
466 let type_ = "object";
467 let items = None;
468 let mut props = IndexMap::new();
469
470 let default_result = match method_name {
472 "getnetworkhashps" => default_property(type_, items.clone(), u64::default())?,
475 "getblocksubsidy" => {
476 default_property(type_, items.clone(), GetBlockSubsidyResponse::default())?
477 }
478 "getmininginfo" => {
479 default_property(type_, items.clone(), GetMiningInfoResponse::default())?
480 }
481 "getnetworksolps" => default_property(type_, items.clone(), u64::default())?,
482 "submitblock" => default_property(type_, items.clone(), SubmitBlockResponse::default())?,
483 "validateaddress" => {
485 default_property(type_, items.clone(), ValidateAddressResponse::default())?
486 }
487 "z_validateaddress" => {
488 default_property(type_, items.clone(), ZValidateAddressResponse::default())?
489 }
490 "getaddressbalance" => {
492 default_property(type_, items.clone(), GetAddressBalanceResponse::default())?
493 }
494 "getaddressutxos" => default_property(type_, items.clone(), Utxo::default())?,
495 "getaddresstxids" => default_property(type_, items.clone(), Vec::<String>::default())?,
496 "getpeerinfo" => default_property(type_, items.clone(), vec![PeerInfo::default()])?,
498 "getdifficulty" => default_property(type_, items.clone(), f64::default())?,
500 "getblockchaininfo" => {
501 default_property(type_, items.clone(), GetBlockchainInfoResponse::default())?
502 }
503 "getrawmempool" => default_property(type_, items.clone(), Vec::<String>::default())?,
504 "getblockhash" => default_property(type_, items.clone(), GetBlockHashResponse::default())?,
505 "z_getsubtreesbyindex" => {
506 default_property(type_, items.clone(), GetSubtreesByIndexResponse::default())?
507 }
508 "z_gettreestate" => {
509 default_property(type_, items.clone(), GetTreestateResponse::default())?
510 }
511 "getblockcount" => default_property(type_, items.clone(), u32::default())?,
512 "getbestblockhash" => {
513 default_property(type_, items.clone(), GetBlockHashResponse::default())?
514 }
515 "getblock" => default_property(type_, items.clone(), GetBlockResponse::default())?,
516 "z_listunifiedreceivers" => default_property(
518 type_,
519 items.clone(),
520 ZListUnifiedReceiversResponse::default(),
521 )?,
522 "getinfo" => default_property(type_, items.clone(), GetInfoResponse::default())?,
524 "stop" => default_property(type_, items.clone(), ())?,
525 "sendrawtransaction" => {
527 default_property(type_, items.clone(), SendRawTransactionResponse::default())?
528 }
529 "getrawtransaction" => {
530 default_property(type_, items.clone(), GetRawTransactionResponse::default())?
531 }
532 _ => Property {
534 type_: type_.to_string(),
535 items: None,
536 default: "{}".to_string(),
537 },
538 };
539
540 props.insert("result".to_string(), default_result);
541 Ok(props)
542}