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}