bitcoin_rpc_types/
types.rs

1//! Core schema types for Bitcoin RPC API definitions
2//!
3//! Fundamental, serde-friendly types to represent Bitcoin RPC method
4//! definitions, arguments, and results.
5
6use std::collections::BTreeMap;
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12/// Bitcoin method argument specification
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct BtcArgument {
15    /// Names of the argument
16    pub names: Vec<String>,
17    /// Description of the argument
18    pub description: String,
19    /// One-line description of the argument
20    #[serde(default, rename = "oneline_description")]
21    pub oneline_description: String,
22    /// Whether the argument can also be passed positionally
23    #[serde(default, rename = "also_positional")]
24    pub also_positional: bool,
25    /// Type string representation
26    #[serde(default, rename = "type_str")]
27    pub type_str: Option<Vec<String>>,
28    /// Whether the argument is required
29    pub required: bool,
30    /// Whether the argument is hidden from documentation
31    #[serde(default)]
32    pub hidden: bool,
33    /// Type of the argument
34    #[serde(rename = "type")]
35    pub type_: String,
36}
37
38/// Bitcoin method result specification
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct BtcResult {
41    /// Type of the result
42    #[serde(rename = "type")]
43    pub type_: String,
44    /// Whether the result is optional
45    #[serde(default, rename = "optional")]
46    pub optional: bool,
47    /// Description of the result
48    pub description: String,
49    /// Whether to skip type checking for this result
50    #[serde(default, rename = "skip_type_check")]
51    pub skip_type_check: bool,
52    /// Key name for the result
53    #[serde(default, rename = "key_name")]
54    pub key_name: String,
55    /// Condition for when this result is present
56    #[serde(default)]
57    pub condition: String,
58    /// Inner results for nested structures
59    #[serde(default)]
60    pub inner: Vec<BtcResult>,
61}
62
63impl Default for BtcResult {
64    /// Creates a default BtcResult with empty values
65    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    /// Creates a new BtcResult with the specified parameters
80    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    /// Returns whether the result is required (computed from optional)
93    pub fn required(&self) -> bool { !self.optional }
94}
95
96/// Bitcoin method definition
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BtcMethod {
99    /// Name of the method
100    pub name: String,
101    /// Description of the method
102    pub description: String,
103    /// Example usage of the method
104    #[serde(default)]
105    pub examples: String,
106    /// Names of the arguments
107    #[serde(default, rename = "argument_names")]
108    pub argument_names: Vec<String>,
109    /// Arguments for the method
110    pub arguments: Vec<BtcArgument>,
111    /// Results returned by the method
112    pub results: Vec<BtcResult>,
113}
114
115/// A collection of all Bitcoin RPC methods and their details
116#[derive(Debug, Default, Clone, Serialize, Deserialize)]
117pub struct ApiDefinition {
118    /// List of methods sorted by the method name
119    pub rpcs: BTreeMap<String, BtcMethod>,
120}
121
122impl ApiDefinition {
123    /// Creates a new empty API definition
124    pub fn new() -> Self { Self { rpcs: BTreeMap::new() } }
125
126    /// Loads an API definition from a JSON file
127    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    /// Gets a method by name
134    pub fn get_method(&self, name: &str) -> Option<&BtcMethod> { self.rpcs.get(name) }
135}
136
137/// Error types for schema operations
138#[derive(Error, Debug)]
139pub enum SchemaError {
140    /// JSON parsing error
141    #[error("Failed to parse JSON: {0}")]
142    JsonParse(#[from] serde_json::Error),
143
144    /// IO error
145    #[error("IO error: {0}")]
146    Io(#[from] std::io::Error),
147}
148
149/// Result type for schema operations
150pub 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        // Main result should have required = !optional = false
225        assert!(!result.required());
226        assert!(result.optional);
227
228        // Inner result should have required = !optional = true
229        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        // Create a temporary JSON file with results that need post-processing
245        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        // Test loading from file
295        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        // Verify results are properly computed - the main result should be optional
305        assert!(!method.results[0].required());
306        assert!(method.results[0].optional);
307
308        // Verify inner results are properly computed - the inner result should be required
309        assert!(method.results[0].inner[0].required());
310        assert!(!method.results[0].inner[0].optional);
311
312        // Clean up
313        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        // Create a minimal JSON file to test the success path
322        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        // Test that the success path (Ok(api_def)) is covered
341        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        // Clean up
349        std::fs::remove_file(temp_file).unwrap();
350    }
351
352    #[test]
353    fn test_api_definition_from_file_error_cases() {
354        // Test file not found error
355        let result = ApiDefinition::from_file("nonexistent_file.json");
356        assert!(result.is_err());
357        match result.unwrap_err() {
358            SchemaError::Io(_) => {} // Expected IO error
359            _ => panic!("Expected IO error for nonexistent file"),
360        }
361
362        // Test invalid JSON error
363        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(_) => {} // Expected JSON parse error
375            _ => panic!("Expected JSON parse error for invalid JSON"),
376        }
377
378        // Clean up
379        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        // Test getting method from empty API definition
387        assert!(api_def.get_method("nonexistent").is_none());
388
389        // Add a method
390        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        // Test getting existing method
401        let retrieved_method = api_def.get_method("getblock");
402        assert!(retrieved_method.is_some());
403        assert_eq!(retrieved_method.unwrap().name, "getblock");
404
405        // Test getting non-existent method
406        assert!(api_def.get_method("gettransaction").is_none());
407    }
408}