Skip to content

Commit

Permalink
Use proc macro to build code
Browse files Browse the repository at this point in the history
  • Loading branch information
spenserblack committed Jul 27, 2023
1 parent 15b2074 commit 564a345
Show file tree
Hide file tree
Showing 15 changed files with 648 additions and 431 deletions.
358 changes: 15 additions & 343 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[workspace]
members = ["gengo", "gengo-bin"]
members = ["gengo", "gengo-bin", "gengo-impl"]
21 changes: 21 additions & 0 deletions gengo-impl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "gengo-impl"
version = "0.1.0"
edition = "2021"

exclude = ["fixtures/", "src/snapshots/"]

[lib]
proc-macro = true

[dependencies]
indexmap = { version = "2", features = ["serde"] }
proc-macro2 = "1"
quote = "1"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
syn = "2"

[dev-dependencies]
insta = "1"
paste = "1"
22 changes: 22 additions & 0 deletions gengo-impl/fixtures/default_analyzers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
Plain Text:
category: prose
color: "#000000"
matchers:
extensions:
- ".text"
- ".txt"
filenames:
- "LICENCE"
- "LICENSE"
Dockerfile:
category: programming
color: "#0000AA"
heuristics:
- "^(?m)ENTRYPOINT\\ "
matchers:
filenames:
- Dockerfile
patterns:
- "Dockerfile\\.[\\w\\d]+$"
priority: 0.75
17 changes: 17 additions & 0 deletions gengo-impl/fixtures/language_enum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
Plain Text:
category: prose
color: "#000000"
matchers:
extensions:
- ".text"
- ".txt"
filenames:
- "LICENCE"
- "LICENSE"
Rust:
category: programming
color: "#FF0000"
matchers:
extensions:
- ".rs"
File renamed without changes.
207 changes: 207 additions & 0 deletions gengo-impl/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//! Crate for reading the `languages.yaml` file to generate code.
//!
//! You probably don't want to use this.
use indexmap::IndexMap;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro2::{Ident, Literal, Span};
use quote::quote;
use serde::{Deserialize, Serialize};
use syn::{parse_macro_input, LitStr};

const LANGUAGES_YAML: &str = include_str!("../languages.yaml");

type Languages = IndexMap<String, Language>;

#[derive(Debug, Deserialize, Serialize)]
struct Language {
category: LanguageCategory,
color: String,
matchers: Matchers,
#[serde(default)]
heuristics: Vec<String>,
#[serde(default = "default_priority")]
priority: f32,
}

fn default_priority() -> f32 {
0.5
}

#[derive(Debug, Deserialize, Serialize)]
struct Matchers {
#[serde(default)]
extensions: Vec<String>,
#[serde(default)]
filenames: Vec<String>,
#[serde(default)]
patterns: Vec<String>,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
enum LanguageCategory {
Data,
Markup,
Programming,
Prose,
}

fn rustify(s: &str) -> String {
s.replace(' ', "")
}

fn language_enum_impl(languages: &str) -> TokenStream2 {
let languages: Languages = serde_yaml::from_str(languages).unwrap();
let variants: Vec<TokenStream2> = languages
.iter()
.map(|(name, _)| rustify(name))
.map(|name| {
let variant = Ident::new(&name, Span::call_site());
quote! { #variant, }
})
.collect();
quote! {
enum Language {
#(#variants)*
}
}
}

#[proc_macro]
pub fn language_enum(input: TokenStream) -> TokenStream {
if !input.is_empty() {
panic!("language_enum takes no arguments");
}
let tokens = language_enum_impl(LANGUAGES_YAML);
tokens.into()
}

fn default_analyzers_impl(languages: &str) -> TokenStream2 {
let languages: Languages = serde_yaml::from_str(languages).unwrap();
let initializers: Vec<TokenStream2> = languages
.iter()
.map(|(name, language)| {
let name = rustify(name);
let name = Ident::new(&name, Span::call_site());
let name = quote! { Language::#name };

let category = match language.category {
LanguageCategory::Data => "Data",
LanguageCategory::Markup => "Markup",
LanguageCategory::Programming => "Programming",
LanguageCategory::Prose => "Prose",
};
let category = Ident::new(category, Span::call_site());
let category = quote! { Category::#category };

let color = Literal::string(&language.color);

let extensions: Vec<TokenStream2> = language
.matchers
.extensions
.iter()
.map(|ext| {
Literal::string(ext);
quote! { #ext }
})
.collect();
let extensions = quote! { &[#(#extensions ,)*] };

let filenames: Vec<TokenStream2> = language
.matchers
.filenames
.iter()
.map(|filename| {
Literal::string(filename);
quote! { #filename }
})
.collect();
let filenames = quote! { &[#(#filenames ,)*] };

let patterns: Vec<TokenStream2> = language
.matchers
.patterns
.iter()
.map(|pattern| {
Literal::string(pattern);
quote! { #pattern }
})
.collect();
let patterns = quote! { &[#(#patterns ,)*] };

let heuristics: Vec<TokenStream2> = language
.heuristics
.iter()
.map(|heuristic| {
Literal::string(heuristic);
quote! { #heuristic }
})
.collect();
let heuristics = quote! { &[#(#heuristics ,)*] };

let priority = Literal::f32_suffixed(language.priority);

quote! {
Analyzer::new(
#name,
#category,
#color,
#extensions,
#filenames,
#patterns,
#heuristics,
#priority,
)
}
})
.collect();
quote! {
vec![
#(#initializers ,)*
]
}
}

#[proc_macro]
pub fn default_analyzers(input: TokenStream) -> TokenStream {
if !input.is_empty() {
panic!("default_analyzers takes no arguments");
}
let tokens = default_analyzers_impl(LANGUAGES_YAML);
tokens.into()
}

#[cfg(test)]
mod tests {
use super::*;
use insta::{assert_debug_snapshot, assert_snapshot};
use paste::paste;

macro_rules! fixture {
($name:literal $(,)?) => {
include_str!(concat!("../fixtures/", $name, ".yaml"))
};
}

macro_rules! snapshot_test_macro_impl {
($name:ident , $fixture:literal $(,)?) => {
paste! {
#[test]
fn [< $name _rendered >]() {
let tokens = $name(fixture!($fixture));
assert_snapshot!(tokens.to_string());
}

#[test]
fn [< $name _tokens >]() {
let tokens = $name(fixture!($fixture));
assert_debug_snapshot!(tokens);
}
}
};
}

snapshot_test_macro_impl!(language_enum_impl, "language_enum");
snapshot_test_macro_impl!(default_analyzers_impl, "default_analyzers");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: gengo-impl/src/lib.rs
expression: tokens.to_string()
---
vec ! [Analyzer :: new (Language :: PlainText , Category :: Prose , "#000000" , & [".text" , ".txt" ,] , & ["LICENCE" , "LICENSE" ,] , & [] , & [] , 0.5f32 ,) , Analyzer :: new (Language :: Dockerfile , Category :: Programming , "#0000AA" , & [] , & ["Dockerfile" ,] , & ["Dockerfile\\.[\\w\\d]+$" ,] , & ["^(?m)ENTRYPOINT\\ " ,] , 0.75f32 ,) ,]
Loading

0 comments on commit 564a345

Please sign in to comment.