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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Create files and directories at compile time using a procedural macro in Rust.
//!
//! # Example
//!
//! ```rust
//! use compile_time_create_file::create_file;
//!
//! create_file!(
//!     "migrations/users.sql",
//!     "create table if not exists users (
//!     id serial,
//!     username varchar(128) not null,
//!     password varchar(512) not null,
//!     email varchar(256) not null,
//!     enabled boolean not null default true
//! );
//! "
//! );
//! ```

use proc_macro::TokenStream;
use std::env::current_dir;
use std::fs::{create_dir_all, File};
use std::io::prelude::*;
use std::path::{is_separator, PathBuf};

use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_macro_input, LitStr, Result}; //Error

struct FilenameContent {
    filename: String,
    content: Option<String>,
}

impl Parse for FilenameContent {
    fn parse(input: ParseStream) -> Result<Self> {
        type Inner = Punctuated<LitStr, Comma>;
        let mut args = Inner::parse_terminated(input)?;

        let arg2 = args.pop();
        let arg1 = args.pop();
        if arg1.is_none() {
            Ok(Self {
                // arg2 really is the first and unique argument here
                filename: arg2.unwrap().into_value().value(),
                content: None,
            })
        } else {
            Ok(Self {
                filename: arg1.unwrap().into_value().value(),
                content: Some(arg2.unwrap().into_value().value()),
            })
        }
    }
}

/// Create a file or directory using a relative or absolute path, creating non existent
/// parent subdirectories of the target file and expanding to nothing. If file or directory
/// already exists, doesn't overwrites it.
///
/// It takes, at least, one argument:
///
/// 1. (`&str`) Path to the file or directory to create. If the literal string starts with relative
///    path syntax like `../` or `./../` the created node is relative to the current directory
///    from which the build is triggered. If you want to create a directory you must end the
///    string with a path separator character like `/`.
/// 2. (`Option<&str>`) Content for the created file. If the path is pointing to a directory, this
///    argument will be ignored.
///
/// # Examples
///
/// ```rust
/// use compile_time_create_file::create_file;
///
/// // create a file (migrations/ will be created if not exists)
/// create_file!(
///     "migrations/users.sql",
///     "create table if not exists users (
///     id serial,
///     username varchar(128) not null,
///     password varchar(512) not null,
///     email varchar(256) not null,
///     enabled boolean not null default true
/// );
/// "
/// );
///
/// // create a directory outside of your project
/// create_file!("../created-outside-of-your-project/");
///
/// // create an empty file
/// create_file!("./../created-outside-of-your-project.txt");  // or:
/// create_file!("../created-outside-of-your-project.txt", "");
///
/// // create a directory by absolute path
/// create_file!("/tmp/my-crazy-app-logs-directory/");
/// ```
#[proc_macro]
pub fn create_file(tokens: TokenStream) -> TokenStream {
    // parse arguments list
    let args = parse_macro_input!(tokens as FilenameContent);

    // build path
    let mut path: PathBuf = current_dir().unwrap();
    let path_to_join: PathBuf = PathBuf::from(args.filename);
    let path_to_join_parent = path_to_join.parent();

    // if passed filename contains a non existent directory, create it
    if path_to_join_parent.is_some() {
        create_dir_all(path_to_join_parent.unwrap()).ok();
    }
    path.push(&path_to_join);

    // don't overwrite existing files
    if !path.exists() {
        let last_path_character = path_to_join.to_str().unwrap().chars().last().unwrap();
        if is_separator(last_path_character) {
            // path is a directory, just create it
            create_dir_all(path_to_join).ok();
        } else {
            // create file without overwriting it
            let mut file = File::create(path).unwrap();
            if args.content.is_some() {
                file.write_all(args.content.unwrap().as_bytes()).unwrap();
            }
        }
    }

    TokenStream::new()
}

#[cfg(test)]
mod tests {
    #[test]
    fn ui() {
        let t = trybuild::TestCases::new();

        t.pass("ui/one-line.rs");
        t.pass("ui/multiple-lines.rs");
        t.pass("ui/multiple-lines-end-newline.rs");
        t.pass("ui/not-overwrite-file.rs");
        t.pass("ui/not-overwrite-directory.rs");
        t.pass("ui/relative-path.rs");
        t.pass("ui/path-to-file-in-subdirectory.rs");
        t.pass("ui/path-to-subdirectory.rs");
        t.pass("ui/path-to-absolute-file.rs");
        t.pass("ui/path-to-absolute-directory.rs");
        t.pass("ui/empty-file-one-arg.rs");
        t.pass("ui/empty-file-string-content.rs");
        t.compile_fail("ui/empty-file-null-content.rs");
    }
}