use super::{OperationMetadata, UPnPOperation, Validate, ValidationError, ValidationLevel};
use std::marker::PhantomData;
use std::time::Duration;
pub struct OperationBuilder<Op: UPnPOperation> {
request: Op::Request,
validation: ValidationLevel,
timeout: Option<Duration>,
_phantom: PhantomData<Op>,
}
impl<Op: UPnPOperation> OperationBuilder<Op> {
pub fn new(request: Op::Request) -> Self {
Self {
request,
validation: ValidationLevel::default(),
timeout: None,
_phantom: PhantomData,
}
}
pub fn with_validation(mut self, level: ValidationLevel) -> Self {
self.validation = level;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self) -> Result<ComposableOperation<Op>, ValidationError> {
self.request.validate(self.validation)?;
Ok(ComposableOperation {
request: self.request,
validation: self.validation,
timeout: self.timeout,
metadata: Op::metadata(),
_phantom: PhantomData,
})
}
pub fn build_unchecked(self) -> ComposableOperation<Op> {
ComposableOperation {
request: self.request,
validation: ValidationLevel::None,
timeout: self.timeout,
metadata: Op::metadata(),
_phantom: PhantomData,
}
}
pub fn validation_level(&self) -> ValidationLevel {
self.validation
}
pub fn timeout(&self) -> Option<Duration> {
self.timeout
}
}
pub struct ComposableOperation<Op: UPnPOperation> {
pub(crate) request: Op::Request,
pub(crate) validation: ValidationLevel,
pub(crate) timeout: Option<Duration>,
pub(crate) metadata: OperationMetadata,
_phantom: PhantomData<Op>,
}
impl<Op: UPnPOperation> ComposableOperation<Op> {
pub fn request(&self) -> &Op::Request {
&self.request
}
pub fn validation_level(&self) -> ValidationLevel {
self.validation
}
pub fn timeout(&self) -> Option<Duration> {
self.timeout
}
pub fn metadata(&self) -> &OperationMetadata {
&self.metadata
}
pub fn build_payload(&self) -> Result<String, ValidationError> {
Op::build_payload(&self.request)
}
pub fn parse_response(
&self,
xml: &xmltree::Element,
) -> Result<Op::Response, crate::error::ApiError> {
Op::parse_response(xml)
}
}
impl<Op: UPnPOperation> std::fmt::Debug for ComposableOperation<Op> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ComposableOperation")
.field("service", &self.metadata.service)
.field("action", &self.metadata.action)
.field("validation", &self.validation)
.field("timeout", &self.timeout)
.finish()
}
}
impl<Op: UPnPOperation> Clone for ComposableOperation<Op>
where
Op::Request: Clone,
{
fn clone(&self) -> Self {
Self {
request: self.request.clone(),
validation: self.validation,
timeout: self.timeout,
metadata: self.metadata.clone(),
_phantom: PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::operation::{Validate, ValidationError, ValidationLevel};
use crate::service::Service;
use serde::{Deserialize, Serialize};
use xmltree::Element;
#[derive(Serialize, Clone, Debug, PartialEq)]
struct TestRequest {
value: i32,
}
impl Validate for TestRequest {
fn validate_basic(&self) -> Result<(), ValidationError> {
if self.value < 0 || self.value > 100 {
Err(ValidationError::range_error("value", 0, 100, self.value))
} else {
Ok(())
}
}
}
#[derive(Deserialize, Debug, PartialEq)]
struct TestResponse {
result: String,
}
struct TestOperation;
impl UPnPOperation for TestOperation {
type Request = TestRequest;
type Response = TestResponse;
const SERVICE: Service = Service::AVTransport;
const ACTION: &'static str = "TestAction";
fn build_payload(request: &Self::Request) -> Result<String, ValidationError> {
request.validate(ValidationLevel::Basic)?;
Ok(format!(
"<TestRequest><Value>{}</Value></TestRequest>",
request.value
))
}
fn parse_response(xml: &Element) -> Result<Self::Response, crate::error::ApiError> {
Ok(TestResponse {
result: xml
.get_child("Result")
.and_then(|e| e.get_text())
.map(|s| s.to_string())
.unwrap_or_else(|| "default".to_string()),
})
}
}
#[test]
fn test_operation_builder_new() {
let request = TestRequest { value: 50 };
let builder = OperationBuilder::<TestOperation>::new(request);
assert_eq!(builder.validation_level(), ValidationLevel::Basic);
assert_eq!(builder.timeout(), None);
}
#[test]
fn test_operation_builder_fluent() {
let request = TestRequest { value: 50 };
let builder = OperationBuilder::<TestOperation>::new(request)
.with_validation(ValidationLevel::Basic)
.with_timeout(Duration::from_secs(30));
assert_eq!(builder.validation_level(), ValidationLevel::Basic);
assert_eq!(builder.timeout(), Some(Duration::from_secs(30)));
}
#[test]
fn test_operation_builder_build_success() {
let request = TestRequest { value: 50 };
let operation = OperationBuilder::<TestOperation>::new(request)
.with_validation(ValidationLevel::Basic)
.build()
.expect("Should build successfully");
assert_eq!(operation.request().value, 50);
assert_eq!(operation.validation_level(), ValidationLevel::Basic);
assert_eq!(operation.metadata().action, "TestAction");
}
#[test]
fn test_operation_builder_build_validation_error() {
let request = TestRequest { value: 150 }; let result = OperationBuilder::<TestOperation>::new(request)
.with_validation(ValidationLevel::Basic)
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("150"));
}
#[test]
fn test_operation_builder_build_unchecked() {
let request = TestRequest { value: 150 }; let operation = OperationBuilder::<TestOperation>::new(request)
.with_validation(ValidationLevel::Basic)
.build_unchecked();
assert_eq!(operation.request().value, 150);
assert_eq!(operation.validation_level(), ValidationLevel::None);
}
#[test]
fn test_composable_operation_build_payload() {
let request = TestRequest { value: 42 };
let operation = OperationBuilder::<TestOperation>::new(request)
.build()
.expect("Should build successfully");
let payload = operation.build_payload().expect("Should build payload");
assert!(payload.contains("<Value>42</Value>"));
}
#[test]
fn test_composable_operation_debug() {
let request = TestRequest { value: 42 };
let operation = OperationBuilder::<TestOperation>::new(request)
.with_timeout(Duration::from_secs(10))
.build()
.expect("Should build successfully");
let debug_str = format!("{operation:?}");
assert!(debug_str.contains("TestAction"));
assert!(debug_str.contains("AVTransport"));
}
#[test]
fn test_composable_operation_clone() {
let request = TestRequest { value: 42 };
let operation = OperationBuilder::<TestOperation>::new(request)
.build()
.expect("Should build successfully");
let cloned = operation.clone();
assert_eq!(operation.request().value, cloned.request().value);
assert_eq!(operation.validation_level(), cloned.validation_level());
}
}