line-protocol-derive 0.1.0

Implementation of Influx's line protocol (derive macros)
Documentation
use syn::Attribute;
use syn::{DataStruct, Error, Field, Ident, LitStr};

use crate::line_protocol_struct::generator::{
    GeneratorField, GeneratorImpl, GeneratorMeasurement, GeneratorMember, GeneratorTag, GeneratorTimestamp,
};

#[derive(PartialEq)]
pub enum StructFieldKind {
    Tag,
    Field,
    Timestamp,
}

pub enum StructFieldGenerator {
    Tag(GeneratorTag),
    Field(GeneratorField),
    Timestamp(GeneratorTimestamp),
}

pub struct StructFieldSettings<'a> {
    pub target: &'a Ident,
    pub field: &'a Field,
    pub index: usize,
    pub kind: Option<StructFieldKind>,
    pub rename: Option<String>,
}

impl<'a> StructFieldSettings<'a> {
    fn new(target: &'a Ident, field: &'a Field, index: usize) -> Result<Option<Self>, Error> {
        let mut settings = Self {
            target,
            field,
            index,
            kind: None,
            rename: None,
        };

        for attr in &field.attrs {
            attr.parse_nested_meta(|meta| {
                if meta.path.require_ident()? == "tag" {
                    settings.set_tag()?;
                } else if meta.path.require_ident()? == "field" {
                    settings.set_field()?;
                } else if meta.path.require_ident()? == "timestamp" {
                    settings.set_timestamp()?;
                } else if meta.path.require_ident()? == "rename" {
                    let value = meta.value()?;
                    let literal = value.parse::<LitStr>()?;
                    settings.set_rename(literal.value())?;
                } else {
                    Err(settings.error(&format!("unknown attribute '{}'", meta.path.require_ident()?)))?;
                }

                Ok(())
            })?;
        }

        if settings.kind.is_none() {
            Ok(None)
        } else {
            Ok(Some(settings))
        }
    }

    fn set_tag(&mut self) -> Result<(), Error> {
        let message = match self.kind.replace(StructFieldKind::Tag) {
            None => return Ok(()),
            Some(StructFieldKind::Tag) => "#[line_protocol(tag)] can only be used once per field",
            Some(StructFieldKind::Field) => "#[line_protocol(tag)] cannot be used with [line_protocol(field)]",
            Some(StructFieldKind::Timestamp) => "#[line_protocol(tag)] cannot be used with [line_protocol(timestamp)]",
        };

        Err(self.error(message))
    }

    fn set_field(&mut self) -> Result<(), Error> {
        let message = match self.kind.replace(StructFieldKind::Field) {
            None => return Ok(()),
            Some(StructFieldKind::Tag) => "#[line_protocol(field)] cannot be used with [line_protocol(tag)]",
            Some(StructFieldKind::Field) => "#[line_protocol(field)] can only be used once per field",
            Some(StructFieldKind::Timestamp) => {
                "#[line_protocol(field)] cannot be used with [line_protocol(timestamp)]"
            }
        };

        Err(self.error(message))
    }

    fn set_timestamp(&mut self) -> Result<(), Error> {
        let message = match self.kind.replace(StructFieldKind::Timestamp) {
            None => return Ok(()),
            Some(StructFieldKind::Tag) => "#[line_protocol(timestamp)] cannot be used with [line_protocol(tag)]",
            Some(StructFieldKind::Field) => "#[line_protocol(timestamp)] cannot be used with [line_protocol(field)]",
            Some(StructFieldKind::Timestamp) => "#[line_protocol(timestamp)] can only be used once per field",
        };

        Err(self.error(message))
    }

    fn set_rename(&mut self, value: String) -> Result<(), Error> {
        let message = match self.rename.replace(value) {
            None => return Ok(()),
            Some(_) => "#[line_protocol(rename = \"...\")] can only be used once per field",
        };

        Err(self.error(message))
    }

    fn error(&self, message: &str) -> Error {
        Error::new(self.field.ident.as_ref().unwrap_or(self.target).span(), message)
    }

    fn generator(&self) -> Result<StructFieldGenerator, Error> {
        let ident = self.field.ident.clone().map(|i| i.to_string());
        let rename = self.rename.clone();
        let member = match &self.field.ident {
            Some(ident) => GeneratorMember::Named(ident.to_string()),
            None => GeneratorMember::Unnamed(self.index),
        };

        let kind = match &self.kind {
            None => return Err(self.error("internal error")),
            Some(kind) => kind,
        };

        Ok(match (kind, rename, ident) {
            (StructFieldKind::Tag, Some(name), _) => StructFieldGenerator::Tag(GeneratorTag { name, member }),
            (StructFieldKind::Tag, None, Some(name)) => StructFieldGenerator::Tag(GeneratorTag { name, member }),
            (StructFieldKind::Field, Some(name), _) => StructFieldGenerator::Field(GeneratorField { name, member }),
            (StructFieldKind::Field, None, Some(name)) => StructFieldGenerator::Field(GeneratorField { name, member }),
            (StructFieldKind::Timestamp, None, _) => StructFieldGenerator::Timestamp(GeneratorTimestamp { member }),
            (StructFieldKind::Timestamp, Some(_), _) => {
                return Err(
                    self.error("#[line_protocol(timestamp)] cannot be used with #[line_protocol(rename = \"...\")]")
                );
            }
            (_, None, _) => {
                return Err(self.error("#[line_protocol(rename = \"...\")] must be used on an unnamed field"));
            }
        })
    }
}

pub struct StructSettings<'a> {
    pub target: &'a Ident,
    pub fields: Vec<StructFieldSettings<'a>>,
    pub measurement: Option<String>,
}

impl<'a> StructSettings<'a> {
    pub fn new(target: &'a Ident, data: &'a DataStruct, attrs: &[Attribute]) -> Result<Self, Error> {
        let mut settings = Self {
            target,
            fields: data
                .fields
                .iter()
                .enumerate()
                .filter_map(|(i, f)| StructFieldSettings::new(target, f, i).transpose())
                .collect::<Result<Vec<_>, _>>()?,
            measurement: None,
        };

        for attr in attrs {
            attr.parse_nested_meta(|meta| {
                if meta.path.require_ident()? == "measurement" {
                    let value = meta.value()?;
                    let literal = value.parse::<LitStr>()?;
                    settings.set_measurement(literal.value())?;
                } else {
                    Err(settings.error(&format!("unknown attribute '{}'", meta.path.require_ident()?)))?;
                }

                Ok(())
            })?;
        }

        Ok(settings)
    }

    fn set_measurement(&mut self, value: String) -> Result<(), Error> {
        let message = match self.measurement.replace(value) {
            None => return Ok(()),
            Some(_) => "#[line_protocol(measurement = \"...\")] can only be used once",
        };

        Err(self.error(message))
    }

    fn error(&self, message: &str) -> Error {
        Error::new(self.target.span(), message)
    }

    pub fn generator(&self) -> Result<GeneratorImpl, Error> {
        let mut tags = Vec::new();
        let mut fields = Vec::new();
        let mut timestamp = None;

        for field in &self.fields {
            match field.generator()? {
                StructFieldGenerator::Tag(g) => tags.push(g),
                StructFieldGenerator::Field(g) => fields.push(g),
                StructFieldGenerator::Timestamp(g) => {
                    if timestamp.replace(g).is_some() {
                        return Err(self.error("#[line_protocol(timestamp)] cannot be used on multiple fields"));
                    }
                }
            }
        }

        if fields.is_empty() {
            return Err(self.error("#[line_protocol(field)] must be used on at least one field"));
        }

        let measurement = match &self.measurement {
            Some(measurement) => measurement,
            None => return Err(self.error("#[line_protocol(measurement = \"...\")] must be defined once")),
        };

        Ok(GeneratorImpl {
            target: self.target.to_string(),
            measurement: GeneratorMeasurement {
                name: measurement.to_string(),
            },
            tags,
            fields,
            timestamp,
        })
    }
}