1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#![recursion_limit = "128"]
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
use quote::quote;
use quote::ToTokens;
use refinery_core::{find_migration_files, MigrationType};
use std::path::PathBuf;
use std::{env, fs};
use syn::{parse_macro_input, Ident, LitStr};
pub(crate) fn crate_root() -> PathBuf {
let crate_root = env::var("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR environment variable not present");
PathBuf::from(crate_root)
}
fn migration_fn_quoted<T: ToTokens>(_migrations: Vec<T>) -> TokenStream2 {
let result = quote! {
use refinery::{Migration, Runner};
pub fn runner() -> Runner {
let quoted_migrations: Vec<(&str, String)> = vec![#(#_migrations),*];
let mut migrations: Vec<Migration> = Vec::new();
for module in quoted_migrations.into_iter() {
migrations.push(Migration::unapplied(module.0, &module.1).unwrap());
}
Runner::new(&migrations)
}
};
result
}
#[proc_macro]
pub fn embed_migrations(input: TokenStream) -> TokenStream {
let location = if input.is_empty() {
crate_root().join("migrations")
} else {
let location: LitStr = parse_macro_input!(input);
crate_root().join(location.value())
};
let migration_files =
find_migration_files(location, MigrationType::All).expect("error getting migration files");
let mut migrations_mods = Vec::new();
let mut _migrations = Vec::new();
for migration in migration_files {
let filename = migration
.file_stem()
.and_then(|file| file.to_os_string().into_string().ok())
.unwrap();
let path = migration.display().to_string();
let extension = migration.extension().unwrap();
if extension == "sql" {
_migrations.push(quote! {(#filename, include_str!(#path).to_string())});
} else if extension == "rs" {
let rs_content = fs::read_to_string(&path)
.unwrap()
.parse::<TokenStream2>()
.unwrap();
let ident = Ident::new(&filename, Span2::call_site());
let mig_mod = quote! {pub mod #ident {
#rs_content
const _recompile_if_changed: &str = include_str!(#path);
}};
_migrations.push(quote! {(#filename, #ident::migration())});
migrations_mods.push(mig_mod);
}
}
let fnq = migration_fn_quoted(_migrations);
(quote! {
pub mod migrations {
#(#migrations_mods)*
#fnq
}
})
.into()
}
#[cfg(test)]
mod tests {
use super::{migration_fn_quoted, quote};
#[test]
fn test_quote_fn() {
let migs = vec![quote!("V1__first", "valid_sql_file")];
let expected = concat! {
"use refinery :: { Migration , Runner } ; ",
"pub fn runner () -> Runner { ",
"let quoted_migrations : Vec < (& str , String) > = vec ! [\"V1__first\" , \"valid_sql_file\"] ; ",
"let mut migrations : Vec < Migration > = Vec :: new () ; ",
"for module in quoted_migrations . into_iter () { ",
"migrations . push (Migration :: unapplied (module . 0 , & module . 1) . unwrap ()) ; ",
"} ",
"Runner :: new (& migrations) }"
};
assert_eq!(expected, migration_fn_quoted(migs).to_string());
}
}