meta.rs

  1// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7//! # Parse Rust attributes
  8//!
  9//! This module is concerned with parsing attributes from the Rust "meta"
 10//! annotations on structs, enums, enum variants and fields.
 11
 12use std::borrow::Cow;
 13
 14use proc_macro2::{Span, TokenStream};
 15use quote::{quote, quote_spanned};
 16use syn::{spanned::Spanned, *};
 17
 18use rxml_validation::NcName;
 19
 20/// Value for the `#[xml(namespace = ..)]` attribute.
 21#[derive(Debug)]
 22pub(crate) enum NamespaceRef {
 23    /// The XML namespace is specified as a string literal.
 24    LitStr(LitStr),
 25
 26    /// The XML namespace is specified as a path.
 27    Path(Path),
 28}
 29
 30impl syn::parse::Parse for NamespaceRef {
 31    fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
 32        if input.peek(syn::LitStr) {
 33            Ok(Self::LitStr(input.parse()?))
 34        } else {
 35            Ok(Self::Path(input.parse()?))
 36        }
 37    }
 38}
 39
 40impl quote::ToTokens for NamespaceRef {
 41    fn to_tokens(&self, tokens: &mut TokenStream) {
 42        match self {
 43            Self::LitStr(ref lit) => lit.to_tokens(tokens),
 44            Self::Path(ref path) => path.to_tokens(tokens),
 45        }
 46    }
 47}
 48
 49/// Value for the `#[xml(name = .. )]` attribute.
 50#[derive(Debug)]
 51pub(crate) enum NameRef {
 52    /// The XML name is specified as a string literal.
 53    Literal {
 54        /// The validated XML name.
 55        value: NcName,
 56
 57        /// The span of the original [`syn::LitStr`].
 58        span: Span,
 59    },
 60
 61    /// The XML name is specified as a path.
 62    Path(Path),
 63}
 64
 65impl NameRef {
 66    /// Access a representation of the XML name as str.
 67    ///
 68    /// If this name reference is a [`Self::Path`], this will return the name
 69    /// of the rightmost identifier in the path.
 70    ///
 71    /// If this name reference is a [`Self::Literal`], this will return the
 72    /// contents of the literal.
 73    pub(crate) fn repr_to_string(&self) -> Cow<'_, str> {
 74        match self {
 75            Self::Literal { ref value, .. } => Cow::Borrowed(value.as_str()),
 76            Self::Path(ref path) => path.segments.last().unwrap().ident.to_string().into(),
 77        }
 78    }
 79}
 80
 81impl syn::parse::Parse for NameRef {
 82    fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
 83        if input.peek(syn::LitStr) {
 84            let s: LitStr = input.parse()?;
 85            let span = s.span();
 86            match NcName::try_from(s.value()) {
 87                Ok(value) => Ok(Self::Literal { value, span }),
 88                Err(e) => Err(Error::new(
 89                    span,
 90                    format!("not a valid XML element name: {}", e),
 91                )),
 92            }
 93        } else {
 94            let p: Path = input.parse()?;
 95            Ok(Self::Path(p))
 96        }
 97    }
 98}
 99
100impl quote::ToTokens for NameRef {
101    fn to_tokens(&self, tokens: &mut TokenStream) {
102        match self {
103            Self::Literal { ref value, span } => {
104                let span = *span;
105                let value = value.as_str();
106                let value = quote_spanned! { span=> #value };
107                // SAFETY: self.0 is a known-good NcName, so converting it to an
108                // NcNameStr is known to be safe.
109                // NOTE: we cannot use `quote_spanned! { self.span=> }` for the unsafe
110                // block as that would then in fact trip a `#[deny(unsafe_code)]` lint
111                // at the use site of the macro.
112                tokens.extend(quote! {
113                    unsafe { ::xso::exports::rxml::NcNameStr::from_str_unchecked(#value) }
114                })
115            }
116            Self::Path(ref path) => path.to_tokens(tokens),
117        }
118    }
119}
120
121/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
122#[derive(Debug)]
123pub(crate) struct XmlCompoundMeta {
124    /// The span of the `#[xml(..)]` meta from which this was parsed.
125    ///
126    /// This is useful for error messages.
127    pub(crate) span: Span,
128
129    /// The value assigned to `namespace` inside `#[xml(..)]`, if any.
130    pub(crate) namespace: Option<NamespaceRef>,
131
132    /// The value assigned to `name` inside `#[xml(..)]`, if any.
133    pub(crate) name: Option<NameRef>,
134}
135
136impl XmlCompoundMeta {
137    /// Parse the meta values from a `#[xml(..)]` attribute.
138    ///
139    /// Undefined options or options with incompatible values are rejected
140    /// with an appropriate compile-time error.
141    fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
142        let mut namespace = None;
143        let mut name = None;
144
145        attr.parse_nested_meta(|meta| {
146            if meta.path.is_ident("name") {
147                if name.is_some() {
148                    return Err(Error::new_spanned(meta.path, "duplicate `name` key"));
149                }
150                name = Some(meta.value()?.parse()?);
151                Ok(())
152            } else if meta.path.is_ident("namespace") {
153                if namespace.is_some() {
154                    return Err(Error::new_spanned(meta.path, "duplicate `namespace` key"));
155                }
156                namespace = Some(meta.value()?.parse()?);
157                Ok(())
158            } else {
159                Err(Error::new_spanned(meta.path, "unsupported key"))
160            }
161        })?;
162
163        Ok(Self {
164            span: attr.span(),
165            namespace,
166            name,
167        })
168    }
169
170    /// Search through `attrs` for a single `#[xml(..)]` attribute and parse
171    /// it.
172    ///
173    /// Undefined options or options with incompatible values are rejected
174    /// with an appropriate compile-time error.
175    ///
176    /// If more than one `#[xml(..)]` attribute is found, an error is
177    /// emitted.
178    ///
179    /// If no `#[xml(..)]` attribute is found, `None` is returned.
180    pub(crate) fn try_parse_from_attributes(attrs: &[Attribute]) -> Result<Option<Self>> {
181        let mut result = None;
182        for attr in attrs {
183            if !attr.path().is_ident("xml") {
184                continue;
185            }
186            if result.is_some() {
187                return Err(syn::Error::new_spanned(
188                    attr.path(),
189                    "only one #[xml(..)] per struct or enum variant allowed",
190                ));
191            }
192            result = Some(Self::parse_from_attribute(attr)?);
193        }
194        Ok(result)
195    }
196
197    /// Search through `attrs` for a single `#[xml(..)]` attribute and parse
198    /// it.
199    ///
200    /// Undefined options or options with incompatible values are rejected
201    /// with an appropriate compile-time error.
202    ///
203    /// If more than one or no `#[xml(..)]` attribute is found, an error is
204    /// emitted.
205    pub(crate) fn parse_from_attributes(attrs: &[Attribute]) -> Result<Self> {
206        match Self::try_parse_from_attributes(attrs)? {
207            Some(v) => Ok(v),
208            None => Err(syn::Error::new(
209                Span::call_site(),
210                "#[xml(..)] attribute required on struct or enum variant",
211            )),
212        }
213    }
214}