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(&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 #[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 }; 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 }; let operation = OperationBuilder::<TestOperation>::new(request)
296 .with_validation(ValidationLevel::Basic)
297 .build_unchecked(); 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}