dto_derive 0.1.1

Derive for automatic mapping DTOs to Entities and vice versa.
Documentation
use proc_macro2::Span;
use std::collections::{HashMap, HashSet};
use syn::{Error, Generics, Ident, Result, TypePath};

use crate::dto_info::{DtoInfo, DtoKind};
use crate::mapping::{Mapping, MappingSource, MappingTarget};

#[derive(Debug)]
pub(crate) struct Container<'a> {
    info: &'a DtoInfo<'a>,
    entity: Option<TypePath>,
    kind: Option<DtoKind>,
    fields: HashSet<Ident>,
    mapping: HashMap<MappingTarget, MappingSource>,
    skipped: HashSet<Ident>,
    mapped: HashSet<Ident>,
}

#[derive(Debug)]
pub(crate) struct SealedContainer<'a> {
    pub(crate) generics: &'a Generics,
    pub(crate) entity: &'a TypePath,
    pub(crate) kind: &'a DtoKind,
    pub(crate) mapping: &'a HashMap<MappingTarget, MappingSource>,
    pub(crate) dto_type: &'a Ident,
}

impl<'a> Container<'a> {
    pub(crate) fn new(info: &'a DtoInfo) -> Container<'a> {
        let mut fields = HashSet::with_capacity(info.fields.len());
        fields.extend(info.fields.iter().map(|field| field.ident.clone().unwrap()));

        let mut mapping = HashMap::with_capacity(info.fields.len());
        mapping.extend(info.fields.iter().map(|field| {
            (
                MappingTarget(field.ident.clone().unwrap()),
                MappingSource::Field(field.ident.clone().unwrap()),
            )
        }));

        Container {
            info,
            entity: None,
            kind: None,
            fields,
            mapping,
            skipped: HashSet::new(),
            mapped: HashSet::new(),
        }
    }

    pub(crate) fn set_entity(&mut self, entity: TypePath, span: Span) -> Result<()> {
        if self.entity.is_none() {
            self.entity = Some(entity);
            Ok(())
        } else {
            Err(Error::new(span, "duplicate `entity` attribute"))
        }
    }

    pub(crate) fn add_mapping(&mut self, mapping: Mapping, span: Span) -> Result<()> {
        if !self.mapped.contains(&mapping.target) {
            self.mapping.remove(&MappingTarget(match mapping.source {
                MappingSource::Field(ref ident) => ident.clone(),
            }));
            self.mapped.insert(mapping.target.0.clone());
            self.mapping.insert(mapping.target, mapping.source);
            Ok(())
        } else {
            Err(Error::new(
                span,
                format!("cannot map already mapped field '{}'", mapping.target.0),
            ))
        }
    }

    pub(crate) fn add_skips<'b, T>(&mut self, skip: &'b T, span: Span) -> Result<()>
    where
        &'b T: IntoIterator<Item = &'b Ident>,
    {
        for s in skip {
            if self.skipped.contains(s) {
                return Err(Error::new(
                    span,
                    format!("cannot skip already skipped field '{}'", s),
                ));
            }
            self.skipped.insert(s.clone());

            if self.mapping.remove(&MappingTarget(s.clone())).is_none() {
                return Err(Error::new(
                    span,
                    format!("cannot skip non-existent field '{}'", s),
                ));
            }
        }
        Ok(())
    }

    pub(crate) fn set_request(&mut self, span: Span) -> Result<()> {
        if self.kind.is_none() {
            self.kind = Some(DtoKind::Request);
            Ok(())
        } else {
            Err(Error::new(span, "duplicate `request`/`response` attribute"))
        }
    }

    pub(crate) fn set_response(&mut self, span: Span) -> Result<()> {
        if self.kind.is_none() {
            self.kind = Some(DtoKind::Response);
            Ok(())
        } else {
            Err(Error::new(span, "duplicate `request`/`response` attribute"))
        }
    }

    pub(crate) fn seal(&self) -> Result<SealedContainer> {
        let entity = self
            .entity
            .as_ref()
            .ok_or(Error::new(Span::call_site(), "required `entity` attribute"))?;
        let kind = self.get_kind()?;
        self.check_mappings(kind)?;
        Ok(SealedContainer {
            entity,
            kind,
            mapping: &self.mapping,
            generics: self.info.generics,
            dto_type: self.info.dto_type,
        })
    }

    fn get_kind(&self) -> Result<&DtoKind> {
        self.kind.as_ref().or(self.info.kind).ok_or(Error::new(
            Span::call_site(),
            "required `request`/`response` attribute or struct name ending with `Request`/`Response`",
        ))
    }

    fn check_mappings(&self, kind: &DtoKind) -> Result<()> {
        match kind {
            DtoKind::Request => {
                for s in self.mapping.values().filter_map(|v| match v {
                    MappingSource::Field(ident) => Some(ident),
                }) {
                    if !self.fields.contains(s) {
                        return Err(Error::new(
                            Span::call_site(),
                            format!("cannot map non-existent field '{}'", s),
                        ));
                    }
                }
            }
            DtoKind::Response => {
                for t in self.mapping.keys() {
                    if !self.fields.contains(t) {
                        return Err(Error::new(
                            Span::call_site(),
                            format!("cannot map non-existent field '{}'", t.0),
                        ));
                    }
                }
            }
        }
        Ok(())
    }
}