compile_time_create_file/lib.rs
1//! Create files and directories at compile time using a procedural macro in Rust.
2//!
3//! # Example
4//!
5//! ```rust
6//! use compile_time_create_file::create_file;
7//!
8//! create_file!(
9//! "migrations/users.sql",
10//! "create table if not exists users (
11//! id serial,
12//! username varchar(128) not null,
13//! password varchar(512) not null,
14//! email varchar(256) not null,
15//! enabled boolean not null default true
16//! );
17//! "
18//! );
19//! ```
20
21use proc_macro::TokenStream;
22use std::env::current_dir;
23use std::fs::{create_dir_all, File};
24use std::io::prelude::*;
25use std::path::{is_separator, PathBuf};
26
27use syn::parse::{Parse, ParseStream};
28use syn::punctuated::Punctuated;
29use syn::token::Comma;
30use syn::{parse_macro_input, LitStr, Result}; //Error
31
32struct FilenameContent {
33 filename: String,
34 content: Option<String>,
35}
36
37impl Parse for FilenameContent {
38 fn parse(input: ParseStream) -> Result<Self> {
39 type Inner = Punctuated<LitStr, Comma>;
40 let mut args = Inner::parse_terminated(input)?;
41
42 let arg2 = args.pop();
43 let arg1 = args.pop();
44 if arg1.is_none() {
45 Ok(Self {
46 // arg2 really is the first and unique argument here
47 filename: arg2.unwrap().into_value().value(),
48 content: None,
49 })
50 } else {
51 Ok(Self {
52 filename: arg1.unwrap().into_value().value(),
53 content: Some(arg2.unwrap().into_value().value()),
54 })
55 }
56 }
57}
58
59/// Create a file or directory using a relative or absolute path, creating non existent
60/// parent subdirectories of the target file and expanding to nothing. If file or directory
61/// already exists, doesn't overwrites it.
62///
63/// It takes, at least, one argument:
64///
65/// 1. (`&str`) Path to the file or directory to create. If the literal string starts with relative
66/// path syntax like `../` or `./../` the created node is relative to the current directory
67/// from which the build is triggered. If you want to create a directory you must end the
68/// string with a path separator character like `/`.
69/// 2. (`Option<&str>`) Content for the created file. If the path is pointing to a directory, this
70/// argument will be ignored.
71///
72/// # Examples
73///
74/// ```rust
75/// use compile_time_create_file::create_file;
76///
77/// // create a file (migrations/ will be created if not exists)
78/// create_file!(
79/// "migrations/users.sql",
80/// "create table if not exists users (
81/// id serial,
82/// username varchar(128) not null,
83/// password varchar(512) not null,
84/// email varchar(256) not null,
85/// enabled boolean not null default true
86/// );
87/// "
88/// );
89///
90/// // create a directory outside of your project
91/// create_file!("../created-outside-of-your-project/");
92///
93/// // create an empty file
94/// create_file!("./../created-outside-of-your-project.txt"); // or:
95/// create_file!("../created-outside-of-your-project.txt", "");
96///
97/// // create a directory by absolute path
98/// create_file!("/tmp/my-crazy-app-logs-directory/");
99/// ```
100#[proc_macro]
101pub fn create_file(tokens: TokenStream) -> TokenStream {
102 // parse arguments list
103 let args = parse_macro_input!(tokens as FilenameContent);
104
105 // build path
106 let mut path: PathBuf = current_dir().unwrap();
107 let path_to_join: PathBuf = PathBuf::from(args.filename);
108 let path_to_join_parent = path_to_join.parent();
109
110 // if passed filename contains a non existent directory, create it
111 if path_to_join_parent.is_some() {
112 create_dir_all(path_to_join_parent.unwrap()).ok();
113 }
114 path.push(&path_to_join);
115
116 // don't overwrite existing files
117 if !path.exists() {
118 let last_path_character = path_to_join.to_str().unwrap().chars().last().unwrap();
119 if is_separator(last_path_character) {
120 // path is a directory, just create it
121 create_dir_all(path_to_join).ok();
122 } else {
123 // create file without overwriting it
124 let mut file = File::create(path).unwrap();
125 if args.content.is_some() {
126 file.write_all(args.content.unwrap().as_bytes()).unwrap();
127 }
128 }
129 }
130
131 TokenStream::new()
132}
133
134#[cfg(test)]
135mod tests {
136 #[test]
137 fn ui() {
138 let t = trybuild::TestCases::new();
139
140 t.pass("ui/one-line.rs");
141 t.pass("ui/multiple-lines.rs");
142 t.pass("ui/multiple-lines-end-newline.rs");
143 t.pass("ui/not-overwrite-file.rs");
144 t.pass("ui/not-overwrite-directory.rs");
145 t.pass("ui/relative-path.rs");
146 t.pass("ui/path-to-file-in-subdirectory.rs");
147 t.pass("ui/path-to-subdirectory.rs");
148 t.pass("ui/path-to-absolute-file.rs");
149 t.pass("ui/path-to-absolute-directory.rs");
150 t.pass("ui/empty-file-one-arg.rs");
151 t.pass("ui/empty-file-string-content.rs");
152 t.compile_fail("ui/empty-file-null-content.rs");
153 }
154}