amplify_syn 1.1.4

Amplifying syn capabilities: helper functions for creating proc macro libraries
Documentation
// Rust language amplification derive library providing multiple generic trait
// implementations, type wrappers, derive macros and other language enhancements
//
// Written in 2019-2021 by
//     Dr. Maxim Orlovsky <orlovsky@pandoracore.com>
//
// To the extent possible under law, the author(s) have dedicated all
// copyright and related and neighboring rights to this software to
// the public domain worldwide. This software is distributed without
// any warranty.
//
// You should have received a copy of the MIT License
// along with this software.
// If not, see <https://opensource.org/licenses/MIT>.

use std::collections::{HashMap};
use std::convert::TryInto;
use syn::{Path, LitChar, LitInt, LitFloat};
use quote::ToTokens;

use crate::{Error, ValueClass, ArgValue};

/// Structure requirements for parametrized attribute
#[derive(Clone)]
pub struct AttrReq {
    /// Specifies all named arguments and which requirements they must meet
    pub arg_req: HashMap<String, ArgValueReq>,

    /// Specifies whether path arguments are allowed and with which
    /// requirements.
    pub path_req: ListReq<Path>,

    /// Whether integer literals are allowed as an attribute argument and, if
    /// yes, with which requirements
    pub char_req: ListReq<LitChar>,

    /// Whether integer literals are allowed as an attribute argument and, if
    /// yes, with which requirements
    pub integer_req: ListReq<LitInt>,

    /// Whether integer literals are allowed as an attribute argument and, if
    /// yes, with which requirements
    pub float_req: ListReq<LitFloat>,

    /// Whether string literal is allowed as an attribute argument and, if
    /// yes, with which requirements
    pub string_req: ValueReq,

    /// Whether byte string literal is allowed as an attribute argument and, if
    /// yes, with which requirements
    pub bytes_req: ValueReq,

    /// Whether boolean literal is allowed as an attribute argument and, if
    /// yes, with which requirements
    pub bool_req: ValueReq,
}

impl AttrReq {
    /// Constructor creating [`AttrReq`] accepting only name-value arguments
    /// with the provided parameters
    pub fn with(args: HashMap<&str, ArgValueReq>) -> AttrReq {
        let args = args
            .into_iter()
            .map(|(name, req)| (name.to_owned(), req))
            .collect();

        AttrReq {
            arg_req: args,
            path_req: ListReq::Deny,
            char_req: ListReq::Deny,
            integer_req: ListReq::Deny,
            float_req: ListReq::Deny,
            string_req: ValueReq::Prohibited,
            bytes_req: ValueReq::Prohibited,
            bool_req: ValueReq::Prohibited,
        }
    }
}

/// Requirements for attribute or named argument value presence
#[derive(Clone)]
pub enum ArgValueReq {
    /// Argument must hold a value with the provided class
    Required {
        /// Default value
        default: Option<ArgValue>,
        /// Type of the value literal
        class: ValueClass,
    },

    /// Argument or an attribute may or may not hold a value
    Optional(ValueClass),

    /// Argument or an attribute must not hold a value
    Prohibited,
}

impl ArgValueReq {
    /// Constructs argument requirements object with default value
    pub fn with_default(default: impl Into<ArgValue>) -> ArgValueReq {
        let value = default.into();
        ArgValueReq::Required {
            class: value
                .value_class()
                .expect("Default argument value must not be `ArgValue::None`"),
            default: Some(value),
        }
    }

    /// Construct [`ArgValueReq::Required`] variant with no default value
    pub fn required(class: ValueClass) -> ArgValueReq {
        ArgValueReq::Required {
            default: None,
            class,
        }
    }

    /// Returns value class requirements, if any
    pub fn value_class(&self) -> Option<ValueClass> {
        match self {
            ArgValueReq::Required { class, .. } | ArgValueReq::Optional(class) => Some(*class),
            ArgValueReq::Prohibited => None,
        }
    }

    /// Returns default argument value. If not default is provided within the
    /// requirement, returns [`ArgValue::None`] (since this is *de facto*
    /// default value for any argument).
    pub fn default_value(&self) -> ArgValue {
        match self {
            ArgValueReq::Required {
                default: Some(d), ..
            } => d.clone(),
            _ => ArgValue::None,
        }
    }

    /// Determines whether argument is required to have a value
    pub fn is_required(&self) -> bool {
        // Ancient rust versions do not known about `matches!` macro
        #[allow(clippy::match_like_matches_macro)]
        match self {
            ArgValueReq::Required { default: None, .. } => true,
            _ => false,
        }
    }

    /// Checks the argument against current requirements, generating [`Error`]
    /// if the requirements are not met.
    pub fn check(
        &self,
        value: &mut ArgValue,
        attr: impl ToString,
        arg: impl ToString,
    ) -> Result<(), Error> {
        let value = match (value, self) {
            (ref val, ArgValueReq::Required { default: None, .. }) if val.is_none() => {
                return Err(Error::ArgValueRequired {
                    attr: attr.to_string(),
                    arg: arg.to_string(),
                })
            }

            (ref val, ArgValueReq::Prohibited) if val.is_some() => {
                return Err(Error::ArgMustNotHaveValue {
                    attr: attr.to_string(),
                    arg: arg.to_string(),
                })
            }

            (
                ref val,
                ArgValueReq::Required {
                    default: Some(d), ..
                },
            ) if val.value_class() != d.value_class() && val.value_class().is_some() => {
                panic!(
                    "Default value class {:?} does not match argument value class {:?} for attribute {}, argument {}",
                    d.value_class(),
                    val.value_class(),
                    attr.to_string(),
                    arg.to_string()
                );
            }

            (val, req) => {
                if val.is_none() {
                    if let ArgValueReq::Required {
                        default: Some(d), ..
                    } = req
                    {
                        *val = d.clone();
                    }
                }
                val
            }
        };

        if let Some(value_class) = self.value_class() {
            value_class.check(value, attr, arg)?;
        }

        Ok(())
    }
}

/// Requirements for attribute or named argument value presence for a values
/// with known class. If the value class is not known, use [`ArgValueReq`]
/// instead.
#[derive(Clone)]
pub enum ValueReq {
    /// Argument or an attribute must hold a value
    Required,

    /// Argument or an attribute must hold a value; if the value is not present
    /// it will be substituted for the default value provided as the inner field
    Default(ArgValue),

    /// Argument or an attribute may or may not hold a value
    Optional,

    /// Argument or an attribute must not hold a value
    Prohibited,
}

impl ValueReq {
    /// Detects if the presence of the value is required
    #[inline]
    pub fn is_required(&self) -> bool {
        // Ancient rust versions do not known about `matches!` macro
        #[allow(clippy::match_like_matches_macro)]
        match self {
            ValueReq::Required => true,
            _ => false,
        }
    }

    /// Checks the value against current requirements, generating [`Error`] if
    /// the requirements are not met.
    pub fn check<T>(
        &self,
        value: &mut T,
        attr: impl ToString,
        arg: impl ToString,
    ) -> Result<(), Error>
    where
        T: Clone,
        ArgValue: From<T> + TryInto<T>,
        Error: From<<ArgValue as TryInto<T>>::Error>,
    {
        let attr = attr.to_string();
        let arg_value = ArgValue::from(value.clone());
        match (self, value) {
            (ValueReq::Required, _) if arg_value.is_none() => Err(Error::ArgValueRequired {
                attr,
                arg: arg.to_string(),
            }),
            (ValueReq::Prohibited, _) if arg_value.is_some() => Err(Error::ArgMustNotHaveValue {
                attr,
                arg: arg.to_string(),
            }),
            (ValueReq::Default(ref val), ref mut v) if arg_value.is_none() => {
                **v = val.clone().try_into()?;
                Ok(())
            }
            _ => Ok(()),
        }
    }
}

/// Requirements for list elements. For instance, used in [`AttrReq`] for
/// providing [`ParametrizedAttr`] fields requirements.
#[derive(Clone)]
pub enum ListReq<T>
where
    T: Clone,
{
    /// Only a single value allowed and it must be present
    Single {
        /// Restricts set of possible values to the given whitelist
        ///
        /// NB: If whitelist does not contain values from the `default` field,
        /// they are still accepted as valid, i.e. "automatically whitelisted"
        whitelist: Option<Vec<T>>,

        /// Default value assigned as a signe list item if no values are
        /// provided
        ///
        /// NB: If whitelist does not contain values from the `default` field,
        /// they are still accepted as valid, i.e. "automatically whitelisted"
        default: Option<T>,
    },

    /// Any number of any elements may be present
    Many {
        /// Restricts set of possible values to the given whitelist
        whitelist: Option<Vec<T>>,

        /// Require that at least one value is present
        required: bool,

        /// Restricts the maximum number of items
        max_no: Option<u8>,
    },

    /// Any number of any elements may not be present; if none of the elements
    /// is present the list will use default vec of the values
    Predefined {
        /// Restricts set of possible values to the given whitelist.
        ///
        /// NB: If whitelist does not contain values from the `default` field,
        /// they are still accepted as valid, i.e. "automatically whitelisted"
        whitelist: Option<Vec<T>>,

        /// Default set of values for the list used if no values are provided
        ///
        /// NB: If whitelist does not contain values from the `default` field,
        /// they are still accepted as valid, i.e. "automatically whitelisted"
        default: Vec<T>,
    },

    /// Element must not be present
    Deny,
}

impl<T> ListReq<T>
where
    T: Clone + ToTokens,
{
    /// Checks the value against the list requirements, generating [`Error`] if
    /// the requirements are not met.
    pub fn check(
        &self,
        value: &mut Vec<T>,
        attr: impl ToString,
        arg: impl ToString,
    ) -> Result<(), Error> {
        match (self, value.len()) {
            // Checking are we allowed to have a value
            (ListReq::Deny, x) if x > 0 => {
                return Err(Error::ArgTypeProhibited {
                    attr: attr.to_string(),
                    arg: arg.to_string(),
                })
            }

            // Checking are we required to have a value while no value is available
            (ListReq::Many { required: true, .. }, 0)
            | (ListReq::Single { default: None, .. }, 0) => {
                return Err(Error::ArgRequired {
                    attr: attr.to_string(),
                    arg: arg.to_string(),
                })
            }

            // Checking not to the exceed maximally allowed number of values
            (
                ListReq::Many {
                    max_no: Some(max_no),
                    ..
                },
                no,
            ) if no > *max_no as usize => {
                return Err(Error::ArgNumberExceedsMax {
                    attr: attr.to_string(),
                    type_name: arg.to_string(),
                    no,
                    max_no: *max_no,
                })
            }

            // Checking that arguments are matching whitelist
            (
                ListReq::Many {
                    whitelist: Some(whitelist),
                    ..
                },
                len,
            )
            | (
                ListReq::Predefined {
                    whitelist: Some(whitelist),
                    ..
                },
                len,
            )
            | (
                ListReq::Single {
                    whitelist: Some(whitelist),
                    ..
                },
                len,
            ) if len > 0 => {
                for item in value {
                    if !whitelist.iter().any(|i| {
                        i.to_token_stream().to_string() == item.to_token_stream().to_string()
                    }) {
                        return Err(Error::AttributeUnknownArgument {
                            attr: attr.to_string(),
                            arg: arg.to_string(),
                        });
                    }
                }
            }

            // Defaulting if no value is provided
            (
                ListReq::Single {
                    default: Some(d), ..
                },
                0,
            ) => value.push(d.clone()),
            (ListReq::Predefined { default, .. }, 0) => *value = default.clone(),

            // Otherwise we are good
            _ => {}
        }
        Ok(())
    }
}