openzeppelin_relayer/models/network/
request.rs

1//! API request models and validation for network endpoints.
2//!
3//! This module handles incoming HTTP requests for network operations, providing:
4//!
5//! - **Request Models**: Structures for updating network configurations via API
6//! - **Input Validation**: Sanitization and validation of user-provided data
7//!
8//! Serves as the entry point for network data from external clients, ensuring
9//! all input is properly validated before reaching the core business logic.
10
11use crate::models::{deserialize_rpc_urls, ApiError, RpcConfig};
12use serde::{Deserialize, Serialize};
13use utoipa::ToSchema;
14
15/// Schema-only type representing a flexible RPC URL entry.
16/// Used for OpenAPI documentation to show that rpc_urls can accept
17/// either strings or RpcConfig objects.
18///
19/// This is NOT used for actual deserialization - the custom deserializer
20/// handles the conversion. This type exists solely for schema generation.
21#[derive(Serialize, ToSchema)]
22#[serde(untagged)]
23#[schema(as = RpcUrlEntry)]
24#[allow(dead_code)] // Only used for schema generation
25pub enum RpcUrlEntry {
26    /// Simple string format (e.g., "https://rpc.example.com")
27    /// Defaults to weight 100.
28    String(String),
29    /// Extended object format with explicit weight
30    Config(RpcConfig),
31}
32
33/// Request structure for updating a network configuration.
34/// Currently supports updating RPC URLs only. Can be extended to support other fields.
35#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, ToSchema)]
36#[serde(deny_unknown_fields)]
37pub struct UpdateNetworkRequest {
38    /// List of RPC endpoint configurations for connecting to the network.
39    /// Supports multiple formats:
40    /// - Array of strings: `["https://rpc.example.com"]` (defaults to weight 100)
41    /// - Array of RpcConfig objects: `[{"url": "https://rpc.example.com", "weight": 100}]`
42    /// - Mixed array: `["https://rpc1.com", {"url": "https://rpc2.com", "weight": 100}]`
43    ///   Must be non-empty and contain valid HTTP/HTTPS URLs if provided.
44    #[serde(
45        default,
46        skip_serializing_if = "Option::is_none",
47        deserialize_with = "deserialize_rpc_urls"
48    )]
49    #[schema(
50        nullable = false,
51        example = json!([{"url": "https://rpc.example.com", "weight": 100}]),
52        value_type = Vec<RpcUrlEntry>
53    )]
54    pub rpc_urls: Option<Vec<RpcConfig>>,
55}
56
57impl UpdateNetworkRequest {
58    /// Validates the request data.
59    ///
60    /// # Returns
61    /// - `Ok(())` if the request is valid
62    /// - `Err(ApiError)` if validation fails
63    pub fn validate(&self) -> Result<(), ApiError> {
64        // Validate RPC URLs if provided
65        if let Some(ref rpc_urls) = self.rpc_urls {
66            // Check that rpc_urls is not empty
67            if rpc_urls.is_empty() {
68                return Err(ApiError::BadRequest(
69                    "rpc_urls must contain at least one RPC endpoint".to_string(),
70                ));
71            }
72
73            // Validate all RPC URLs
74            RpcConfig::validate_list(rpc_urls)
75                .map_err(|e| ApiError::BadRequest(format!("Invalid RPC URL configuration: {e}")))?;
76        }
77
78        Ok(())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_update_network_request_validation_empty_rpc_urls() {
88        let request = UpdateNetworkRequest {
89            rpc_urls: Some(vec![]),
90        };
91        assert!(request.validate().is_err());
92    }
93
94    #[test]
95    fn test_update_network_request_validation_valid() {
96        let request = UpdateNetworkRequest {
97            rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
98        };
99        assert!(request.validate().is_ok());
100    }
101
102    #[test]
103    fn test_update_network_request_validation_invalid_url() {
104        let request = UpdateNetworkRequest {
105            rpc_urls: Some(vec![RpcConfig::new("ftp://invalid.com".to_string())]),
106        };
107        assert!(request.validate().is_err());
108    }
109
110    #[test]
111    fn test_update_network_request_validation_none_rpc_urls() {
112        let request = UpdateNetworkRequest { rpc_urls: None };
113        assert!(request.validate().is_ok());
114    }
115
116    #[test]
117    fn test_deserialize_rpc_urls_simple_format() {
118        let json = r#"{"rpc_urls": ["https://rpc1.com", "https://rpc2.com"]}"#;
119        let request: UpdateNetworkRequest = serde_json::from_str(json).unwrap();
120        assert_eq!(request.rpc_urls.as_ref().unwrap().len(), 2);
121        assert_eq!(
122            request.rpc_urls.as_ref().unwrap()[0].url,
123            "https://rpc1.com"
124        );
125        assert_eq!(request.rpc_urls.as_ref().unwrap()[0].weight, 100u8); // Default weight
126        assert_eq!(
127            request.rpc_urls.as_ref().unwrap()[1].url,
128            "https://rpc2.com"
129        );
130        assert_eq!(request.rpc_urls.as_ref().unwrap()[1].weight, 100u8); // Default weight
131    }
132
133    #[test]
134    fn test_deserialize_rpc_urls_extended_format() {
135        let json = r#"{"rpc_urls": [{"url": "https://rpc1.com", "weight": 50}, {"url": "https://rpc2.com", "weight": 75}]}"#;
136        let request: UpdateNetworkRequest = serde_json::from_str(json).unwrap();
137        assert_eq!(request.rpc_urls.as_ref().unwrap().len(), 2);
138        assert_eq!(
139            request.rpc_urls.as_ref().unwrap()[0].url,
140            "https://rpc1.com"
141        );
142        assert_eq!(request.rpc_urls.as_ref().unwrap()[0].weight, 50u8);
143        assert_eq!(
144            request.rpc_urls.as_ref().unwrap()[1].url,
145            "https://rpc2.com"
146        );
147        assert_eq!(request.rpc_urls.as_ref().unwrap()[1].weight, 75u8);
148    }
149
150    #[test]
151    fn test_deserialize_rpc_urls_mixed_format() {
152        let json =
153            r#"{"rpc_urls": ["https://rpc1.com", {"url": "https://rpc2.com", "weight": 50}]}"#;
154        let request: UpdateNetworkRequest = serde_json::from_str(json).unwrap();
155        assert_eq!(request.rpc_urls.as_ref().unwrap().len(), 2);
156        assert_eq!(
157            request.rpc_urls.as_ref().unwrap()[0].url,
158            "https://rpc1.com"
159        );
160        assert_eq!(request.rpc_urls.as_ref().unwrap()[0].weight, 100u8); // Default weight for string
161        assert_eq!(
162            request.rpc_urls.as_ref().unwrap()[1].url,
163            "https://rpc2.com"
164        );
165        assert_eq!(request.rpc_urls.as_ref().unwrap()[1].weight, 50u8); // Explicit weight from object
166    }
167
168    #[test]
169    fn test_deserialize_rpc_urls_none() {
170        let json = r#"{}"#;
171        let request: UpdateNetworkRequest = serde_json::from_str(json).unwrap();
172        assert!(request.rpc_urls.is_none());
173    }
174
175    #[test]
176    fn test_deserialize_rpc_urls_invalid_format() {
177        let json = r#"{"rpc_urls": [123, 456]}"#;
178        let result: Result<UpdateNetworkRequest, _> = serde_json::from_str(json);
179        assert!(result.is_err());
180    }
181}