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 parsed XML response element
161    ///
162    /// # Returns
163    /// The parsed response or an API error
164    pub fn parse_response(
165        &self,
166        xml: &xmltree::Element,
167    ) -> Result<Op::Response, crate::error::ApiError> {
168        Op::parse_response(xml)
169    }
170}
171
172impl<Op: UPnPOperation> std::fmt::Debug for ComposableOperation<Op> {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.debug_struct("ComposableOperation")
175            .field("service", &self.metadata.service)
176            .field("action", &self.metadata.action)
177            .field("validation", &self.validation)
178            .field("timeout", &self.timeout)
179            .finish()
180    }
181}
182
183impl<Op: UPnPOperation> Clone for ComposableOperation<Op>
184where
185    Op::Request: Clone,
186{
187    fn clone(&self) -> Self {
188        Self {
189            request: self.request.clone(),
190            validation: self.validation,
191            timeout: self.timeout,
192            metadata: self.metadata.clone(),
193            _phantom: PhantomData,
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::operation::{Validate, ValidationError, ValidationLevel};
202    use crate::service::Service;
203    use serde::{Deserialize, Serialize};
204    use xmltree::Element;
205
206    // Mock types for testing
207    #[derive(Serialize, Clone, Debug, PartialEq)]
208    struct TestRequest {
209        value: i32,
210    }
211
212    impl Validate for TestRequest {
213        fn validate_basic(&self) -> Result<(), ValidationError> {
214            if self.value < 0 || self.value > 100 {
215                Err(ValidationError::range_error("value", 0, 100, self.value))
216            } else {
217                Ok(())
218            }
219        }
220    }
221
222    #[derive(Deserialize, Debug, PartialEq)]
223    struct TestResponse {
224        result: String,
225    }
226
227    struct TestOperation;
228
229    impl UPnPOperation for TestOperation {
230        type Request = TestRequest;
231        type Response = TestResponse;
232
233        const SERVICE: Service = Service::AVTransport;
234        const ACTION: &'static str = "TestAction";
235
236        fn build_payload(request: &Self::Request) -> Result<String, ValidationError> {
237            request.validate(ValidationLevel::Basic)?;
238            Ok(format!(
239                "<TestRequest><Value>{}</Value></TestRequest>",
240                request.value
241            ))
242        }
243
244        fn parse_response(xml: &Element) -> Result<Self::Response, crate::error::ApiError> {
245            Ok(TestResponse {
246                result: xml
247                    .get_child("Result")
248                    .and_then(|e| e.get_text())
249                    .map(|s| s.to_string())
250                    .unwrap_or_else(|| "default".to_string()),
251            })
252        }
253    }
254
255    #[test]
256    fn test_operation_builder_new() {
257        let request = TestRequest { value: 50 };
258        let builder = OperationBuilder::<TestOperation>::new(request);
259
260        assert_eq!(builder.validation_level(), ValidationLevel::Basic);
261        assert_eq!(builder.timeout(), None);
262    }
263
264    #[test]
265    fn test_operation_builder_fluent() {
266        let request = TestRequest { value: 50 };
267        let builder = OperationBuilder::<TestOperation>::new(request)
268            .with_validation(ValidationLevel::Basic)
269            .with_timeout(Duration::from_secs(30));
270
271        assert_eq!(builder.validation_level(), ValidationLevel::Basic);
272        assert_eq!(builder.timeout(), Some(Duration::from_secs(30)));
273    }
274
275    #[test]
276    fn test_operation_builder_build_success() {
277        let request = TestRequest { value: 50 };
278        let operation = OperationBuilder::<TestOperation>::new(request)
279            .with_validation(ValidationLevel::Basic)
280            .build()
281            .expect("Should build successfully");
282
283        assert_eq!(operation.request().value, 50);
284        assert_eq!(operation.validation_level(), ValidationLevel::Basic);
285        assert_eq!(operation.metadata().action, "TestAction");
286    }
287
288    #[test]
289    fn test_operation_builder_build_validation_error() {
290        let request = TestRequest { value: 150 }; // Invalid value
291        let result = OperationBuilder::<TestOperation>::new(request)
292            .with_validation(ValidationLevel::Basic)
293            .build();
294
295        assert!(result.is_err());
296        assert!(result.unwrap_err().to_string().contains("150"));
297    }
298
299    #[test]
300    fn test_operation_builder_build_unchecked() {
301        let request = TestRequest { value: 150 }; // Invalid value
302        let operation = OperationBuilder::<TestOperation>::new(request)
303            .with_validation(ValidationLevel::Basic)
304            .build_unchecked(); // Should succeed despite invalid value
305
306        assert_eq!(operation.request().value, 150);
307        assert_eq!(operation.validation_level(), ValidationLevel::None);
308    }
309
310    #[test]
311    fn test_composable_operation_build_payload() {
312        let request = TestRequest { value: 42 };
313        let operation = OperationBuilder::<TestOperation>::new(request)
314            .build()
315            .expect("Should build successfully");
316
317        let payload = operation.build_payload().expect("Should build payload");
318        assert!(payload.contains("<Value>42</Value>"));
319    }
320
321    #[test]
322    fn test_composable_operation_debug() {
323        let request = TestRequest { value: 42 };
324        let operation = OperationBuilder::<TestOperation>::new(request)
325            .with_timeout(Duration::from_secs(10))
326            .build()
327            .expect("Should build successfully");
328
329        let debug_str = format!("{operation:?}");
330        assert!(debug_str.contains("TestAction"));
331        assert!(debug_str.contains("AVTransport"));
332    }
333
334    #[test]
335    fn test_composable_operation_clone() {
336        let request = TestRequest { value: 42 };
337        let operation = OperationBuilder::<TestOperation>::new(request)
338            .build()
339            .expect("Should build successfully");
340
341        let cloned = operation.clone();
342        assert_eq!(operation.request().value, cloned.request().value);
343        assert_eq!(operation.validation_level(), cloned.validation_level());
344    }
345}