sonos_api/operation/
builder.rs1use super::{OperationMetadata, UPnPOperation, Validate, ValidationError, ValidationLevel};
7use std::marker::PhantomData;
8use std::time::Duration;
9
10pub 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 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 pub fn with_validation(mut self, level: ValidationLevel) -> Self {
49 self.validation = level;
50 self
51 }
52
53 pub fn with_timeout(mut self, timeout: Duration) -> Self {
61 self.timeout = Some(timeout);
62 self
63 }
64
65 pub fn build(self) -> Result<ComposableOperation<Op>, ValidationError> {
73 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 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 pub fn validation_level(&self) -> ValidationLevel {
104 self.validation
105 }
106
107 pub fn timeout(&self) -> Option<Duration> {
109 self.timeout
110 }
111}
112
113pub 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 pub fn request(&self) -> &Op::Request {
131 &self.request
132 }
133
134 pub fn validation_level(&self) -> ValidationLevel {
136 self.validation
137 }
138
139 pub fn timeout(&self) -> Option<Duration> {
141 self.timeout
142 }
143
144 pub fn metadata(&self) -> &OperationMetadata {
146 &self.metadata
147 }
148
149 pub fn build_payload(&self) -> Result<String, ValidationError> {
154 Op::build_payload(&self.request)
155 }
156
157 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 #[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 }; 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 }; let operation = OperationBuilder::<TestOperation>::new(request)
303 .with_validation(ValidationLevel::Basic)
304 .build_unchecked(); 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}