Skip to main content

sigmf_derive/
lib.rs

1//! Derive macro for typed [SigMF](https://sigmf.org) metadata extensions.
2//!
3//! This crate is an implementation detail of [`wcsng-sigmf`]. Enable its
4//! `derive` feature and use the macro as `sigmf::SigmfExtension` rather than
5//! depending on this crate directly:
6//!
7//! ```toml
8//! [dependencies]
9//! wcsng-sigmf = { version = "0.1", features = ["derive"] }
10//! ```
11//!
12//! [`wcsng-sigmf`]: https://docs.rs/wcsng-sigmf
13#![warn(missing_docs)]
14
15use proc_macro::TokenStream;
16use quote::quote;
17use syn::{Attribute, Data, DeriveInput, Fields, LitStr, Path, Result, parse_macro_input};
18
19/// Implements `sigmf::extensions::SigmfExtension` for a struct.
20///
21/// # Container attributes
22///
23/// `#[sigmf(...)]` on the struct accepts:
24///
25/// - `namespace = "..."` (required): the extension namespace. It must be a
26///   non-empty ASCII identifier (letters, digits, `_`, `-`) other than `core`.
27/// - `version = "..."` (required): the extension version declared in
28///   `core:extensions`.
29/// - `scope = "..."` (required): where the fields live: `global`, `capture`,
30///   `annotation`, or `collection`.
31/// - `crate = "..."` (optional): path to the `sigmf` library if it is renamed
32///   in `Cargo.toml`; defaults to `::sigmf`.
33///
34/// # Field requirements
35///
36/// The struct must have named fields, each with an explicit
37/// `#[serde(rename = "<namespace>:<name>")]` that uses the container's
38/// namespace. Serde attributes that change the wire shape (`flatten`, `skip*`,
39/// `with`, `rename_all`, `tag`, `untagged`, `transparent`, ...) are rejected so
40/// that the generated `FIELDS` list always matches the serialized keys.
41///
42/// # Example
43///
44/// ```ignore
45/// use serde::{Deserialize, Serialize};
46/// use sigmf::SigmfExtension;
47///
48/// #[derive(Serialize, Deserialize, SigmfExtension)]
49/// #[sigmf(namespace = "lab", version = "1.0.0", scope = "global")]
50/// struct LabReceiver {
51///     #[serde(rename = "lab:gain_db")]
52///     gain_db: f64,
53///     #[serde(rename = "lab:device_id", skip_serializing_if = "Option::is_none")]
54///     device_id: Option<String>,
55/// }
56/// ```
57#[proc_macro_derive(SigmfExtension, attributes(sigmf))]
58pub fn derive_sigmf_extension(input: TokenStream) -> TokenStream {
59    let input = parse_macro_input!(input as DeriveInput);
60    expand(input)
61        .unwrap_or_else(syn::Error::into_compile_error)
62        .into()
63}
64
65fn expand(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
66    let mut namespace = None;
67    let mut version = None;
68    let mut scope = None;
69    let mut crate_path: Path = syn::parse_quote!(::sigmf);
70    let mut crate_seen = false;
71
72    for attribute in input
73        .attrs
74        .iter()
75        .filter(|attr| attr.path().is_ident("sigmf"))
76    {
77        attribute.parse_nested_meta(|meta| {
78            if meta.path.is_ident("namespace") {
79                if namespace.is_some() {
80                    return Err(meta.error("duplicate sigmf namespace attribute"));
81                }
82                namespace = Some(meta.value()?.parse::<LitStr>()?);
83            } else if meta.path.is_ident("version") {
84                if version.is_some() {
85                    return Err(meta.error("duplicate sigmf version attribute"));
86                }
87                version = Some(meta.value()?.parse::<LitStr>()?);
88            } else if meta.path.is_ident("scope") {
89                if scope.is_some() {
90                    return Err(meta.error("duplicate sigmf scope attribute"));
91                }
92                scope = Some(meta.value()?.parse::<LitStr>()?);
93            } else if meta.path.is_ident("crate") {
94                if crate_seen {
95                    return Err(meta.error("duplicate sigmf crate attribute"));
96                }
97                crate_seen = true;
98                let path = meta.value()?.parse::<LitStr>()?;
99                crate_path = path.parse()?;
100            } else {
101                return Err(meta.error("unsupported sigmf attribute"));
102            }
103            Ok(())
104        })?;
105    }
106
107    let namespace = namespace.ok_or_else(|| {
108        syn::Error::new_spanned(&input.ident, "missing #[sigmf(namespace = \"...\")]")
109    })?;
110    let version = version.ok_or_else(|| {
111        syn::Error::new_spanned(&input.ident, "missing #[sigmf(version = \"...\")]")
112    })?;
113    let scope = scope.ok_or_else(|| {
114        syn::Error::new_spanned(&input.ident, "missing #[sigmf(scope = \"...\")]")
115    })?;
116
117    let namespace_value = namespace.value();
118    if namespace_value == "core"
119        || namespace_value.is_empty()
120        || !namespace_value
121            .bytes()
122            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
123    {
124        return Err(syn::Error::new_spanned(
125            namespace,
126            "namespace must be a non-empty ASCII identifier and cannot be \"core\"",
127        ));
128    }
129    if version.value().is_empty() {
130        return Err(syn::Error::new_spanned(
131            version,
132            "version must not be empty",
133        ));
134    }
135
136    let scope_variant = match scope.value().as_str() {
137        "global" => quote!(Global),
138        "capture" => quote!(Capture),
139        "annotation" => quote!(Annotation),
140        "collection" => quote!(Collection),
141        _ => {
142            return Err(syn::Error::new_spanned(
143                scope,
144                "scope must be one of global, capture, annotation, or collection",
145            ));
146        }
147    };
148
149    let fields = match &input.data {
150        Data::Struct(data) => match &data.fields {
151            Fields::Named(fields) => &fields.named,
152            _ => {
153                return Err(syn::Error::new_spanned(
154                    &input.ident,
155                    "SigmfExtension requires a struct with named fields",
156                ));
157            }
158        },
159        _ => {
160            return Err(syn::Error::new_spanned(
161                &input.ident,
162                "SigmfExtension can only be derived for structs",
163            ));
164        }
165    };
166
167    reject_conflicting_container_attributes(&input.attrs)?;
168    let mut names = Vec::<LitStr>::new();
169    let mut seen = std::collections::HashSet::new();
170    for field in fields {
171        let wire_name = serde_wire_name(&field.attrs)?;
172        let wire_value = wire_name.value();
173        let expected_prefix = format!("{namespace_value}:");
174        if !wire_value.starts_with(&expected_prefix) || wire_value.len() == expected_prefix.len() {
175            return Err(syn::Error::new_spanned(
176                wire_name,
177                format!("extension fields must use the {expected_prefix:?} namespace"),
178            ));
179        }
180        if !seen.insert(wire_value.clone()) {
181            return Err(syn::Error::new_spanned(
182                wire_name,
183                "duplicate extension wire field name",
184            ));
185        }
186        names.push(wire_name);
187    }
188
189    let ident = &input.ident;
190    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
191    Ok(quote! {
192        impl #impl_generics #crate_path::extensions::SigmfExtension
193            for #ident #ty_generics #where_clause
194        {
195            const NAMESPACE: &'static str = #namespace_value;
196            const VERSION: &'static str = #version;
197            const SCOPE: #crate_path::extensions::ExtensionScope =
198                #crate_path::extensions::ExtensionScope::#scope_variant;
199            const FIELDS: &'static [&'static str] = &[#(#names),*];
200        }
201    })
202}
203
204fn reject_conflicting_container_attributes(attributes: &[Attribute]) -> Result<()> {
205    for attribute in attributes
206        .iter()
207        .filter(|attr| attr.path().is_ident("serde"))
208    {
209        attribute.parse_nested_meta(|meta| {
210            if [
211                "rename_all",
212                "rename_all_fields",
213                "transparent",
214                "tag",
215                "content",
216                "untagged",
217                "from",
218                "try_from",
219            ]
220            .iter()
221            .any(|unsupported| meta.path.is_ident(unsupported))
222            {
223                return Err(
224                    meta.error("this Serde container attribute is not supported by SigmfExtension")
225                );
226            }
227            if meta.input.peek(syn::Token![=]) {
228                let _: syn::Expr = meta.value()?.parse()?;
229            }
230            Ok(())
231        })?;
232    }
233    Ok(())
234}
235
236fn serde_wire_name(attributes: &[Attribute]) -> Result<LitStr> {
237    let mut wire_name = None;
238    for attribute in attributes
239        .iter()
240        .filter(|attr| attr.path().is_ident("serde"))
241    {
242        attribute.parse_nested_meta(|meta| {
243            if meta.path.is_ident("rename") {
244                if wire_name.is_some() {
245                    return Err(meta.error("multiple Serde rename attributes are unsupported"));
246                }
247                wire_name = Some(meta.value()?.parse::<LitStr>()?);
248            } else if [
249                "flatten",
250                "with",
251                "serialize_with",
252                "deserialize_with",
253                "skip",
254                "skip_serializing",
255                "skip_deserializing",
256            ]
257            .iter()
258            .any(|unsupported| meta.path.is_ident(unsupported))
259            {
260                return Err(
261                    meta.error("this Serde field attribute is not supported by SigmfExtension")
262                );
263            }
264            if meta.input.peek(syn::Token![=]) {
265                let _: syn::Expr = meta.value()?.parse()?;
266            }
267            Ok(())
268        })?;
269    }
270    wire_name.ok_or_else(|| {
271        syn::Error::new(
272            proc_macro2::Span::call_site(),
273            "each extension field requires an explicit #[serde(rename = \"namespace:field\")]",
274        )
275    })
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    fn diagnostic(source: &str) -> String {
283        let input = syn::parse_str::<DeriveInput>(source).unwrap();
284        expand(input).unwrap_err().to_string()
285    }
286
287    #[test]
288    fn reports_missing_and_reserved_extension_attributes() {
289        assert!(diagnostic("struct Missing;").contains("missing #[sigmf(namespace"));
290        let error = diagnostic(
291            r#"
292                #[sigmf(namespace = "core", version = "1.0.0", scope = "global")]
293                struct Reserved {
294                    #[serde(rename = "core:value")]
295                    value: bool,
296                }
297            "#,
298        );
299        assert!(error.contains("cannot be \"core\""));
300    }
301
302    #[test]
303    fn rejects_invalid_wire_shapes_with_field_diagnostics() {
304        let error = diagnostic(
305            r#"
306                #[sigmf(namespace = "lab", version = "1.0.0", scope = "capture")]
307                struct WrongNamespace {
308                    #[serde(rename = "other:value")]
309                    value: bool,
310                }
311            "#,
312        );
313        assert!(error.contains("must use the \"lab:\" namespace"));
314
315        let error = diagnostic(
316            r#"
317                #[sigmf(namespace = "lab", version = "1.0.0", scope = "capture")]
318                struct Flattened {
319                    #[serde(rename = "lab:value", flatten)]
320                    value: bool,
321                }
322            "#,
323        );
324        assert!(error.contains("not supported by SigmfExtension"));
325
326        let error = diagnostic(
327            r#"
328                #[sigmf(namespace = "lab", version = "1.0.0", scope = "capture")]
329                struct Skipped {
330                    #[serde(rename = "lab:value", skip)]
331                    value: bool,
332                }
333            "#,
334        );
335        assert!(error.contains("not supported by SigmfExtension"));
336    }
337
338    #[test]
339    fn rejects_unsupported_derive_shapes() {
340        let error = diagnostic(
341            r#"
342                #[sigmf(namespace = "lab", version = "1.0.0", scope = "capture")]
343                enum Unsupported {
344                    Value,
345                }
346            "#,
347        );
348        assert!(error.contains("can only be derived for structs"));
349
350        let error = diagnostic(
351            r#"
352                #[sigmf(namespace = "lab", namespace = "vendor", version = "1.0.0", scope = "global")]
353                struct DuplicateNamespace {
354                    #[serde(rename = "lab:value")]
355                    value: bool,
356                }
357            "#,
358        );
359        assert!(error.contains("duplicate sigmf namespace attribute"));
360    }
361}