├── .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 | [![Test](https://github.com/KSXGitHub/build-fs-tree/workflows/Test/badge.svg)](https://github.com/KSXGitHub/build-fs-tree/actions?query=workflow%3ATest) 4 | [![Crates.io Version](https://img.shields.io/crates/v/build-fs-tree?logo=rust)](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 | [![Packaging Status](https://repology.org/badge/vertical-allrepos/build-fs-tree.svg)](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 | --------------------------------------------------------------------------------