1use std::collections::BTreeMap;
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct BtcArgument {
15 pub names: Vec<String>,
17 pub description: String,
19 #[serde(default, rename = "oneline_description")]
21 pub oneline_description: String,
22 #[serde(default, rename = "also_positional")]
24 pub also_positional: bool,
25 #[serde(default, rename = "type_str")]
27 pub type_str: Option<Vec<String>>,
28 pub required: bool,
30 #[serde(default)]
32 pub hidden: bool,
33 #[serde(rename = "type")]
35 pub type_: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct BtcResult {
41 #[serde(rename = "type")]
43 pub type_: String,
44 #[serde(default, rename = "optional")]
46 pub optional: bool,
47 pub description: String,
49 #[serde(default, rename = "skip_type_check")]
51 pub skip_type_check: bool,
52 #[serde(default, rename = "key_name")]
54 pub key_name: String,
55 #[serde(default)]
57 pub condition: String,
58 #[serde(default)]
60 pub inner: Vec<BtcResult>,
61}
62
63impl Default for BtcResult {
64 fn default() -> Self {
66 Self {
67 type_: String::new(),
68 optional: false,
69 description: String::new(),
70 skip_type_check: false,
71 key_name: String::new(),
72 condition: String::new(),
73 inner: Vec::new(),
74 }
75 }
76}
77
78impl BtcResult {
79 pub fn new(
81 type_: String,
82 optional: bool,
83 description: String,
84 skip_type_check: bool,
85 key_name: String,
86 condition: String,
87 inner: Vec<BtcResult>,
88 ) -> Self {
89 Self { type_, optional, description, skip_type_check, key_name, condition, inner }
90 }
91
92 pub fn required(&self) -> bool { !self.optional }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BtcMethod {
99 pub name: String,
101 pub description: String,
103 #[serde(default)]
105 pub examples: String,
106 #[serde(default, rename = "argument_names")]
108 pub argument_names: Vec<String>,
109 pub arguments: Vec<BtcArgument>,
111 pub results: Vec<BtcResult>,
113}
114
115#[derive(Debug, Default, Clone, Serialize, Deserialize)]
117pub struct ApiDefinition {
118 pub rpcs: BTreeMap<String, BtcMethod>,
120}
121
122impl ApiDefinition {
123 pub fn new() -> Self { Self { rpcs: BTreeMap::new() } }
125
126 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
128 let content = std::fs::read_to_string(path)?;
129 let api_def: ApiDefinition = serde_json::from_str(&content)?;
130 Ok(api_def)
131 }
132
133 pub fn get_method(&self, name: &str) -> Option<&BtcMethod> { self.rpcs.get(name) }
135}
136
137#[derive(Error, Debug)]
139pub enum SchemaError {
140 #[error("Failed to parse JSON: {0}")]
142 JsonParse(#[from] serde_json::Error),
143
144 #[error("IO error: {0}")]
146 Io(#[from] std::io::Error),
147}
148
149pub type Result<T> = std::result::Result<T, SchemaError>;
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_btc_result_default() {
158 let result = BtcResult::default();
159 assert_eq!(result.type_, "");
160 assert!(!result.optional);
161 assert!(result.required());
162 assert_eq!(result.description, "");
163 assert!(!result.skip_type_check);
164 assert_eq!(result.key_name, "");
165 assert_eq!(result.condition, "");
166 assert!(result.inner.is_empty());
167 }
168
169 #[test]
170 fn test_btc_result_new() {
171 let inner_result = BtcResult::new(
172 "string".to_string(),
173 true,
174 "inner description".to_string(),
175 false,
176 "inner_key".to_string(),
177 "condition".to_string(),
178 vec![],
179 );
180
181 let result = BtcResult::new(
182 "object".to_string(),
183 false,
184 "main description".to_string(),
185 true,
186 "main_key".to_string(),
187 "main_condition".to_string(),
188 vec![inner_result.clone()],
189 );
190
191 assert_eq!(result.type_, "object");
192 assert!(!result.optional);
193 assert!(result.required());
194 assert_eq!(result.description, "main description");
195 assert!(result.skip_type_check);
196 assert_eq!(result.key_name, "main_key");
197 assert_eq!(result.condition, "main_condition");
198 assert_eq!(result.inner.len(), 1);
199 assert_eq!(result.inner[0].type_, "string");
200 assert!(result.inner[0].optional);
201 assert!(!result.inner[0].required());
202 }
203
204 #[test]
205 fn test_btc_result_required_getter() {
206 let result = BtcResult {
207 type_: "string".to_string(),
208 optional: true,
209 description: "test".to_string(),
210 skip_type_check: false,
211 key_name: "test_key".to_string(),
212 condition: "test_condition".to_string(),
213 inner: vec![BtcResult {
214 type_: "number".to_string(),
215 optional: false,
216 description: "inner".to_string(),
217 skip_type_check: false,
218 key_name: "inner_key".to_string(),
219 condition: "inner_condition".to_string(),
220 inner: vec![],
221 }],
222 };
223
224 assert!(!result.required());
226 assert!(result.optional);
227
228 assert!(result.inner[0].required());
230 assert!(!result.inner[0].optional);
231 }
232
233 #[test]
234 fn test_api_definition_new() {
235 let api_def = ApiDefinition::new();
236 assert!(api_def.rpcs.is_empty());
237 }
238
239 #[test]
240 fn test_api_definition_from_file() {
241 use std::fs::File;
242 use std::io::Write;
243
244 let json_content = r#"{
246 "rpcs": {
247 "getblock": {
248 "name": "getblock",
249 "description": "Get block information",
250 "examples": "",
251 "argument_names": ["blockhash", "verbosity"],
252 "arguments": [
253 {
254 "names": ["blockhash"],
255 "description": "The block hash",
256 "oneline_description": "",
257 "also_positional": false,
258 "type_str": null,
259 "required": true,
260 "hidden": false,
261 "type": "string"
262 }
263 ],
264 "results": [
265 {
266 "type": "object",
267 "optional": true,
268 "description": "Block information",
269 "skip_type_check": false,
270 "key_name": "",
271 "condition": "",
272 "inner": [
273 {
274 "type": "string",
275 "optional": false,
276 "description": "Inner result",
277 "skip_type_check": false,
278 "key_name": "inner_key",
279 "condition": "",
280 "inner": []
281 }
282 ]
283 }
284 ]
285 }
286 }
287 }"#;
288
289 let temp_file = "test_api.json";
290 let mut file = File::create(temp_file).unwrap();
291 file.write_all(json_content.as_bytes()).unwrap();
292 drop(file);
293
294 let api_def = ApiDefinition::from_file(temp_file).unwrap();
296 assert_eq!(api_def.rpcs.len(), 1);
297 assert!(api_def.rpcs.contains_key("getblock"));
298
299 let method = api_def.rpcs.get("getblock").unwrap();
300 assert_eq!(method.name, "getblock");
301 assert_eq!(method.arguments.len(), 1);
302 assert_eq!(method.results.len(), 1);
303
304 assert!(!method.results[0].required());
306 assert!(method.results[0].optional);
307
308 assert!(method.results[0].inner[0].required());
310 assert!(!method.results[0].inner[0].optional);
311
312 std::fs::remove_file(temp_file).unwrap();
314 }
315
316 #[test]
317 fn test_api_definition_from_file_success_path() {
318 use std::fs::File;
319 use std::io::Write;
320
321 let json_content = r#"{
323 "rpcs": {
324 "simple_method": {
325 "name": "simple_method",
326 "description": "A simple method",
327 "examples": "",
328 "argument_names": [],
329 "arguments": [],
330 "results": []
331 }
332 }
333 }"#;
334
335 let temp_file = "test_simple_api.json";
336 let mut file = File::create(temp_file).unwrap();
337 file.write_all(json_content.as_bytes()).unwrap();
338 drop(file);
339
340 let result = ApiDefinition::from_file(temp_file);
342 assert!(result.is_ok());
343
344 let api_def = result.unwrap();
345 assert_eq!(api_def.rpcs.len(), 1);
346 assert!(api_def.rpcs.contains_key("simple_method"));
347
348 std::fs::remove_file(temp_file).unwrap();
350 }
351
352 #[test]
353 fn test_api_definition_from_file_error_cases() {
354 let result = ApiDefinition::from_file("nonexistent_file.json");
356 assert!(result.is_err());
357 match result.unwrap_err() {
358 SchemaError::Io(_) => {} _ => panic!("Expected IO error for nonexistent file"),
360 }
361
362 use std::fs::File;
364 use std::io::Write;
365
366 let temp_file = "test_invalid.json";
367 let mut file = File::create(temp_file).unwrap();
368 file.write_all(b"invalid json content").unwrap();
369 drop(file);
370
371 let result = ApiDefinition::from_file(temp_file);
372 assert!(result.is_err());
373 match result.unwrap_err() {
374 SchemaError::JsonParse(_) => {} _ => panic!("Expected JSON parse error for invalid JSON"),
376 }
377
378 std::fs::remove_file(temp_file).unwrap();
380 }
381
382 #[test]
383 fn test_api_definition_get_method() {
384 let mut api_def = ApiDefinition::new();
385
386 assert!(api_def.get_method("nonexistent").is_none());
388
389 let method = BtcMethod {
391 name: "getblock".to_string(),
392 description: "Get block information".to_string(),
393 examples: "".to_string(),
394 argument_names: vec!["blockhash".to_string()],
395 arguments: vec![],
396 results: vec![],
397 };
398 api_def.rpcs.insert("getblock".to_string(), method);
399
400 let retrieved_method = api_def.get_method("getblock");
402 assert!(retrieved_method.is_some());
403 assert_eq!(retrieved_method.unwrap().name, "getblock");
404
405 assert!(api_def.get_method("gettransaction").is_none());
407 }
408}