mod parse;
use parse::{Attributes, Field, Struct};
use proc_macro::TokenStream;
#[proc_macro_derive(Model, attributes(model))]
pub fn derive_model(input: TokenStream) -> TokenStream {
match parse::parse_struct(input).and_then(expand) {
Ok(tokens) => tokens,
Err(message) => format!("::core::compile_error!({:?});", message).parse().unwrap(),
}
}
fn expand(parsed: Struct) -> Result<TokenStream, String> {
let Struct { name, fields, attributes } = parsed;
let stored: Vec<&Field> = fields.iter().filter(|f| !f.attributes.skip).collect();
if stored.is_empty() {
return Err(format!("`{name}` has no persisted fields"));
}
let table = attributes.table.clone().unwrap_or_else(|| pluralize(&snake_case(&name)));
let db = attributes.krate.clone().unwrap_or_else(|| "::rustlavel::db".to_string());
let primary = stored
.iter()
.find(|f| f.attributes.primary_key)
.or_else(|| stored.iter().find(|f| column_of(f) == "id"))
.ok_or_else(|| {
format!(
"`{name}` has no primary key. Add a field called `id`, or mark one \
`#[model(primary_key)]`."
)
})?;
let primary_column = column_of(primary);
let primary_field = primary.name.clone();
let primary_type = primary.type_text.clone();
let writable: Vec<&&Field> = stored
.iter()
.filter(|f| !f.attributes.generated && !f.attributes.primary_key && column_of(f) != "id")
.filter(|f| !matches!(column_of(f).as_str(), "created_at" | "updated_at"))
.collect();
let column_list = stored
.iter()
.map(|f| format!("\"{}\"", column_of(f)))
.collect::<Vec<_>>()
.join(", ");
let rest = if stored.len() == fields.len() {
String::new()
} else {
" ..::core::default::Default::default()\n".to_string()
};
let from_row = stored
.iter()
.map(|field| {
format!(
" {}: row.get({:?})?,",
field.name,
column_of(field)
)
})
.collect::<Vec<_>>()
.join("\n");
let to_values = writable
.iter()
.map(|field| {
format!(
" ({:?}, {db}::Value::from(self.{}.clone())),",
column_of(field),
field.name
)
})
.collect::<Vec<_>>()
.join("\n");
let generated = format!(
r#"
impl {db}::model::Model for {name} {{
type Key = {primary_type};
const TABLE: &'static str = {table:?};
const PRIMARY_KEY: &'static str = {primary_column:?};
const COLUMNS: &'static str = {column_list:?};
fn from_row(row: &{db}::Row) -> {db}::Result<Self> {{
Ok({name} {{
{from_row}
{rest} }})
}}
fn key(&self) -> Self::Key {{
self.{primary_field}.clone()
}}
fn set_key(&mut self, key: Self::Key) {{
self.{primary_field} = key;
}}
fn values(&self) -> ::std::vec::Vec<(&'static str, {db}::Value)> {{
::std::vec![
{to_values}
]
}}
}}
"#
);
generated.parse().map_err(|e| format!("generated code did not parse: {e}"))
}
fn column_of(field: &Field) -> String {
field.attributes.column.clone().unwrap_or_else(|| field.name.clone())
}
fn snake_case(input: &str) -> String {
let mut out = String::with_capacity(input.len() + 4);
for (index, ch) in input.chars().enumerate() {
if ch.is_uppercase() {
if index > 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
} else {
out.push(ch);
}
}
out
}
fn pluralize(word: &str) -> String {
if word.ends_with('s') && !word.ends_with("us") && !word.ends_with("ss") {
return word.to_string();
}
if let Some(stem) = word.strip_suffix('y')
&& !stem.ends_with(['a', 'e', 'i', 'o', 'u']) {
return format!("{stem}ies");
}
if word.ends_with(['s', 'x', 'z']) || word.ends_with("ch") || word.ends_with("sh") {
return format!("{word}es");
}
format!("{word}s")
}
const _: Option<&Attributes> = None;
#[proc_macro_attribute]
pub fn main(_attributes: TokenStream, item: TokenStream) -> TokenStream {
runtime_wrapper(item, "main")
}
#[proc_macro_attribute]
pub fn test(_attributes: TokenStream, item: TokenStream) -> TokenStream {
runtime_wrapper(item, "test")
}
fn runtime_wrapper(item: TokenStream, which: &str) -> TokenStream {
let source = item.to_string();
let Some(async_at) = source.find("async fn ") else {
return compile_error(&format!(
"#[rustlavel::{which}] goes on an `async fn`. This one is not async — if it does \
not await anything, it does not need the attribute."
));
};
let leading = &source[..async_at];
let rest = &source[async_at + "async ".len()..];
let Some(brace_at) = rest.find('{') else {
return compile_error(&format!(
"#[rustlavel::{which}] needs a function with a body."
));
};
let signature = &rest[..brace_at];
let body = &rest[brace_at..];
let test_attribute = if which == "test" { "#[::core::prelude::v1::test]" } else { "" };
format!(
"{test_attribute}{leading}{signature}{{
::rustlavel::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect(\"the async runtime could not be started\")
.block_on(async {body})
}}"
)
.parse()
.unwrap_or_else(|error| {
compile_error(&format!("#[rustlavel::{which}] produced code that will not parse: {error}"))
})
}
fn compile_error(message: &str) -> TokenStream {
format!("::core::compile_error!({:?});", message).parse().expect("a literal parses")
}
#[cfg(test)]
mod tests {
use super::{pluralize, snake_case};
#[test]
fn derives_conventional_table_names() {
assert_eq!(pluralize(&snake_case("User")), "users");
assert_eq!(pluralize(&snake_case("UserProfile")), "user_profiles");
assert_eq!(pluralize(&snake_case("Category")), "categories");
assert_eq!(pluralize(&snake_case("Address")), "addresses");
}
}