tier-derive 0.1.17

Derive macros for tier Rust configuration metadata
Documentation
use std::cmp::Ordering;

use quote::quote;
use syn::Expr;

#[derive(Debug, Default)]
pub(crate) struct TierAttrs {
    pub(crate) secret: bool,
    pub(crate) leaf: bool,
    pub(crate) sources: Vec<TierSourceKind>,
    pub(crate) deny_sources: Vec<TierSourceKind>,
    pub(crate) env: Option<String>,
    pub(crate) doc: Option<String>,
    pub(crate) example: Option<String>,
    pub(crate) deprecated: Option<String>,
    pub(crate) merge: Option<String>,
    pub(crate) non_empty: bool,
    pub(crate) min: Option<NumericLiteral>,
    pub(crate) max: Option<NumericLiteral>,
    pub(crate) min_length: Option<usize>,
    pub(crate) max_length: Option<usize>,
    pub(crate) min_items: Option<usize>,
    pub(crate) max_items: Option<usize>,
    pub(crate) min_properties: Option<usize>,
    pub(crate) max_properties: Option<usize>,
    pub(crate) multiple_of: Option<NumericLiteral>,
    pub(crate) pattern: Option<String>,
    pub(crate) unique_items: bool,
    pub(crate) one_of: Vec<Expr>,
    pub(crate) hostname: bool,
    pub(crate) url: bool,
    pub(crate) email: bool,
    pub(crate) ip_addr: bool,
    pub(crate) socket_addr: bool,
    pub(crate) absolute_path: bool,
    pub(crate) env_decode: Option<String>,
    pub(crate) validation_messages: Vec<(String, String)>,
    pub(crate) validation_levels: Vec<(String, String)>,
    pub(crate) validation_tags: Vec<(String, Vec<String>)>,
}

impl TierAttrs {
    pub(crate) fn has_any(&self) -> bool {
        self.secret
            || self.leaf
            || !self.sources.is_empty()
            || !self.deny_sources.is_empty()
            || self.env.is_some()
            || self.doc.is_some()
            || self.example.is_some()
            || self.deprecated.is_some()
            || self.merge.is_some()
            || self.non_empty
            || self.min.is_some()
            || self.max.is_some()
            || self.min_length.is_some()
            || self.max_length.is_some()
            || self.min_items.is_some()
            || self.max_items.is_some()
            || self.min_properties.is_some()
            || self.max_properties.is_some()
            || self.multiple_of.is_some()
            || self.pattern.is_some()
            || self.unique_items
            || !self.one_of.is_empty()
            || self.hostname
            || self.url
            || self.email
            || self.ip_addr
            || self.socket_addr
            || self.absolute_path
            || self.env_decode.is_some()
            || !self.validation_messages.is_empty()
            || !self.validation_levels.is_empty()
            || !self.validation_tags.is_empty()
    }
}

#[derive(Debug, Default)]
pub(crate) struct PatchAttrs {
    pub(crate) path: Option<String>,
    pub(crate) path_expr: Option<Expr>,
    pub(crate) nested: bool,
    pub(crate) skip: bool,
}

impl PatchAttrs {
    pub(crate) fn has_any(&self) -> bool {
        self.path.is_some() || self.path_expr.is_some() || self.nested || self.skip
    }

    pub(crate) fn has_non_skip(&self) -> bool {
        self.path.is_some() || self.path_expr.is_some() || self.nested
    }
}

#[derive(Debug, Default)]
pub(crate) struct TierContainerAttrs {
    pub(crate) checks: Vec<ContainerValidationCheck>,
}

#[derive(Debug, Clone)]
pub(crate) struct NumericLiteral {
    pub(crate) tokens: proc_macro2::TokenStream,
    pub(crate) value: NumericLiteralValue,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NumericLiteralValue {
    negative: bool,
    numerator: u128,
    denominator: u128,
}

impl NumericLiteralValue {
    pub(crate) fn new(negative: bool, numerator: u128, denominator: u128) -> Self {
        debug_assert!(denominator > 0);
        let divisor = gcd_u128(numerator, denominator);
        let numerator = numerator / divisor;
        Self {
            negative: negative && numerator != 0,
            numerator,
            denominator: denominator / divisor,
        }
    }

    pub(crate) fn is_positive(&self) -> bool {
        !self.negative && self.numerator > 0
    }

    pub(crate) fn cmp_exact(&self, other: &Self) -> Option<Ordering> {
        match (self.negative, other.negative) {
            (true, false) => return Some(Ordering::Less),
            (false, true) => return Some(Ordering::Greater),
            _ => {}
        }

        let left = self.numerator.checked_mul(other.denominator)?;
        let right = other.numerator.checked_mul(self.denominator)?;
        let ordering = left.cmp(&right);
        Some(if self.negative {
            ordering.reverse()
        } else {
            ordering
        })
    }
}

fn gcd_u128(mut left: u128, mut right: u128) -> u128 {
    while right != 0 {
        let remainder = left % right;
        left = right;
        right = remainder;
    }
    left.max(1)
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum TierSourceKind {
    Default,
    File,
    Environment,
    Arguments,
    Normalization,
    Custom,
}

impl TierSourceKind {
    pub(super) fn parse(value: &str, span: proc_macro2::Span) -> syn::Result<Self> {
        match value {
            "default" => Ok(Self::Default),
            "file" => Ok(Self::File),
            "env" | "environment" => Ok(Self::Environment),
            "cli" | "arguments" => Ok(Self::Arguments),
            "normalize" | "normalization" => Ok(Self::Normalization),
            "custom" => Ok(Self::Custom),
            _ => Err(syn::Error::new(
                span,
                "unsupported tier source kind, expected default|file|env|cli|normalize|custom",
            )),
        }
    }

    pub(crate) fn tokens(self) -> proc_macro2::TokenStream {
        match self {
            Self::Default => quote! { ::tier::SourceKind::Default },
            Self::File => quote! { ::tier::SourceKind::File },
            Self::Environment => quote! { ::tier::SourceKind::Environment },
            Self::Arguments => quote! { ::tier::SourceKind::Arguments },
            Self::Normalization => quote! { ::tier::SourceKind::Normalization },
            Self::Custom => quote! { ::tier::SourceKind::Custom },
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) enum ContainerPathSpec {
    String(String),
    Expr(Expr),
}

#[derive(Debug, Clone)]
pub(crate) enum ContainerPathListSpec {
    Strings(Vec<String>),
    Exprs(Vec<Expr>),
}

#[derive(Debug, Clone)]
pub(crate) enum ContainerValidationCheck {
    AtLeastOneOf(ContainerPathListSpec),
    ExactlyOneOf(ContainerPathListSpec),
    MutuallyExclusive(ContainerPathListSpec),
    RequiredWith {
        path: ContainerPathSpec,
        requires: ContainerPathListSpec,
    },
    RequiredIf {
        path: ContainerPathSpec,
        equals: Expr,
        requires: ContainerPathListSpec,
    },
}