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}