Skip to main content

sonos_api/operation/
macros.rs

1//! Declarative macros for UPnP operation and service definitions
2//!
3//! This module provides macros that dramatically reduce boilerplate when defining
4//! UPnP operations. Instead of manually implementing traits and structs, developers
5//! can use simple declarative syntax to generate all necessary code.
6
7/// Simplified macro for defining UPnP operations with minimal boilerplate
8///
9/// This macro generates all the necessary structs and trait implementations
10/// for a UPnP operation.
11///
12/// # Example
13/// ```rust,ignore
14/// define_upnp_operation! {
15///     operation: PlayOperation,
16///     action: "Play",
17///     service: AVTransport,
18///     request: {
19///         speed: String,
20///     },
21///     response: (),
22///     payload: |req| format!("<InstanceID>{}</InstanceID><Speed>{}</Speed>", req.instance_id, req.speed),
23///     parse: |_xml| Ok(()),
24/// }
25/// ```
26#[macro_export]
27macro_rules! define_upnp_operation {
28    (
29        operation: $op_struct:ident,
30        action: $action:literal,
31        service: $service:ident,
32        request: {
33            $($field:ident: $field_type:ty),* $(,)?
34        },
35        response: $response_type:ty,
36        payload: |$req_param:ident| $payload_expr:expr,
37        parse: |$xml_param:ident| $parse_expr:expr $(,)?
38    ) => {
39        paste! {
40            #[derive(serde::Serialize, Clone, Debug, PartialEq)]
41            pub struct [<$op_struct Request>] {
42                $(pub $field: $field_type,)*
43                pub instance_id: u32,
44            }
45
46            // Note: Validate implementation can be provided manually if needed
47            // Default empty implementation is not generated to avoid conflicts
48
49            #[derive(serde::Deserialize, Debug, Clone, PartialEq)]
50            pub struct [<$op_struct Response>];
51
52            pub struct $op_struct;
53
54            impl $crate::operation::UPnPOperation for $op_struct {
55                type Request = [<$op_struct Request>];
56                type Response = $response_type;
57
58                const SERVICE: $crate::service::Service = $crate::service::Service::$service;
59                const ACTION: &'static str = $action;
60
61                fn build_payload(request: &Self::Request) -> Result<String, $crate::operation::ValidationError> {
62                    request.validate($crate::operation::ValidationLevel::Basic)?;
63                    let $req_param = request;
64                    Ok($payload_expr)
65                }
66
67                fn parse_response(xml: &xmltree::Element) -> Result<Self::Response, $crate::error::ApiError> {
68                    let $xml_param = xml;
69                    $parse_expr
70                }
71            }
72
73            // Generate convenience function
74            pub fn [<$op_struct:snake>]($($field: $field_type),*) -> $crate::operation::OperationBuilder<$op_struct> {
75                let request = [<$op_struct Request>] {
76                    $($field,)*
77                    instance_id: 0,
78                };
79                $crate::operation::OperationBuilder::new(request)
80            }
81        }
82    };
83}
84
85/// Macro for defining operations with XML response parsing
86///
87/// # Example
88/// ```rust,ignore
89/// define_operation_with_response! {
90///     operation: GetVolumeOperation,
91///     action: "GetVolume",
92///     service: RenderingControl,
93///     request: {
94///         channel: String,
95///     },
96///     response: GetVolumeResponse {
97///         current_volume: u8,
98///     },
99///     xml_mapping: {
100///         current_volume: "CurrentVolume",
101///     },
102/// }
103/// ```
104#[macro_export]
105macro_rules! define_operation_with_response {
106    (
107        operation: $op_struct:ident,
108        action: $action:literal,
109        service: $service:ident,
110        request: {
111            $($field:ident: $field_type:ty),* $(,)?
112        },
113        response: $response_struct:ident {
114            $($resp_field:ident: $resp_type:ty),* $(,)?
115        },
116        xml_mapping: {
117            $($xml_field:ident: $xml_path:literal),* $(,)?
118        } $(,)?
119    ) => {
120        paste! {
121            #[derive(serde::Serialize, Clone, Debug, PartialEq)]
122            pub struct [<$op_struct Request>] {
123                $(pub $field: $field_type,)*
124                pub instance_id: u32,
125            }
126
127            // Note: Validate implementation can be provided manually if needed
128            // Default empty implementation is not generated to avoid conflicts
129
130            #[derive(serde::Deserialize, Debug, Clone, PartialEq)]
131            pub struct $response_struct {
132                $(pub $resp_field: $resp_type,)*
133            }
134
135            pub struct $op_struct;
136
137            impl $crate::operation::UPnPOperation for $op_struct {
138                type Request = [<$op_struct Request>];
139                type Response = $response_struct;
140
141                const SERVICE: $crate::service::Service = $crate::service::Service::$service;
142                const ACTION: &'static str = $action;
143
144                fn build_payload(request: &Self::Request) -> Result<String, $crate::operation::ValidationError> {
145                    request.validate($crate::operation::ValidationLevel::Basic)?;
146
147                    #[allow(unused_mut)]
148                    let mut xml = format!("<InstanceID>{}</InstanceID>", request.instance_id);
149                    $(
150                        // Capitalize the first letter for proper Sonos XML element names
151                        let field_name = stringify!($field);
152                        let capitalized = if field_name.is_empty() {
153                            field_name.to_string()
154                        } else {
155                            let mut chars = field_name.chars();
156                            match chars.next() {
157                                None => String::new(),
158                                Some(first) => first.to_uppercase().chain(chars).collect(),
159                            }
160                        };
161                        let escaped = $crate::operation::xml_escape(&format!("{}", request.$field));
162                        xml.push_str(&format!("<{}>{}</{}>",
163                            capitalized,
164                            escaped,
165                            capitalized));
166                    )*
167                    Ok(xml)
168                }
169
170                fn parse_response(xml: &xmltree::Element) -> Result<Self::Response, $crate::error::ApiError> {
171                    // Create a temporary mapping from field names to XML paths
172                    $(let $xml_field = xml
173                        .get_child($xml_path)
174                        .and_then(|e| e.get_text())
175                        .and_then(|s| s.parse().ok())
176                        .unwrap_or_default();)*
177
178                    Ok($response_struct {
179                        $($resp_field: $xml_field,)*
180                    })
181                }
182            }
183
184            // Generate convenience function
185            pub fn [<$op_struct:snake>]($($field: $field_type),*) -> $crate::operation::OperationBuilder<$op_struct> {
186                let request = [<$op_struct Request>] {
187                    $($field,)*
188                    instance_id: 0,
189                };
190                $crate::operation::OperationBuilder::new(request)
191            }
192        }
193    };
194}
195
196#[cfg(test)]
197mod tests {
198    #[test]
199    fn test_macro_compilation() {
200        // Test that our macros compile without errors
201        // This is mainly a compilation test to ensure the macro syntax is correct
202
203        // Note: Actual usage tests would go in the services modules where the macros are used
204        // since we can't easily test macro expansion here without a more complex test setup
205    }
206}