get_fields 0.1.0

Provides a procedural macro that generates an array of the field names of a named struct
Documentation
use syn::{Lit, Meta, NestedMeta};

pub trait ParseAttribute {
  fn parse(m: &NestedMeta) -> Self;
}

pub enum ContainerAttribute {
  RenameAll(RenameAll),
}

impl ContainerAttribute {
  pub fn apply(&self, v: &str) -> String {
    match self {
      Self::RenameAll(rn) => rn.apply(v),
    }
  }
}

impl ParseAttribute for ContainerAttribute {
  fn parse(m: &NestedMeta) -> Self {
    match m {
      NestedMeta::Meta(m) => {
        let ident = m.path().get_ident();

        match ident {
          Some(i) if i == "skip" => {
            panic!(
              "skip is a field attribute, not a container attribute"
            );
          }
          Some(i) if i == "rename_all" => match m {
            Meta::NameValue(mnv) => match &mnv.lit {
              Lit::Str(ls) => {
                Self::RenameAll(RenameAll::from_str(&ls.value()))
              }
              _ => panic!(
                "attribute rename_all expects a string as value"
              ),
            },
            _ => panic!("attribute rename_all badly formatted"),
          },
          _ => panic!("unknown attribute"),
        }
      }
      NestedMeta::Lit(_) => panic!("unable to parse attribute"),
    }
  }
}

pub enum FieldAttribute {
  Skip,
}

impl ParseAttribute for FieldAttribute {
  fn parse(m: &NestedMeta) -> Self {
    match m {
      NestedMeta::Meta(m) => match m.path().get_ident() {
        Some(i) if i == "skip" => Self::Skip,
        Some(i) if i == "rename_all" => {
          panic!(
            "rename_all is a container attribute, not a field attribute"
          );
        }
        _ => panic!("unknown attribute"),
      },
      NestedMeta::Lit(_) => panic!("unable to parse attribute"),
    }
  }
}

#[derive(Clone, Copy)]
pub enum RenameAll {
  Lower,
  Upper,
  Pascal,
  Camel,
  Snake,
  ScreamingSnake,
  Kebab,
  ScreamingKebab,
}

impl RenameAll {
  const FROM_STR: &'static [(&'static str, Self)] = &[
    ("lowercase", Self::Lower),
    ("UPPERCASE", Self::Upper),
    ("PascalCase", Self::Pascal),
    ("camelCase", Self::Camel),
    ("snake_case", Self::Snake),
    ("SCREAMING_SNAKE_CASE", Self::ScreamingSnake),
    ("kebab-case", Self::Kebab),
    ("SCREAMING-KEBAB-CASE", Self::ScreamingKebab),
  ];

  fn from_str(s: &str) -> Self {
    for (v, r) in Self::FROM_STR {
      if v == &s {
        return *r;
      }
    }

    panic!("unable to parse rename_all rule: {}", s);
  }

  fn apply(self, v: &str) -> String {
    match self {
      Self::Lower | Self::Snake => v.to_owned(),
      Self::Upper | Self::ScreamingSnake => v.to_ascii_uppercase(),
      Self::Pascal => {
        let mut pascal = String::new();
        let mut capitalize = true;
        for ch in v.chars() {
          if ch == '_' {
            capitalize = true;
          } else if capitalize {
            pascal.push(ch.to_ascii_uppercase());
            capitalize = false;
          } else {
            pascal.push(ch);
          }
        }
        pascal
      }
      Self::Camel => {
        let pascal = Self::Pascal.apply(v);
        pascal[..1].to_ascii_lowercase() + &pascal[1..]
      }
      Self::Kebab => v.replace('_', "-"),
      Self::ScreamingKebab => {
        Self::ScreamingSnake.apply(v).replace('_', "-")
      }
    }
  }
}

#[cfg(test)]
mod tests {
  use super::RenameAll;

  #[test]
  fn rename_fields() {
    for &(
      original,
      upper,
      pascal,
      camel,
      screaming,
      kebab,
      screaming_kebab,
    ) in &[
      (
        "outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME",
        "outcome", "OUTCOME",
      ),
      (
        "very_tasty",
        "VERY_TASTY",
        "VeryTasty",
        "veryTasty",
        "VERY_TASTY",
        "very-tasty",
        "VERY-TASTY",
      ),
      ("a", "A", "A", "a", "A", "a", "A"),
      ("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"),
    ] {
      assert_eq!(RenameAll::Upper.apply(original), upper);
      assert_eq!(RenameAll::Pascal.apply(original), pascal);
      assert_eq!(RenameAll::Camel.apply(original), camel);
      assert_eq!(RenameAll::Snake.apply(original), original);
      assert_eq!(
        RenameAll::ScreamingSnake.apply(original),
        screaming
      );
      assert_eq!(RenameAll::Kebab.apply(original), kebab);
      assert_eq!(
        RenameAll::ScreamingKebab.apply(original),
        screaming_kebab
      );
    }
  }
}