Skip to main content

sonos_api/operation/
builder.rs

1//! Operation builder pattern for fluent operation construction
2//!
3//! This module provides the builder pattern for constructing UPnP operations
4//! with validation, timeout, and retry configuration.
5
6use super::{OperationMetadata, UPnPOperation, Validate, ValidationError, ValidationLevel};
7use std::marker::PhantomData;
8use std::time::Duration;
9
10/// Builder for constructing UPnP operations with configuration
11///
12/// The OperationBuilder allows for fluent construction of operations with
13/// validation levels, timeouts, and other configuration.
14///
15/// # Type Parameters
16/// * `Op` - The UPnP operation type being built
17pub struct OperationBuilder<Op: UPnPOperation> {
18    request: Op::Request,
19    validation: ValidationLevel,
20    timeout: Option<Duration>,
21    _phantom: PhantomData<Op>,
22}
23
24impl<Op: UPnPOperation> OperationBuilder<Op> {
25    /// Create a new operation builder with the given request
26    ///
27    /// # Arguments
28    /// * `request` - The typed request data for the operation
29    ///
30    /// # Returns
31    /// A new operation builder with default configuration
32    pub fn new(request: Op::Request) -> Self {
33        Self {
34            request,
35            validation: ValidationLevel::default(),
36            timeout: None,
37            _phantom: PhantomData,
38        }
39    }
40
41    /// Set the validation level for the operation
42    ///
43    /// # Arguments
44    /// * `level` - The validation level to use
45    ///
46    /// # Returns
47    /// The builder for method chaining
48    pub fn with_validation(mut self, level: ValidationLevel) -> Self {
49        self.validation = level;
50        self
51    }
52
53    /// Set a timeout for the operation
54    ///
55    /// # Arguments
56    /// * `timeout` - The timeout duration
57    ///
58    /// # Returns
59    /// The builder for method chaining
60    pub fn with_timeout(mut self, timeout: Duration) -> Self {
61        self.timeout = Some(timeout);
62        self
63    }
64
65    /// Build the final composable operation
66    ///
67    /// This validates the request according to the configured validation level
68    /// and creates a ComposableOperation ready for execution.
69    ///
70    /// # Returns
71    /// A ComposableOperation or a validation error
72    pub fn build(self) -> Result<ComposableOperation<Op>, ValidationError> {
73        // Validate the request according to the configured level
74        self.request.validate(self.validation)?;
75
76        Ok(ComposableOperation {
77            request: self.request,
78            validation: self.validation,
79            timeout: self.timeout,
80            metadata: Op::metadata(),
81            _phantom: PhantomData,
82        })
83    }
84
85    /// Build without validation (for performance-critical scenarios)
86    ///
87    /// This bypasses validation and creates the operation directly.
88    /// Use with caution - invalid requests may cause runtime errors.
89    ///
90    /// # Returns
91    /// A ComposableOperation without validation
92    pub fn build_unchecked(self) -> ComposableOperation<Op> {
93        ComposableOperation {
94            request: self.request,
95            validation: ValidationLevel::None,
96            timeout: self.timeout,
97            metadata: Op::metadata(),
98            _phantom: PhantomData,
99        }
100    }
101
102    /// Get the current validation level
103    pub fn validation_level(&self) -> ValidationLevel {
104        self.validation
105    }
106
107    /// Get the current timeout setting
108    pub fn timeout(&self) -> Option<Duration> {
109        self.timeout
110    }
111}
112
113/// A composable operation ready for execution
114///
115/// This represents a fully configured UPnP operation that can be executed
116/// directly or composed with other operations through chaining, batching, etc.
117///
118/// # Type Parameters
119/// * `Op` - The UPnP operation type
120pub struct ComposableOperation<Op: UPnPOperation> {
121    pub(crate) request: Op::Request,
122    pub(crate) validation: ValidationLevel,
123    pub(crate) timeout: Option<Duration>,
124    pub(crate) metadata: OperationMetadata,
125    _phantom: PhantomData<Op>,
126}
127
128impl<Op: UPnPOperation> ComposableOperation<Op> {
129    /// Get the request data for this operation
130    pub fn request(&self) -> &Op::Request {
131        &self.request
132    }
133
134    /// Get the validation level used for this operation
135    pub fn validation_level(&self) -> ValidationLevel {
136        self.validation
137    }
138
139    /// Get the timeout for this operation
140    pub fn timeout(&self) -> Option<Duration> {
141        self.timeout
142    }
143
144    /// Get the operation metadata
145    pub fn metadata(&self) -> &OperationMetadata {
146        &self.metadata
147    }
148
149    /// Build the SOAP payload for this operation
150    ///
151    /// # Returns
152    /// The XML payload string or a validation error
153    pub fn build_payload(&self) -> Result<String, ValidationError> {
154        Op::build_payload(&self.request)
155    }
156
157    /// Parse a response for this operation
158    ///
159    /// # Arguments
160    /// * `xml` - The raw SOAP response body
161    ///
162    /// # Returns
163    /// The parsed response or an API error
164    pub fn parse_response(&self, xml: &str) -> Result<Op::Response, crate::error::ApiError> {
165        Op::parse_response(xml)
166    }
167}
168
169impl<Op: UPnPOperation> std::fmt::Debug for ComposableOperation<Op> {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("ComposableOperation")
172            .field("service", &self.metadata.service)
173            .field("action", &self.metadata.action)
174            .field("validation", &self.validation)
175            .field("timeout", &self.timeout)
176            .finish()
177    }
178}
179
180impl<Op: UPnPOperation> Clone for ComposableOperation<Op>
181where
182    Op::Request: Clone,
183{
184    fn clone(&self) -> Self {
185        Self {
186            request: self.request.clone(),
187            validation: self.validation,
188            timeout: self.timeout,
189            metadata: self.metadata.clone(),
190            _phantom: PhantomData,
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::operation::{Validate, ValidationError, ValidationLevel};
199    use crate::service::Service;
200    use serde::{Deserialize, Serialize};
201
202    // Mock types for testing
203    #[derive(Serialize, Clone, Debug, PartialEq)]
204    struct TestRequest {
205        value: i32,
206    }
207
208    impl Validate for TestRequest {
209        fn validate_basic(&self) -> Result<(), ValidationError> {
210            if self.value < 0 || self.value > 100 {
211                Err(ValidationError::range_error("value", 0, 100, self.value))
212            } else {
213                Ok(())
214            }
215        }
216    }
217
218    #[derive(Deserialize, Debug, PartialEq)]
219    struct TestResponse {
220        result: String,
221    }
222
223    struct TestOperation;
224
225    impl UPnPOperation for TestOperation {
226        type Request = TestRequest;
227        type Response = TestResponse;
228
229        const SERVICE: Service = Service::AVTransport;
230        const ACTION: &'static str = "TestAction";
231
232        fn build_payload(request: &Self::Request) -> Result<String, ValidationError> {
233            request.validate(ValidationLevel::Basic)?;
234            Ok(format!(
235                "<TestRequest><Value>{}</Value></TestRequest>",
236                request.value
237            ))
238        }
239
240        fn parse_response(xml: &str) -> Result<Self::Response, crate::error::ApiError> {
241            Ok(TestResponse {
242                result: crate::operation::response_text(xml, "Result")
243                    .unwrap_or_else(|| "default".to_string()),
244            })
245        }
246    }
247
248    #[test]
249    fn test_operation_builder_new() {
250        let request = TestRequest { value: 50 };
251        let builder = OperationBuilder::<TestOperation>::new(request);
252
253        assert_eq!(builder.validation_level(), ValidationLevel::Basic);
254        assert_eq!(builder.timeout(), None);
255    }
256
257    #[test]
258    fn test_operation_builder_fluent() {
259        let request = TestRequest { value: 50 };
260        let builder = OperationBuilder::<TestOperation>::new(request)
261            .with_validation(ValidationLevel::Basic)
262            .with_timeout(Duration::from_secs(30));
263
264        assert_eq!(builder.validation_level(), ValidationLevel::Basic);
265        assert_eq!(builder.timeout(), Some(Duration::from_secs(30)));
266    }
267
268    #[test]
269    fn test_operation_builder_build_success() {
270        let request = TestRequest { value: 50 };
271        let operation = OperationBuilder::<TestOperation>::new(request)
272            .with_validation(ValidationLevel::Basic)
273            .build()
274            .expect("Should build successfully");
275
276        assert_eq!(operation.request().value, 50);
277        assert_eq!(operation.validation_level(), ValidationLevel::Basic);
278        assert_eq!(operation.metadata().action, "TestAction");
279    }
280
281    #[test]
282    fn test_operation_builder_build_validation_error() {
283        let request = TestRequest { value: 150 }; // Invalid value
284        let result = OperationBuilder::<TestOperation>::new(request)
285            .with_validation(ValidationLevel::Basic)
286            .build();
287
288        assert!(result.is_err());
289        assert!(result.unwrap_err().to_string().contains("150"));
290    }
291
292    #[test]
293    fn test_operation_builder_build_unchecked() {
294        let request = TestRequest { value: 150 }; // Invalid value
295        let operation = OperationBuilder::<TestOperation>::new(request)
296            .with_validation(ValidationLevel::Basic)
297            .build_unchecked(); // Should succeed despite invalid value
298
299        assert_eq!(operation.request().value, 150);
300        assert_eq!(operation.validation_level(), ValidationLevel::None);
301    }
302
303    #[test]
304    fn test_composable_operation_build_payload() {
305        let request = TestRequest { value: 42 };
306        let operation = OperationBuilder::<TestOperation>::new(request)
307            .build()
308            .expect("Should build successfully");
309
310        let payload = operation.build_payload().expect("Should build payload");
311        assert!(payload.contains("<Value>42</Value>"));
312    }
313
314    #[test]
315    fn test_composable_operation_debug() {
316        let request = TestRequest { value: 42 };
317        let operation = OperationBuilder::<TestOperation>::new(request)
318            .with_timeout(Duration::from_secs(10))
319            .build()
320            .expect("Should build successfully");
321
322        let debug_str = format!("{operation:?}");
323        assert!(debug_str.contains("TestAction"));
324        assert!(debug_str.contains("AVTransport"));
325    }
326
327    #[test]
328    fn test_composable_operation_clone() {
329        let request = TestRequest { value: 42 };
330        let operation = OperationBuilder::<TestOperation>::new(request)
331            .build()
332            .expect("Should build successfully");
333
334        let cloned = operation.clone();
335        assert_eq!(operation.request().value, cloned.request().value);
336        assert_eq!(operation.validation_level(), cloned.validation_level());
337    }
338}