ownership-derive 0.3.0

Derive macros for obtaining ownership.
Documentation
use std::marker::PhantomData;

use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{Attribute as AttributeInput, Error, Meta, Path, meta::ParseNestedMeta};

use crate::{
    context::Context,
    name::{ConstStr, Name},
};

pub struct Attribute<'c, T> {
    context: &'c Context,
    name: Name,
    tokens: TokenStream,
    value: Option<T>,
}

impl<'c, T> Attribute<'c, T> {
    fn none(context: &'c Context, name: Name) -> Self {
        Self {
            context,
            name,
            tokens: TokenStream::new(),
            value: None,
        }
    }

    fn set<I: ToTokens>(&mut self, tokens: I, value: T) {
        if self.value.is_some() {
            let message = format!(
                "duplicate `{ownership}` attribute `{name}`",
                ownership = Name::OWNERSHIP,
                name = self.name
            );

            self.context.error_spanned_by(tokens, message);
        } else {
            self.tokens = tokens.into_token_stream();

            self.value = Some(value);
        }
    }

    fn get(self) -> Option<T> {
        self.value
    }
}

struct BoolAttribute<'c> {
    attribute: Attribute<'c, ()>,
}

impl<'c> BoolAttribute<'c> {
    const fn new(attribute: Attribute<'c, ()>) -> Self {
        Self { attribute }
    }

    fn none(context: &'c Context, name: Name) -> Self {
        Self::new(Attribute::none(context, name))
    }

    fn set<I: ToTokens>(&mut self, item: I) {
        self.attribute.set(item, ());
    }

    fn get(self) -> bool {
        self.attribute.get().is_some()
    }
}

const SPACE: char = ' ';
const EMPTY: &str = "";

mod sealed {
    pub trait Sealed {}
}

pub trait Kind: sealed::Sealed {
    const NAME: ConstStr;

    fn unknown_meta(meta: &ParseNestedMeta<'_>, path: &Path) -> Error {
        let name = path.to_token_stream().to_string().replace(SPACE, EMPTY);

        let message = format!(
            "unknown `{ownership}` {kind} attribute `{name}`",
            ownership = Name::OWNERSHIP,
            kind = Self::NAME
        );

        meta.error(message)
    }
}

pub struct Container {
    private: PhantomData<()>,
}

pub struct Field {
    private: PhantomData<()>,
}

pub struct Variant {
    private: PhantomData<()>,
}

impl sealed::Sealed for Container {}
impl sealed::Sealed for Field {}
impl sealed::Sealed for Variant {}

impl Kind for Container {
    const NAME: ConstStr = "container";
}

impl Kind for Field {
    const NAME: ConstStr = "field";
}

impl Kind for Variant {
    const NAME: ConstStr = "variant";
}

pub struct Attributes<K: Kind> {
    as_is: bool,
    kind: PhantomData<K>,
}

pub type ContainerAttributes = Attributes<Container>;
pub type FieldAttributes = Attributes<Field>;
pub type VariantAttributes = Attributes<Variant>;

impl<K: Kind> Attributes<K> {
    pub fn from_ast<'a, A: IntoIterator<Item = &'a AttributeInput>>(
        context: &Context,
        attrs: A,
    ) -> Self {
        let mut as_is = BoolAttribute::none(context, Name::AS_IS);

        for attr in attrs {
            if !Name::OWNERSHIP.match_path(attr.path()) {
                continue;
            }

            if let Meta::List(ref meta) = attr.meta
                && meta.tokens.is_empty()
            {
                continue;
            }

            if let Err(error) = attr.parse_nested_meta(|meta| {
                let path = &meta.path;

                if Name::AS_IS.match_path(path) {
                    as_is.set(path);
                } else {
                    return Err(K::unknown_meta(&meta, path));
                }

                Ok(())
            }) {
                context.error(error);
            }
        }

        Self {
            as_is: as_is.get(),
            kind: PhantomData,
        }
    }

    pub const fn as_is(&self) -> bool {
        self.as_is
    }
}