├── .editorconfig
├── .gitattributes
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── deploy.yaml
│ └── test.yaml
├── .gitignore
├── .shellcheckrc
├── Cargo.lock
├── Cargo.toml
├── LICENSE.md
├── README.md
├── ci
└── github-actions
│ ├── create-checksums.sh
│ ├── expose-release-artifacts.sh
│ ├── generate-pkgbuild.py3
│ ├── publish-crates.sh
│ └── release-type.py3
├── clippy.sh
├── exports
├── completion.bash
├── completion.elv
├── completion.fish
├── completion.ps1
└── completion.zsh
├── fmt.sh
├── generate-completions.sh
├── run.sh
├── rust-toolchain
├── src
├── Cargo.toml
├── LICENSE.md
├── README.md
├── _cli
│ ├── build-fs-tree-completions.rs
│ └── build-fs-tree.rs
├── build.rs
├── build
│ ├── error.rs
│ ├── impl_mergeable_tree.rs
│ └── impl_tree.rs
├── lib.rs
├── macros.rs
├── node.rs
├── program.rs
├── program
│ ├── completions.rs
│ ├── main.rs
│ └── main
│ │ ├── app.rs
│ │ ├── args.rs
│ │ ├── error.rs
│ │ └── run.rs
├── tree.rs
└── tree
│ ├── dir_content.rs
│ └── methods.rs
├── template
├── build-fs-tree-bin
│ └── PKGBUILD
└── build-fs-tree
│ └── PKGBUILD
├── test.sh
└── test
├── Cargo.toml
├── build.rs
├── completions.rs
├── lib.rs
├── macros.rs
├── program.rs
├── rust_version.rs
├── tree.rs
├── tree
├── path.rs
└── path_mut.rs
└── yaml.rs
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | charset = utf-8
3 | end_of_line = lf
4 | insert_final_newline = true
5 | trim_trailing_whitespace = true
6 |
7 | [*.rs, *.md]
8 | indent_style = space
9 | indent_size = 4
10 |
11 | [*.gitattributes]
12 | indent_style = tab
13 | tab_width = 24
14 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | exports/completion* binary
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: khai96_
5 | open_collective: # Collective unavailable
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # disabled
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: "/"
5 | schedule:
6 | interval: weekly
7 | open-pull-requests-limit: 10
8 | labels:
9 | - dependabot
10 | - github-actions
11 | - package-ecosystem: cargo
12 | directory: "/"
13 | schedule:
14 | interval: weekly
15 | open-pull-requests-limit: 10
16 | labels:
17 | - dependencies
18 | - dependabot
19 | - rust
20 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Deployment
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*.*.*'
7 |
8 | jobs:
9 | test:
10 | name: Test
11 |
12 | runs-on: ${{ matrix.os }}
13 |
14 | strategy:
15 | fail-fast: true
16 | matrix:
17 | os:
18 | - ubuntu-latest
19 | - windows-latest
20 | - macos-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 |
25 | - uses: actions-rs/toolchain@v1
26 | with:
27 | profile: minimal
28 | components: clippy, rustfmt
29 | override: 'true'
30 | default: 'true'
31 |
32 | - name: Test
33 | env:
34 | RUST_BACKTRACE: '1'
35 | FMT: 'false'
36 | LINT: 'true'
37 | DOC: 'true'
38 | BUILD: 'true'
39 | TEST: 'true'
40 | BUILD_FLAGS: '--locked'
41 | TEST_FLAGS: '--no-fail-fast'
42 | run: ./test.sh
43 |
44 | build_linux:
45 | name: Build
46 |
47 | runs-on: ubuntu-latest
48 |
49 | strategy:
50 | fail-fast: true
51 | matrix:
52 | target:
53 | - x86_64-unknown-linux-gnu
54 | - x86_64-unknown-linux-musl
55 |
56 | steps:
57 | - uses: actions/checkout@v4
58 |
59 | - uses: actions-rs/toolchain@v1.0.6
60 | with:
61 | profile: minimal
62 | target: ${{ matrix.target }}
63 | override: 'true'
64 | default: 'true'
65 |
66 | - name: Build
67 | run: cargo build --features=cli --bin=build-fs-tree --target=${{ matrix.target }} --release
68 |
69 | - name: Strip all debug symbols
70 | run: strip --strip-all target/${{ matrix.target }}/release/build-fs-tree
71 |
72 | - name: Upload build artifact
73 | uses: actions/upload-artifact@v4
74 | with:
75 | name: build-fs-tree-${{ matrix.target }}
76 | path: target/${{ matrix.target }}/release/build-fs-tree
77 |
78 | build_macos:
79 | name: Build
80 |
81 | runs-on: macos-latest
82 |
83 | strategy:
84 | fail-fast: true
85 | matrix:
86 | target:
87 | - x86_64-apple-darwin
88 |
89 | steps:
90 | - uses: actions/checkout@v4
91 |
92 | - uses: actions-rs/toolchain@v1.0.6
93 | with:
94 | profile: minimal
95 | target: ${{ matrix.target }}
96 | override: 'true'
97 | default: 'true'
98 |
99 | - name: Build
100 | run: cargo build --features=cli --bin=build-fs-tree --target=${{ matrix.target }} --release
101 |
102 | - name: Strip all debug symbols
103 | run: strip target/${{ matrix.target }}/release/build-fs-tree
104 |
105 | - name: Upload build artifact
106 | uses: actions/upload-artifact@v4
107 | with:
108 | name: build-fs-tree-${{ matrix.target }}
109 | path: target/${{ matrix.target }}/release/build-fs-tree
110 |
111 | build_windows:
112 | name: Build
113 |
114 | runs-on: windows-latest
115 |
116 | strategy:
117 | fail-fast: true
118 | matrix:
119 | target:
120 | - x86_64-pc-windows-gnu
121 | - x86_64-pc-windows-msvc
122 |
123 | steps:
124 | - uses: actions/checkout@v4
125 |
126 | - uses: actions-rs/toolchain@v1.0.6
127 | with:
128 | profile: minimal
129 | target: ${{ matrix.target }}
130 | override: 'true'
131 | default: 'true'
132 |
133 | - name: Build
134 | run: cargo build --features=cli --bin=build-fs-tree --target=${{ matrix.target }} --release
135 |
136 | - name: Upload build artifact
137 | uses: actions/upload-artifact@v4
138 | with:
139 | name: build-fs-tree-${{ matrix.target }}
140 | path: target/${{ matrix.target }}/release/build-fs-tree.exe
141 |
142 | create_release:
143 | name: Create Release
144 |
145 | needs:
146 | - test
147 | - build_linux
148 | - build_macos
149 | - build_windows
150 |
151 | runs-on: ubuntu-latest
152 |
153 | outputs:
154 | upload_url: ${{ steps.create_release.outputs.upload_url }}
155 | release_type: ${{ steps.release_type.outputs.release_type }}
156 | is_release: ${{ steps.release_type.outputs.is_release }}
157 | is_prerelease: ${{ steps.release_type.outputs.is_prerelease }}
158 | release_tag: ${{ steps.release_type.outputs.release_tag }}
159 |
160 | steps:
161 | - uses: actions/checkout@v4
162 |
163 | - name: Install APT packages
164 | run: sudo apt install -y python3 python3-toml
165 |
166 | - name: Determine release type
167 | id: release_type
168 | run: ./ci/github-actions/release-type.py3
169 | env:
170 | RELEASE_TAG: ${{ github.ref }}
171 |
172 | - name: Create Release
173 | id: create_release
174 | if: steps.release_type.outputs.is_release == 'true'
175 | uses: actions/create-release@v1.1.4
176 | env:
177 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
178 | with:
179 | tag_name: ${{ steps.release_type.outputs.release_tag }}
180 | release_name: ${{ steps.release_type.outputs.release_tag }}
181 | draft: 'false'
182 | prerelease: ${{ steps.release_type.outputs.is_prerelease }}
183 |
184 | upload_generated_files:
185 | name: Upload Generated Files
186 |
187 | needs:
188 | - create_release
189 | - test
190 |
191 | runs-on: ubuntu-latest
192 |
193 | steps:
194 | - uses: actions/checkout@v4
195 |
196 | - name: Upload Tab-Completion file for Bash
197 | uses: actions/upload-release-asset@v1.0.2
198 | env:
199 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
200 | with:
201 | upload_url: ${{ needs.create_release.outputs.upload_url }}
202 | asset_path: ./exports/completion.bash
203 | asset_name: completion.bash
204 | asset_content_type: text/plain
205 |
206 | - name: Upload Tab-Completion file for Fish
207 | uses: actions/upload-release-asset@v1.0.2
208 | env:
209 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
210 | with:
211 | upload_url: ${{ needs.create_release.outputs.upload_url }}
212 | asset_path: ./exports/completion.fish
213 | asset_name: completion.fish
214 | asset_content_type: text/plain
215 |
216 | - name: Upload Tab-Completion file for Zsh
217 | uses: actions/upload-release-asset@v1.0.2
218 | env:
219 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
220 | with:
221 | upload_url: ${{ needs.create_release.outputs.upload_url }}
222 | asset_path: ./exports/completion.zsh
223 | asset_name: completion.zsh
224 | asset_content_type: text/plain
225 |
226 | - name: Upload Tab-Completion file for PowerShell
227 | uses: actions/upload-release-asset@v1.0.2
228 | env:
229 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
230 | with:
231 | upload_url: ${{ needs.create_release.outputs.upload_url }}
232 | asset_path: ./exports/completion.ps1
233 | asset_name: completion.ps1
234 | asset_content_type: text/plain
235 |
236 | - name: Upload Tab-Completion file for Elvish
237 | uses: actions/upload-release-asset@v1.0.2
238 | env:
239 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
240 | with:
241 | upload_url: ${{ needs.create_release.outputs.upload_url }}
242 | asset_path: ./exports/completion.elv
243 | asset_name: completion.elv
244 | asset_content_type: text/plain
245 |
246 | upload_release_assets:
247 | name: Upload Release Assets
248 |
249 | needs:
250 | - create_release
251 | - test
252 | - build_linux
253 | - build_macos
254 | - build_windows
255 |
256 | runs-on: ubuntu-latest
257 |
258 | if: needs.create_release.outputs.is_release == 'true'
259 |
260 | strategy:
261 | fail-fast: true
262 | matrix:
263 | target:
264 | - x86_64-unknown-linux-gnu
265 | - x86_64-unknown-linux-musl
266 | - x86_64-pc-windows-gnu
267 | - x86_64-pc-windows-msvc
268 | - x86_64-apple-darwin
269 |
270 | steps:
271 | - uses: actions/checkout@v4
272 |
273 | - name: Download artifact
274 | uses: actions/download-artifact@v4.3.0
275 | with:
276 | name: build-fs-tree-${{ matrix.target }}
277 |
278 | - name: Release executable (UNIX)
279 | if: "!contains(matrix.target, 'windows')"
280 | uses: actions/upload-release-asset@v1.0.2
281 | env:
282 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
283 | with:
284 | upload_url: ${{ needs.create_release.outputs.upload_url }}
285 | asset_path: ./build-fs-tree
286 | asset_name: build-fs-tree-${{ matrix.target }}
287 | asset_content_type: application/x-pie-executable
288 |
289 | - name: Release executable (Windows)
290 | if: contains(matrix.target, 'windows')
291 | uses: actions/upload-release-asset@v1.0.2
292 | env:
293 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
294 | with:
295 | upload_url: ${{ needs.create_release.outputs.upload_url }}
296 | asset_path: ./build-fs-tree.exe
297 | asset_name: build-fs-tree-${{ matrix.target }}.exe
298 | asset_content_type: application/x-dosexec
299 |
300 | upload_checksums:
301 | name: Upload Checksums
302 |
303 | needs:
304 | - create_release
305 | - test
306 | - build_linux
307 | - build_macos
308 | - build_windows
309 |
310 | if: needs.create_release.outputs.is_release == 'true'
311 |
312 | runs-on: ubuntu-latest
313 |
314 | steps:
315 | - uses: actions/checkout@v4
316 |
317 | - name: Download all artifacts
318 | uses: actions/download-artifact@v4.3.0
319 | with:
320 | path: ./downloads
321 |
322 | - name: Flatten directory
323 | run: ./ci/github-actions/expose-release-artifacts.sh
324 |
325 | - name: Create checksums
326 | run: ./ci/github-actions/create-checksums.sh
327 |
328 | - name: Upload as artifacts
329 | uses: actions/upload-artifact@v4
330 | with:
331 | name: checksums
332 | path: sha*sum.txt
333 |
334 | - name: Release sha1sum
335 | uses: actions/upload-release-asset@v1.0.2
336 | env:
337 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
338 | with:
339 | upload_url: ${{ needs.create_release.outputs.upload_url }}
340 | asset_path: ./sha1sum.txt
341 | asset_name: sha1sum.txt
342 | asset_content_type: text/plain
343 |
344 | - name: Release sha256sum
345 | uses: actions/upload-release-asset@v1.0.2
346 | env:
347 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
348 | with:
349 | upload_url: ${{ needs.create_release.outputs.upload_url }}
350 | asset_path: ./sha256sum.txt
351 | asset_name: sha256sum.txt
352 | asset_content_type: text/plain
353 |
354 | - name: Release sha512sum
355 | uses: actions/upload-release-asset@v1.0.2
356 | env:
357 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
358 | with:
359 | upload_url: ${{ needs.create_release.outputs.upload_url }}
360 | asset_path: ./sha512sum.txt
361 | asset_name: sha512sum.txt
362 | asset_content_type: text/plain
363 |
364 | publish_aur_package:
365 | name: Publish AUR package
366 |
367 | needs:
368 | - create_release
369 | - test
370 | - build_linux
371 | - upload_release_assets
372 |
373 | if: needs.create_release.outputs.release_type == 'official'
374 |
375 | runs-on: ubuntu-latest
376 |
377 | strategy:
378 | fail-fast: true
379 | matrix:
380 | target:
381 | - x86_64-unknown-linux-gnu
382 |
383 | steps:
384 | - uses: actions/checkout@v4
385 |
386 | - name: Download checksums
387 | uses: actions/download-artifact@v4.3.0
388 | with:
389 | name: checksums
390 | path: ./checksums
391 |
392 | - name: Generate PKGBUILD
393 | env:
394 | TARGET: ${{ matrix.target }}
395 | RELEASE_TAG: ${{ needs.create_release.outputs.release_tag }}
396 | run: ./ci/github-actions/generate-pkgbuild.py3
397 |
398 | - name: Publish build-fs-tree to the AUR
399 | uses: KSXGitHub/github-actions-deploy-aur@v4.1.1
400 | with:
401 | pkgname: build-fs-tree
402 | pkgbuild: ./pkgbuild/build-fs-tree/PKGBUILD
403 | commit_username: ${{ secrets.AUR_USERNAME }}
404 | commit_email: ${{ secrets.AUR_EMAIL }}
405 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
406 | commit_message: ${{ needs.create_release.outputs.release_tag }}
407 | force_push: 'true'
408 |
409 | - name: Publish build-fs-tree-bin to the AUR
410 | uses: KSXGitHub/github-actions-deploy-aur@v4.1.1
411 | with:
412 | pkgname: build-fs-tree-bin
413 | pkgbuild: ./pkgbuild/build-fs-tree-bin/PKGBUILD
414 | commit_username: ${{ secrets.AUR_USERNAME }}
415 | commit_email: ${{ secrets.AUR_EMAIL }}
416 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
417 | commit_message: ${{ needs.create_release.outputs.release_tag }}
418 | force_push: 'true'
419 |
420 | publish_cargo_crate:
421 | name: Publish Cargo crate
422 |
423 | needs:
424 | - create_release
425 | - test
426 |
427 | if: needs.create_release.outputs.release_type == 'official'
428 |
429 | runs-on: ubuntu-latest
430 |
431 | steps:
432 | - uses: actions/checkout@v4
433 |
434 | - uses: actions-rs/toolchain@v1.0.6
435 | with:
436 | profile: minimal
437 | override: 'true'
438 | default: 'true'
439 |
440 | - name: Login
441 | run: cargo login ${{ secrets.CRATE_AUTH_TOKEN }}
442 |
443 | - name: Publish
444 | env:
445 | RELEASE_TAG: ${{ needs.create_release.outputs.release_tag }}
446 | run: ./ci/github-actions/publish-crates.sh
447 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | test:
9 | name: Test
10 |
11 | runs-on: ${{ matrix.os }}
12 |
13 | strategy:
14 | fail-fast: true
15 | matrix:
16 | os:
17 | - ubuntu-latest
18 | - windows-latest
19 | - macos-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - name: Cache
25 | uses: actions/cache@v4
26 | timeout-minutes: 1
27 | continue-on-error: true
28 | if: matrix.os != 'macos-latest' # Cache causes errors on macOS
29 | with:
30 | path: |
31 | ~/.cargo/registry
32 | ~/.cargo/git
33 | target
34 | key: ${{ github.job }}-${{ runner.os }}-${{ hashFiles('rust-toolchain') }}-${{ hashFiles('**/Cargo.lock') }}
35 | restore-keys: |
36 | ${{ github.job }}-${{ runner.os }}-${{ hashFiles('rust-toolchain') }}-${{ hashFiles('**/Cargo.lock') }}
37 | ${{ github.job }}-${{ runner.os }}-${{ hashFiles('rust-toolchain') }}-
38 |
39 | - uses: actions-rs/toolchain@v1
40 | with:
41 | profile: minimal
42 | components: rustfmt,clippy
43 | override: 'true'
44 | default: 'true'
45 |
46 | - name: Fmt
47 | run: cargo fmt -- --check
48 |
49 | - name: Clippy
50 | env:
51 | FMT: 'false'
52 | LINT: 'true'
53 | DOC: 'false'
54 | BUILD: 'false'
55 | TEST: 'false'
56 | run: ./test.sh
57 |
58 | - name: Doc
59 | env:
60 | FMT: 'false'
61 | LINT: 'false'
62 | DOC: 'true'
63 | BUILD: 'false'
64 | TEST: 'false'
65 | run: ./test.sh
66 |
67 | - name: Test
68 | env:
69 | RUST_BACKTRACE: '1'
70 | FMT: 'false'
71 | LINT: 'false'
72 | DOC: 'false'
73 | BUILD: 'true'
74 | TEST: 'true'
75 | BUILD_FLAGS: '--locked'
76 | TEST_FLAGS: '--no-fail-fast'
77 | run: ./test.sh
78 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /.vscode
3 | tmp
4 | *.tmp
5 | tmp.*
6 | *.mypy_cache
7 |
--------------------------------------------------------------------------------
/.shellcheckrc:
--------------------------------------------------------------------------------
1 | disable=SC2294
2 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "anstream"
7 | version = "0.6.15"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
10 | dependencies = [
11 | "anstyle",
12 | "anstyle-parse",
13 | "anstyle-query",
14 | "anstyle-wincon",
15 | "colorchoice",
16 | "is_terminal_polyfill",
17 | "utf8parse",
18 | ]
19 |
20 | [[package]]
21 | name = "anstyle"
22 | version = "1.0.8"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
25 |
26 | [[package]]
27 | name = "anstyle-parse"
28 | version = "0.2.5"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
31 | dependencies = [
32 | "utf8parse",
33 | ]
34 |
35 | [[package]]
36 | name = "anstyle-query"
37 | version = "1.1.1"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
40 | dependencies = [
41 | "windows-sys",
42 | ]
43 |
44 | [[package]]
45 | name = "anstyle-wincon"
46 | version = "3.0.4"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
49 | dependencies = [
50 | "anstyle",
51 | "windows-sys",
52 | ]
53 |
54 | [[package]]
55 | name = "build-fs-tree"
56 | version = "0.7.1"
57 | dependencies = [
58 | "clap",
59 | "clap-utilities",
60 | "derive_more",
61 | "pipe-trait",
62 | "serde",
63 | "serde_yaml",
64 | "text-block-macros",
65 | ]
66 |
67 | [[package]]
68 | name = "byteorder"
69 | version = "1.5.0"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
72 |
73 | [[package]]
74 | name = "cargo_toml"
75 | version = "0.20.4"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "ad639525b1c67b6a298f378417b060fbc04618bea559482a8484381cce27d965"
78 | dependencies = [
79 | "serde",
80 | "toml",
81 | ]
82 |
83 | [[package]]
84 | name = "cfg-if"
85 | version = "1.0.0"
86 | source = "registry+https://github.com/rust-lang/crates.io-index"
87 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
88 |
89 | [[package]]
90 | name = "clap"
91 | version = "4.5.16"
92 | source = "registry+https://github.com/rust-lang/crates.io-index"
93 | checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
94 | dependencies = [
95 | "clap_builder",
96 | "clap_derive",
97 | ]
98 |
99 | [[package]]
100 | name = "clap-utilities"
101 | version = "0.2.0"
102 | source = "registry+https://github.com/rust-lang/crates.io-index"
103 | checksum = "15bcff807ef65113605e59223ac0ce77adc2cc0976e3ece014e0f2c17e4a7798"
104 | dependencies = [
105 | "clap",
106 | "clap_complete",
107 | "pipe-trait",
108 | "thiserror",
109 | ]
110 |
111 | [[package]]
112 | name = "clap_builder"
113 | version = "4.5.15"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
116 | dependencies = [
117 | "anstream",
118 | "anstyle",
119 | "clap_lex",
120 | "strsim",
121 | ]
122 |
123 | [[package]]
124 | name = "clap_complete"
125 | version = "4.0.3"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "dfe581a2035db4174cdbdc91265e1aba50f381577f0510d0ad36c7bc59cc84a3"
128 | dependencies = [
129 | "clap",
130 | ]
131 |
132 | [[package]]
133 | name = "clap_derive"
134 | version = "4.5.13"
135 | source = "registry+https://github.com/rust-lang/crates.io-index"
136 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
137 | dependencies = [
138 | "heck",
139 | "proc-macro2",
140 | "quote",
141 | "syn 2.0.74",
142 | ]
143 |
144 | [[package]]
145 | name = "clap_lex"
146 | version = "0.7.2"
147 | source = "registry+https://github.com/rust-lang/crates.io-index"
148 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
149 |
150 | [[package]]
151 | name = "colorchoice"
152 | version = "1.0.2"
153 | source = "registry+https://github.com/rust-lang/crates.io-index"
154 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
155 |
156 | [[package]]
157 | name = "command-extra"
158 | version = "1.0.0"
159 | source = "registry+https://github.com/rust-lang/crates.io-index"
160 | checksum = "b1b4fe32ea3f2a8d975b6c5cdd3f02ac358f471ca24dbb18d7a4ca58b3193d2d"
161 |
162 | [[package]]
163 | name = "derive_more"
164 | version = "1.0.0"
165 | source = "registry+https://github.com/rust-lang/crates.io-index"
166 | checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
167 | dependencies = [
168 | "derive_more-impl",
169 | ]
170 |
171 | [[package]]
172 | name = "derive_more-impl"
173 | version = "1.0.0"
174 | source = "registry+https://github.com/rust-lang/crates.io-index"
175 | checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
176 | dependencies = [
177 | "proc-macro2",
178 | "quote",
179 | "syn 2.0.74",
180 | "unicode-xid",
181 | ]
182 |
183 | [[package]]
184 | name = "diff"
185 | version = "0.1.13"
186 | source = "registry+https://github.com/rust-lang/crates.io-index"
187 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
188 |
189 | [[package]]
190 | name = "equivalent"
191 | version = "1.0.1"
192 | source = "registry+https://github.com/rust-lang/crates.io-index"
193 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
194 |
195 | [[package]]
196 | name = "getrandom"
197 | version = "0.2.15"
198 | source = "registry+https://github.com/rust-lang/crates.io-index"
199 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
200 | dependencies = [
201 | "cfg-if",
202 | "libc",
203 | "wasi",
204 | ]
205 |
206 | [[package]]
207 | name = "hashbrown"
208 | version = "0.14.5"
209 | source = "registry+https://github.com/rust-lang/crates.io-index"
210 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
211 |
212 | [[package]]
213 | name = "heck"
214 | version = "0.5.0"
215 | source = "registry+https://github.com/rust-lang/crates.io-index"
216 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
217 |
218 | [[package]]
219 | name = "indexmap"
220 | version = "2.4.0"
221 | source = "registry+https://github.com/rust-lang/crates.io-index"
222 | checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
223 | dependencies = [
224 | "equivalent",
225 | "hashbrown",
226 | ]
227 |
228 | [[package]]
229 | name = "is_terminal_polyfill"
230 | version = "1.70.1"
231 | source = "registry+https://github.com/rust-lang/crates.io-index"
232 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
233 |
234 | [[package]]
235 | name = "itoa"
236 | version = "1.0.3"
237 | source = "registry+https://github.com/rust-lang/crates.io-index"
238 | checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
239 |
240 | [[package]]
241 | name = "libc"
242 | version = "0.2.156"
243 | source = "registry+https://github.com/rust-lang/crates.io-index"
244 | checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a"
245 |
246 | [[package]]
247 | name = "maplit"
248 | version = "1.0.2"
249 | source = "registry+https://github.com/rust-lang/crates.io-index"
250 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
251 |
252 | [[package]]
253 | name = "memchr"
254 | version = "2.7.4"
255 | source = "registry+https://github.com/rust-lang/crates.io-index"
256 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
257 |
258 | [[package]]
259 | name = "pipe-trait"
260 | version = "0.4.0"
261 | source = "registry+https://github.com/rust-lang/crates.io-index"
262 | checksum = "c1be1ec9e59f0360aefe84efa6f699198b685ab0d5718081e9f72aa2344289e2"
263 |
264 | [[package]]
265 | name = "ppv-lite86"
266 | version = "0.2.20"
267 | source = "registry+https://github.com/rust-lang/crates.io-index"
268 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
269 | dependencies = [
270 | "zerocopy",
271 | ]
272 |
273 | [[package]]
274 | name = "pretty_assertions"
275 | version = "1.4.0"
276 | source = "registry+https://github.com/rust-lang/crates.io-index"
277 | checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
278 | dependencies = [
279 | "diff",
280 | "yansi",
281 | ]
282 |
283 | [[package]]
284 | name = "private-test-utils"
285 | version = "0.0.0"
286 | dependencies = [
287 | "build-fs-tree",
288 | "cargo_toml",
289 | "command-extra",
290 | "derive_more",
291 | "maplit",
292 | "pipe-trait",
293 | "pretty_assertions",
294 | "rand",
295 | "semver",
296 | "serde",
297 | "serde_yaml",
298 | "text-block-macros",
299 | ]
300 |
301 | [[package]]
302 | name = "proc-macro2"
303 | version = "1.0.86"
304 | source = "registry+https://github.com/rust-lang/crates.io-index"
305 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
306 | dependencies = [
307 | "unicode-ident",
308 | ]
309 |
310 | [[package]]
311 | name = "quote"
312 | version = "1.0.36"
313 | source = "registry+https://github.com/rust-lang/crates.io-index"
314 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
315 | dependencies = [
316 | "proc-macro2",
317 | ]
318 |
319 | [[package]]
320 | name = "rand"
321 | version = "0.8.5"
322 | source = "registry+https://github.com/rust-lang/crates.io-index"
323 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
324 | dependencies = [
325 | "libc",
326 | "rand_chacha",
327 | "rand_core",
328 | ]
329 |
330 | [[package]]
331 | name = "rand_chacha"
332 | version = "0.3.1"
333 | source = "registry+https://github.com/rust-lang/crates.io-index"
334 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
335 | dependencies = [
336 | "ppv-lite86",
337 | "rand_core",
338 | ]
339 |
340 | [[package]]
341 | name = "rand_core"
342 | version = "0.6.4"
343 | source = "registry+https://github.com/rust-lang/crates.io-index"
344 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
345 | dependencies = [
346 | "getrandom",
347 | ]
348 |
349 | [[package]]
350 | name = "ryu"
351 | version = "1.0.11"
352 | source = "registry+https://github.com/rust-lang/crates.io-index"
353 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
354 |
355 | [[package]]
356 | name = "semver"
357 | version = "1.0.23"
358 | source = "registry+https://github.com/rust-lang/crates.io-index"
359 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
360 |
361 | [[package]]
362 | name = "serde"
363 | version = "1.0.208"
364 | source = "registry+https://github.com/rust-lang/crates.io-index"
365 | checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
366 | dependencies = [
367 | "serde_derive",
368 | ]
369 |
370 | [[package]]
371 | name = "serde_derive"
372 | version = "1.0.208"
373 | source = "registry+https://github.com/rust-lang/crates.io-index"
374 | checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
375 | dependencies = [
376 | "proc-macro2",
377 | "quote",
378 | "syn 2.0.74",
379 | ]
380 |
381 | [[package]]
382 | name = "serde_spanned"
383 | version = "0.6.7"
384 | source = "registry+https://github.com/rust-lang/crates.io-index"
385 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
386 | dependencies = [
387 | "serde",
388 | ]
389 |
390 | [[package]]
391 | name = "serde_yaml"
392 | version = "0.9.34+deprecated"
393 | source = "registry+https://github.com/rust-lang/crates.io-index"
394 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
395 | dependencies = [
396 | "indexmap",
397 | "itoa",
398 | "ryu",
399 | "serde",
400 | "unsafe-libyaml",
401 | ]
402 |
403 | [[package]]
404 | name = "strsim"
405 | version = "0.11.1"
406 | source = "registry+https://github.com/rust-lang/crates.io-index"
407 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
408 |
409 | [[package]]
410 | name = "syn"
411 | version = "1.0.104"
412 | source = "registry+https://github.com/rust-lang/crates.io-index"
413 | checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce"
414 | dependencies = [
415 | "proc-macro2",
416 | "quote",
417 | "unicode-ident",
418 | ]
419 |
420 | [[package]]
421 | name = "syn"
422 | version = "2.0.74"
423 | source = "registry+https://github.com/rust-lang/crates.io-index"
424 | checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
425 | dependencies = [
426 | "proc-macro2",
427 | "quote",
428 | "unicode-ident",
429 | ]
430 |
431 | [[package]]
432 | name = "text-block-macros"
433 | version = "0.1.1"
434 | source = "registry+https://github.com/rust-lang/crates.io-index"
435 | checksum = "7f8b59b4da1c1717deaf1de80f0179a9d8b4ac91c986d5fd9f4a8ff177b84049"
436 |
437 | [[package]]
438 | name = "thiserror"
439 | version = "1.0.39"
440 | source = "registry+https://github.com/rust-lang/crates.io-index"
441 | checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
442 | dependencies = [
443 | "thiserror-impl",
444 | ]
445 |
446 | [[package]]
447 | name = "thiserror-impl"
448 | version = "1.0.39"
449 | source = "registry+https://github.com/rust-lang/crates.io-index"
450 | checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
451 | dependencies = [
452 | "proc-macro2",
453 | "quote",
454 | "syn 1.0.104",
455 | ]
456 |
457 | [[package]]
458 | name = "toml"
459 | version = "0.8.19"
460 | source = "registry+https://github.com/rust-lang/crates.io-index"
461 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
462 | dependencies = [
463 | "serde",
464 | "serde_spanned",
465 | "toml_datetime",
466 | "toml_edit",
467 | ]
468 |
469 | [[package]]
470 | name = "toml_datetime"
471 | version = "0.6.8"
472 | source = "registry+https://github.com/rust-lang/crates.io-index"
473 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
474 | dependencies = [
475 | "serde",
476 | ]
477 |
478 | [[package]]
479 | name = "toml_edit"
480 | version = "0.22.20"
481 | source = "registry+https://github.com/rust-lang/crates.io-index"
482 | checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
483 | dependencies = [
484 | "indexmap",
485 | "serde",
486 | "serde_spanned",
487 | "toml_datetime",
488 | "winnow",
489 | ]
490 |
491 | [[package]]
492 | name = "unicode-ident"
493 | version = "1.0.3"
494 | source = "registry+https://github.com/rust-lang/crates.io-index"
495 | checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
496 |
497 | [[package]]
498 | name = "unicode-xid"
499 | version = "0.2.4"
500 | source = "registry+https://github.com/rust-lang/crates.io-index"
501 | checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
502 |
503 | [[package]]
504 | name = "unsafe-libyaml"
505 | version = "0.2.11"
506 | source = "registry+https://github.com/rust-lang/crates.io-index"
507 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
508 |
509 | [[package]]
510 | name = "utf8parse"
511 | version = "0.2.2"
512 | source = "registry+https://github.com/rust-lang/crates.io-index"
513 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
514 |
515 | [[package]]
516 | name = "wasi"
517 | version = "0.11.0+wasi-snapshot-preview1"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
520 |
521 | [[package]]
522 | name = "windows-sys"
523 | version = "0.52.0"
524 | source = "registry+https://github.com/rust-lang/crates.io-index"
525 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
526 | dependencies = [
527 | "windows-targets",
528 | ]
529 |
530 | [[package]]
531 | name = "windows-targets"
532 | version = "0.52.6"
533 | source = "registry+https://github.com/rust-lang/crates.io-index"
534 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
535 | dependencies = [
536 | "windows_aarch64_gnullvm",
537 | "windows_aarch64_msvc",
538 | "windows_i686_gnu",
539 | "windows_i686_gnullvm",
540 | "windows_i686_msvc",
541 | "windows_x86_64_gnu",
542 | "windows_x86_64_gnullvm",
543 | "windows_x86_64_msvc",
544 | ]
545 |
546 | [[package]]
547 | name = "windows_aarch64_gnullvm"
548 | version = "0.52.6"
549 | source = "registry+https://github.com/rust-lang/crates.io-index"
550 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
551 |
552 | [[package]]
553 | name = "windows_aarch64_msvc"
554 | version = "0.52.6"
555 | source = "registry+https://github.com/rust-lang/crates.io-index"
556 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
557 |
558 | [[package]]
559 | name = "windows_i686_gnu"
560 | version = "0.52.6"
561 | source = "registry+https://github.com/rust-lang/crates.io-index"
562 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
563 |
564 | [[package]]
565 | name = "windows_i686_gnullvm"
566 | version = "0.52.6"
567 | source = "registry+https://github.com/rust-lang/crates.io-index"
568 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
569 |
570 | [[package]]
571 | name = "windows_i686_msvc"
572 | version = "0.52.6"
573 | source = "registry+https://github.com/rust-lang/crates.io-index"
574 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
575 |
576 | [[package]]
577 | name = "windows_x86_64_gnu"
578 | version = "0.52.6"
579 | source = "registry+https://github.com/rust-lang/crates.io-index"
580 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
581 |
582 | [[package]]
583 | name = "windows_x86_64_gnullvm"
584 | version = "0.52.6"
585 | source = "registry+https://github.com/rust-lang/crates.io-index"
586 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
587 |
588 | [[package]]
589 | name = "windows_x86_64_msvc"
590 | version = "0.52.6"
591 | source = "registry+https://github.com/rust-lang/crates.io-index"
592 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
593 |
594 | [[package]]
595 | name = "winnow"
596 | version = "0.6.18"
597 | source = "registry+https://github.com/rust-lang/crates.io-index"
598 | checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
599 | dependencies = [
600 | "memchr",
601 | ]
602 |
603 | [[package]]
604 | name = "yansi"
605 | version = "0.5.1"
606 | source = "registry+https://github.com/rust-lang/crates.io-index"
607 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
608 |
609 | [[package]]
610 | name = "zerocopy"
611 | version = "0.7.35"
612 | source = "registry+https://github.com/rust-lang/crates.io-index"
613 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
614 | dependencies = [
615 | "byteorder",
616 | "zerocopy-derive",
617 | ]
618 |
619 | [[package]]
620 | name = "zerocopy-derive"
621 | version = "0.7.35"
622 | source = "registry+https://github.com/rust-lang/crates.io-index"
623 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
624 | dependencies = [
625 | "proc-macro2",
626 | "quote",
627 | "syn 2.0.74",
628 | ]
629 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = [
4 | "src",
5 | "test",
6 | ]
7 |
8 | [workspace.dependencies]
9 | cargo_toml = "^0.20.4"
10 | clap = "^4.5.16"
11 | clap-utilities = "^0.2.0"
12 | command-extra = "^1.0.0"
13 | derive_more = { version = "^1.0.0", features = ["as_ref", "deref", "deref_mut", "display", "error", "from", "index", "into", "try_into"] }
14 | maplit = "^1.0.2"
15 | pipe-trait = "^0.4.0"
16 | pretty_assertions = "^1.4.0"
17 | rand = "^0.8.5"
18 | semver = "^1.0.23"
19 | serde = { version = "^1.0.208", features = ["derive"] }
20 | serde_yaml = "^0.9.33"
21 | text-block-macros = "^0.1.1"
22 |
23 | [profile.release]
24 | opt-level = "s"
25 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License
2 |
3 | Copyright © 2021 Hoàng Văn Khải
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # build-fs-tree
2 |
3 | [](https://github.com/KSXGitHub/build-fs-tree/actions?query=workflow%3ATest)
4 | [](https://crates.io/crates/build-fs-tree)
5 |
6 | Generate a filesystem tree from a macro or a YAML tree.
7 |
8 | ## Description
9 |
10 | When I write integration tests, I often find myself needing to create temporary files and directories. Therefore, I created this crate which provides both a library to use in a Rust code and a CLI program that generates a filesystem tree according to a YAML structure.
11 |
12 | ## Usage Examples
13 |
14 | ### The Library
15 |
16 | Go to [docs.rs](https://docs.rs/build-fs-tree/) for the full API reference.
17 |
18 | #### `FileSystemTree`
19 |
20 | `FileSystemTree::build` is faster than `MergeableFileSystemTree::build` but it does not write over an existing directory and it does not create parent directories when they don't exist.
21 |
22 | ```rust
23 | use build_fs_tree::{FileSystemTree, Build, dir, file};
24 | let tree: FileSystemTree<&str, &str> = dir! {
25 | "index.html" => file!(r#"
26 |
27 |
28 |
29 | "#)
30 | "scripts" => dir! {
31 | "main.js" => file!(r#"document.write('Hello World')"#)
32 | }
33 | "styles" => dir! {
34 | "style.css" => file!(r#":root { color: red; }"#)
35 | }
36 | };
37 | tree.build("public").unwrap();
38 | ```
39 |
40 | #### `MergeableFileSystemTree`
41 |
42 | Unlike `FileSystemTree::build`, `MergeableFileSystemTree::build` can write over an existing directory and create parent directories that were not exist before at the cost of performance.
43 |
44 | You can convert a `FileSystemTree` into a `MergeableFileSystemTree` via `From::from`/`Into::into` and vice versa.
45 |
46 | ```rust
47 | use build_fs_tree::{MergeableFileSystemTree, Build, dir, file};
48 | let tree = MergeableFileSystemTree::<&str, &str>::from(dir! {
49 | "public" => dir! {
50 | "index.html" => file!(r#"
51 |
52 |
53 |
54 | "#)
55 | "scripts/main.js" => file!(r#"document.write('Hello World')"#)
56 | "scripts/style.css" => file!(r#":root { color: red; }"#)
57 | }
58 | });
59 | tree.build(".").unwrap();
60 | ```
61 |
62 | #### Serialization and Deserialization
63 |
64 | Both `FileSystemTree` and `MergeableFileSystemTree` implement `serde::Deserialize` and `serde::Serialize`.
65 |
66 | ### The Program
67 |
68 | The name of the command is `build-fs-tree`. It has 2 subcommands: [`create`](#create) and [`populate`](#populate).
69 |
70 | #### `create`
71 |
72 | This command reads YAML from stdin and creates a new filesystem tree. It is the CLI equivalent of [`FileSystemTree`](#filesystemtree).
73 |
74 | _Create two text files in a new directory:_
75 |
76 | ```sh
77 | echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree create foo-and-bar
78 | ```
79 |
80 | _Create a text file and its parent directories:_
81 |
82 | ```sh
83 | echo '{ text-files: { foo.txt: HELLO } }' | build-fs-tree create files
84 | ```
85 |
86 | _Create a new filesystem tree from a YAML file:_
87 |
88 | ```sh
89 | build-fs-tree create root < fs-tree.yaml
90 | ```
91 |
92 | #### `populate`
93 |
94 | This command reads YAML from stdin and either creates a new filesystem tree or add files and directories to an already existing directories. It is the CLI equivalent of [`MergeableFileSystemTree`](#mergeablefilesystemtree).
95 |
96 | _Create two text files in the current directory:_
97 |
98 | ```sh
99 | echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree populate .
100 | ```
101 |
102 | _Create a text file and its parent directories:_
103 |
104 | ```sh
105 | echo '{ files/text-files/foo.txt: HELLO }' | build-fs-tree populate .
106 | ```
107 |
108 | _Populate the current directory with filesystem tree as described in a YAML file:_
109 |
110 | ```sh
111 | build-fs-tree populate . < fs-tree.yaml
112 | ```
113 |
114 | ## Packaging Status
115 |
116 | [](https://repology.org/project/build-fs-tree/versions)
117 |
118 | ## Frequently Asked Questions
119 |
120 | ### Why YAML?
121 |
122 | It has the features I desired: Easy to read and write, multiline strings done right.
123 |
124 | ### What about this cool configuration format?
125 |
126 | According to the UNIX philosophy, you may pipe your cool configuration format to a program that converts it to JSON (YAML is a superset of JSON) and then pipe the JSON output to `build-fs-tree`.
127 |
128 | ## License
129 |
130 | [MIT](https://git.io/JOkew) © [Hoàng Văn Khải](https://ksxgithub.github.io/).
131 |
--------------------------------------------------------------------------------
/ci/github-actions/create-checksums.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | # shellcheck disable=SC2035
3 | cd ./flatten || exit $?
4 | sha1sum * >../sha1sum.txt || exit $?
5 | sha256sum * >../sha256sum.txt || exit $?
6 | sha512sum * >../sha512sum.txt || exit $?
7 |
--------------------------------------------------------------------------------
/ci/github-actions/expose-release-artifacts.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | mkdir ./flatten
3 |
4 | [ -d ./downloads ] || {
5 | echo Folder ./downloads does not exist >/dev/stderr
6 | exit 1
7 | }
8 |
9 | # shellcheck disable=SC2012
10 | ls ./downloads | while read -r name; do
11 | case "$name" in
12 | *wasm*) suffix=.wasm ;;
13 | *windows*) suffix=.exe ;;
14 | *) suffix='' ;;
15 | esac
16 |
17 | src="./downloads/${name}/build-fs-tree${suffix}"
18 | dst="./flatten/${name}${suffix}"
19 | echo Copying "$src" to "$dst"...
20 | cp "$src" "$dst" || exit $?
21 | done
22 |
--------------------------------------------------------------------------------
/ci/github-actions/generate-pkgbuild.py3:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | from os import environ, makedirs
3 | import re
4 |
5 | target = environ.get('TARGET')
6 | if not target:
7 | print('::error ::TARGET is required but missing')
8 | exit(1)
9 |
10 | release_tag = environ.get('RELEASE_TAG')
11 | if not release_tag:
12 | print('::error ::RELEASE_TAG is required but missing')
13 | exit(1)
14 |
15 | checksum = None
16 | word_splitter = re.compile(r'\s+')
17 | for line in open('checksums/sha1sum.txt').readlines():
18 | line = line.strip()
19 | if line.endswith(target):
20 | checksum, _ = word_splitter.split(line)
21 |
22 | maintainer = '# Maintainer: Hoàng Văn Khải \n'
23 | readme_url = f'https://raw.githubusercontent.com/KSXGitHub/build-fs-tree/{release_tag}/README.md'
24 | license_url = f'https://raw.githubusercontent.com/KSXGitHub/build-fs-tree/{release_tag}/LICENSE.md'
25 |
26 | opening = maintainer + '\n# This file is automatically generated. Do not edit.\n'
27 |
28 | print('Generating PKGBUILD for build-fs-tree...')
29 | makedirs('./pkgbuild/build-fs-tree', exist_ok=True)
30 | with open('./pkgbuild/build-fs-tree/PKGBUILD', 'w') as pkgbuild:
31 | content = opening + '\n'
32 | content += 'pkgname=build-fs-tree\n'
33 | content += f'pkgver={release_tag}\n'
34 | source_url = f'https://github.com/KSXGitHub/build-fs-tree/archive/{release_tag}.tar.gz'
35 | content += f'source=(build-fs-tree-{release_tag}.tar.gz::{source_url})\n'
36 | content += 'sha1sums=(SKIP)\n'
37 | content += open('./template/build-fs-tree/PKGBUILD').read() + '\n'
38 | pkgbuild.write(content)
39 |
40 | print('Generating PKGBUILD for build-fs-tree-bin...')
41 | makedirs('./pkgbuild/build-fs-tree-bin', exist_ok=True)
42 | with open('./pkgbuild/build-fs-tree-bin/PKGBUILD', 'w') as pkgbuild:
43 | content = opening + '\n'
44 | content += 'pkgname=build-fs-tree-bin\n'
45 | content += f'pkgver={release_tag}\n'
46 | source_url_prefix = f'https://github.com/KSXGitHub/build-fs-tree/releases/download/{release_tag}'
47 | source_url = f'{source_url_prefix}/build-fs-tree-{target}'
48 | supported_completions = ['bash', 'fish', 'zsh']
49 | completion_source = ' '.join(
50 | f'completion.{release_tag}.{ext}::{source_url_prefix}/completion.{ext}'
51 | for ext in supported_completions
52 | )
53 | content += f'source=(build-fs-tree-{checksum}::{source_url} {completion_source} {readme_url} {license_url})\n'
54 | content += f'_checksum={checksum}\n'
55 | completion_checksums = ' '.join('SKIP' for _ in supported_completions)
56 | content += f'_completion_checksums=({completion_checksums})\n'
57 | content += open('./template/build-fs-tree-bin/PKGBUILD').read() + '\n'
58 | pkgbuild.write(content)
59 |
--------------------------------------------------------------------------------
/ci/github-actions/publish-crates.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -o errexit -o nounset
3 |
4 | if [ -z "$RELEASE_TAG" ]; then
5 | echo '::error::Environment variable RELEASE_TAG is required but missing'
6 | exit 1
7 | fi
8 |
9 | echo "Publishing build-fs-tree@$RELEASE_TAG..."
10 | cd src
11 | exec cargo publish
12 |
--------------------------------------------------------------------------------
/ci/github-actions/release-type.py3:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | from os import environ
3 | import re
4 | import toml
5 |
6 | release_tag = environ.get('RELEASE_TAG', None)
7 |
8 | if not release_tag:
9 | print('::error ::Environment variable RELEASE_TAG is required but missing')
10 | exit(1)
11 |
12 | tag_prefix = 'refs/tags/'
13 | if release_tag.startswith(tag_prefix):
14 | release_tag = release_tag.replace(tag_prefix, '', 1)
15 |
16 | def dict_path(data, head: str, *tail: str):
17 | if type(data) != dict: raise ValueError('Not a dict', data)
18 | value = data.get(head)
19 | if not tail: return value
20 | return dict_path(value, *tail)
21 |
22 | with open('src/Cargo.toml') as cargo_toml:
23 | data = toml.load(cargo_toml)
24 | version = dict_path(data, 'package', 'version')
25 |
26 | if version != release_tag:
27 | print(f'::warning ::RELEASE_TAG ({release_tag}) does not match /src/Cargo.toml#package.version ({version})')
28 | print('::set-output name=release_type::none')
29 | print('::set-output name=is_release::false')
30 | print('::set-output name=is_prerelease::false')
31 | print(f'::set-output name=release_tag::{release_tag}')
32 | exit(0)
33 |
34 | if re.match(r'^[0-9]+\.[0-9]+\.[0-9]+-.+$', release_tag):
35 | print('::set-output name=release_type::prerelease')
36 | print('::set-output name=is_release::true')
37 | print('::set-output name=is_prerelease::true')
38 | print(f'::set-output name=release_tag::{release_tag}')
39 | exit(0)
40 |
41 | if re.match(r'^[0-9]+\.[0-9]+\.[0-9]+$', release_tag):
42 | print('::set-output name=release_type::official')
43 | print('::set-output name=is_release::true')
44 | print('::set-output name=is_prerelease::false')
45 | print(f'::set-output name=release_tag::{release_tag}')
46 | exit(0)
47 |
48 | print('::set-output name=release_type::none')
49 | print('::set-output name=is_release::false')
50 | print('::set-output name=is_prerelease::false')
51 | print(f'::set-output name=release_tag::{release_tag}')
52 | exit(0)
53 |
--------------------------------------------------------------------------------
/clippy.sh:
--------------------------------------------------------------------------------
1 | #! /bin/sh
2 | exec cargo clippy --all-targets -- -D warnings
3 |
--------------------------------------------------------------------------------
/exports/completion.bash:
--------------------------------------------------------------------------------
1 | _build-fs-tree() {
2 | local i cur prev opts cmds
3 | COMPREPLY=()
4 | cur="${COMP_WORDS[COMP_CWORD]}"
5 | prev="${COMP_WORDS[COMP_CWORD-1]}"
6 | cmd=""
7 | opts=""
8 |
9 | for i in ${COMP_WORDS[@]}
10 | do
11 | case "${cmd},${i}" in
12 | ",$1")
13 | cmd="build__fs__tree"
14 | ;;
15 | build__fs__tree,create)
16 | cmd="build__fs__tree__create"
17 | ;;
18 | build__fs__tree,help)
19 | cmd="build__fs__tree__help"
20 | ;;
21 | build__fs__tree,populate)
22 | cmd="build__fs__tree__populate"
23 | ;;
24 | build__fs__tree__help,create)
25 | cmd="build__fs__tree__help__create"
26 | ;;
27 | build__fs__tree__help,help)
28 | cmd="build__fs__tree__help__help"
29 | ;;
30 | build__fs__tree__help,populate)
31 | cmd="build__fs__tree__help__populate"
32 | ;;
33 | *)
34 | ;;
35 | esac
36 | done
37 |
38 | case "${cmd}" in
39 | build__fs__tree)
40 | opts="-h -V --help --version create populate help"
41 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
42 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
43 | return 0
44 | fi
45 | case "${prev}" in
46 | *)
47 | COMPREPLY=()
48 | ;;
49 | esac
50 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
51 | return 0
52 | ;;
53 | build__fs__tree__create)
54 | opts="-h --help "
55 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
56 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
57 | return 0
58 | fi
59 | case "${prev}" in
60 | *)
61 | COMPREPLY=()
62 | ;;
63 | esac
64 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
65 | return 0
66 | ;;
67 | build__fs__tree__help)
68 | opts="create populate help"
69 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
70 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
71 | return 0
72 | fi
73 | case "${prev}" in
74 | *)
75 | COMPREPLY=()
76 | ;;
77 | esac
78 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
79 | return 0
80 | ;;
81 | build__fs__tree__help__create)
82 | opts=""
83 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
84 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
85 | return 0
86 | fi
87 | case "${prev}" in
88 | *)
89 | COMPREPLY=()
90 | ;;
91 | esac
92 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
93 | return 0
94 | ;;
95 | build__fs__tree__help__help)
96 | opts=""
97 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
98 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
99 | return 0
100 | fi
101 | case "${prev}" in
102 | *)
103 | COMPREPLY=()
104 | ;;
105 | esac
106 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
107 | return 0
108 | ;;
109 | build__fs__tree__help__populate)
110 | opts=""
111 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
112 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
113 | return 0
114 | fi
115 | case "${prev}" in
116 | *)
117 | COMPREPLY=()
118 | ;;
119 | esac
120 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
121 | return 0
122 | ;;
123 | build__fs__tree__populate)
124 | opts="-h --help "
125 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
126 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
127 | return 0
128 | fi
129 | case "${prev}" in
130 | *)
131 | COMPREPLY=()
132 | ;;
133 | esac
134 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
135 | return 0
136 | ;;
137 | esac
138 | }
139 |
140 | complete -F _build-fs-tree -o bashdefault -o default build-fs-tree
141 |
--------------------------------------------------------------------------------
/exports/completion.elv:
--------------------------------------------------------------------------------
1 |
2 | use builtin;
3 | use str;
4 |
5 | set edit:completion:arg-completer[build-fs-tree] = {|@words|
6 | fn spaces {|n|
7 | builtin:repeat $n ' ' | str:join ''
8 | }
9 | fn cand {|text desc|
10 | edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
11 | }
12 | var command = 'build-fs-tree'
13 | for word $words[1..-1] {
14 | if (str:has-prefix $word '-') {
15 | break
16 | }
17 | set command = $command';'$word
18 | }
19 | var completions = [
20 | &'build-fs-tree'= {
21 | cand -h 'Print help (see more with ''--help'')'
22 | cand --help 'Print help (see more with ''--help'')'
23 | cand -V 'Print version'
24 | cand --version 'Print version'
25 | cand create 'Read YAML from stdin and create a new filesystem tree'
26 | cand populate 'Read YAML from stdin and populate an existing filesystem tree'
27 | cand help 'Print this message or the help of the given subcommand(s)'
28 | }
29 | &'build-fs-tree;create'= {
30 | cand -h 'Print help (see more with ''--help'')'
31 | cand --help 'Print help (see more with ''--help'')'
32 | }
33 | &'build-fs-tree;populate'= {
34 | cand -h 'Print help (see more with ''--help'')'
35 | cand --help 'Print help (see more with ''--help'')'
36 | }
37 | &'build-fs-tree;help'= {
38 | cand create 'Read YAML from stdin and create a new filesystem tree'
39 | cand populate 'Read YAML from stdin and populate an existing filesystem tree'
40 | cand help 'Print this message or the help of the given subcommand(s)'
41 | }
42 | &'build-fs-tree;help;create'= {
43 | }
44 | &'build-fs-tree;help;populate'= {
45 | }
46 | &'build-fs-tree;help;help'= {
47 | }
48 | ]
49 | $completions[$command]
50 | }
51 |
--------------------------------------------------------------------------------
/exports/completion.fish:
--------------------------------------------------------------------------------
1 | complete -c build-fs-tree -n "__fish_use_subcommand" -s h -l help -d 'Print help (see more with \'--help\')'
2 | complete -c build-fs-tree -n "__fish_use_subcommand" -s V -l version -d 'Print version'
3 | complete -c build-fs-tree -n "__fish_use_subcommand" -f -a "create" -d 'Read YAML from stdin and create a new filesystem tree'
4 | complete -c build-fs-tree -n "__fish_use_subcommand" -f -a "populate" -d 'Read YAML from stdin and populate an existing filesystem tree'
5 | complete -c build-fs-tree -n "__fish_use_subcommand" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
6 | complete -c build-fs-tree -n "__fish_seen_subcommand_from create" -s h -l help -d 'Print help (see more with \'--help\')'
7 | complete -c build-fs-tree -n "__fish_seen_subcommand_from populate" -s h -l help -d 'Print help (see more with \'--help\')'
8 | complete -c build-fs-tree -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from create; and not __fish_seen_subcommand_from populate; and not __fish_seen_subcommand_from help" -f -a "create" -d 'Read YAML from stdin and create a new filesystem tree'
9 | complete -c build-fs-tree -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from create; and not __fish_seen_subcommand_from populate; and not __fish_seen_subcommand_from help" -f -a "populate" -d 'Read YAML from stdin and populate an existing filesystem tree'
10 | complete -c build-fs-tree -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from create; and not __fish_seen_subcommand_from populate; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
11 |
--------------------------------------------------------------------------------
/exports/completion.ps1:
--------------------------------------------------------------------------------
1 |
2 | using namespace System.Management.Automation
3 | using namespace System.Management.Automation.Language
4 |
5 | Register-ArgumentCompleter -Native -CommandName 'build-fs-tree' -ScriptBlock {
6 | param($wordToComplete, $commandAst, $cursorPosition)
7 |
8 | $commandElements = $commandAst.CommandElements
9 | $command = @(
10 | 'build-fs-tree'
11 | for ($i = 1; $i -lt $commandElements.Count; $i++) {
12 | $element = $commandElements[$i]
13 | if ($element -isnot [StringConstantExpressionAst] -or
14 | $element.StringConstantType -ne [StringConstantType]::BareWord -or
15 | $element.Value.StartsWith('-') -or
16 | $element.Value -eq $wordToComplete) {
17 | break
18 | }
19 | $element.Value
20 | }) -join ';'
21 |
22 | $completions = @(switch ($command) {
23 | 'build-fs-tree' {
24 | [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
25 | [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
26 | [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version')
27 | [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
28 | [CompletionResult]::new('create', 'create', [CompletionResultType]::ParameterValue, 'Read YAML from stdin and create a new filesystem tree')
29 | [CompletionResult]::new('populate', 'populate', [CompletionResultType]::ParameterValue, 'Read YAML from stdin and populate an existing filesystem tree')
30 | [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)')
31 | break
32 | }
33 | 'build-fs-tree;create' {
34 | [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
35 | [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
36 | break
37 | }
38 | 'build-fs-tree;populate' {
39 | [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
40 | [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
41 | break
42 | }
43 | 'build-fs-tree;help' {
44 | [CompletionResult]::new('create', 'create', [CompletionResultType]::ParameterValue, 'Read YAML from stdin and create a new filesystem tree')
45 | [CompletionResult]::new('populate', 'populate', [CompletionResultType]::ParameterValue, 'Read YAML from stdin and populate an existing filesystem tree')
46 | [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)')
47 | break
48 | }
49 | 'build-fs-tree;help;create' {
50 | break
51 | }
52 | 'build-fs-tree;help;populate' {
53 | break
54 | }
55 | 'build-fs-tree;help;help' {
56 | break
57 | }
58 | })
59 |
60 | $completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
61 | Sort-Object -Property ListItemText
62 | }
63 |
--------------------------------------------------------------------------------
/exports/completion.zsh:
--------------------------------------------------------------------------------
1 | #compdef build-fs-tree
2 |
3 | autoload -U is-at-least
4 |
5 | _build-fs-tree() {
6 | typeset -A opt_args
7 | typeset -a _arguments_options
8 | local ret=1
9 |
10 | if is-at-least 5.2; then
11 | _arguments_options=(-s -S -C)
12 | else
13 | _arguments_options=(-s -C)
14 | fi
15 |
16 | local context curcontext="$curcontext" state line
17 | _arguments "${_arguments_options[@]}" \
18 | '-h[Print help (see more with '\''--help'\'')]' \
19 | '--help[Print help (see more with '\''--help'\'')]' \
20 | '-V[Print version]' \
21 | '--version[Print version]' \
22 | ":: :_build-fs-tree_commands" \
23 | "*::: :->build-fs-tree" \
24 | && ret=0
25 | case $state in
26 | (build-fs-tree)
27 | words=($line[1] "${words[@]}")
28 | (( CURRENT += 1 ))
29 | curcontext="${curcontext%:*:*}:build-fs-tree-command-$line[1]:"
30 | case $line[1] in
31 | (create)
32 | _arguments "${_arguments_options[@]}" \
33 | '-h[Print help (see more with '\''--help'\'')]' \
34 | '--help[Print help (see more with '\''--help'\'')]' \
35 | ':TARGET:_files' \
36 | && ret=0
37 | ;;
38 | (populate)
39 | _arguments "${_arguments_options[@]}" \
40 | '-h[Print help (see more with '\''--help'\'')]' \
41 | '--help[Print help (see more with '\''--help'\'')]' \
42 | ':TARGET:_files' \
43 | && ret=0
44 | ;;
45 | (help)
46 | _arguments "${_arguments_options[@]}" \
47 | ":: :_build-fs-tree__help_commands" \
48 | "*::: :->help" \
49 | && ret=0
50 |
51 | case $state in
52 | (help)
53 | words=($line[1] "${words[@]}")
54 | (( CURRENT += 1 ))
55 | curcontext="${curcontext%:*:*}:build-fs-tree-help-command-$line[1]:"
56 | case $line[1] in
57 | (create)
58 | _arguments "${_arguments_options[@]}" \
59 | && ret=0
60 | ;;
61 | (populate)
62 | _arguments "${_arguments_options[@]}" \
63 | && ret=0
64 | ;;
65 | (help)
66 | _arguments "${_arguments_options[@]}" \
67 | && ret=0
68 | ;;
69 | esac
70 | ;;
71 | esac
72 | ;;
73 | esac
74 | ;;
75 | esac
76 | }
77 |
78 | (( $+functions[_build-fs-tree_commands] )) ||
79 | _build-fs-tree_commands() {
80 | local commands; commands=(
81 | 'create:Read YAML from stdin and create a new filesystem tree' \
82 | 'populate:Read YAML from stdin and populate an existing filesystem tree' \
83 | 'help:Print this message or the help of the given subcommand(s)' \
84 | )
85 | _describe -t commands 'build-fs-tree commands' commands "$@"
86 | }
87 | (( $+functions[_build-fs-tree__create_commands] )) ||
88 | _build-fs-tree__create_commands() {
89 | local commands; commands=()
90 | _describe -t commands 'build-fs-tree create commands' commands "$@"
91 | }
92 | (( $+functions[_build-fs-tree__help__create_commands] )) ||
93 | _build-fs-tree__help__create_commands() {
94 | local commands; commands=()
95 | _describe -t commands 'build-fs-tree help create commands' commands "$@"
96 | }
97 | (( $+functions[_build-fs-tree__help_commands] )) ||
98 | _build-fs-tree__help_commands() {
99 | local commands; commands=(
100 | 'create:Read YAML from stdin and create a new filesystem tree' \
101 | 'populate:Read YAML from stdin and populate an existing filesystem tree' \
102 | 'help:Print this message or the help of the given subcommand(s)' \
103 | )
104 | _describe -t commands 'build-fs-tree help commands' commands "$@"
105 | }
106 | (( $+functions[_build-fs-tree__help__help_commands] )) ||
107 | _build-fs-tree__help__help_commands() {
108 | local commands; commands=()
109 | _describe -t commands 'build-fs-tree help help commands' commands "$@"
110 | }
111 | (( $+functions[_build-fs-tree__help__populate_commands] )) ||
112 | _build-fs-tree__help__populate_commands() {
113 | local commands; commands=()
114 | _describe -t commands 'build-fs-tree help populate commands' commands "$@"
115 | }
116 | (( $+functions[_build-fs-tree__populate_commands] )) ||
117 | _build-fs-tree__populate_commands() {
118 | local commands; commands=()
119 | _describe -t commands 'build-fs-tree populate commands' commands "$@"
120 | }
121 |
122 | _build-fs-tree "$@"
123 |
--------------------------------------------------------------------------------
/fmt.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -o errexit -o pipefail
3 |
4 | if [ "$FMT_UPDATE" = 'true' ]; then
5 | cargo_fmt_flag=()
6 | elif [ "$FMT_UPDATE" = 'false' ] || [ "$FMT_UPDATE" = '' ]; then
7 | cargo_fmt_flag=('--check')
8 | fi
9 |
10 | exec cargo fmt -- "${cargo_fmt_flag[@]}"
11 |
--------------------------------------------------------------------------------
/generate-completions.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -o errexit -o pipefail -o nounset
3 |
4 | cd "$(dirname "$0")"
5 | mkdir -p exports
6 |
7 | gen() {
8 | ./run.sh build-fs-tree-completions --name='build-fs-tree' --shell="$1" --output="exports/$2"
9 | }
10 |
11 | gen bash completion.bash
12 | gen fish completion.fish
13 | gen zsh completion.zsh
14 | gen powershell completion.ps1
15 | gen elvish completion.elv
16 |
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -o errexit -o pipefail -o nounset
3 | exec cargo run --all-features --bin="$1" -- "${@:2}"
4 |
--------------------------------------------------------------------------------
/rust-toolchain:
--------------------------------------------------------------------------------
1 | 1.80.1
2 |
--------------------------------------------------------------------------------
/src/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "build-fs-tree"
3 | description = "Generate a filesystem tree from a macro or a YAML tree"
4 | version = "0.7.1"
5 | rust-version = "1.80"
6 | authors = ["khai96_ "]
7 | edition = "2021"
8 | build = false
9 | readme = "README.md"
10 | license = "MIT"
11 | documentation = "https://docs.rs/build-fs-tree"
12 | repository = "https://github.com/KSXGitHub/build-fs-tree.git"
13 | keywords = [
14 | "file",
15 | "directory",
16 | "filesystem",
17 | "tree",
18 | "yaml",
19 | ]
20 | categories = [
21 | "command-line-utilities",
22 | "development-tools",
23 | "filesystem",
24 | "rust-patterns",
25 | ]
26 | include = [
27 | "*.rs",
28 | "/Cargo.toml",
29 | "/README.md",
30 | "/LICENSE.md",
31 | ]
32 |
33 | [lib]
34 | name = "build_fs_tree"
35 | path = "lib.rs"
36 | doc = true
37 |
38 | [[bin]]
39 | name = "build-fs-tree"
40 | path = "_cli/build-fs-tree.rs"
41 | required-features = ["cli"]
42 | doc = false
43 |
44 | [[bin]]
45 | name = "build-fs-tree-completions"
46 | path = "_cli/build-fs-tree-completions.rs"
47 | required-features = ["cli-completions"]
48 | doc = false
49 |
50 | [features]
51 | default = []
52 | cli = ["clap/derive", "clap-utilities"]
53 | cli-completions = ["cli"]
54 |
55 | [dependencies]
56 | clap = { workspace = true, optional = true }
57 | clap-utilities = { workspace = true, optional = true }
58 | derive_more.workspace = true
59 | pipe-trait.workspace = true
60 | serde_yaml.workspace = true
61 | serde.workspace = true
62 | text-block-macros.workspace = true
63 |
--------------------------------------------------------------------------------
/src/LICENSE.md:
--------------------------------------------------------------------------------
1 | ../LICENSE.md
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/src/_cli/build-fs-tree-completions.rs:
--------------------------------------------------------------------------------
1 | #![doc(hidden)]
2 |
3 | fn main() -> std::process::ExitCode {
4 | build_fs_tree::program::completions::main()
5 | }
6 |
--------------------------------------------------------------------------------
/src/_cli/build-fs-tree.rs:
--------------------------------------------------------------------------------
1 | #![doc(hidden)]
2 |
3 | fn main() -> std::process::ExitCode {
4 | build_fs_tree::program::main::main()
5 | }
6 |
--------------------------------------------------------------------------------
/src/build.rs:
--------------------------------------------------------------------------------
1 | mod error;
2 | mod impl_mergeable_tree;
3 | mod impl_tree;
4 |
5 | pub use error::*;
6 |
7 | use crate::{Node, NodeContent};
8 |
9 | /// Applying [`FileSystemTree`](crate::FileSystemTree) to the filesystem.
10 | ///
11 | /// **Generic parameters:**
12 | /// * `Name`: Identification of a child item.
13 | /// * `Error`: Error type used by the other functions.
14 | pub trait Build: Node + Sized
15 | where
16 | Self::DirectoryContent: IntoIterator- ,
17 | {
18 | /// Build target.
19 | type BorrowedPath: ToOwned + ?Sized;
20 | /// Locations of the items in the filesystem.
21 | type OwnedPath: AsRef;
22 | /// Add prefix to the root of the tree.
23 | fn join(prefix: &Self::BorrowedPath, name: &Name) -> Self::OwnedPath;
24 | /// Write content to a file.
25 | fn write_file(path: &Self::BorrowedPath, content: &Self::FileContent) -> Result<(), Error>;
26 | /// Create a directory at root.
27 | fn create_dir(path: &Self::BorrowedPath) -> Result<(), Error>;
28 |
29 | /// Build the tree into the filesystem.
30 | fn build(self, path: Path) -> Result<(), BuildError>
31 | where
32 | Path: AsRef,
33 | {
34 | let path = path.as_ref();
35 |
36 | let children = match self.read() {
37 | NodeContent::File(content) => {
38 | return Self::write_file(path, &content).map_err(|error| BuildError {
39 | operation: FailedOperation::WriteFile,
40 | path: path.to_owned(),
41 | error,
42 | })
43 | }
44 | NodeContent::Directory(children) => children,
45 | };
46 |
47 | Self::create_dir(path).map_err(|error| BuildError {
48 | operation: FailedOperation::CreateDir,
49 | path: path.to_owned(),
50 | error,
51 | })?;
52 |
53 | for (name, child) in children {
54 | child.build(Self::join(path, &name))?;
55 | }
56 |
57 | Ok(())
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/build/error.rs:
--------------------------------------------------------------------------------
1 | use derive_more::{Display, Error};
2 | use std::fmt::{self, Debug, Formatter};
3 |
4 | /// Error caused by [`Build::build`](crate::Build::build).
5 | #[derive(Debug, Display, Error)]
6 | #[display("{operation} {path:?}: {error}")]
7 | #[display(bound(Path: Debug, Error: Display))]
8 | pub struct BuildError {
9 | /// Operation that caused the error.
10 | pub operation: FailedOperation,
11 | /// Path where the error occurred.
12 | pub path: Path,
13 | /// The error.
14 | pub error: Error,
15 | }
16 |
17 | /// Operation that causes an error.
18 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
19 | pub enum FailedOperation {
20 | /// The operation was to write a file.
21 | WriteFile,
22 | /// The operation was to create a directory.
23 | CreateDir,
24 | }
25 |
26 | impl FailedOperation {
27 | /// Convert to a string.
28 | pub const fn name(self) -> &'static str {
29 | use FailedOperation::*;
30 | match self {
31 | WriteFile => "write_file",
32 | CreateDir => "create_dir",
33 | }
34 | }
35 | }
36 |
37 | impl fmt::Display for FailedOperation {
38 | fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), fmt::Error> {
39 | write!(formatter, "{}", self.name())
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/build/impl_mergeable_tree.rs:
--------------------------------------------------------------------------------
1 | use crate::{Build, MergeableFileSystemTree};
2 | use std::{
3 | fs::{create_dir_all, write},
4 | io::Error,
5 | path::{Path, PathBuf},
6 | };
7 |
8 | impl Build for MergeableFileSystemTree
9 | where
10 | Name: AsRef + Ord,
11 | FileContent: AsRef<[u8]>,
12 | {
13 | type BorrowedPath = Path;
14 | type OwnedPath = PathBuf;
15 |
16 | fn join(prefix: &Self::BorrowedPath, name: &Name) -> Self::OwnedPath {
17 | prefix.join(name)
18 | }
19 |
20 | fn write_file(path: &Self::BorrowedPath, content: &Self::FileContent) -> Result<(), Error> {
21 | if let Some(dir) = path.parent() {
22 | create_dir_all(dir)?;
23 | }
24 | write(path, content)
25 | }
26 |
27 | fn create_dir(path: &Self::BorrowedPath) -> Result<(), Error> {
28 | create_dir_all(path)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/build/impl_tree.rs:
--------------------------------------------------------------------------------
1 | use crate::{Build, FileSystemTree};
2 | use std::{
3 | fs::{create_dir, write},
4 | io::Error,
5 | path::{Path, PathBuf},
6 | };
7 |
8 | impl Build for FileSystemTree
9 | where
10 | Name: AsRef + Ord,
11 | FileContent: AsRef<[u8]>,
12 | {
13 | type BorrowedPath = Path;
14 | type OwnedPath = PathBuf;
15 |
16 | fn join(prefix: &Self::BorrowedPath, name: &Name) -> Self::OwnedPath {
17 | prefix.join(name)
18 | }
19 |
20 | fn write_file(path: &Self::BorrowedPath, content: &Self::FileContent) -> Result<(), Error> {
21 | write(path, content)
22 | }
23 |
24 | fn create_dir(path: &Self::BorrowedPath) -> Result<(), Error> {
25 | create_dir(path)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! # [`FileSystemTree`]
2 | //!
3 | //! [`FileSystemTree::build`](Build::build) is faster than
4 | //! [`MergeableFileSystemTree::build`](MergeableFileSystemTree) but it does not write over an existing
5 | //! directory and it does not create parent directories when they don't exist.
6 | //!
7 | //! **Example:**
8 | //!
9 | //! ```no_run
10 | //! use build_fs_tree::{FileSystemTree, Build, dir, file};
11 | //! let tree: FileSystemTree<&str, &str> = dir! {
12 | //! "index.html" => file!(r#"
13 | //!
14 | //!
15 | //!
16 | //! "#)
17 | //! "scripts" => dir! {
18 | //! "main.js" => file!(r#"document.write('Hello World')"#)
19 | //! }
20 | //! "styles" => dir! {
21 | //! "style.css" => file!(r#":root { color: red; }"#)
22 | //! }
23 | //! };
24 | //! tree.build("public").unwrap();
25 | //! ```
26 | //!
27 | //! # [`MergeableFileSystemTree`]
28 | //!
29 | //! Unlike [`FileSystemTree::build`](FileSystemTree), [`MergeableFileSystemTree::build`](Build::build)
30 | //! can write over an existing directory and create parent directories that were not exist before at the
31 | //! cost of performance.
32 | //!
33 | //! You can convert a `FileSystemTree` into a `MergeableFileSystemTree` via [`From::from`]/[`Into::into`]
34 | //! and vice versa.
35 | //!
36 | //! **Example:**
37 | //!
38 | //! ```no_run
39 | //! use build_fs_tree::{MergeableFileSystemTree, Build, dir, file};
40 | //! let tree = MergeableFileSystemTree::<&str, &str>::from(dir! {
41 | //! "public" => dir! {
42 | //! "index.html" => file!(r#"
43 | //!
44 | //!
45 | //!
46 | //! "#)
47 | //! "scripts/main.js" => file!(r#"document.write('Hello World')"#)
48 | //! "scripts/style.css" => file!(r#":root { color: red; }"#)
49 | //! }
50 | //! });
51 | //! tree.build(".").unwrap();
52 | //! ```
53 | //!
54 | //! # Serialization and Deserialization
55 | //!
56 | //! Both [`FileSystemTree`] and [`MergeableFileSystemTree`] implement [`serde::Deserialize`]
57 | //! and [`serde::Serialize`].
58 |
59 | #![deny(warnings)]
60 |
61 | mod build;
62 | mod macros;
63 | mod node;
64 | mod tree;
65 |
66 | pub use build::*;
67 | pub use node::*;
68 | pub use tree::*;
69 |
70 | #[cfg(feature = "cli")]
71 | pub mod program;
72 |
73 | pub use serde;
74 | pub use serde_yaml;
75 |
--------------------------------------------------------------------------------
/src/macros.rs:
--------------------------------------------------------------------------------
1 | #![no_implicit_prelude]
2 |
3 | /// Create representation of a [directory](crate::FileSystemTree::Directory).
4 | ///
5 | /// **NOTES:**
6 | /// * **Syntax:** The syntax used by this macro is similar to the syntax used by
7 | /// [maplit](https://docs.rs/maplit/1.0.2/maplit/) except that in this macro, commas are
8 | /// optional.
9 | /// * **Typings:** The types of `Path` and `FileContent` generic parameter isn't required to be
10 | /// the types provided by the expressions that users wrote as long as they implement
11 | /// [`From`](::std::convert::From) where `X` is the types of the aforementioned user
12 | /// provided expressions.
13 | ///
14 | /// # Syntax
15 | ///
16 | /// The syntax used by this macro is similar to the syntax used by
17 | /// [maplit](https://docs.rs/maplit/1.0.2/maplit/) except that in this macro, commas are optional.
18 | ///
19 | /// **Example:** Without commas
20 | ///
21 | /// ```
22 | /// use build_fs_tree::{FileSystemTree, dir, file};
23 | ///
24 | /// let tree: FileSystemTree<&str, &str> = dir! {
25 | /// "a" => file!("foo")
26 | /// "b" => file!("bar")
27 | /// "c" => dir! {
28 | /// "x" => file!("baz")
29 | /// }
30 | /// };
31 | ///
32 | /// # dbg!(&tree);
33 | /// assert_eq!(
34 | /// tree.dir_content().unwrap()
35 | /// .get("a").unwrap().file_content().unwrap(),
36 | /// &"foo",
37 | /// );
38 | /// assert_eq!(
39 | /// tree.dir_content().unwrap()
40 | /// .get("b").unwrap().file_content().unwrap(),
41 | /// &"bar",
42 | /// );
43 | /// assert_eq!(
44 | /// tree.dir_content().unwrap()
45 | /// .get("c").unwrap().dir_content().unwrap()
46 | /// .get("x").unwrap().file_content().unwrap(),
47 | /// &"baz",
48 | /// );
49 | /// ```
50 | ///
51 | /// **Example:** With commas
52 | ///
53 | /// ```
54 | /// use build_fs_tree::{FileSystemTree, dir, file};
55 | ///
56 | /// let tree: FileSystemTree<&str, &str> = dir! {
57 | /// "a" => file!("foo"),
58 | /// "b" => file!("bar"),
59 | /// "c" => dir! {
60 | /// "x" => file!("baz"),
61 | /// },
62 | /// };
63 | ///
64 | /// # dbg!(&tree);
65 | /// assert_eq!(
66 | /// tree.dir_content().unwrap()
67 | /// .get("a").unwrap().file_content().unwrap(),
68 | /// &"foo",
69 | /// );
70 | /// assert_eq!(
71 | /// tree.dir_content().unwrap()
72 | /// .get("b").unwrap().file_content().unwrap(),
73 | /// &"bar",
74 | /// );
75 | /// assert_eq!(
76 | /// tree.dir_content().unwrap()
77 | /// .get("c").unwrap().dir_content().unwrap()
78 | /// .get("x").unwrap().file_content().unwrap(),
79 | /// &"baz",
80 | /// );
81 | /// ```
82 | ///
83 | /// # Typings
84 | ///
85 | /// The types of `Path` and `FileContent` generic parameter isn't required to be the types
86 | /// provided by the expressions that users wrote as long as they implement
87 | /// [`From`](::std::convert::From) where `X` is the types of the aforementioned user
88 | /// provided expressions.
89 | ///
90 | /// **Example:** Where `Path` is a `String`
91 | ///
92 | /// ```
93 | /// use build_fs_tree::{FileSystemTree, dir, file};
94 | ///
95 | /// let tree: FileSystemTree = dir! {
96 | /// "a" => file!("foo")
97 | /// "b" => file!("bar")
98 | /// "c" => dir! {
99 | /// "x" => file!("baz")
100 | /// }
101 | /// };
102 | ///
103 | /// # dbg!(&tree);
104 | /// assert_eq!(
105 | /// tree.dir_content().unwrap()
106 | /// .get("a").unwrap().file_content().unwrap(),
107 | /// &"foo",
108 | /// );
109 | /// assert_eq!(
110 | /// tree.dir_content().unwrap()
111 | /// .get("b").unwrap().file_content().unwrap(),
112 | /// &"bar",
113 | /// );
114 | /// assert_eq!(
115 | /// tree.dir_content().unwrap()
116 | /// .get("c").unwrap().dir_content().unwrap()
117 | /// .get("x").unwrap().file_content().unwrap(),
118 | /// &"baz",
119 | /// );
120 | /// ```
121 | ///
122 | /// **Example:** Where `Path` is a `PathBuf` and `FileContent` is a `Vec`
123 | ///
124 | /// ```
125 | /// use build_fs_tree::{FileSystemTree, dir, file};
126 | /// use std::path::PathBuf;
127 | ///
128 | /// let tree: FileSystemTree> = dir! {
129 | /// "a" => file!("foo")
130 | /// "b" => file!("bar")
131 | /// "c" => dir! {
132 | /// "x" => file!("baz")
133 | /// }
134 | /// };
135 | ///
136 | /// # dbg!(&tree);
137 | /// assert_eq!(
138 | /// tree.dir_content().unwrap()
139 | /// .get(&PathBuf::from("a")).unwrap().file_content().unwrap(),
140 | /// &Vec::from("foo"),
141 | /// );
142 | /// assert_eq!(
143 | /// tree.dir_content().unwrap()
144 | /// .get(&PathBuf::from("b")).unwrap().file_content().unwrap(),
145 | /// &Vec::from("bar"),
146 | /// );
147 | /// assert_eq!(
148 | /// tree.dir_content().unwrap()
149 | /// .get(&PathBuf::from("c")).unwrap().dir_content().unwrap()
150 | /// .get(&PathBuf::from("x")).unwrap().file_content().unwrap(),
151 | /// &Vec::from("baz"),
152 | /// );
153 | /// ```
154 | #[macro_export]
155 | macro_rules! dir {
156 | ($($key:expr => $value:expr $(,)?)*) => {{
157 | let mut __directory_content = ::std::collections::BTreeMap::new();
158 | $(
159 | let _ = ::std::collections::BTreeMap::insert(
160 | &mut __directory_content,
161 | ::std::convert::From::from($key),
162 | $value
163 | );
164 | )*
165 | $crate::FileSystemTree::Directory(__directory_content)
166 | }};
167 | }
168 |
169 | /// Create representation of a [file](crate::FileSystemTree::File).
170 | ///
171 | /// # Syntax
172 | ///
173 | /// **Example:**
174 | ///
175 | /// ```
176 | /// use build_fs_tree::{FileSystemTree, file};
177 | /// let file: FileSystemTree<&str, &str> = file!("CONTENT OF THE FILE");
178 | /// assert_eq!(file, FileSystemTree::File("CONTENT OF THE FILE"));
179 | /// ```
180 | ///
181 | /// # Typings
182 | ///
183 | /// This macro calls [`From::from`](::std::convert::From::from) under the hood.
184 | ///
185 | /// **Example:** Where `FileContent` is a `String`
186 | ///
187 | /// ```
188 | /// use build_fs_tree::{FileSystemTree, file};
189 | /// let file: FileSystemTree<&str, String> = file!("CONTENT OF THE FILE");
190 | /// assert_eq!(file, FileSystemTree::File("CONTENT OF THE FILE".to_string()));
191 | /// ```
192 | ///
193 | /// **Example:** Where `FileContent` is a `Vec`
194 | ///
195 | /// ```
196 | /// use build_fs_tree::{FileSystemTree, file};
197 | /// let file: FileSystemTree<&str, Vec> = file!("CONTENT OF THE FILE");
198 | /// assert_eq!(file, FileSystemTree::File("CONTENT OF THE FILE".into()));
199 | /// ```
200 | #[macro_export]
201 | macro_rules! file {
202 | ($content:expr) => {
203 | $crate::FileSystemTree::File(::std::convert::From::from($content))
204 | };
205 | }
206 |
--------------------------------------------------------------------------------
/src/node.rs:
--------------------------------------------------------------------------------
1 | use crate::{make_unmergeable_dir_content_mergeable, FileSystemTree, MergeableFileSystemTree};
2 | use pipe_trait::Pipe;
3 | use std::collections::BTreeMap;
4 |
5 | /// Node of a filesystem tree.
6 | pub trait Node {
7 | /// Content of the node if it is a file.
8 | type FileContent;
9 | /// Content of the node if it is a directory.
10 | type DirectoryContent;
11 | /// Read the content of the node.
12 | fn read(self) -> NodeContent;
13 | }
14 |
15 | /// Content of a node in the filesystem tree
16 | ///
17 | /// **Generic parameters:**
18 | /// * `FileContent`: Content of the node if it is a file.
19 | /// * `DirectoryContent`: Content of the node if it is a directory.
20 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21 | pub enum NodeContent {
22 | /// The node is a file.
23 | File(FileContent),
24 | /// The node is a directory.
25 | Directory(DirectoryContent),
26 | }
27 |
28 | impl Node for FileSystemTree
29 | where
30 | Path: Ord,
31 | {
32 | type FileContent = FileContent;
33 | type DirectoryContent = BTreeMap;
34 |
35 | fn read(self) -> NodeContent {
36 | match self {
37 | FileSystemTree::File(content) => NodeContent::File(content),
38 | FileSystemTree::Directory(content) => NodeContent::Directory(content),
39 | }
40 | }
41 | }
42 |
43 | impl Node for MergeableFileSystemTree
44 | where
45 | Path: Ord,
46 | {
47 | type FileContent = FileContent;
48 | type DirectoryContent = BTreeMap;
49 |
50 | fn read(self) -> NodeContent {
51 | match self.into() {
52 | FileSystemTree::File(content) => NodeContent::File(content),
53 | FileSystemTree::Directory(content) => content
54 | .pipe(make_unmergeable_dir_content_mergeable)
55 | .pipe(NodeContent::Directory),
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/program.rs:
--------------------------------------------------------------------------------
1 | //! Components of the CLI programs.
2 |
3 | #[cfg(feature = "cli-completions")]
4 | pub mod completions;
5 | pub mod main;
6 |
7 | pub use clap;
8 | pub use clap_utilities;
9 | pub use clap_utilities::clap_complete;
10 |
--------------------------------------------------------------------------------
/src/program/completions.rs:
--------------------------------------------------------------------------------
1 | //! Components that make up the program that generates shell completions for the main program.
2 | use super::main::Args;
3 | use clap_utilities::CommandFactoryExtra;
4 | use std::process::ExitCode;
5 |
6 | /// Run the completions generator.
7 | pub fn main() -> ExitCode {
8 | Args::run_completion_generator()
9 | }
10 |
--------------------------------------------------------------------------------
/src/program/main.rs:
--------------------------------------------------------------------------------
1 | //! Components that make up the main program.
2 |
3 | mod app;
4 | mod args;
5 | mod error;
6 | mod run;
7 |
8 | pub use app::*;
9 | pub use args::*;
10 | pub use error::*;
11 | pub use run::*;
12 |
13 | use std::process::ExitCode;
14 |
15 | /// The main program.
16 | pub fn main() -> ExitCode {
17 | match App::from_env().run() {
18 | Ok(()) => ExitCode::SUCCESS,
19 | Err(error_message) => {
20 | eprintln!("{}", error_message);
21 | ExitCode::FAILURE
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/program/main/app.rs:
--------------------------------------------------------------------------------
1 | use super::{Args, Command, RuntimeError, CREATE, POPULATE};
2 | use clap::Parser;
3 | use derive_more::{From, Into};
4 | use std::path::PathBuf;
5 |
6 | /// The main application.
7 | #[derive(Debug, From, Into)]
8 | pub struct App {
9 | /// Parse result of CLI arguments.
10 | pub args: Args,
11 | }
12 |
13 | impl App {
14 | /// Initialize the application from environment parameters.
15 | pub fn from_env() -> Self {
16 | Args::parse().into()
17 | }
18 |
19 | /// Run the application.
20 | pub fn run(self) -> Result<(), RuntimeError> {
21 | match self.args.command {
22 | Command::Create { target } => CREATE(&target),
23 | Command::Populate { target } => POPULATE(&target),
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/program/main/args.rs:
--------------------------------------------------------------------------------
1 | use clap::{ColorChoice, Parser, Subcommand};
2 | use std::path::PathBuf;
3 | use text_block_macros::text_block;
4 |
5 | /// Parse result of CLI arguments.
6 | #[derive(Debug, Parser)]
7 | #[clap(
8 | version,
9 | name = "build-fs-tree",
10 | color = ColorChoice::Never,
11 |
12 | about = "Create a filesystem tree from YAML",
13 |
14 | long_about = text_block! {
15 | "Create a filesystem tree from YAML"
16 | ""
17 | "Source: https://github.com/KSXGitHub/build-fs-tree"
18 | "Issues: https://github.com/KSXGitHub/build-fs-tree/issues"
19 | "Donate: https://patreon.com/khai96_"
20 | },
21 |
22 | after_help = text_block! {
23 | "Examples:"
24 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree create foo-and-bar"
25 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree populate ."
26 | " $ build-fs-tree create root < fs-tree.yaml"
27 | " $ build-fs-tree populate . < fs-tree.yaml"
28 | },
29 |
30 | after_long_help = text_block! {
31 | "Examples:"
32 | " Create two text files in a new directory"
33 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree create foo-and-bar"
34 | ""
35 | " Create two text files in the current directory"
36 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree populate ."
37 | ""
38 | " Create a new filesystem tree from a YAML file"
39 | " $ build-fs-tree create root < fs-tree.yaml"
40 | ""
41 | " Populate the current directory with filesystem tree as described in a YAML file"
42 | " $ build-fs-tree populate . < fs-tree.yaml"
43 | },
44 | )]
45 | pub struct Args {
46 | /// Command to execute.
47 | #[structopt(subcommand)]
48 | pub command: Command,
49 | }
50 |
51 | /// Subcommands of the program.
52 | #[derive(Debug, Subcommand)]
53 | #[clap(
54 | rename_all = "kebab-case",
55 | about = "Create a filesystem tree from YAML"
56 | )]
57 | pub enum Command {
58 | /// Invoke [`FileSystemTree::build`](crate::FileSystemTree).
59 | #[clap(
60 | about = "Read YAML from stdin and create a new filesystem tree",
61 |
62 | long_about = concat!(
63 | "Read YAML from stdin and create a new filesystem tree at . ",
64 | "Merged paths are not allowed",
65 | ),
66 |
67 | after_help = text_block! {
68 | "EXAMPLES:"
69 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree create foo-and-bar"
70 | " $ echo '{ text-files: { foo.txt: HELLO } }' | build-fs-tree create files"
71 | " $ build-fs-tree create root < fs-tree.yaml"
72 | },
73 |
74 | after_long_help = text_block! {
75 | "EXAMPLES:"
76 | " Create two text files in a new directory"
77 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree create foo-and-bar"
78 | ""
79 | " Create a text file and its parent directories"
80 | " $ echo '{ text-files: { foo.txt: HELLO } }' | build-fs-tree create files"
81 | ""
82 | " Create a new filesystem tree from a YAML file"
83 | " $ build-fs-tree create root < fs-tree.yaml"
84 | },
85 | )]
86 | Create {
87 | #[clap(name = "TARGET")]
88 | target: PathBuf,
89 | },
90 |
91 | /// Invoke [`MergeableFileSystemTree::build`](crate::MergeableFileSystemTree).
92 | #[clap(
93 | about = "Read YAML from stdin and populate an existing filesystem tree",
94 |
95 | long_about = concat!(
96 | "Read YAML from stdin and populate an existing filesystem tree at . ",
97 | "Parent directories would be created if they are not already exist",
98 | ),
99 |
100 | after_help = text_block! {
101 | "EXAMPLES:"
102 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree populate ."
103 | " $ echo '{ files/text-files/foo.txt: HELLO }' | build-fs-tree populate ."
104 | " $ build-fs-tree populate . < fs-tree.yaml"
105 | },
106 |
107 | after_long_help = text_block! {
108 | "EXAMPLES:"
109 | " Create two text files in the current directory"
110 | " $ echo '{ foo.txt: HELLO, bar.txt: WORLD }' | build-fs-tree populate ."
111 | ""
112 | " Create a text file and its parent directories"
113 | " $ echo '{ files/text-files/foo.txt: HELLO }' | build-fs-tree populate ."
114 | ""
115 | " Populate the current directory with filesystem tree as described in a YAML file"
116 | " $ build-fs-tree populate . < fs-tree.yaml"
117 | },
118 | )]
119 | Populate {
120 | #[clap(name = "TARGET")]
121 | target: PathBuf,
122 | },
123 | }
124 |
--------------------------------------------------------------------------------
/src/program/main/error.rs:
--------------------------------------------------------------------------------
1 | use crate::BuildError;
2 | use derive_more::{Display, Error, From};
3 | use std::{fmt::Debug, io};
4 |
5 | /// Error when execute the [`run`](super::run::run) function.
6 | #[derive(Debug, Display, From, Error)]
7 | pub enum RuntimeError {
8 | /// Failed to parse YAML from stdin.
9 | Yaml(serde_yaml::Error),
10 | /// Failed to create the filesystem tree.
11 | Build(BuildError),
12 | }
13 |
--------------------------------------------------------------------------------
/src/program/main/run.rs:
--------------------------------------------------------------------------------
1 | use super::RuntimeError;
2 | use crate::{Build, FileSystemTree, MergeableFileSystemTree, Node};
3 | use pipe_trait::Pipe;
4 | use serde::de::DeserializeOwned;
5 | use serde_yaml::from_reader;
6 | use std::{
7 | io,
8 | path::{Path, PathBuf},
9 | };
10 |
11 | /// Read a YAML from stdin and build a filesystem tree.
12 | pub fn run(target: &Path) -> Result<(), RuntimeError>
13 | where
14 | Path: ToOwned + AsRef + ?Sized,
15 | Path::Owned: AsRef,
16 | Tree: Build
17 | + Node
18 | + DeserializeOwned,
19 | Tree::DirectoryContent: IntoIterator
- ,
20 | Path: Ord,
21 | {
22 | io::stdin()
23 | .pipe(from_reader::<_, Tree>)?
24 | .build(target)?
25 | .pipe(Ok)
26 | }
27 |
28 | /// Read a YAML from stdin and build a filesystem tree.
29 | pub type Run = fn(&Path) -> Result<(), RuntimeError>;
30 | /// Read a YAML from stdin and create a new filesystem tree.
31 | pub const CREATE: Run = run::, Path>;
32 | /// Read a YAML from stdin and populate the target filesystem tree.
33 | pub const POPULATE: Run = run::, Path>;
34 |
--------------------------------------------------------------------------------
/src/tree.rs:
--------------------------------------------------------------------------------
1 | mod dir_content;
2 |
3 | pub use dir_content::*;
4 |
5 | use derive_more::{AsMut, AsRef, Deref, DerefMut, From, Into};
6 | use serde::{Deserialize, Serialize};
7 | use std::collections::BTreeMap;
8 |
9 | /// Representation of a filesystem which contains only [files](FileSystemTree::File)
10 | /// and [directories](FileSystemTree::Directory).
11 | ///
12 | /// The serialization of `FileContent` (content of file) and `BTreeMap`
13 | /// (children of directory) must not share the same type. That is, `FileContent` must
14 | /// be serialized to things other than a dictionary.
15 | ///
16 | /// **Note:** [`FileSystemTree::build`](crate::Build::build) cannot write over an existing
17 | /// directory. Use [`MergeableFileSystemTree`] instead if you desire such behavior.
18 | ///
19 | /// **Generic parameters:**
20 | /// * `Path`: Reference to a file in the filesystem.
21 | /// * `FileContent`: Content of a file.
22 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23 | #[serde(untagged)]
24 | pub enum FileSystemTree
25 | where
26 | Path: Ord,
27 | {
28 | /// Represents a file with its content.
29 | /// Its YAML representation must not have the same type as [`FileSystemTree::Directory`]'s.
30 | File(FileContent),
31 | /// Represents a directory with its children.
32 | /// It is a set of name-to-subtree mappings.
33 | /// Its YAML representation must not have the same type as [`FileSystemTree::File`]'s.
34 | Directory(BTreeMap),
35 | }
36 |
37 | /// Representation of a filesystem which contains only [files](FileSystemTree::File)
38 | /// and [directories](FileSystemTree::Directory).
39 | ///
40 | /// The serialization of `FileContent` (content of file) and `BTreeMap`
41 | /// (children of directory) must not share the same type. That is, `FileContent` must
42 | /// be serialized to things other than a dictionary.
43 | ///
44 | /// **Generic parameters:**
45 | /// * `Path`: Reference to a file in the filesystem.
46 | /// * `FileContent`: Content of a file.
47 | ///
48 | /// **Difference from [`FileSystemTree`]:**
49 | /// [`FileSystemTree::build`](crate::Build::build) cannot write over an existing directory.
50 | /// On the other hand, [`MergeableFileSystemTree::build`](crate::Build::build) either merges
51 | /// the two directories if there's no conflict.
52 | #[derive(
53 | Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsMut, AsRef, Deref, DerefMut, From, Into,
54 | )]
55 | pub struct MergeableFileSystemTree(FileSystemTree)
56 | where
57 | Path: Ord;
58 |
59 | mod methods;
60 |
--------------------------------------------------------------------------------
/src/tree/dir_content.rs:
--------------------------------------------------------------------------------
1 | use crate::{FileSystemTree, MergeableFileSystemTree};
2 | use std::{collections::BTreeMap, mem::transmute};
3 |
4 | macro_rules! map_type {
5 | ($(#[$attribute:meta])* $name:ident = $tree:ident) => {
6 | $(#[$attribute])*
7 | pub type $name = BTreeMap>;
8 | };
9 | }
10 |
11 | map_type!(
12 | /// Directory content of [`FileSystemTree`].
13 | DirectoryContent = FileSystemTree
14 | );
15 |
16 | map_type!(
17 | /// Directory content of [`MergeableFileSystemTree`].
18 | MergeableDirectoryContent = MergeableFileSystemTree
19 | );
20 |
21 | macro_rules! function {
22 | ($(#[$attribute:meta])* $name:ident :: $input:ident -> $output:ident) => {
23 | $(#[$attribute])*
24 | pub fn $name(map: $input) -> $output
25 | where
26 | Path: Ord,
27 | {
28 | unsafe { transmute(map) }
29 | }
30 | };
31 | }
32 |
33 | function!(
34 | /// Transmute a [`DirectoryContent`] into a [`MergeableDirectoryContent`].
35 | make_unmergeable_dir_content_mergeable :: DirectoryContent -> MergeableDirectoryContent
36 | );
37 |
38 | function!(
39 | /// Transmute a [`MergeableDirectoryContent`] into a [`DirectoryContent`].
40 | make_mergeable_dir_content_unmergeable :: MergeableDirectoryContent -> DirectoryContent
41 | );
42 |
--------------------------------------------------------------------------------
/src/tree/methods.rs:
--------------------------------------------------------------------------------
1 | use crate::FileSystemTree::{self, *};
2 | use std::collections::BTreeMap;
3 |
4 | macro_rules! get_content {
5 | ($variant:ident, $source:expr) => {
6 | if let $variant(content) = $source {
7 | Some(content)
8 | } else {
9 | None
10 | }
11 | };
12 | }
13 |
14 | impl FileSystemTree
15 | where
16 | Path: Ord,
17 | {
18 | /// Get immutable reference to the file content.
19 | pub fn file_content(&self) -> Option<&'_ FileContent> {
20 | get_content!(File, self)
21 | }
22 |
23 | /// Get immutable reference to the directory content.
24 | pub fn dir_content(&self) -> Option<&'_ BTreeMap> {
25 | get_content!(Directory, self)
26 | }
27 |
28 | /// Get immutable reference to a descendant of any level.
29 | pub fn path<'a>(&'a self, path: &'a mut impl Iterator
- ) -> Option<&'a Self> {
30 | if let Some(current) = path.next() {
31 | self.dir_content()?.get(current)?.path(path)
32 | } else {
33 | Some(self)
34 | }
35 | }
36 |
37 | /// Get mutable reference to the file content.
38 | pub fn file_content_mut(&mut self) -> Option<&'_ mut FileContent> {
39 | get_content!(File, self)
40 | }
41 |
42 | /// Get mutable reference to the directory content.
43 | pub fn dir_content_mut(&mut self) -> Option<&'_ mut BTreeMap> {
44 | get_content!(Directory, self)
45 | }
46 |
47 | /// Get mutable reference to a descendant of any level.
48 | pub fn path_mut<'a>(
49 | &'a mut self,
50 | path: &'a mut impl Iterator
- ,
51 | ) -> Option<&'a mut Self> {
52 | if let Some(current) = path.next() {
53 | self.dir_content_mut()?.get_mut(current)?.path_mut(path)
54 | } else {
55 | Some(self)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/template/build-fs-tree-bin/PKGBUILD:
--------------------------------------------------------------------------------
1 | # This PKGBUILD is not a full PKGBUILD
2 | # pkgname, pkgver, source, and sha1sums are to be generated
3 | pkgdesc='Create a filesystem tree from YAML'
4 | pkgrel=1
5 | arch=(x86_64)
6 | license=(MIT)
7 | url='https://github.com/KSXGitHub/build-fs-tree'
8 | provides=(build-fs-tree)
9 | conflicts=(build-fs-tree)
10 | sha1sums=(
11 | "$_checksum" # for the build-fs-tree binary
12 | "${_completion_checksums[@]}" # for the completion files
13 | SKIP # for the readme file
14 | SKIP # for the license file
15 | )
16 |
17 | package() {
18 | install -Dm755 "build-fs-tree-$_checksum" "$pkgdir/usr/bin/build-fs-tree"
19 | install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
20 | install -Dm644 LICENSE.md "$pkgdir/usr/share/licenses/$pkgname/LICENSE.md"
21 | install -Dm644 "completion.$pkgver.bash" "$pkgdir/usr/share/bash-completion/completions/build-fs-tree"
22 | install -Dm644 "completion.$pkgver.fish" "$pkgdir/usr/share/fish/completions/build-fs-tree.fish"
23 | install -Dm644 "completion.$pkgver.zsh" "$pkgdir/usr/share/zsh/site-functions/_build-fs-tree"
24 | }
25 |
--------------------------------------------------------------------------------
/template/build-fs-tree/PKGBUILD:
--------------------------------------------------------------------------------
1 | # This PKGBUILD is not a full PKGBUILD
2 | # pkgname, pkgver, source, and sha1sums are to be generated
3 | pkgdesc='Create a filesystem tree from YAML'
4 | pkgrel=1
5 | arch=(x86_64)
6 | license=(MIT)
7 | url='https://github.com/KSXGitHub/build-fs-tree'
8 | makedepends=(cargo)
9 |
10 | build() {
11 | cd "$srcdir/build-fs-tree-$pkgver"
12 | cargo build --release --locked --bin=build-fs-tree --features=cli
13 | }
14 |
15 | package() {
16 | cd "$srcdir/build-fs-tree-$pkgver"
17 | install -Dm755 target/release/build-fs-tree "$pkgdir/usr/bin/build-fs-tree"
18 | install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
19 | install -Dm644 LICENSE.md "$pkgdir/usr/share/licenses/$pkgname/LICENSE.md"
20 | install -Dm644 exports/completion.bash "$pkgdir/usr/share/bash-completion/completions/build-fs-tree"
21 | install -Dm644 exports/completion.fish "$pkgdir/usr/share/fish/completions/build-fs-tree.fish"
22 | install -Dm644 exports/completion.zsh "$pkgdir/usr/share/zsh/site-functions/_build-fs-tree"
23 | }
24 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -o errexit -o pipefail -o nounset
3 |
4 | run() (
5 | echo >&2
6 | echo "exec> $*" >&2
7 | "$@"
8 | )
9 |
10 | skip() (
11 | echo >&2
12 | echo "skip> $*" >&2
13 | )
14 |
15 | run_if() (
16 | condition="$1"
17 | shift
18 | case "$condition" in
19 | true) run "$@" ;;
20 | false) skip "$@" ;;
21 | *)
22 | echo "error: Invalid condition: $condition" >&2
23 | exit 1
24 | ;;
25 | esac
26 | )
27 |
28 | unit() (
29 | eval run_if "${LINT:-true}" cargo clippy "$@" -- -D warnings
30 | eval run_if "${DOC:-false}" cargo doc "$@"
31 | eval run_if "${BUILD:-true}" cargo build "${BUILD_FLAGS:-}" "$@"
32 | eval run_if "${TEST:-true}" cargo test "${TEST_FLAGS:-}" "$@"
33 | )
34 |
35 | run_if "${FMT:-true}" cargo fmt -- --check
36 | unit "$@"
37 | unit --no-default-features "$@"
38 | unit --all-features "$@"
39 | unit --features cli "$@"
40 | unit --features cli-completions "$@"
41 |
--------------------------------------------------------------------------------
/test/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "private-test-utils"
3 | version = "0.0.0"
4 | edition = "2021"
5 | build = false
6 |
7 | [lib]
8 | name = "private_test_utils"
9 | path = "lib.rs"
10 |
11 | [features]
12 | default = []
13 | cli = ["build-fs-tree/cli"]
14 | cli-completions = ["build-fs-tree/cli-completions"]
15 |
16 | [dependencies]
17 | build-fs-tree = { path = "../src", default-features = false }
18 |
19 | cargo_toml.workspace = true
20 | command-extra.workspace = true
21 | derive_more.workspace = true
22 | maplit.workspace = true
23 | pipe-trait.workspace = true
24 | pretty_assertions.workspace = true
25 | rand.workspace = true
26 | semver.workspace = true
27 | serde_yaml.workspace = true
28 | serde.workspace = true
29 | text-block-macros.workspace = true
30 |
--------------------------------------------------------------------------------
/test/build.rs:
--------------------------------------------------------------------------------
1 | use crate::{assert_dir, assert_file, create_temp_dir, sample_tree, string_set, test_sample_tree};
2 | use build_fs_tree::{dir, file, Build, MergeableFileSystemTree};
3 | use pipe_trait::Pipe;
4 | use pretty_assertions::assert_eq;
5 | use std::path::PathBuf;
6 |
7 | type MergeableTree = MergeableFileSystemTree<&'static str, &'static str>;
8 |
9 | macro_rules! test_case {
10 | ($name:ident, $root:expr, $key:ty, $value:ty $(,)?) => {
11 | #[test]
12 | fn $name() {
13 | let temp = create_temp_dir();
14 | let target = PathBuf::from(temp.join($root));
15 | sample_tree::<$key, $value>().build(&target).unwrap();
16 | test_sample_tree(&target);
17 | }
18 | };
19 | }
20 |
21 | test_case!(string_string, "root", String, String);
22 | test_case!(str_slice_string, "root", &'static str, String);
23 | test_case!(string_str_slice, "root", String, &'static str);
24 | test_case!(str_slice_str_slice, "root", &'static str, &'static str);
25 | test_case!(path_buf_str_slice, "root", PathBuf, &'static str);
26 | test_case!(path_buf_u8_vec, "root", PathBuf, ::std::vec::Vec);
27 |
28 | /// Error message when attempting to create a directory at location of a file.
29 | const DIR_ON_FILE_ERROR_SUFFIX: &str = if cfg!(windows) {
30 | "Cannot create a file when that file already exists. (os error 183)"
31 | } else {
32 | "File exists (os error 17)"
33 | };
34 |
35 | /// Error message when attempting to create a file at location of a directory.
36 | const FILE_ON_DIR_ERROR_SUFFIX: &str = if cfg!(windows) {
37 | "Access is denied. (os error 5)"
38 | } else {
39 | "Is a directory (os error 21)"
40 | };
41 |
42 | #[test]
43 | fn unmergeable_build() {
44 | let temp = create_temp_dir();
45 | let target = temp.join("root");
46 | sample_tree::<&str, &str>()
47 | .build(&target)
48 | .expect("build for the first time");
49 | test_sample_tree(&target);
50 | let actual_error = sample_tree::<&str, &str>()
51 | .build(&target)
52 | .expect_err("build for the second time")
53 | .to_string();
54 | let expected_error = format!("create_dir {:?}: {}", &target, DIR_ON_FILE_ERROR_SUFFIX);
55 | assert_eq!(actual_error, expected_error);
56 | }
57 |
58 | #[test]
59 | fn mergeable_build() {
60 | let temp = create_temp_dir();
61 | let target = temp.join("root");
62 | sample_tree::<&str, &str>()
63 | .build(&target)
64 | .expect("build for the first time");
65 | test_sample_tree(&target);
66 | sample_tree::<&str, &str>()
67 | .pipe(MergeableTree::from)
68 | .build(&target)
69 | .expect("build for the second time");
70 | test_sample_tree(&target);
71 | MergeableTree::from(dir! {
72 | "a" => dir! {
73 | "ghi" => dir! {
74 | "0" => dir! {}
75 | "1" => file!("content of a/ghi/1")
76 | }
77 | }
78 | "z" => dir! {
79 | "x" => dir! {
80 | "c" => file!("content of z/x/c")
81 | }
82 | }
83 | })
84 | .build(&target)
85 | .expect("build for the third time: add some items");
86 | eprintln!("Check new files...");
87 | assert_dir!(&target, string_set!("a", "b", "z"));
88 | assert_dir!(target.join("a"), string_set!("abc", "def", "ghi"));
89 | assert_dir!(target.join("a").join("ghi"), string_set!("0", "1"));
90 | assert_dir!(target.join("a").join("ghi").join("0"), string_set!());
91 | assert_file!(target.join("a").join("ghi").join("1"), "content of a/ghi/1");
92 | assert_dir!(target.join("z"), string_set!("x"));
93 | assert_dir!(target.join("z").join("x"), string_set!("c"));
94 | assert_file!(target.join("z").join("x").join("c"), "content of z/x/c");
95 | eprintln!("Check old files...");
96 | assert_dir!(target.join("a").join("abc"), string_set!());
97 | assert_file!(target.join("a").join("def"), "content of a/def");
98 | assert_dir!(target.join("b"), string_set!("foo"));
99 | assert_dir!(target.join("b").join("foo"), string_set!("bar"));
100 | assert_file!(
101 | target.join("b").join("foo").join("bar"),
102 | "content of b/foo/bar",
103 | );
104 | }
105 |
106 | #[test]
107 | fn mergeable_build_conflict_file_on_dir() {
108 | let temp = create_temp_dir();
109 | let target = temp.join("root");
110 | sample_tree::<&str, &str>()
111 | .build(&target)
112 | .expect("build for the first time");
113 | test_sample_tree(&target);
114 | let actual_error = MergeableTree::from(dir! {
115 | "a" => file!("should not exist")
116 | })
117 | .build(&target)
118 | .expect_err("build for the second time")
119 | .to_string();
120 | let expected_error = format!(
121 | "write_file {:?}: {}",
122 | target.join("a"),
123 | FILE_ON_DIR_ERROR_SUFFIX,
124 | );
125 | assert_eq!(actual_error, expected_error);
126 | }
127 |
128 | #[test]
129 | fn mergeable_build_conflict_dir_on_file() {
130 | let temp = create_temp_dir();
131 | let target = temp.join("root");
132 | sample_tree::<&str, &str>()
133 | .build(&target)
134 | .expect("build for the first time");
135 | test_sample_tree(&target);
136 | let actual_error = MergeableTree::from(dir! {
137 | "a" => dir! {
138 | "def" => dir! {
139 | "b" => file!("should not exist")
140 | }
141 | }
142 | })
143 | .build(&target)
144 | .expect_err("build for the second time")
145 | .to_string();
146 | let expected_error = format!(
147 | "create_dir {:?}: {}",
148 | target.join("a").join("def"),
149 | DIR_ON_FILE_ERROR_SUFFIX,
150 | );
151 | assert_eq!(actual_error, expected_error);
152 | }
153 | #[test]
154 | fn mergeable_build_ensure_dir_to_write_file() {
155 | let temp = create_temp_dir();
156 | MergeableTree::from(dir! {
157 | "a/b/c" => file!("a/b/c")
158 | "d/e/f" => dir! {
159 | "foo" => file!("d/e/f/foo")
160 | "bar/baz" => file!("d/e/f/bar/baz")
161 | }
162 | })
163 | .build(temp.as_path())
164 | .expect("build filesystem tree");
165 | assert_file!(temp.join("a").join("b").join("c"), "a/b/c");
166 | assert_file!(temp.join("d").join("e").join("f").join("foo"), "d/e/f/foo");
167 | assert_file!(
168 | temp.join("d").join("e").join("f").join("bar").join("baz"),
169 | "d/e/f/bar/baz",
170 | );
171 | }
172 |
--------------------------------------------------------------------------------
/test/completions.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "cli")]
2 | use build_fs_tree::program::{
3 | clap_complete::Shell, clap_utilities::CommandFactoryExtra, main::Args,
4 | };
5 |
6 | macro_rules! check {
7 | ($name:ident: $shell:ident -> $path:literal) => {
8 | #[test]
9 | fn $name() {
10 | eprintln!(
11 | "check!({name}: {shell} -> {path});",
12 | name = stringify!($name),
13 | shell = stringify!($shell),
14 | path = $path,
15 | );
16 | let received = Args::get_completion_string("build-fs-tree", Shell::$shell)
17 | .expect("get completion string");
18 | let expected = include_str!($path);
19 | let panic_message = concat!(
20 | stringify!($variant),
21 | " completion is outdated. Re-run generate-completions.sh to update",
22 | );
23 | assert!(received == expected, "{panic_message}");
24 | }
25 | };
26 | }
27 |
28 | check!(bash: Bash -> "../exports/completion.bash");
29 | check!(fish: Fish -> "../exports/completion.fish");
30 | check!(zsh: Zsh -> "../exports/completion.zsh");
31 | check!(powershell: PowerShell -> "../exports/completion.ps1");
32 | check!(elvish: Elvish -> "../exports/completion.elv");
33 |
--------------------------------------------------------------------------------
/test/lib.rs:
--------------------------------------------------------------------------------
1 | #![deny(warnings)]
2 | use build_fs_tree::*;
3 | use cargo_toml::Manifest;
4 | use command_extra::CommandExtra;
5 | use derive_more::{AsRef, Deref};
6 | use maplit::btreemap;
7 | use pipe_trait::Pipe;
8 | use pretty_assertions::assert_eq;
9 | use rand::{distributions::Alphanumeric, thread_rng, Rng};
10 | use semver::Version;
11 | use std::{
12 | collections,
13 | env::temp_dir,
14 | ffi::OsString,
15 | fs::{create_dir, read_dir, read_to_string, remove_dir_all},
16 | io::Error,
17 | path::{Path, PathBuf},
18 | process::Command,
19 | str,
20 | sync::LazyLock,
21 | };
22 | use text_block_macros::text_block_fnl;
23 |
24 | use FileSystemTree::{Directory, File};
25 |
26 | /// Representation of a temporary filesystem item.
27 | ///
28 | /// **NOTE:** Delete this once is resolved.
29 | #[derive(Debug, AsRef, Deref)]
30 | pub struct Temp(PathBuf);
31 |
32 | impl Temp {
33 | /// Create a temporary directory.
34 | pub fn new_dir() -> Result {
35 | let path = thread_rng()
36 | .sample_iter(&Alphanumeric)
37 | .take(15)
38 | .map(char::from)
39 | .collect::()
40 | .pipe(|name| temp_dir().join(name));
41 | if path.exists() {
42 | return Self::new_dir();
43 | }
44 | create_dir(&path)?;
45 | path.pipe(Temp).pipe(Ok)
46 | }
47 | }
48 |
49 | impl Drop for Temp {
50 | fn drop(&mut self) {
51 | let path = &self.0;
52 | if let Err(error) = remove_dir_all(path) {
53 | eprintln!("warning: Failed to delete {:?}: {}", path, error);
54 | }
55 | }
56 | }
57 |
58 | /// Create a YAML representation of a sample tree.
59 | pub const SAMPLE_YAML: &str = text_block_fnl! {
60 | "---"
61 | "a:"
62 | " abc: {}"
63 | " def: content of a/def"
64 | "b:"
65 | " foo:"
66 | " bar: content of b/foo/bar"
67 | };
68 |
69 | /// Create a sample tree.
70 | pub fn sample_tree() -> FileSystemTree
71 | where
72 | Path: Ord,
73 | &'static str: Into + Into,
74 | {
75 | Directory(btreemap! {
76 | "a".into() => Directory(btreemap! {
77 | "abc".into() => Directory(btreemap! {}),
78 | "def".into() => File("content of a/def".into()),
79 | }),
80 | "b".into() => Directory(btreemap! {
81 | "foo".into() => Directory(btreemap! {
82 | "bar".into() => File("content of b/foo/bar".into()),
83 | }),
84 | }),
85 | })
86 | }
87 |
88 | /// Create a sample tree (but with `dir!` and `file!` macros).
89 | #[macro_export]
90 | macro_rules! sample_tree {
91 | () => {
92 | dir! {
93 | "a" => dir! {
94 | "abc" => dir! {}
95 | "def" => file!("content of a/def")
96 | }
97 | "b" => dir! {
98 | "foo" => dir! {
99 | "bar" => file!("content of b/foo/bar")
100 | }
101 | }
102 | }
103 | };
104 | }
105 |
106 | /// Create a temporary folder.
107 | pub fn create_temp_dir() -> Temp {
108 | Temp::new_dir().expect("create a temporary directory")
109 | }
110 |
111 | /// Create a set of `String` from `str` slices.
112 | #[macro_export]
113 | macro_rules! string_set {
114 | ($($element:expr),* $(,)?) => {
115 | ::maplit::btreeset! { $(::std::string::String::from($element)),* }
116 | };
117 | }
118 |
119 | /// List names of children of a directory.
120 | pub fn list_children_names(path: impl AsRef) -> collections::BTreeSet {
121 | read_dir(path)
122 | .expect("read_dir")
123 | .filter_map(Result::ok)
124 | .map(|entry| entry.file_name())
125 | .map(OsString::into_string)
126 | .filter_map(Result::ok)
127 | .collect()
128 | }
129 |
130 | /// Read content of a text file.
131 | pub fn read_text_file(path: impl AsRef) -> String {
132 | read_to_string(path).expect("read_to_string")
133 | }
134 |
135 | /// Assert that a directory has a only has certain children.
136 | #[macro_export]
137 | macro_rules! assert_dir {
138 | ($path:expr, $expected:expr $(,)?) => {
139 | match ($crate::list_children_names($path), $expected) {
140 | (actual, expected) => {
141 | eprintln!("CASE: {} => {}", stringify!($path), stringify!($expected));
142 | dbg!(&actual, &expected);
143 | assert_eq!(
144 | actual,
145 | expected,
146 | "{} => {}",
147 | stringify!($path),
148 | stringify!($expected),
149 | );
150 | }
151 | }
152 | };
153 | }
154 |
155 | /// Assert that content of a file is a certain text.
156 | #[macro_export]
157 | macro_rules! assert_file {
158 | ($path:expr, $expected:expr $(,)?) => {
159 | match ($crate::read_text_file($path), $expected) {
160 | (actual, expected) => {
161 | eprintln!("CASE: {} => {}", stringify!($path), stringify!($expected));
162 | dbg!(&actual, &expected);
163 | assert_eq!(
164 | actual,
165 | expected,
166 | "{} => {}",
167 | stringify!($path),
168 | stringify!($expected),
169 | );
170 | }
171 | }
172 | };
173 | }
174 |
175 | /// Test the structure of an actual filesystem tree
176 | pub fn test_sample_tree(root: &Path) {
177 | assert_dir!(root, string_set!("a", "b"));
178 | assert_dir!(root.join("a"), string_set!("abc", "def"));
179 | assert_dir!(root.join("a").join("abc"), string_set!());
180 | assert_file!(root.join("a").join("def"), "content of a/def");
181 | assert_dir!(root.join("b"), string_set!("foo"));
182 | assert_dir!(root.join("b").join("foo"), string_set!("bar"));
183 | assert_file!(
184 | root.join("b").join("foo").join("bar"),
185 | "content of b/foo/bar",
186 | );
187 | }
188 |
189 | /// Path to the `Cargo.toml` file at the root of the repo
190 | pub static WORKSPACE_MANIFEST: LazyLock = LazyLock::new(|| {
191 | env!("CARGO")
192 | .pipe(Command::new)
193 | .with_arg("locate-project")
194 | .with_arg("--workspace")
195 | .with_arg("--message-format=plain")
196 | .output()
197 | .expect("cargo locate-project")
198 | .stdout
199 | .pipe_as_ref(str::from_utf8)
200 | .expect("convert stdout to UTF-8")
201 | .trim()
202 | .pipe(PathBuf::from)
203 | });
204 |
205 | /// Content of the `rust-toolchain` file
206 | pub static RUST_TOOLCHAIN: LazyLock = LazyLock::new(|| {
207 | WORKSPACE_MANIFEST
208 | .parent()
209 | .expect("get workspace dir")
210 | .join("rust-toolchain")
211 | .pipe(read_to_string)
212 | .expect("read rust-toolchain")
213 | .trim()
214 | .pipe(Version::parse)
215 | .expect("parse rust-toolchain as semver")
216 | });
217 |
218 | /// Minimal supported rust version as defined by `src/Cargo.toml`
219 | pub static RUST_VERSION: LazyLock = LazyLock::new(|| {
220 | WORKSPACE_MANIFEST
221 | .parent()
222 | .expect("get workspace dir")
223 | .join("src")
224 | .join("Cargo.toml")
225 | .pipe(Manifest::from_path)
226 | .expect("load src/Cargo.toml")
227 | .package
228 | .expect("read package")
229 | .rust_version
230 | .expect("read rust_version")
231 | .get()
232 | .expect("read rust_version as string")
233 | .to_string()
234 | });
235 |
236 | #[cfg(test)]
237 | mod build;
238 | #[cfg(test)]
239 | mod completions;
240 | #[cfg(test)]
241 | mod macros;
242 | #[cfg(test)]
243 | mod program;
244 | #[cfg(test)]
245 | mod rust_version;
246 | #[cfg(test)]
247 | mod tree;
248 | #[cfg(test)]
249 | mod yaml;
250 |
--------------------------------------------------------------------------------
/test/macros.rs:
--------------------------------------------------------------------------------
1 | #![no_implicit_prelude]
2 |
3 | use crate::sample_tree;
4 | use ::build_fs_tree::{dir, file};
5 |
6 | macro_rules! test_case {
7 | ($name:ident, $key:ty, $value:ty) => {
8 | #[test]
9 | fn $name() {
10 | type Tree = ::build_fs_tree::FileSystemTree<$key, $value>;
11 | let actual: Tree = sample_tree!();
12 | let expected: Tree = sample_tree();
13 | ::pretty_assertions::assert_eq!(actual, expected);
14 | }
15 | };
16 | }
17 |
18 | test_case!(string_string, ::std::string::String, ::std::string::String);
19 | test_case!(str_slice_string, &'static str, ::std::string::String);
20 | test_case!(string_str_slice, ::std::string::String, &'static str);
21 | test_case!(str_slice_str_slice, &'static str, &'static str);
22 | test_case!(path_buf_str_slice, ::std::path::PathBuf, &'static str);
23 | test_case!(path_buf_u8_vec, ::std::path::PathBuf, ::std::vec::Vec);
24 |
25 | #[test]
26 | fn optional_commas() {
27 | type Tree = ::build_fs_tree::FileSystemTree<&'static str, &'static str>;
28 | let actual: Tree = dir! {
29 | "a" => file!("foo"),
30 | "b" => file!("bar"),
31 | };
32 | let expected: Tree = dir! {
33 | "a" => file!("foo")
34 | "b" => file!("bar")
35 | };
36 | ::pretty_assertions::assert_eq!(actual, expected);
37 | }
38 |
--------------------------------------------------------------------------------
/test/program.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "cli")]
2 | use crate::{test_sample_tree, Temp, SAMPLE_YAML, WORKSPACE_MANIFEST};
3 | use command_extra::CommandExtra;
4 | use pipe_trait::Pipe;
5 | use pretty_assertions::assert_eq;
6 | use std::{
7 | io::Write,
8 | process::{Command, Output, Stdio},
9 | };
10 |
11 | /// Name of the directory that stores all compilation artifacts.
12 | const TARGET_DIR: &str = if cfg!(debug_assertions) {
13 | "debug"
14 | } else {
15 | "release"
16 | };
17 |
18 | /// Run a subcommand of the main command.
19 | fn run_main_subcommand(
20 | working_directory: &Temp,
21 | command: &'static str,
22 | target: &'static str,
23 | input: &'static str,
24 | ) -> (bool, Option, String, String) {
25 | let mut child = WORKSPACE_MANIFEST
26 | .parent()
27 | .expect("get workspace dir")
28 | .join("target")
29 | .join(TARGET_DIR)
30 | .join("build-fs-tree")
31 | .pipe(Command::new)
32 | .with_stdin(Stdio::piped())
33 | .with_stdout(Stdio::piped())
34 | .with_stderr(Stdio::piped())
35 | .with_current_dir(working_directory.as_path())
36 | .with_arg(command)
37 | .with_arg(target)
38 | .spawn()
39 | .expect("spawn the main command");
40 | child
41 | .stdin
42 | .as_mut()
43 | .expect("get stdin")
44 | .write_all(input.as_bytes())
45 | .expect("write input to stdin");
46 | let Output {
47 | status,
48 | stdout,
49 | stderr,
50 | } = child
51 | .wait_with_output()
52 | .expect("get the output of the command");
53 | (
54 | status.success(),
55 | status.code(),
56 | String::from_utf8(stdout).expect("decode stdout as utf-8"),
57 | String::from_utf8(stderr).expect("decode stdout as utf-8"),
58 | )
59 | }
60 |
61 | #[test]
62 | fn create() {
63 | let working_directory = Temp::new_dir().expect("create temporary directory");
64 |
65 | eprintln!("FIRST RUN");
66 | let output = run_main_subcommand(&working_directory, "create", "TARGET", SAMPLE_YAML);
67 | assert_eq!(output, (true, Some(0), "".to_string(), "".to_string()));
68 | test_sample_tree(&working_directory.join("TARGET"));
69 |
70 | eprintln!("SECOND RUN");
71 | let output = run_main_subcommand(&working_directory, "create", "TARGET", SAMPLE_YAML);
72 | assert_eq!(
73 | output,
74 | (
75 | false,
76 | Some(1),
77 | "".to_string(),
78 | String::from(if cfg!(windows) {
79 | "create_dir \"TARGET\": Cannot create a file when that file already exists. (os error 183)\n"
80 | } else {
81 | "create_dir \"TARGET\": File exists (os error 17)\n"
82 | }),
83 | ),
84 | );
85 | test_sample_tree(&working_directory.join("TARGET"));
86 | }
87 |
88 | #[test]
89 | fn populate() {
90 | let working_directory = Temp::new_dir().expect("create temporary directory");
91 |
92 | eprintln!("FIRST RUN");
93 | let output = run_main_subcommand(&working_directory, "populate", "TARGET", SAMPLE_YAML);
94 | assert_eq!(output, (true, Some(0), "".to_string(), "".to_string()));
95 | test_sample_tree(&working_directory.join("TARGET"));
96 |
97 | eprintln!("SECOND RUN");
98 | let output = run_main_subcommand(&working_directory, "populate", "TARGET", SAMPLE_YAML);
99 | assert_eq!(output, (true, Some(0), "".to_string(), "".to_string()));
100 | test_sample_tree(&working_directory.join("TARGET"));
101 | }
102 |
--------------------------------------------------------------------------------
/test/rust_version.rs:
--------------------------------------------------------------------------------
1 | use crate::{RUST_TOOLCHAIN, RUST_VERSION};
2 | use pretty_assertions::assert_eq;
3 |
4 | #[test]
5 | fn rust_version_matches_rust_toolchain() {
6 | let rust_toolchain = &*RUST_TOOLCHAIN;
7 | dbg!(rust_toolchain);
8 | let rust_version = &*RUST_VERSION;
9 | dbg!(rust_version);
10 |
11 | let toolchain_without_patch = format!("{}.{}", rust_toolchain.major, rust_toolchain.minor);
12 | dbg!(&toolchain_without_patch);
13 |
14 | assert_eq!(&toolchain_without_patch, rust_version);
15 | }
16 |
--------------------------------------------------------------------------------
/test/tree.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | mod path;
3 | #[cfg(test)]
4 | mod path_mut;
5 |
--------------------------------------------------------------------------------
/test/tree/path.rs:
--------------------------------------------------------------------------------
1 | use crate::sample_tree;
2 | use build_fs_tree::FileSystemTree;
3 | use build_fs_tree::{dir, file};
4 | use pretty_assertions::assert_eq;
5 |
6 | type Tree = FileSystemTree<&'static str, &'static str>;
7 |
8 | macro_rules! test_case {
9 | ($name:ident, $path:expr, Some $expected:expr $(,)?) => {
10 | #[test]
11 | fn $name() {
12 | let actual_tree: Tree = sample_tree();
13 | let path = $path;
14 | let mut path_iter = path.iter();
15 | let actual = actual_tree.path(&mut path_iter);
16 | let expected_tree: Tree = $expected;
17 | let expected: Option<&Tree> = Some(&expected_tree);
18 | assert_eq!(actual, expected);
19 | }
20 | };
21 |
22 | ($name:ident, $path:expr, None $(,)?) => {
23 | #[test]
24 | fn $name() {
25 | let actual_tree: Tree = sample_tree();
26 | let path = $path;
27 | let mut path_iter = path.iter();
28 | let actual = actual_tree.path(&mut path_iter);
29 | let expected: Option<&Tree> = None;
30 | assert_eq!(actual, expected);
31 | }
32 | };
33 | }
34 |
35 | test_case!(empty_path, [], Some sample_tree());
36 | test_case!(to_a_dir, ["b", "foo"], Some dir! { "bar" => file!("content of b/foo/bar") });
37 | test_case!(to_an_empty_dir, ["a", "abc"], Some dir! {});
38 | test_case!(to_a_file, ["a", "def"], Some file!("content of a/def"));
39 | test_case!(to_nothing_1, ["a", "abc", "not exist"], None);
40 | test_case!(to_nothing_2, ["x"], None);
41 |
--------------------------------------------------------------------------------
/test/tree/path_mut.rs:
--------------------------------------------------------------------------------
1 | use crate::sample_tree;
2 | use build_fs_tree::FileSystemTree;
3 | use build_fs_tree::{dir, file};
4 | use pretty_assertions::assert_eq;
5 |
6 | type Tree = FileSystemTree<&'static str, &'static str>;
7 |
8 | #[test]
9 | fn mutate() {
10 | let mut tree: Tree = sample_tree();
11 | let path = ["a", "def"];
12 | let value = || -> Tree {
13 | dir! {
14 | "ghi" => file!("content of a/def/ghi")
15 | }
16 | };
17 | *tree.path_mut(&mut path.iter()).unwrap() = value();
18 | let expected: Tree = dir! {
19 | "a" => dir! {
20 | "abc" => dir! {}
21 | "def" => value()
22 | }
23 | "b" => dir! {
24 | "foo" => dir! {
25 | "bar" => file!("content of b/foo/bar")
26 | }
27 | }
28 | };
29 | assert_eq!(tree, expected);
30 | }
31 |
32 | #[test]
33 | fn to_nothing() {
34 | let mut tree: Tree = sample_tree();
35 | let path = ["a", "def", "not exist"];
36 | assert_eq!(tree.path_mut(&mut path.iter()), None);
37 | }
38 |
--------------------------------------------------------------------------------
/test/yaml.rs:
--------------------------------------------------------------------------------
1 | use crate::{sample_tree, SAMPLE_YAML};
2 | use build_fs_tree::{FileSystemTree, MergeableFileSystemTree};
3 | use pipe_trait::Pipe;
4 | use serde_yaml::{from_str, to_string, Value};
5 |
6 | type Tree = FileSystemTree;
7 | type MergeableTree = MergeableFileSystemTree;
8 |
9 | #[test]
10 | fn serialize() {
11 | let actual: Tree = from_str(SAMPLE_YAML).expect("parse YAML as FileSystemTree");
12 | let expected: Tree = sample_tree();
13 | dbg!(&actual, &expected);
14 | assert_eq!(actual, expected);
15 | }
16 |
17 | #[test]
18 | fn deserialize() {
19 | let actual = sample_tree()
20 | .pipe(|x: Tree| x)
21 | .pipe_ref(to_string)
22 | .expect("stringify a FileSystemTree as YAML");
23 | let expected = SAMPLE_YAML;
24 | eprintln!("\nACTUAL:\n{}\n", &actual);
25 | eprintln!("\nEXPECTED:\n{}\n", expected);
26 | macro_rules! parse {
27 | ($yaml:expr) => {
28 | from_str::($yaml).expect(concat!(
29 | "parse ",
30 | stringify!($yaml),
31 | " as serde_yaml::Value",
32 | ))
33 | };
34 | }
35 | assert_eq!(parse!(&actual), parse!(expected));
36 | }
37 |
38 | #[test]
39 | fn serialize_mergeable() {
40 | let actual: MergeableTree =
41 | from_str(SAMPLE_YAML).expect("parse YAML as MergeableFileSystemTree");
42 | let expected: MergeableTree = sample_tree().into();
43 | dbg!(&actual, &expected);
44 | assert_eq!(actual, expected);
45 | }
46 |
47 | #[test]
48 | fn deserialize_mergeable() {
49 | let actual = sample_tree()
50 | .pipe(MergeableTree::from)
51 | .pipe_ref(to_string)
52 | .expect("stringify a MergeableFileSystemTree as YAML");
53 | let expected = SAMPLE_YAML;
54 | eprintln!("\nACTUAL:\n{}\n", &actual);
55 | eprintln!("\nEXPECTED:\n{}\n", expected);
56 | macro_rules! parse {
57 | ($yaml:expr) => {
58 | from_str::($yaml).expect(concat!(
59 | "parse ",
60 | stringify!($yaml),
61 | " as serde_yaml::Value",
62 | ))
63 | };
64 | }
65 | assert_eq!(parse!(&actual), parse!(expected));
66 | }
67 |
--------------------------------------------------------------------------------