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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
#[cfg(feature = "web")]
pub mod web;

use proc_macro2::{Ident, Span};
use std::fmt;
use std::path::{Path, PathBuf};
use utils;
use utils::Filter;
use utils::Filter::*;
use utils::FilterListType;
use utils::FilterListType::*;
use walkdir::WalkDir;
use Pipeline;

#[cfg(feature = "web")]
pub use self::web::*;

struct AssetInfo {
    path: String,
    clean_path: String,
}

pub struct Assets {
    ident: String,
    prefix: String,
    path: PathBuf,
    filters: Vec<Filter>,
    filter_list_type: FilterListType,
}

impl Assets {
    /// Creates a new `Assets` Pipeline
    ///
    /// By default, the filter list type is a blacklist.
    pub fn new<S: Into<String>, P: Into<PathBuf>>(identifier: S, path: P) -> Self {
        Assets {
            ident: identifier.into(),
            prefix: "/".to_string(),
            path: path.into(),
            filters: Vec::new(),
            filter_list_type: Blacklist,
        }
    }

    /// Add a filter to the pipeline
    ///
    /// Filters are applied in the order that they were added, the first
    /// matching filter determines how the file entry is handled.  If you want
    /// to include all Lua files, but not the ones in a certain folder, then
    /// you should add the exclusion rule first, and then the inclusion filter.
    ///
    /// If there are no filters then all files are matched.  If there are no
    /// filters and it's a whitelist, no files are matched.
    ///
    /// # Examples
    ///
    /// Matching everything except png and jpg files:
    ///
    /// ```
    /// # use includer_codegen::prelude::*;
    /// #
    /// Assets::new("NON_IMAGE_ASSETS", "../resources")
    ///     .filter(Filter::exclude_extension("png"))
    ///     .filter(Filter::exclude_extension("jpg"))
    ///     .filter(Filter::exclude_extension("jpeg"));
    /// ```
    ///
    /// Include all Lua files and exclude all files in the `admin` subdirectory:
    ///
    /// ```
    /// # use includer_codegen::prelude::*;
    /// #
    /// Assets::new("ASSETS", "../lua_src")
    ///     .whitelist()
    ///     .filter(Filter::exclude_regex(r"^admin/.*$"))
    ///     .filter(Filter::include_extension("lua"));
    /// ```
    ///
    /// If you wanted to only match text and markdown files:
    ///
    /// ```
    /// # use includer_codegen::prelude::*;
    /// #
    /// Assets::new("ASSETS", "../notes")
    ///     .whitelist()
    ///     .filter(Filter::include_extension("txt"))
    ///     .filter(Filter::include_extension("md"));
    /// ```
    ///
    /// or to include all assets in a subdirectory `styles`:
    ///
    /// ```
    /// # use includer_codegen::prelude::*;
    /// #
    /// Assets::new("ASSETS", "../web/dist")
    ///     .whitelist()
    ///     .filter(Filter::include_regex(r"^styles/.*$"));
    /// ```
    ///
    /// NOTE: If you need to care about multi-platform it's your responsibility
    /// to use a proper regex that accounts for the proper path separator.
    /// See [`FilterRule::regex`] for examples that account for this.
    /// AFAIK `std::path` doesn't normalize the separators.
    ///
    /// [`FilterRule::regex`]: ./utils/enum.FilterRule.html#method.regex
    pub fn filter(mut self, filter: Filter) -> Self {
        self.filters.push(filter);
        self
    }

    /// Sets the prefix to use for the normalized path uri.
    ///
    /// This is relative to where your asset path is.  If your asset path is `"./web/dist/assets"`,
    /// with your web root being at `"./web/dist"`, then having a prefix of `"/assets"` would make
    /// the relative URIs align with your web root to make the hit URL correct.
    ///
    /// Defaults to `"/"`
    pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
        self.prefix = prefix.into();
        self
    }

    /// Sets the path to the assets directory.
    pub fn set_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
        self.path = path.into();
        self
    }

    /// Set the filter list type to a blacklist.
    pub fn blacklist(mut self) -> Self {
        self.filter_list_type = Blacklist;
        self
    }

    /// Set the filter list type to a whitelist.
    pub fn whitelist(mut self) -> Self {
        self.filter_list_type = Whitelist;
        self
    }

    /// Boxes up the pipeline to pass to [`Codegen`] easily.
    ///
    /// [`Codegen`]: ../struct.Codegen.html
    pub fn build(self) -> Box<Self> {
        Box::new(self)
    }
}

impl fmt::Display for Assets {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut entries = Vec::new();
        for maybe_entry in WalkDir::new(&self.path) {
            let entry = maybe_entry.expect("Couldn't read DirEntry");

            // We don't have special rules for directories, but we can't use
            // walkdir's entry filter because we don't want files under
            // directories to be skipped.
            if entry.file_type().is_dir() {
                utils::watch_path(entry.path());
                continue;
            }

            if self.filters.is_empty() {
                match self.filter_list_type {
                    Whitelist => break,
                    Blacklist => {
                        utils::watch_path(entry.path());
                        entries.push(PathBuf::from(entry.path()));
                        continue;
                    }
                }
            }

            let mut matched = true;
            for filter in &self.filters {
                // Skip all filters that don't match the entry
                if !filter.matches(entry.path()) {
                    continue;
                }

                if let Exclude(_) = filter {
                    matched = false;
                }

                break;
            }

            if matched {
                utils::watch_path(entry.path());
                entries.push(PathBuf::from(entry.path()));
            }
        }

        let asset_info: Vec<AssetInfo> = entries
            .iter()
            .map(|p| AssetInfo {
                path: utils::path_to_string(p),
                clean_path: normalize_path(p, &self.path, &self.prefix),
            }).collect();

        if asset_info.is_empty() {
            panic!("No assets were matched, something is wrong")
        }

        let code = generate_asset_const(&self.ident, asset_info);
        write!(f, "{}", code)
    }
}

impl Pipeline for Assets {}

fn normalize_path(path: &Path, dir: &Path, prefix: &str) -> String {
    let relative = path.strip_prefix(&dir).expect("Couldn't strip path prefix");
    let path = PathBuf::from("/").join(prefix).join(&relative);
    utils::path_to_string(path)
}

fn generate_asset_const(ident_str: &str, raw_assets: Vec<AssetInfo>) -> String {
    let len = raw_assets.len();
    let mut structs = Vec::new();

    for AssetInfo { path, clean_path } in raw_assets {
        structs.push(quote! {
            Asset {
                uri: #clean_path,
                data: include_bytes!(#path),
            }
        });
    }

    let ident = Ident::new(ident_str, Span::call_site());

    let tokens = quote! {
        const #ident: [Asset; #len] = [#(#structs),*];
    };

    format!("{}", tokens)
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(1 + 1, 2);
    }
}