iftree/lib.rs
1//! Include many files in your Rust code for self-contained binaries.
2//!
3//! **Highlights**:
4//!
5//! - Include or exclude files with **path patterns**.
6//! - File lookups **checked** at compile time are **fast** at runtime (constant
7//! time).
8//! - **Customizable**: associate any data with files.
9//! - Many [**examples**](https://github.com/evolutics/iftree/tree/main/examples),
10//! including [recipes](#recipes).
11//!
12//! See also [**related projects**](#related-work).
13//!
14//! 
15//! [](https://crates.io/crates/iftree)
16//!
17//! # Motivation
18//!
19//! Self-contained binaries are easy to ship, as they come with any required file
20//! data such as game assets, web templates, etc.
21//!
22//! The standard library's `std::include_str!` includes the contents of a given
23//! file. Iftree generalizes this in two ways:
24//!
25//! - Not just one, but many files can be included at once with **path patterns** in
26//! a `.gitignore`-like format. Patterns are flexible: you can include multiple
27//! folders, skip hidden files, filter by filename extension, select a fixed file
28//! list, etc.
29//! - Instead of including the file contents only, files can be associated with
30//! **any data** fields such as additional file metadata.
31//!
32//! Conceptually:
33//!
34//! ```text
35//! std: include_str!("my_file")
36//! Iftree: any_macro!("my_files/**")
37//! ```
38//!
39//! # Usage
40//!
41//! Now that you know the why and the what, learn the how. The following quick start
42//! shows the basic usage.
43//!
44//! ## Quick start
45//!
46//! ```rust
47//! // Say you have these files:
48//! //
49//! // my_assets/
50//! // ├── file_a
51//! // ├── file_b
52//! // └── folder/
53//! // └── file_c
54//!
55//! // Include data from this file tree in your code like so:
56//! #[iftree::include_file_tree("paths = '/my_assets/**'")]
57//! pub struct MyAsset {
58//! relative_path: &'static str,
59//! contents_str: &'static str,
60//! }
61//!
62//! fn main() {
63//! // Based on this, an array `ASSETS` of `MyAsset` instances is generated:
64//! assert_eq!(ASSETS.len(), 3);
65//! assert_eq!(ASSETS[0].relative_path, "my_assets/file_a");
66//! assert_eq!(ASSETS[0].contents_str, "… contents file_a\n");
67//! assert_eq!(ASSETS[1].contents_str, "… contents file_b\n");
68//! assert_eq!(ASSETS[2].contents_str, "… file_c\n");
69//!
70//! // Also, variables `base::x::y::MY_FILE` are generated (named by file path):
71//! assert_eq!(base::my_assets::FILE_A.relative_path, "my_assets/file_a");
72//! assert_eq!(base::my_assets::FILE_A.contents_str, "… contents file_a\n");
73//! assert_eq!(base::my_assets::FILE_B.contents_str, "… contents file_b\n");
74//! assert_eq!(base::my_assets::folder::FILE_C.contents_str, "… file_c\n");
75//! }
76//! ```
77//!
78//! ## Detailed guide
79//!
80//! 1. Add the **dependency** `iftree = "1.0"` to your manifest (`Cargo.toml`).
81//!
82//! 1. Define your **asset type**, which is just a custom `struct` or type alias.
83//! Example:
84//!
85//! ```rust
86//! pub struct MyAsset;
87//! ```
88//!
89//! 1. Next, **filter files** to be included by annotating your asset type. Example:
90//!
91//! ```ignore
92//! #[iftree::include_file_tree("paths = '/my_assets/**'")]
93//! pub struct MyAsset;
94//! ```
95//!
96//! The macro argument is a [TOML](https://toml.io) string literal. Its `paths`
97//! option here supports `.gitignore`-like path patterns, with one pattern per
98//! line. These paths are relative to the folder with your manifest by default.
99//! See the [`paths` configuration](#paths) for more.
100//!
101//! 1. Define the **data fields** of your asset type. Example:
102//!
103//! ```ignore
104//! #[iftree::include_file_tree("paths = '/my_assets/**'")]
105//! pub struct MyAsset {
106//! relative_path: &'static str,
107//! contents_bytes: &'static [u8],
108//! }
109//! ```
110//!
111//! When building your project, code is generated that instantiates the asset
112//! type once per file.
113//!
114//! By default, a field `relative_path` (if any) is populated with the file path,
115//! a field `contents_bytes` is populated with the raw file contents, and a
116//! couple of other [standard fields](#standard-fields) are recognized by name.
117//!
118//! However, you can [customize](#custom-file-data) this to include arbitrary
119//! file data.
120//!
121//! 1. Now you can **access** your file data via the generated `ASSETS` array.
122//! Example:
123//!
124//! ```ignore
125//! assert_eq!(ASSETS[0].relative_path, "my_assets/my_file");
126//! assert_eq!(ASSETS[0].contents_bytes, b"file contents");
127//! ```
128//!
129//! Additionally, for each file `x/y/my_file`, a variable `base::x::y::MY_FILE`
130//! is generated (unless disabled via
131//! [`template.identifiers` configuration](#templateidentifiers)). Such a
132//! variable is a reference to the respective element of the `ASSETS` array.
133//! Example:
134//!
135//! ```ignore
136//! assert_eq!(base::my_assets::MY_FILE.relative_path, "my_assets/my_file");
137//! assert_eq!(base::my_assets::MY_FILE.contents_bytes, b"file contents");
138//! ```
139//!
140//! ## Examples
141//!
142//! If you like to explore by example, there is an
143//! [**`examples` folder**](https://github.com/evolutics/iftree/tree/main/examples).
144//! The documentation links to individual examples where helpful.
145//!
146//! You could get started with the
147//! [basic example](https://github.com/evolutics/iftree/blob/main/examples/basic.rs).
148//! For a more complex case, see the
149//! [showcase example](https://github.com/evolutics/iftree/blob/main/examples/showcase.rs).
150//!
151//! Note that some examples need extra dependencies from the `dev-dependencies` of
152//! the [manifest](https://github.com/evolutics/iftree/tree/main/Cargo.toml).
153//!
154//! ## Standard fields
155//!
156//! When you use a subset of the following fields only, an initializer for your
157//! asset type is generated without further configuration. See
158//! [example](https://github.com/evolutics/iftree/blob/main/examples/basics_standard_fields.rs).
159//!
160//! - **`contents_bytes`**`: &'static [u8]`
161//!
162//! File contents as a byte array, using
163//! [`std::include_bytes`](https://doc.rust-lang.org/std/macro.include_bytes.html).
164//!
165//! - **`contents_str`**`: &'static str`
166//!
167//! File contents interpreted as a UTF-8 string, using
168//! [`std::include_str`](https://doc.rust-lang.org/std/macro.include_str.html).
169//!
170//! - **`get_bytes`**`: fn() -> std::borrow::Cow<'static, [u8]>`
171//!
172//! In debug builds (that is, when
173//! [`debug_assertions`](https://doc.rust-lang.org/reference/conditional-compilation.html#debug_assertions)
174//! is enabled), this function reads the file afresh on each call at runtime. It
175//! panics if there is any error such as if the file does not exist. This helps
176//! with faster development, as it avoids rebuilding if asset file contents are
177//! changed only (note that you still need to rebuild if assets are added,
178//! renamed, or removed). The asset file is located based on its absolute path in
179//! the build environment, likely rendering the binary unfit for distribution.
180//!
181//! In release builds, it returns the file contents included at compile time,
182//! using
183//! [`std::include_bytes`](https://doc.rust-lang.org/std/macro.include_bytes.html).
184//!
185//! - **`get_str`**`: fn() -> std::borrow::Cow<'static, str>`
186//!
187//! Same as `get_bytes` but for the file contents interpreted as a UTF-8 string,
188//! using
189//! [`std::include_str`](https://doc.rust-lang.org/std/macro.include_str.html).
190//!
191//! - **`relative_path`**`: &'static str`
192//!
193//! File path relative to the base folder, which is the folder with your manifest
194//! (`Cargo.toml`) by default. Path components are separated by a slash `/`,
195//! independent of your platform.
196//!
197//! ## Custom file data
198//!
199//! To associate custom data with your files, you can plug in a macro that
200//! initializes each asset. Toy example:
201//!
202//! ```rust
203//! macro_rules! my_initialize {
204//! ($relative_path:literal, $absolute_path:literal) => {
205//! MyAsset {
206//! path: $relative_path,
207//! size_in_bytes: include_bytes!($absolute_path).len(),
208//! }
209//! };
210//! }
211//!
212//! #[iftree::include_file_tree(
213//! "
214//! paths = '/my_assets/**'
215//! template.initializer = 'my_initialize'
216//! "
217//! )]
218//! pub struct MyAsset {
219//! path: &'static str,
220//! size_in_bytes: usize,
221//! }
222//!
223//! fn main() {
224//! assert_eq!(base::my_assets::FILE_A.path, "my_assets/file_a");
225//! assert_eq!(base::my_assets::FILE_A.size_in_bytes, 20);
226//! assert_eq!(base::my_assets::FILE_B.path, "my_assets/file_b");
227//! }
228//! ```
229//!
230//! The initializer macro (`my_initialize` above) must return a constant expression.
231//! Non-constant data can still be computed (lazily) with
232//! [`std::sync::LazyLock`](https://doc.rust-lang.org/std/sync/struct.LazyLock.html).
233//!
234//! For even more control over code generation, there is the concept of
235//! [visitors](#template-visitors).
236//!
237//! ## Name sanitization
238//!
239//! When generating identifiers based on paths, names are sanitized. For example, a
240//! filename `404_not_found.md` is sanitized to an identifier `_404_NOT_FOUND_MD`.
241//!
242//! The sanitization process is designed to generate valid, conventional
243//! [identifiers](https://doc.rust-lang.org/reference/identifiers.html).
244//! Essentially, it replaces invalid identifier characters by underscores `"_"` and
245//! adjusts the letter case to the context.
246//!
247//! More precisely, these transformations are applied in order:
248//!
249//! 1. The case of letters is adjusted to respect naming conventions:
250//! - All lowercase for folders (because they map to module names).
251//! - All uppercase for filenames (because they map to static variables).
252//! 1. Characters without the property `XID_Continue` are replaced by `"_"`. The set
253//! of `XID_Continue` characters in ASCII is `[0-9A-Z_a-z]`.
254//! 1. If the first character does not belong to `XID_Start` and is not `"_"`, then
255//! `"_"` is prepended. The set of `XID_Start` characters in ASCII is `[A-Za-z]`.
256//! 1. If the name is `"_"`, `"crate"`, `"self"`, `"Self"`, or `"super"`, then `"_"`
257//! is appended.
258//!
259//! ## Portable file paths
260//!
261//! To prevent issues when developing on different platforms, your file paths should
262//! follow these recommendations:
263//!
264//! - Path components are separated by a slash `/` (even on Windows).
265//! - Filenames do not contain backslashes `\` (even on Unix-like systems).
266//!
267//! ## Troubleshooting
268//!
269//! To inspect the generated code, there is a [`debug` configuration](#debug).
270//!
271//! # Recipes
272//!
273//! Here are example solutions for given problems.
274//!
275//! ## Kinds of asset types
276//!
277//! - [Type alias](https://github.com/evolutics/iftree/blob/main/examples/basics_type_alias.rs)
278//! (`type X = …`)
279//! - [Struct](https://github.com/evolutics/iftree/blob/main/examples/basics_type_named_fields.rs)
280//! (`struct` with named fields)
281//! - [Tuple struct](https://github.com/evolutics/iftree/blob/main/examples/basics_type_tuple_fields.rs)
282//! (`struct` with unnamed fields)
283//! - [Unit-like struct](https://github.com/evolutics/iftree/blob/main/examples/basics_type_unit.rs)
284//! (`struct` without field list)
285//!
286//! ## Integration with other libraries
287//!
288//! - Compression with
289//! [`include_flate`](https://github.com/evolutics/iftree/blob/main/examples/library_include_flate.rs)
290//! - File server with
291//! [Actix Web](https://github.com/evolutics/iftree/blob/main/examples/library_actix_web.rs)
292//! - File server with
293//! [Rocket](https://github.com/evolutics/iftree/blob/main/examples/library_rocket.rs)
294//! - File server with
295//! [Tide](https://github.com/evolutics/iftree/blob/main/examples/library_tide.rs)
296//! - File server with
297//! [warp](https://github.com/evolutics/iftree/blob/main/examples/library_warp.rs)
298//! - Media types with
299//! [`mime_guess`](https://github.com/evolutics/iftree/blob/main/examples/library_mime_guess.rs)
300//! - Templates with
301//! [Handlebars](https://github.com/evolutics/iftree/blob/main/examples/library_handlebars.rs)
302//!
303//! ## Including file metadata
304//!
305//! - [File permissions](https://github.com/evolutics/iftree/blob/main/examples/scenario_file_permissions.rs)
306//! - [File timestamps](https://github.com/evolutics/iftree/blob/main/examples/scenario_file_timestamps.rs)
307//! (creation, last access, last modification)
308//! - [Filename](https://github.com/evolutics/iftree/blob/main/examples/scenario_filename.rs)
309//! - [Filename extension](https://github.com/evolutics/iftree/blob/main/examples/scenario_filename_extension.rs)
310//! - Hash with [SHA-256](https://github.com/evolutics/iftree/blob/main/examples/scenario_hash_sha_256.rs)
311//! - [Media type](https://github.com/evolutics/iftree/blob/main/examples/scenario_media_type.rs)
312//! (formerly MIME type)
313//!
314//! ## Custom constructions
315//!
316//! - [Hash map](https://github.com/evolutics/iftree/blob/main/examples/scenario_hash_map.rs)
317//! - [Lazy initialization](https://github.com/evolutics/iftree/blob/main/examples/scenario_lazy_initialization.rs)
318//! - [Nested hash map](https://github.com/evolutics/iftree/blob/main/examples/scenario_nested_hash_map.rs)
319//!
320//! # Related work
321//!
322//! Originally, I've worked on Iftree because I couldn't find a library for this use
323//! case: including files from a folder filtered by filename extension. The project
324//! has since developed into something more flexible.
325//!
326//! Here is how I think Iftree compares to related projects for the given criteria.
327//! Generally, while Iftree has defaults to address common use cases, it comes with
328//! first-class support for arbitrary file data.
329//!
330//! [//]: # "Update-worthy."
331//!
332//! | Project | File selection | Included file data | Data access via |
333//! | ---------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------- |
334//! | [**`include_dir`**](https://github.com/Michael-F-Bryan/include_dir) 0.7 | Single folder | Path, contents, metadata | File path, nested iterators, glob patterns |
335//! | [**`includedir`**](https://github.com/tilpner/includedir) 0.6 | Multiple files, multiple folders | Path, contents | File path, iterator |
336//! | [**Rust Embed**](https://pyrossh.dev/repos/rust-embed) 8.9 | Single folder, inclusion-exclusion path patterns | Path, contents, metadata | File path, iterator |
337//! | [**`std::include_bytes`**](https://doc.rust-lang.org/std/macro.include_bytes.html) | Single file | Contents | File path |
338//! | [**`std::include_str`**](https://doc.rust-lang.org/std/macro.include_str.html) | Single file | Contents | File path |
339//! | **Iftree** | Multiple files by inclusion-exclusion path patterns | Path, contents, custom | File path (via `base::x::y::MY_FILE` variables in constant time), iterator (`ASSETS` array), custom |
340//!
341//! # Configuration reference
342//!
343//! The `iftree::include_file_tree` macro is configured via a
344//! [TOML](https://toml.io) string with the following fields.
345//!
346//! ## `base_folder`
347//!
348//! Path patterns are interpreted as relative to this folder.
349//!
350//! Unless this path is absolute, it is interpreted as relative to the folder given
351//! by the environment variable `CARGO_MANIFEST_DIR`. That is, a path pattern
352//! `x/y/z` resolves to `[CARGO_MANIFEST_DIR]/[base_folder]/x/y/z`.
353//!
354//! See the [`root_folder_variable` configuration](#root_folder_variable) to
355//! customize this.
356//!
357//! **Default**: `""`
358//!
359//! See
360//! [example](https://github.com/evolutics/iftree/blob/main/examples/configuration_base_folder.rs).
361//!
362//! ## `debug`
363//!
364//! Whether to generate a string variable `DEBUG` with debug information such as the
365//! generated code.
366//!
367//! **Default**: `false`
368//!
369//! See
370//! [example](https://github.com/evolutics/iftree/blob/main/examples/configuration_debug.rs).
371//!
372//! ## `paths`
373//!
374//! A string with a path pattern per line to filter files.
375//!
376//! It works like a `.gitignore` file with inverted meaning:
377//!
378//! - If the last matching pattern is negated (with `!`), the file is excluded.
379//! - If the last matching pattern is not negated, the file is included.
380//! - If no pattern matches, the file is excluded.
381//!
382//! The pattern language is as documented in the
383//! [`.gitignore` reference](https://git-scm.com/docs/gitignore), with this
384//! difference: you must use `x/y/*` instead of `x/y/` to include files in a folder
385//! `x/y/`; to also include subfolders (recursively), use `x/y/**`.
386//!
387//! By default, path patterns are relative to the environment variable
388//! `CARGO_MANIFEST_DIR`, which is the folder with your manifest (`Cargo.toml`). See
389//! the [`base_folder` configuration](#base_folder) to customize this.
390//!
391//! Common patterns:
392//!
393//! - Exclude hidden files: `!.*`
394//! - Include files with filename extension `xyz` only: `*.xyz`
395//!
396//! This is a **required** option without default.
397//!
398//! See
399//! [example](https://github.com/evolutics/iftree/blob/main/examples/configuration_paths.rs).
400//!
401//! ## `root_folder_variable`
402//!
403//! An environment variable that is used to resolve a relative
404//! [`base_folder`](#base_folder) to an absolute path.
405//!
406//! The value of the environment variable should be an absolute path.
407//!
408//! **Default**: `"CARGO_MANIFEST_DIR"`
409//!
410//! ## `template.identifiers`
411//!
412//! Whether to generate an identifier per file.
413//!
414//! Given a file `x/y/my_file`, a static variable `base::x::y::MY_FILE` is
415//! generated, nested in modules for folders. Their root module is `base`, which
416//! represents the base folder.
417//!
418//! Each variable is a reference to the corresponding element of the `ASSETS` array.
419//!
420//! Generated identifiers are subject to [name sanitization](#name-sanitization).
421//! Because of this, two files may map to the same identifier, causing an error
422//! about a name being defined multiple times. The code generation does not try to
423//! resolve such collisions automatically, as this would likely cause confusion
424//! about which identifier refers to which file. Instead, you need to rename any
425//! affected paths (but if you have no use for the generated identifiers, you can
426//! just disable them with `template.identifiers = false`).
427//!
428//! **Default**: `true`
429//!
430//! See
431//! [example](https://github.com/evolutics/iftree/blob/main/examples/configuration_template_identifiers.rs).
432//!
433//! ## `template.initializer`
434//!
435//! A macro name used to instantiate the asset type per file.
436//!
437//! As inputs, the macro is passed the following arguments, separated by comma:
438//!
439//! 1. Relative file path as a string literal. Path components are separated by `/`.
440//! 1. Absolute file path as a string literal.
441//!
442//! As an output, the macro must return a
443//! [constant expression](https://doc.rust-lang.org/reference/const_eval.html#constant-expressions).
444//!
445//! **Default**: A default initializer is constructed by recognizing
446//! [standard fields](#standard-fields).
447//!
448//! See
449//! [example](https://github.com/evolutics/iftree/blob/main/examples/configuration_template_initializer.rs).
450//!
451//! ## `template` visitors
452//!
453//! This is the most flexible customization of the code generation process.
454//!
455//! Essentially, a visitor transforms the tree of selected files into code. It does
456//! so by calling custom macros at these levels:
457//!
458//! - For the base folder, a `visit_base` macro is called to wrap everything (top
459//! level).
460//! - For each folder, a `visit_folder` macro is called, wrapping the code generated
461//! from its files and subfolders (recursively).
462//! - For each file, a `visit_file` macro is called (bottom level).
463//!
464//! These macros are passed the following inputs, separated by comma:
465//!
466//! - `visit_base`:
467//! 1. Total number of selected files as a `usize` literal.
468//! 1. Outputs of the visitor applied to the base folder entries, ordered by
469//! filename in Unicode code point order.
470//! - `visit_folder`:
471//! 1. Folder name as a string literal.
472//! 1. [Sanitized](#name-sanitization) folder name as an identifier.
473//! 1. Outputs of the visitor applied to the folder entries, ordered by filename
474//! in Unicode code point order.
475//! - `visit_file`:
476//! 1. Filename as a string literal.
477//! 1. [Sanitized](#name-sanitization) filename as an identifier.
478//! 1. Zero-based index of the file among the selected files as a `usize` literal.
479//! 1. Relative file path as a string literal. Path components are separated by
480//! `/`.
481//! 1. Absolute file path as a string literal.
482//!
483//! The `visit_folder` macro is optional. If missing, the outputs of the
484//! `visit_file` calls are directly passed as an input to the `visit_base` call.
485//! This is useful to generate flat structures such as arrays. Similarly, the
486//! `visit_base` macro is optional.
487//!
488//! You can configure multiple visitors. They are applied in order.
489//!
490//! To plug in visitors, add this to your configuration for each visitor:
491//!
492//! ```toml
493//! [[template]]
494//! visit_base = 'visit_my_base'
495//! visit_folder = 'visit_my_folder'
496//! visit_file = 'visit_my_file'
497//! ```
498//!
499//! `visit_my_…` are the names of your corresponding macros.
500//!
501//! See examples:
502//!
503//! - [Basic](https://github.com/evolutics/iftree/blob/main/examples/configuration_template_visitors.rs)
504//! - [Nesting](https://github.com/evolutics/iftree/blob/main/examples/configuration_template_visitors_nesting.rs)
505//! - [Emulation of default code generation](https://github.com/evolutics/iftree/blob/main/examples/configuration_template_visitors_emulation.rs)
506//!
507//! # Further resources
508//!
509//! - [Changelog](https://github.com/evolutics/iftree/blob/main/CHANGELOG.md)
510//! - [Latest revision of this documentation](https://github.com/evolutics/iftree/blob/main/README.md)
511
512mod generate_view;
513mod go;
514mod list_files;
515mod model;
516mod parse;
517mod print;
518
519/// See the [module level documentation](self).
520#[proc_macro_attribute]
521pub fn include_file_tree(
522 parameters: proc_macro::TokenStream,
523 item: proc_macro::TokenStream,
524) -> proc_macro::TokenStream {
525 let configuration = syn::parse_macro_input!(parameters);
526 let item2 = item.clone().into();
527 let type_ = syn::parse_macro_input!(item);
528
529 match go::main(configuration, item2, type_) {
530 Err(error) => panic!("{error}"),
531 Ok(code) => code.into(),
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use std::fmt::Write;
538 use std::fs;
539
540 #[test]
541 fn readme_includes_manifest_description() {
542 let description = env!("CARGO_PKG_DESCRIPTION");
543 let embedded_description = format!("\n\n{description}.\n\n");
544
545 let actual = get_readme().contains(&embedded_description);
546
547 assert!(actual);
548 }
549
550 fn get_readme() -> &'static str {
551 include_str!("../README.md")
552 }
553
554 #[test]
555 fn readme_includes_basic_example() {
556 let example = include_str!("../examples/basic.rs");
557 let embedded_example = format!("```rust\n{example}```\n");
558
559 let actual = get_readme().contains(&embedded_example);
560
561 assert!(actual);
562 }
563
564 #[test]
565 fn readme_refers_to_current_manifest_version() {
566 let name = env!("CARGO_PKG_NAME");
567 let version_major = env!("CARGO_PKG_VERSION_MAJOR");
568 let version_minor = env!("CARGO_PKG_VERSION_MINOR");
569 let dependency = format!("`{name} = \"{version_major}.{version_minor}\"`");
570
571 let actual = get_readme().contains(&dependency);
572
573 assert!(actual);
574 }
575
576 #[test]
577 fn readme_links_to_each_example_exactly_once() {
578 let mut actual =
579 regex::Regex::new(r"https://github.com/evolutics/iftree/blob/main/examples/([^)]+)")
580 .unwrap()
581 .captures_iter(get_readme())
582 .map(|captures| captures.get(1).unwrap().as_str())
583 .collect::<Vec<_>>();
584
585 actual.sort_unstable();
586 let actual = actual;
587 let mut expected = fs::read_dir("examples")
588 .unwrap()
589 .map(|entry| entry.unwrap().file_name().into_string().unwrap())
590 .filter(|filename| filename.ends_with(".rs"))
591 .collect::<Vec<_>>();
592 expected.sort_unstable();
593 assert_eq!(actual, expected);
594 }
595
596 #[test]
597 fn changelog_contains_current_manifest_version() {
598 let version = env!("CARGO_PKG_VERSION");
599 let version_section = format!("\n\n## [{version}] - ");
600
601 let actual = get_changelog().contains(&version_section);
602
603 assert!(actual);
604 }
605
606 fn get_changelog() -> &'static str {
607 include_str!("../CHANGELOG.md")
608 }
609
610 #[test]
611 fn module_documentation_corresponds_to_readme() {
612 let mut actual = String::from("# Iftree: Include File Tree 🌳\n\n");
613 let mut is_code_block = false;
614 for line in include_str!("lib.rs").lines() {
615 match line.strip_prefix("//!") {
616 None => break,
617 Some(line) => {
618 let line = line.strip_prefix(' ').unwrap_or(line);
619 let line = if !is_code_block && line.starts_with('#') {
620 format!("#{line}")
621 } else {
622 line.replace("```ignore", "```rust")
623 };
624 writeln!(actual, "{line}").unwrap();
625 is_code_block ^= line.starts_with("```");
626 }
627 }
628 }
629 let actual = actual;
630
631 let expected = get_readme();
632 assert_eq!(actual, expected);
633 }
634}