├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release-plz.yml ├── .gitignore ├── .markdownlint.yaml ├── .mise.toml ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── release-plz.toml └── src ├── assign_helpers.rs ├── env_helpers.rs ├── file_helpers.rs ├── http_helpers.rs ├── json_helpers.rs ├── jsonnet_helpers.rs ├── lib.rs ├── outputs.rs ├── path_helpers.rs ├── regex_helpers.rs ├── region_helpers.rs ├── string_helpers.rs └── uuid_helpers.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | # Workflow files stored in the 9 | # default location of `.github/workflows` 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci-flow 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - "releases/*" 9 | # tags-ignore: 10 | # - "[0-9]+.[0-9]+.[0-9]+*" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | os: 25 | - "ubuntu-latest" 26 | - "macOS-latest" 27 | - "windows-latest" 28 | env: 29 | CARGO_TERM_COLOR: always 30 | RUST_BACKTRACE: full 31 | SCCACHE_GHA_ENABLED: "true" 32 | RUSTC_WRAPPER: "sccache" 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: dtolnay/rust-toolchain@stable 36 | - uses: mozilla-actions/sccache-action@v0.0.5 37 | - run: cargo clippy --workspace --all-features --no-deps --all-targets -- --deny warnings 38 | - run: cargo test --all-features 39 | - run: ${SCCACHE_PATH} --show-stats 40 | shell: bash 41 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: release-plz 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | 18 | jobs: 19 | release-plz: 20 | name: Release-plz 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Install Rust toolchain 28 | uses: dtolnay/rust-toolchain@stable 29 | - name: Run release-plz 30 | uses: MarcoIeni/release-plz-action@v0.5 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/rust,git,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=rust,git,visualstudiocode 4 | 5 | ### Git ### 6 | # Created by git for backups. To disable backups in Git: 7 | # $ git config --global mergetool.keepBackup false 8 | *.orig 9 | 10 | # Created by git when using merge tools for conflicts 11 | *.BACKUP.* 12 | *.BASE.* 13 | *.LOCAL.* 14 | *.REMOTE.* 15 | *_BACKUP_*.txt 16 | *_BASE_*.txt 17 | *_LOCAL_*.txt 18 | *_REMOTE_*.txt 19 | 20 | ### Rust ### 21 | # Generated by Cargo 22 | # will have compiled files and executables 23 | /target/ 24 | 25 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 26 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 27 | Cargo.lock 28 | 29 | # These are backup files generated by rustfmt 30 | **/*.rs.bk 31 | 32 | ### VisualStudioCode ### 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | 39 | ### VisualStudioCode Patch ### 40 | # Ignore all local history of files 41 | .history 42 | 43 | # End of https://www.gitignore.io/api/rust,git,visualstudiocode 44 | 45 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | MD013: false 2 | MD033: false 3 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rust = '1.80.0' 3 | # binstall = "1.10" 4 | 5 | # [plugins] 6 | # binstall = "https://github.com/davidB/asdf-cargo-binstall" 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "handlebars_misc_helpers" 3 | version = "0.17.0" 4 | authors = ["David Bernard"] 5 | edition = "2021" 6 | description = "A collection of helpers for handlebars (rust) to manage string, json, yaml, toml, path, file, http request." 7 | readme = "README.md" 8 | license = "CC0-1.0" 9 | keywords = ["handlebars"] 10 | #see https://crates.io/category_slugs 11 | categories = ["template-engine", "value-formatting"] 12 | repository = "https://github.com/davidB/handlebars_misc_helpers" 13 | homepage = "https://github.com/davidB/handlebars_misc_helpers" 14 | exclude = [ 15 | "/.github", 16 | "/.dependabot", 17 | "/docs/**", 18 | "/scripts", 19 | ".gitignore", 20 | "/tests/**", 21 | ] 22 | 23 | [dependencies] 24 | attohttpc = { version = "^0.28", optional = true, default-features = false, features = [ 25 | "compress", 26 | "tls-rustls-webpki-roots", 27 | ] } 28 | enquote = { version = "^1.0", optional = true } 29 | handlebars = "6" 30 | log = "^0.4" 31 | cruet = { version = "^0.14", optional = true } 32 | jmespath = { version = "^0.3", optional = true } 33 | jsonnet-rs = { version = "^0.17", optional = true } 34 | regex = { version = "^1.10", optional = true } 35 | reqwest = { version = "0.12", optional = true, default-features = false, features = [ 36 | "blocking", 37 | "rustls-tls", 38 | ] } 39 | serde = { version = "^1", features = ["rc"], optional = true } 40 | serde_json = { version = "^1", optional = true } 41 | serde_yaml = { version = "^0.9", optional = true } 42 | thiserror = "1.0" 43 | toml = { version = "^0.8", optional = true, features = ["preserve_order"] } 44 | uuid = { version = "^1.8", optional = true, features = ["v4", "v7"] } 45 | 46 | [dev-dependencies] 47 | tempfile = "3" 48 | pretty_assertions = "1" 49 | similar-asserts = "1" 50 | unindent = "0.2" 51 | 52 | [features] 53 | default = ["string", "http_attohttpc", "json", "jsonnet", "regex", "uuid"] 54 | http_attohttpc = ["dep:attohttpc"] 55 | http_reqwest = ["dep:reqwest"] 56 | json = [ 57 | "dep:jmespath", 58 | "dep:serde", 59 | "dep:serde_json", 60 | "dep:serde_yaml", 61 | "dep:toml", 62 | ] 63 | jsonnet = ["dep:jsonnet-rs"] 64 | jsontype = ["dep:serde_json"] 65 | regex = ["dep:regex"] 66 | string = ["dep:cruet", "dep:enquote", "jsontype"] 67 | uuid = ["dep:uuid"] 68 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | 48 | ii. moral rights retained by the original author(s) and/or performer(s); 49 | 50 | iii. publicity and privacy rights pertaining to a person's image or 51 | likeness depicted in a Work; 52 | 53 | iv. rights protecting against unfair competition in regards to a Work, 54 | subject to the limitations in paragraph 4(a), below; 55 | 56 | v. rights protecting the extraction, dissemination, use and reuse of data 57 | in a Work; 58 | 59 | vi. database rights (such as those arising under Directive 96/9/EC of the 60 | European Parliament and of the Council of 11 March 1996 on the legal 61 | protection of databases, and under any national implementation 62 | thereof, including any amended or successor version of such 63 | directive); and 64 | 65 | vii. other similar, equivalent or corresponding rights throughout the 66 | world based on applicable law or treaty, and any national 67 | implementations thereof. 68 | 69 | 2. Waiver. To the greatest extent permitted by, but not in contravention 70 | of, applicable law, Affirmer hereby overtly, fully, permanently, 71 | irrevocably and unconditionally waives, abandons, and surrenders all of 72 | Affirmer's Copyright and Related Rights and associated claims and causes 73 | of action, whether now known or unknown (including existing as well as 74 | future claims and causes of action), in the Work (i) in all territories 75 | worldwide, (ii) for the maximum duration provided by applicable law or 76 | treaty (including future time extensions), (iii) in any current or future 77 | medium and for any number of copies, and (iv) for any purpose whatsoever, 78 | including without limitation commercial, advertising or promotional 79 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 80 | member of the public at large and to the detriment of Affirmer's heirs and 81 | successors, fully intending that such Waiver shall not be subject to 82 | revocation, rescission, cancellation, termination, or any other legal or 83 | equitable action to disrupt the quiet enjoyment of the Work by the public 84 | as contemplated by Affirmer's express Statement of Purpose. 85 | 86 | 3. Public License Fallback. Should any part of the Waiver for any reason 87 | be judged legally invalid or ineffective under applicable law, then the 88 | Waiver shall be preserved to the maximum extent permitted taking into 89 | account Affirmer's express Statement of Purpose. In addition, to the 90 | extent the Waiver is so judged Affirmer hereby grants to each affected 91 | person a royalty-free, non transferable, non sublicensable, non exclusive, 92 | irrevocable and unconditional license to exercise Affirmer's Copyright and 93 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 94 | maximum duration provided by applicable law or treaty (including future 95 | time extensions), (iii) in any current or future medium and for any number 96 | of copies, and (iv) for any purpose whatsoever, including without 97 | limitation commercial, advertising or promotional purposes (the 98 | "License"). The License shall be deemed effective as of the date CC0 was 99 | applied by Affirmer to the Work. Should any part of the License for any 100 | reason be judged legally invalid or ineffective under applicable law, such 101 | partial invalidity or ineffectiveness shall not invalidate the remainder 102 | of the License, and in such case Affirmer hereby affirms that he or she 103 | will not (i) exercise any of his or her remaining Copyright and Related 104 | Rights in the Work or (ii) assert any associated claims and causes of 105 | action with respect to the Work, in either case contrary to Affirmer's 106 | express Statement of Purpose. 107 | 108 | 4. Limitations and Disclaimers. 109 | 110 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 111 | surrendered, licensed or otherwise affected by this document. 112 | 113 | b. Affirmer offers the Work as-is and makes no representations or 114 | warranties of any kind concerning the Work, express, implied, 115 | statutory or otherwise, including without limitation warranties of 116 | title, merchantability, fitness for a particular purpose, non 117 | infringement, or the absence of latent or other defects, accuracy, or 118 | the present or absence of errors, whether or not discoverable, all to 119 | the greatest extent permissible under applicable law. 120 | 121 | c. Affirmer disclaims responsibility for clearing rights of other persons 122 | that may apply to the Work or any use thereof, including without 123 | limitation any person's Copyright and Related Rights in the Work. 124 | Further, Affirmer disclaims responsibility for obtaining any necessary 125 | consents, permissions or other rights required for any use of the 126 | Work. 127 | 128 | d. Affirmer understands and acknowledges that Creative Commons is not a 129 | party to this document and has no duty or obligation with respect to 130 | this CC0 or use of the Work. 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # handlebars_misc_helpers 2 | 3 | [![crates license](https://img.shields.io/crates/l/handlebars_misc_helpers.svg)](http://creativecommons.org/publicdomain/zero/1.0/) 4 | [![crate version](https://img.shields.io/crates/v/handlebars_misc_helpers.svg)](https://crates.io/crates/handlebars_misc_helpers) 5 | [![Documentation](https://docs.rs/handlebars_misc_helpers/badge.svg)](https://docs.rs/handlebars_misc_helpers/) 6 | 7 | [![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip) 8 | [![Actions Status](https://github.com/davidB/handlebars_misc_helpers/workflows/ci-flow/badge.svg)](https://github.com/davidB/handlebars_misc_helpers/actions) 9 | [![test coverage](https://codecov.io/gh/davidB/handlebars_misc_helpers/branch/master/graph/badge.svg)](https://codecov.io/gh/davidB/handlebars_misc_helpers) 10 | 11 | A collection of helpers for handlebars (rust) to manage String, JSON, YAML, TOML, path, file, and HTTP requests. 12 | 13 | Helpers extend the template to generate or transform content. 14 | Few helpers are included, but if you need more helpers, ask via an issue or a PR. 15 | 16 | To use a helper: 17 | 18 | ```handlebars 19 | // arguments are space separated 20 | {{ helper_name arguments}} 21 | ``` 22 | 23 | To chain helpers, use parenthesis: 24 | 25 | ```handlebars 26 | {{ to_upper_case (to_singular "Hello foo-bars") }} 27 | // -> BAR 28 | 29 | {{ first_non_empty (unquote (json_str_query "package.edition" (read_to_str "Cargo.toml") format="toml")) (env_var "MY_VERSION") "foo" }} 30 | // -> 2018 31 | ``` 32 | 33 | see [Handlebars templating language](https://handlebarsjs.com/) 34 | 35 | To not "import" useless dependencies, use the crate's features: 36 | 37 | ```toml 38 | default = ["string", "http_attohttpc", "json", "jsonnet", "regex", "uuid"] 39 | http_attohttpc = ["dep:attohttpc"] 40 | http_reqwest = ["dep:reqwest"] 41 | json = [ 42 | "dep:jmespath", 43 | "dep:serde", 44 | "dep:serde_json", 45 | "dep:serde_yaml", 46 | "dep:toml", 47 | ] 48 | jsonnet = ["dep:jsonnet-rs"] 49 | jsontype = ["dep:serde_json"] 50 | regex = ["dep:regex"] 51 | string = ["dep:cruet", "dep:enquote", "jsontype"] 52 | uuid = ["dep:uuid"] 53 | ``` 54 | 55 | 56 | 57 | - [String transformation](#string-transformation) 58 | - [Regular expression](#regular-expression) 59 | - [UUID](#uuid) 60 | - [HTTP content](#http-content) 61 | - [Path extraction](#path-extraction) 62 | - [File](#file) 63 | - [Environment variable](#environment-variable) 64 | - [JSON \& YAML \& TOML](#json--yaml--toml) 65 | - [Helpers](#helpers) 66 | - [Blocks](#blocks) 67 | - [Edition via Jsonnet](#edition-via-jsonnet) 68 | - [Assign, set](#assign-set) 69 | - [Replace section](#replace-section) 70 | 71 | 72 | 73 | ## String transformation 74 | 75 | | helper signature | usage sample | sample out | 76 | | ---------------------------------------- | ------------------------------------------ | --------------------- | 77 | | `replace s:String from:String to:String` | `replace "Hello old" "old" "new"` | `"Hello new"` | 78 | | `to_lower_case s:String` | `to_lower_case "Hello foo-bars"` | `"hello foo-bars"` | 79 | | `to_upper_case s:String` | `to_upper_case "Hello foo-bars"` | `"HELLO FOO-BARS"` | 80 | | `to_camel_case s:String` | `to_camel_case "Hello foo-bars"` | `"helloFooBars"` | 81 | | `to_pascal_case s:String` | `to_pascal_case "Hello foo-bars"` | `"HelloFooBars"` | 82 | | `to_snake_case s:String` | `to_snake_case "Hello foo-bars"` | `"hello_foo_bars"` | 83 | | `to_screaming_snake_case s:String` | `to_screaming_snake_case "Hello foo-bars"` | `"HELLO_FOO_BARS"` | 84 | | `to_kebab_case s:String` | `to_kebab_case "Hello foo-bars"` | `"hello-foo-bars"` | 85 | | `to_train_case s:String` | `to_train_case "Hello foo-bars"` | `"Hello-Foo-Bars"` | 86 | | `to_sentence_case s:String` | `to_sentence_case "Hello foo-bars"` | `"Hello foo" bars` | 87 | | `to_title_case s:String` | `to_title_case "Hello foo-bars"` | `"Hello Foo Bars"` | 88 | | `to_class_case s:String` | `to_class_case "Hello foo-bars"` | `"HelloFooBar"` | 89 | | `to_table_case s:String` | `to_table_case "Hello foo-bars"` | `"hello_foo_bars"` | 90 | | `to_plural s:String` | `to_plural "Hello foo-bars"` | `"bars"` | 91 | | `to_singular s:String` | `to_singular "Hello foo-bars"` | `"bar"` | 92 | | `to_foreign_key s:String` | `to_foreign_key "Hello foo-bars"` | `"hello_foo_bars_id"` | 93 | | `demodulize s:String` | `demodulize "Test::Foo::Bar"` | `"Bar"` | 94 | | `ordinalize s:String+` | `ordinalize "9"` | `"9th"` | 95 | | `deordinalize s:String+` | `deordinalize "9th"` | `"9"` | 96 | | `trim s:String` | `trim " foo "` | `"foo"` | 97 | | `trim_start s:String` | `trim_start " foo "` | `"foo "` | 98 | | `trim_end s:String` | `trim_end " foo "` | `" foo"` | 99 | | `unquote s:String` | `unquote "\"foo\""` | `"foo"` | 100 | | `enquote symbol:String s:String` | `enquote "" "foo"` | `"\"foo\""` | 101 | | `first_non_empty s:String+` | `first_non_empty "" null "foo" "bar"` | `"foo"` | 102 | 103 | ## Regular expression 104 | 105 | | usage | output | 106 | | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | 107 | | `{{ regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}` | `true` | 108 | | `{{#if (regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}ok{{/if}}` | `ok` | 109 | | `{{ regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}` | `[object]` | 110 | | `{{ json_to_str( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}` | `{"_0":"today","_1":"t","_2":"o","_3":"y","first":"t","last":"y"}` | 111 | | `{{ set captures=( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}{{ captures.last }}` | `y` | 112 | 113 | ## UUID 114 | 115 | | usage | output | 116 | | ------------------------- | -------------------------------------- | 117 | | `{{ uuid_new_v4 }}` | `6db4d8a7-8117-4b72-9dbc-988e6ee2a6e3` | 118 | | `{{ len (uuid_new_v4) }}` | `36` | 119 | | `{{ uuid_new_v7 }}` | `94d7bb75-9b16-40dd-878d-5fbb37b8ae2c` | 120 | | `{{ len (uuid_new_v7) }}` | `36` | 121 | 122 | ## HTTP content 123 | 124 | The helpers can render the body's response from an HTTP request. 125 | 126 | | helper signature | usage sample | 127 | | ------------------------------- | -------------------------------------- | 128 | | `http_get url:String` | `http_get "http://hello/..."` | 129 | | `gitignore_io templates:String` | `gitignore_io "rust,visualstudiocode"` | 130 | 131 | ## Path extraction 132 | 133 | Helper able to extract (or transform) path (defined as string). 134 | 135 | for the same input: `"/hello/bar/foo.txt"` 136 | 137 | | helper_name | sample output | 138 | | ----------- | -------------- | 139 | | file_name | `"foo.txt"` | 140 | | parent | `"/hello/bar"` | 141 | | extension | `"txt"` | 142 | 143 | ## File 144 | 145 | Helper to read file content. 146 | 147 | | usage | output | 148 | | ----------------------------------------- | -------------------------- | 149 | | `{{ read_to_str "/foo/bar" }}` | content of file `/foo/bar` | 150 | | `{{ read_to_str "file/does/not/exist" }}` | empty string | 151 | 152 | ## Environment variable 153 | 154 | The helper can get environment variables. 155 | 156 | | helper_name | usage | 157 | | ----------- | ---------------- | 158 | | env_var | `env_var "HOME"` | 159 | 160 | Some special environment variables are predefined (some of them come from [`std::env::consts` - Rust](https://doc.rust-lang.org/std/env/consts/index.html)): 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 182 | 187 | 191 | 196 | 200 | 206 | 210 | 222 | 223 | 224 | 230 | 231 | 232 |
variablepossible values
"ARCH"
    171 |
  • x86
  • 172 |
  • x86_64
  • 173 |
  • arm
  • 174 |
  • aarch64
  • 175 |
  • mips
  • 176 |
  • mips64
  • 177 |
  • powerpc
  • 178 |
  • powerpc64
  • 179 |
  • s390x
  • 180 |
  • sparc64
  • 181 |
"DLL_EXTENSION"
    183 |
  • so
  • 184 |
  • dylib
  • 185 |
  • dll
  • 186 |
"DLL_PREFIX"
    188 |
  • lib
  • 189 |
  • "" (an empty string)
  • 190 |
"DLL_SUFFIX"
    192 |
  • .so
  • 193 |
  • .dylib
  • 194 |
  • .dll
  • 195 |
"EXE_EXTENSION"
    197 |
  • exe
  • 198 |
  • "" (an empty string)
  • 199 |
"EXE_SUFFIX"
    201 |
  • .exe
  • 202 |
  • .nexe
  • 203 |
  • .pexe
  • 204 |
  • "" (an empty string)
  • 205 |
"FAMILY"
    207 |
  • unix
  • 208 |
  • windows
  • 209 |
"OS"
    211 |
  • linux
  • 212 |
  • macos
  • 213 |
  • ios
  • 214 |
  • freebsd
  • 215 |
  • dragonfly
  • 216 |
  • netbsd
  • 217 |
  • openbsd
  • 218 |
  • solaris
  • 219 |
  • android
  • 220 |
  • windows
  • 221 |
"USERNAME"try to find the current username, in the order:
    225 |
  1. environment variable "USERNAME"
  2. 226 |
  3. environment variable "username"
  4. 227 |
  5. environment variable "USER"
  6. 228 |
  7. environment variable "user"
  8. 229 |
233 | 234 | ## JSON & YAML & TOML 235 | 236 | ### Helpers 237 | 238 | - `json_query query:String data:Json`: Helper able to extract information from JSON using [JMESPath](http://jmespath.org/) syntax for `query`. 239 | - `json_str_query query:String data:String`: Helper able to extract information from JSON using [JMESPath](http://jmespath.org/) syntax for `query`, data follows the requested `format`. 240 | - `json_to_str data:Json`: convert JSON data into a string following the requested `format`. 241 | - `str_to_json data:String`: convert(parse) a string into a JSON following the requested `format`. 242 | 243 | The optional requested `format`, is the format of the string with data: 244 | 245 | - `"json"` (default if omitted) 246 | - `"json_pretty"` JSON with indentation,... 247 | - `"yaml"` 248 | - `"toml"` 249 | - `"toml_pretty"` 250 | 251 | | usage | output | 252 | | -------------------------------------------------------------------------------------------------- | ------------------------------- | 253 | | `{{ json_query "foo" {} }}` | `` | 254 | | `{{ json_to_str ( json_query "foo" {"foo":{"bar":{"baz":true}}} ) }}` | `{"bar":{"baz":true}}` | 255 | | `{{ json_to_str ( json_query "foo" (str_to_json "{\"foo\":{\"bar\":{\"baz\":true}}}" ) ) }}` | `{"bar":{"baz":true}}` | 256 | | `{{ json_str_query "foo" "{\"foo\":{\"bar\":{\"baz\":true}}}" }}` | `{"bar":{"baz":true}}` | 257 | | `{{ json_str_query "foo.bar.baz" "{\"foo\":{\"bar\":{\"baz\":true}}}" }}` | `true` | 258 | | `{{ json_str_query "foo" "foo:\n bar:\n baz: true\n" format="yaml"}}` | `bar:\n baz: true\n` | 259 | | `{{ json_to_str ( str_to_json "{\"foo\":{\"bar\":{\"baz\":true}}}" format="json") format="yaml"}}` | `foo:\n bar:\n baz: true\n` | 260 | 261 | ### Blocks 262 | 263 | 264 | 265 | 270 | 275 | 276 | 277 | 283 | 291 | 292 | 293 | 297 | 300 | 301 | 302 | 308 | 315 | 316 |
{{#from_json format="toml"}}
266 | {"foo": {"hello":"1.2.3", "bar":{"baz":true} } }
267 | {{/from_json}}
268 | 
269 |
[foo]
271 | hello = "1.2.3"
272 | 
273 | [foo.bar]
274 | baz = true
{{#to_json format="toml"}}
278 | [foo]
279 | bar = { baz = true }
280 | hello = "1.2.3"
281 | {{/to_json}}
282 |
{
284 |   "foo": {
285 |     "bar": {
286 |       "baz": true
287 |     },
288 |     "hello": "1.2.3"
289 |   }
290 | }
{{#from_json format="yaml"}}
294 | {"foo":{"bar":{"baz":true}}}
295 | {{/from_json}}
296 |
foo:
298 |   bar:
299 |     baz: true
{{#to_json format="yaml"}}
303 | foo:
304 |     bar:
305 |         baz: true
306 | {{/to_json}}
307 |
{
309 |   "foo": {
310 |     "bar": {
311 |       "baz": true
312 |     }
313 |   }
314 | }
317 | 318 | Note: YAML & TOML content are used as input and output formats for JSON data. So capabilities are limited to what JSON support (eg. no date-time type like in TOML). 319 | 320 | ### Edition via Jsonnet 321 | 322 | For a more advanced edition of JSON, you can use Jsonnet. 323 | 324 | > A data templating language for app and tool developers 325 | 326 | - See the doc of [jsonnet](https://jsonnet.org/learning/tutorial.html) for more samples, and syntax info,... 327 | - This block can be combined with conversion helper/block for YAML & TOML to provide edition capabilities for those format 328 | - the output should be a valid JSON, except if `string_output = false` is set as a parameter of the block. 329 | 330 | 331 | 332 | 343 | 351 | 352 |
{{#jsonnet}}
333 | local v = {"foo":{"bar":{"baz":false}}};
334 | v {
335 |   "foo" +: {
336 |       "bar" +: {
337 |           "baz2": true
338 |       }
339 |   }
340 | }
341 | {{/jsonnet}}
342 |
{
344 |   "foo": {
345 |       "bar": {
346 |           "baz": false,
347 |           "baz2": true
348 |       }
349 |   }
350 | }
353 | 354 | ## Assign, set 355 | 356 | The helpers can assign a variable to use later in the template. 357 | 358 | ⚠️ `assign` is deprecated and replaced by `set` (more compact and allows multiple assignments in one call) 359 | 360 | | usage | output | 361 | | ----------------------------------------------------------------- | --------------- | 362 | | `{{ assign "foo" "hello world" }}{{ foo }}` | `hello world` | 363 | | `{{ set foo="{}" }}` | `` | 364 | | `{{ set foo="{}" }}{{ foo }}` | `{}` | 365 | | `{{ set foo="hello world" }}{{ foo }}` | `hello world` | 366 | | `{{ set foo={} }}{{ foo }}` | `[object]` | 367 | | `{{ set foo={"bar": 33} }}{{ foo }}` | `[object]` | 368 | | `{{ set foo={"bar": 33} }}{{ foo.bar }}` | `33` | 369 | | `{{ set foo="world" bar="hello" }}>{{ bar }} {{ foo }}<` | `>hello world<` | 370 | | `{{ set foo="world" }}{{ set bar="hello" }}>{{ bar }} {{ foo }}<` | `>hello world<` | 371 | 372 | ## Replace section 373 | 374 | This helper can replace a section delimited by a boundary. 375 | 376 | For example with this template: 377 | 378 | ```handlebars 379 | {{~#replace_section begin="" end="" content }} 380 | This is the new content of the block 381 | {{~/replace_section}} 382 | ``` 383 | 384 | The `content` having 385 | 386 | ```html 387 | 388 | 389 | 390 | 391 | 392 | 393 | Document 394 | 395 | 396 | 397 | Something by default 398 | 399 | 400 | 401 | ``` 402 | 403 | The content between `` and `` is replaced by the result of the inner template: 404 | 405 | ```html 406 | 407 | 408 | 409 | 410 | 411 | 412 | Document 413 | 414 | 415 | This is the new content of the block 416 | 417 | 418 | ``` 419 | 420 | Note: you can remove the boundary by adding `remove_boundaries=true`. 421 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | # [Configuration | Release-plz](https://release-plz.ieni.dev/docs/config) 2 | # [workspace] 3 | # features_always_increment_minor = true 4 | 5 | [changelog] 6 | sort_commits = "newest" 7 | commit_preprocessors = [ 8 | # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, # remove issue numbers from commits 9 | # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers 10 | ] 11 | # regex for parsing and grouping commits 12 | # try to follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) 13 | commit_parsers = [ 14 | { message = "^(🔒️|🔐)", group = "Security" }, 15 | { body = ".*security", group = "Security" }, 16 | { message = "^(fix|🐛|🚑️|👽️)", group = "Fixed" }, 17 | { message = "^(test|✅)", group = "Fixed", skip = true }, 18 | { message = "^.*: add", group = "Added" }, 19 | { message = "^.*: support", group = "Added" }, 20 | { message = "^(feat|✨|💥)", group = "Added" }, 21 | { message = "^.*: remove", group = "Removed" }, 22 | { message = "^.*: delete", group = "Removed" }, 23 | { message = "^(style|💄)", group = "Changed" }, 24 | { message = "^(doc|✏️|📝)", group = "Changed" }, 25 | { message = "^(perf|⚡️)", group = "Changed" }, 26 | { message = "^(chore|ci|💚|👷|🚧)", group = "Changed", skip = true }, 27 | { message = "^revert", group = "Changed" }, 28 | { message = "^(chore\\(deps\\)|⬇️|⬆️|➕|➖)", group = "Changed" }, 29 | { message = "^(refactor|🎨|🔥|♻️)", group = "Refactor", skip = true }, 30 | { message = "^(chore\\(release\\): prepare for|🔖|🚀)", skip = true }, 31 | { message = "^chore\\(pr\\)", skip = true }, 32 | { message = "^chore\\(pull\\)", skip = true }, 33 | ] 34 | -------------------------------------------------------------------------------- /src/assign_helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{ 2 | Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderErrorReason, 3 | }; 4 | 5 | fn assign_fct( 6 | h: &Helper, 7 | _: &Handlebars, 8 | ctx: &Context, 9 | rc: &mut RenderContext, 10 | _: &mut dyn Output, 11 | ) -> HelperResult { 12 | // get parameter from helper or throw an error 13 | let name = h 14 | .param(0) 15 | .and_then(|v| v.value().as_str()) 16 | .ok_or(RenderErrorReason::ParamNotFoundForIndex("assign", 0))?; 17 | let value = h 18 | .param(1) 19 | .map(|v| v.value()) 20 | .cloned() 21 | .ok_or(RenderErrorReason::ParamNotFoundForIndex("assign", 1))?; 22 | let mut ctx = rc.context().as_deref().unwrap_or(ctx).clone(); 23 | if let Some(ref mut m) = ctx.data_mut().as_object_mut() { 24 | m.insert(name.to_owned(), value); 25 | } 26 | rc.set_context(ctx); 27 | Ok(()) 28 | } 29 | 30 | fn set_fct( 31 | h: &Helper, 32 | _: &Handlebars, 33 | ctx: &Context, 34 | rc: &mut RenderContext, 35 | _: &mut dyn Output, 36 | ) -> HelperResult { 37 | let mut ctx = rc.context().as_deref().unwrap_or(ctx).clone(); 38 | if let Some(ref mut m) = ctx.data_mut().as_object_mut() { 39 | for (k, v) in h.hash() { 40 | m.insert(k.to_string(), v.value().clone()); 41 | } 42 | } 43 | rc.set_context(ctx); 44 | Ok(()) 45 | } 46 | 47 | pub fn register(handlebars: &mut Handlebars) { 48 | handlebars.register_helper("assign", Box::new(assign_fct)); 49 | handlebars.register_helper("set", Box::new(set_fct)); 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use crate::assert_renders; 55 | use std::error::Error; 56 | 57 | #[test] 58 | fn test_helper_assign() -> Result<(), Box> { 59 | assert_renders![ 60 | (r##"{{ assign "foo" "{}" }}"##, r##""##), 61 | (r##"{{ assign "foo" "{}" }}{{ foo }}"##, r##"{}"##), 62 | (r##"{{ assign "foo" {} }}{{ foo }}"##, r##"[object]"##), 63 | ( 64 | r##"{{ assign "foo" {"bar": 33} }}{{ foo }}"##, 65 | r##"[object]"##, 66 | ), 67 | ( 68 | r##"{{ assign "foo" "hello world" }}{{ foo }}"##, 69 | r##"hello world"##, 70 | ), 71 | ( 72 | r##"{{ assign "foo" "world" }}{{ assign "bar" "hello" }}>{{ bar }} {{ foo }}<"##, 73 | r##">hello world<"##, 74 | ) 75 | ] 76 | } 77 | 78 | #[test] 79 | fn test_helper_set() -> Result<(), Box> { 80 | assert_renders![ 81 | (r##"{{ set foo="{}" }}"##, r##""##), 82 | (r##"{{ set foo="{}" }}{{ foo }}"##, r##"{}"##), 83 | (r##"{{ set foo={} }}{{ foo }}"##, r##"[object]"##), 84 | (r##"{{ set foo={"bar": 33} }}{{ foo }}"##, r##"[object]"##,), 85 | ( 86 | r##"{{ set foo={"bar": 33} }}{{ json_to_str foo }}"##, 87 | r##"{"bar":33}"##, 88 | ), 89 | (r##"{{ set foo={"bar": 33} }}{{ foo.bar }}"##, r##"33"##,), 90 | ( 91 | r##"{{ set foo="hello world" }}{{ foo }}"##, 92 | r##"hello world"##, 93 | ), 94 | ( 95 | r##"{{ set foo="world" bar="hello" }}>{{ bar }} {{ foo }}<"##, 96 | r##">hello world<"##, 97 | ), 98 | ( 99 | r##"{{ set foo="world" }}{{ set bar="hello" }}>{{ bar }} {{ foo }}<"##, 100 | r##">hello world<"##, 101 | ), 102 | (r##"{{ set foo=(eq 12 12) }}{{ foo }}"##, r##"true"##,) 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/env_helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{handlebars_helper, Handlebars}; 2 | 3 | fn env_var_fct>(key: T) -> String { 4 | let key = key.as_ref(); 5 | match key { 6 | "ARCH" => std::env::consts::ARCH.to_owned(), 7 | "DLL_EXTENSION" => std::env::consts::DLL_EXTENSION.to_owned(), 8 | "DLL_PREFIX" => std::env::consts::DLL_PREFIX.to_owned(), 9 | "DLL_SUFFIX" => std::env::consts::DLL_SUFFIX.to_owned(), 10 | "EXE_EXTENSION" => std::env::consts::EXE_EXTENSION.to_owned(), 11 | "EXE_SUFFIX" => std::env::consts::EXE_SUFFIX.to_owned(), 12 | "FAMILY" => std::env::consts::FAMILY.to_owned(), 13 | "OS" => std::env::consts::OS.to_owned(), 14 | "USERNAME" => std::env::var("USERNAME") 15 | .or_else(|_| std::env::var("username")) 16 | .or_else(|_| std::env::var("USER")) 17 | .or_else(|_| std::env::var("user")) 18 | .unwrap_or_else(|_| "noname".to_owned()), 19 | _ => { 20 | match std::env::var(key) { 21 | Ok(s) => s, 22 | Err(e) => { 23 | //TODO better error handler 24 | log::info!( 25 | "helper: env_var failed for key '{:?}' with error '{:?}'", 26 | key, 27 | e 28 | ); 29 | "".to_owned() 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | pub fn register(handlebars: &mut Handlebars) { 37 | handlebars_helper!(env_var: |v: str| env_var_fct(v)); 38 | handlebars.register_helper("env_var", Box::new(env_var)) 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use crate::tests::assert_helpers; 44 | use std::error::Error; 45 | 46 | #[test] 47 | fn test_register_env_helpers() -> Result<(), Box> { 48 | let key = "KEY"; 49 | std::env::set_var(key, "VALUE"); 50 | 51 | assert_helpers(key, vec![("env_var", "VALUE")])?; 52 | assert_helpers("A_DO_NOT_EXIST_ENVVAR", vec![("env_var", "")])?; 53 | Ok(()) 54 | } 55 | 56 | #[test] 57 | fn test_env_consts() -> Result<(), Box> { 58 | let key = "OS"; 59 | let os = std::env::consts::OS; 60 | assert_ne!(os, ""); 61 | assert_helpers(key, vec![("env_var", os)])?; 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/file_helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{handlebars_helper, Handlebars}; 2 | use std::path::Path; 3 | 4 | pub fn register(handlebars: &mut Handlebars) { 5 | handlebars_helper!(read_to_str: |v: str| { 6 | let p = Path::new(v); 7 | if p.exists() { 8 | std::fs::read_to_string(p)? 9 | } else { 10 | log::warn!( 11 | "helper: read_to_str failed for non existing path path '{:?}'", 12 | v 13 | ); 14 | "".to_owned() 15 | } 16 | }); 17 | handlebars.register_helper("read_to_str", Box::new(read_to_str)) 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use crate::assert_renders; 23 | use std::error::Error; 24 | use std::io::Write; 25 | use tempfile::NamedTempFile; 26 | 27 | #[test] 28 | fn test_read_to_str() -> Result<(), Box> { 29 | // Create a file inside of `std::env::temp_dir()`. 30 | let file_content = "Brian was here. Briefly."; 31 | let mut file = NamedTempFile::new()?; 32 | write!(file, "{}", file_content)?; 33 | assert_renders![ 34 | (r##"{{ read_to_str "" }}"##, ""), 35 | (r##"{{ read_to_str "/file/not/exists" }}"##, ""), 36 | ( 37 | &format!("{{{{ read_to_str {:?} }}}}", file.path()), 38 | file_content 39 | ) 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/http_helpers.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http_attohttpc")] 2 | use attohttpc; 3 | use handlebars::{handlebars_helper, Handlebars}; 4 | #[cfg(all(feature = "http_reqwest", not(feature = "http_attohttpc")))] 5 | use reqwest; 6 | 7 | #[cfg(feature = "http_attohttpc")] 8 | fn http_get_fct>(url: T) -> Result { 9 | attohttpc::get(url.as_ref()).send()?.text() 10 | } 11 | 12 | #[cfg(all(feature = "http_reqwest", not(feature = "http_attohttpc")))] 13 | fn http_get_fct>(url: T) -> Result { 14 | reqwest::blocking::get(url.as_ref())?.text() 15 | } 16 | 17 | #[cfg(any(feature = "http_reqwest", feature = "http_attohttpc"))] 18 | pub fn register(handlebars: &mut Handlebars) { 19 | { 20 | handlebars_helper!(http_get: |v: str| http_get_fct(v).map_err(crate::to_nested_error)?); 21 | handlebars.register_helper("http_get", Box::new(http_get)) 22 | } 23 | { 24 | handlebars_helper!(gitignore_io: |v: str| http_get_fct(format!("https://www.gitignore.io/api/{}", v)).map_err(crate::to_nested_error)?); 25 | handlebars.register_helper("gitignore_io", Box::new(gitignore_io)) 26 | } 27 | } 28 | 29 | // #[cfg(test)] 30 | // mod tests { 31 | // use super::*; 32 | // // use crate::tests::assert_renders; 33 | // use pretty_assertions::assert_eq; 34 | // use std::error::Error; 35 | 36 | // #[test] 37 | // fn try_http_get_fct() -> Result<(), Box> { 38 | // assert_eq!(http_get_fct("https://www.gitignore.io/api/text")?, 39 | // r#" 40 | // # Created by https://www.toptal.com/developers/gitignore/api/text 41 | // # Edit at https://www.toptal.com/developers/gitignore?templates=text 42 | 43 | // ### Text ### 44 | // *.doc 45 | // *.docx 46 | // *.log 47 | // *.msg 48 | // *.pages 49 | // *.rtf 50 | // *.txt 51 | // *.wpd 52 | // *.wps 53 | 54 | // # End of https://www.toptal.com/developers/gitignore/api/text 55 | // "# 56 | // .to_string(), 57 | // ); 58 | // Ok(()) 59 | // } 60 | // } 61 | -------------------------------------------------------------------------------- /src/json_helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::outputs::StringOutput; 2 | use handlebars::{ 3 | handlebars_helper, Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, 4 | RenderError, Renderable, ScopedJson, 5 | }; 6 | use jmespath; 7 | use serde::Serialize; 8 | use serde_json::Value as Json; 9 | use std::str::FromStr; 10 | use thiserror::Error; 11 | use toml::value::Table; 12 | 13 | type TablePartition = Vec<(String, toml::Value)>; 14 | 15 | #[derive(Debug, Error)] 16 | enum JsonError { 17 | #[error("query failure for expression '{expression}'")] 18 | JsonQueryFailure { 19 | expression: String, 20 | source: jmespath::JmespathError, 21 | }, 22 | #[error("fail to convert '{input}'")] 23 | ToJsonValueError { 24 | input: String, 25 | source: serde_json::error::Error, 26 | }, 27 | #[error("data format unknown '{format}'")] 28 | DataFormatUnknown { format: String }, 29 | } 30 | 31 | #[derive(Debug, Clone)] 32 | enum DataFormat { 33 | Json, 34 | JsonPretty, 35 | Yaml, 36 | Toml, 37 | TomlPretty, 38 | } 39 | 40 | impl FromStr for DataFormat { 41 | type Err = JsonError; 42 | 43 | fn from_str(s: &str) -> Result { 44 | match s.to_lowercase().as_str() { 45 | "json" => Ok(Self::Json), 46 | "json_pretty" => Ok(Self::JsonPretty), 47 | "yaml" => Ok(Self::Yaml), 48 | "toml" => Ok(Self::Toml), 49 | "toml_pretty" => Ok(Self::TomlPretty), 50 | _ => Err(JsonError::DataFormatUnknown { 51 | format: s.to_string(), 52 | }), 53 | } 54 | } 55 | } 56 | 57 | fn to_opt_res(v: Result, E>) -> Option> { 58 | match v { 59 | Err(e) => Some(Err(e)), 60 | Ok(v) => v.map(Ok), 61 | } 62 | } 63 | 64 | // HACK for toml because 65 | // see: 66 | // - [ValueAfterTable error · Issue #336 · alexcrichton/toml-rs](https://github.com/alexcrichton/toml-rs/issues/336) 67 | // - [ValueAfterTable fix by PSeitz · Pull Request #339 · alexcrichton/toml-rs](https://github.com/alexcrichton/toml-rs/pull/339) 68 | // previous workaround was to use serde_transcode like [PSeitz/toml-to-json-online-converter: toml to json and json to toml online converter - written in rust with wasm](https://github.com/PSeitz/toml-to-json-online-converter) 69 | // but it failed in some case (added into the test section) 70 | // DataFormat::Toml => { 71 | // let mut res = String::new(); 72 | // let input = content.into_string()?; 73 | // let mut deserializer = serde_json::Deserializer::from_str(&input); 74 | // let mut serializer = toml::ser::Serializer::new(&mut res); 75 | // serde_transcode::transcode(&mut deserializer, &mut serializer) 76 | // .map_err(|e| RenderError::from_error("serde_transcode", e))?; 77 | // res 78 | // } 79 | // DataFormat::TomlPretty => { 80 | // let mut res = String::new(); 81 | // let input = content.into_string()?; 82 | // let mut deserializer = serde_json::Deserializer::from_str(&input); 83 | // let mut serializer = toml::ser::Serializer::pretty(&mut res); 84 | // serde_transcode::transcode(&mut deserializer, &mut serializer) 85 | // .map_err(|e| RenderError::from_error("serde_transcode", e))?; 86 | // res 87 | // } 88 | // 89 | // For toml we recreate a tom struct with map that preserve order (indexedmap) 90 | fn to_ordored_toml_value(data: &Json) -> Result, RenderError> { 91 | match data { 92 | Json::String(v) => Ok(Some(toml::Value::from(v.as_str()))), 93 | Json::Array(v) => v 94 | .iter() 95 | .filter_map(|i| to_opt_res(to_ordored_toml_value(i))) 96 | .collect::, _>>() 97 | .map(|a| Some(toml::Value::Array(a))), 98 | Json::Object(ref obj) => obj 99 | .iter() 100 | .filter_map(|kv| { 101 | to_opt_res(to_ordored_toml_value(kv.1)) 102 | .map(|rnv| rnv.map(|nv| (kv.0.to_owned(), nv))) 103 | }) 104 | .collect::>() 105 | .map(|m| Some(toml::Value::Table(sort_toml_map(m)))), 106 | Json::Number(v) => { 107 | if v.is_i64() { 108 | Ok(Some(toml::Value::Integer(v.as_i64().unwrap()))) 109 | } else if let Some(x) = v.as_f64() { 110 | Ok(Some(toml::Value::Float(x))) 111 | } else { 112 | Err(crate::to_other_error(format!( 113 | "to_toml:can not convert a Json Number: {}", 114 | v 115 | ))) 116 | } 117 | } 118 | Json::Bool(v) => Ok(Some(toml::Value::Boolean(*v))), 119 | Json::Null => Ok(None), 120 | } 121 | } 122 | 123 | fn sort_toml_map(data: Table) -> Table { 124 | let (tables, non_tables): (TablePartition, TablePartition) = 125 | data.into_iter().partition(|v| v.1.is_table()); 126 | let (arrays, others): (TablePartition, TablePartition) = 127 | non_tables.into_iter().partition(|v| v.1.is_array()); 128 | let mut m = Table::new(); 129 | m.extend(others); 130 | m.extend(arrays); 131 | m.extend(tables); 132 | m 133 | } 134 | 135 | impl DataFormat { 136 | fn read_string(&self, data: &str) -> Result { 137 | if data.is_empty() { 138 | //return Ok(Json::Null); 139 | return Ok(Json::String("".to_owned())); 140 | } 141 | match self { 142 | DataFormat::Json | DataFormat::JsonPretty => { 143 | serde_json::from_str(data).map_err(crate::to_nested_error) 144 | } 145 | DataFormat::Yaml => serde_yaml::from_str(data).map_err(crate::to_nested_error), 146 | DataFormat::Toml | DataFormat::TomlPretty => { 147 | toml::from_str(data).map_err(crate::to_nested_error) 148 | } 149 | } 150 | } 151 | 152 | fn write_string(&self, data: &Json) -> Result { 153 | match data { 154 | Json::Null => Ok("".to_owned()), 155 | Json::String(c) if c.is_empty() => Ok("".to_owned()), 156 | _ => match self { 157 | DataFormat::Json => serde_json::to_string(data).map_err(crate::to_nested_error), 158 | DataFormat::JsonPretty => { 159 | serde_json::to_string_pretty(data).map_err(crate::to_nested_error) 160 | } 161 | DataFormat::Yaml => serde_yaml::to_string(data) 162 | .map_err(crate::to_nested_error) 163 | .map(|s| s.trim_start_matches("---\n").to_string()), 164 | DataFormat::Toml => { 165 | let data_toml = to_ordored_toml_value(data)?; 166 | toml::to_string(&data_toml).map_err(crate::to_nested_error) 167 | } 168 | DataFormat::TomlPretty => { 169 | let data_toml = to_ordored_toml_value(data)?; 170 | toml::to_string_pretty(&data_toml).map_err(crate::to_nested_error) 171 | } 172 | }, 173 | } 174 | } 175 | } 176 | 177 | #[allow(clippy::result_large_err)] 178 | fn json_query>(expr: E, data: T) -> Result { 179 | // let data = data.to_jmespath(); 180 | let res = jmespath::compile(expr.as_ref()) 181 | .and_then(|e| e.search(data)) 182 | .map_err(|source| JsonError::JsonQueryFailure { 183 | expression: expr.as_ref().to_string(), 184 | source, 185 | })?; 186 | serde_json::to_value(res.as_ref()).map_err(|source| JsonError::ToJsonValueError { 187 | input: format!("{:?}", res), 188 | source, 189 | }) 190 | } 191 | 192 | fn find_data_format(h: &Helper) -> Result { 193 | let param = h 194 | .hash_get("format") 195 | .and_then(|v| v.value().as_str()) 196 | .unwrap_or("json"); 197 | DataFormat::from_str(param).map_err(crate::to_nested_error) 198 | } 199 | 200 | fn find_str_param(pos: usize, h: &Helper) -> Result { 201 | h.param(pos) 202 | .ok_or_else(|| crate::to_other_error(format!("param {} (the string) not found", pos))) 203 | // .and_then(|v| { 204 | // serde_json::from_value::(v.value().clone()).map_err(RenderError::with) 205 | // }) 206 | .map(|v| v.value().as_str().unwrap_or("").to_owned()) 207 | } 208 | 209 | #[allow(non_camel_case_types)] 210 | pub struct str_to_json_fct; 211 | 212 | impl HelperDef for str_to_json_fct { 213 | fn call_inner<'reg: 'rc, 'rc>( 214 | &self, 215 | h: &Helper<'rc>, 216 | _: &'reg Handlebars, 217 | _: &'rc Context, 218 | _: &mut RenderContext<'reg, 'rc>, 219 | ) -> Result, RenderError> { 220 | let data: String = find_str_param(0, h)?; 221 | let format = find_data_format(h)?; 222 | let result = format.read_string(&data)?; 223 | Ok(ScopedJson::Derived(result)) 224 | } 225 | } 226 | 227 | #[allow(non_camel_case_types)] 228 | pub struct json_to_str_fct; 229 | 230 | impl HelperDef for json_to_str_fct { 231 | fn call_inner<'reg: 'rc, 'rc>( 232 | &self, 233 | h: &Helper<'rc>, 234 | _: &'reg Handlebars, 235 | _: &'rc Context, 236 | _: &mut RenderContext<'reg, 'rc>, 237 | ) -> Result, RenderError> { 238 | let format = find_data_format(h)?; 239 | let data = h 240 | .param(0) 241 | .ok_or_else(|| crate::to_other_error("param 0 (the json) not found")) 242 | .map(|v| v.value())?; 243 | let result = format.write_string(data)?; 244 | Ok(ScopedJson::Derived(Json::String(result))) 245 | } 246 | } 247 | 248 | #[allow(non_camel_case_types)] 249 | pub struct json_str_query_fct; 250 | 251 | impl HelperDef for json_str_query_fct { 252 | fn call_inner<'reg: 'rc, 'rc>( 253 | &self, 254 | h: &Helper<'rc>, 255 | _: &'reg Handlebars, 256 | _: &'rc Context, 257 | _: &mut RenderContext<'reg, 'rc>, 258 | ) -> Result, RenderError> { 259 | let format = find_data_format(h)?; 260 | let expr = find_str_param(0, h)?; 261 | let data_str = find_str_param(1, h)?; 262 | let data = format.read_string(&data_str)?; 263 | let result = json_query(expr, data) 264 | .map_err(crate::to_nested_error) 265 | .and_then(|v| { 266 | let output_format = if v.is_array() || v.is_object() { 267 | format 268 | } else { 269 | DataFormat::Json 270 | }; 271 | output_format.write_string(&v).map(|s| { 272 | if v.is_array() || v.is_object() { 273 | s 274 | } else { 275 | s.trim().to_owned() 276 | } 277 | }) 278 | })?; 279 | Ok(ScopedJson::Derived(Json::String(result))) 280 | } 281 | } 282 | 283 | fn from_json_block<'reg, 'rc>( 284 | h: &Helper<'rc>, 285 | r: &'reg Handlebars, 286 | ctx: &'rc Context, 287 | rc: &mut RenderContext<'reg, 'rc>, 288 | out: &mut dyn Output, 289 | ) -> HelperResult { 290 | let format = find_data_format(h)?; 291 | let mut content = StringOutput::default(); 292 | h.template() 293 | .map(|t| t.render(r, ctx, rc, &mut content)) 294 | .unwrap_or(Ok(()))?; 295 | let data = DataFormat::Json.read_string(&content.into_string()?)?; 296 | let res = format.write_string(&data)?; 297 | 298 | out.write(&res).map_err(crate::to_nested_error) 299 | } 300 | 301 | fn to_json_block<'reg, 'rc>( 302 | h: &Helper<'rc>, 303 | r: &'reg Handlebars, 304 | ctx: &'rc Context, 305 | rc: &mut RenderContext<'reg, 'rc>, 306 | out: &mut dyn Output, 307 | ) -> HelperResult { 308 | let format = find_data_format(h)?; 309 | let mut content = StringOutput::default(); 310 | h.template() 311 | .map(|t| t.render(r, ctx, rc, &mut content)) 312 | .unwrap_or(Ok(()))?; 313 | let data = format.read_string(&content.into_string()?)?; 314 | let res = DataFormat::JsonPretty.write_string(&data)?; 315 | out.write(&res).map_err(RenderError::from) 316 | } 317 | 318 | handlebars_helper!(json_query_fct: |expr: str, data: Json| json_query(expr, data).map_err(crate::to_nested_error)?); 319 | 320 | pub fn register(handlebars: &mut Handlebars) { 321 | handlebars.register_helper("json_to_str", Box::new(json_to_str_fct)); 322 | handlebars.register_helper("str_to_json", Box::new(str_to_json_fct)); 323 | handlebars.register_helper("from_json", Box::new(from_json_block)); 324 | handlebars.register_helper("to_json", Box::new(to_json_block)); 325 | handlebars.register_helper("json_query", Box::new(json_query_fct)); 326 | handlebars.register_helper("json_str_query", Box::new(json_str_query_fct)); 327 | } 328 | 329 | #[cfg(test)] 330 | mod tests { 331 | use super::*; 332 | use crate::assert_renders; 333 | use crate::tests::normalize_nl; 334 | use pretty_assertions::assert_eq; 335 | use std::error::Error; 336 | 337 | #[test] 338 | fn test_empty_input_return_empty() -> Result<(), Box> { 339 | assert_renders![ 340 | (r##"{{ json_to_str "" }}"##, ""), 341 | (r##"{{ json_to_str "" format="json"}}"##, ""), 342 | (r##"{{ json_to_str "" format="yaml"}}"##, ""), 343 | (r##"{{ json_to_str "" format="toml"}}"##, ""), 344 | (r##"{{ str_to_json "" }}"##, ""), 345 | (r##"{{ str_to_json "" format="json"}}"##, ""), 346 | (r##"{{ str_to_json "" format="yaml"}}"##, ""), 347 | (r##"{{ str_to_json "" format="toml"}}"##, ""), 348 | (r##"{{ json_to_str (str_to_json "") }}"##, ""), 349 | (r##"{{ str_to_json (json_to_str "") }}"##, ""), 350 | (r##"{{ json_query "foo" "" }}"##, ""), 351 | (r##"{{ json_str_query "foo" "" }}"##, ""), 352 | (r##"{{ json_str_query "foo" "" format="json"}}"##, ""), 353 | (r##"{{ json_str_query "foo" "" format="yaml"}}"##, ""), 354 | (r##"{{ json_str_query "foo" "" format="toml"}}"##, "") 355 | ] 356 | } 357 | 358 | #[test] 359 | fn test_null_input_return_empty() -> Result<(), Box> { 360 | assert_renders![ 361 | (r##"{{ json_to_str null }}"##, ""), 362 | (r##"{{ str_to_json null }}"##, ""), 363 | (r##"{{ json_to_str (str_to_json null) }}"##, ""), 364 | (r##"{{ str_to_json (json_to_str null) }}"##, ""), 365 | (r##"{{ json_query "foo" null }}"##, ""), 366 | (r##"{{ json_str_query "foo" null }}"##, "") 367 | ] 368 | } 369 | 370 | #[test] 371 | fn test_search_object_field() -> Result<(), Box> { 372 | let json: Json = serde_json::from_str(r##"{"foo":{"bar":{"baz":true}}}"##)?; 373 | let result = json_query("foo.bar.baz", json)?; 374 | assert_eq!(result, Json::Bool(true)); 375 | Ok(()) 376 | } 377 | 378 | #[test] 379 | fn test_search_path_in_empty() -> Result<(), Box> { 380 | for v in ["{}", "[]", "null", "\"\""] { 381 | let json: Json = serde_json::from_str(v)?; 382 | let result = json_query("foo.bar.baz", json)?; 383 | assert_eq!(result, Json::Null); 384 | } 385 | Ok(()) 386 | } 387 | 388 | fn assert_data_format_write_eq_read(f: DataFormat, data: &str) -> Result<(), Box> { 389 | let data = normalize_nl(data); 390 | let actual = normalize_nl(&f.write_string(&f.read_string(&data)?)?); 391 | assert_eq!(actual, data); 392 | Ok(()) 393 | } 394 | 395 | #[test] 396 | fn test_data_format_symmetry() -> Result<(), Box> { 397 | assert_data_format_write_eq_read(DataFormat::Json, r##"{"foo":{"bar":{"baz":true}}}"##)?; 398 | assert_data_format_write_eq_read( 399 | DataFormat::JsonPretty, 400 | &normalize_nl( 401 | r##"{ 402 | "foo": { 403 | "bar": { 404 | "baz": true 405 | } 406 | } 407 | }"##, 408 | ), 409 | )?; 410 | assert_data_format_write_eq_read( 411 | DataFormat::Yaml, 412 | &normalize_nl( 413 | r##" 414 | a: a 415 | foo: 416 | bar: 417 | baz: true 418 | "##, 419 | ), 420 | )?; 421 | assert_data_format_write_eq_read( 422 | DataFormat::Toml, 423 | &normalize_nl( 424 | r##" 425 | [foo.bar] 426 | baz = true 427 | "##, 428 | ), 429 | )?; 430 | assert_data_format_write_eq_read( 431 | DataFormat::TomlPretty, 432 | &normalize_nl( 433 | r##" 434 | [foo.bar] 435 | baz = true 436 | "##, 437 | ), 438 | )?; 439 | Ok(()) 440 | } 441 | 442 | #[test] 443 | fn test_helper_json_to_str() -> Result<(), Box> { 444 | assert_renders![ 445 | (r##"{{ json_to_str {} }}"##, r##"{}"##), 446 | ( 447 | r##"{{ json_to_str {"foo":{"bar":{"baz":true}}} }}"##, 448 | r##"{"foo":{"bar":{"baz":true}}}"##, 449 | ), 450 | ( 451 | r##"{{ json_to_str ( str_to_json "{\"foo\":{\"bar\":{\"baz\":true}}}" ) }}"##, 452 | r##"{"foo":{"bar":{"baz":true}}}"##, 453 | ), 454 | ( 455 | r##"{{ json_to_str ( str_to_json "{\"foo\":{\"bar\":{\"baz\":true}}}" ) format="json_pretty"}}"##, 456 | &normalize_nl( 457 | r##"{ 458 | "foo": { 459 | "bar": { 460 | "baz": true 461 | } 462 | } 463 | }"## 464 | ), 465 | ) 466 | ] 467 | } 468 | 469 | #[test] 470 | fn test_helper_json_query() -> Result<(), Box> { 471 | assert_renders![ 472 | (r##"{{ json_query "foo" {} }}"##, r##""##), 473 | ( 474 | r##"{{ json_to_str ( json_query "foo" {"foo":{"bar":{"baz":true}}} ) }}"##, 475 | r##"{"bar":{"baz":true}}"##, 476 | ), 477 | ( 478 | r##"{{ json_to_str ( json_query "foo" (str_to_json "{\"foo\":{\"bar\":{\"baz\":true}}}" ) ) }}"##, 479 | r##"{"bar":{"baz":true}}"##, 480 | ) 481 | ] 482 | } 483 | 484 | #[test] 485 | fn test_helper_json_str_query() -> Result<(), Box> { 486 | assert_renders![ 487 | ( 488 | r##"{{ json_str_query "foo" "{\"foo\":{\"bar\":{\"baz\":true}}}" }}"##, 489 | r##"{"bar":{"baz":true}}"##, 490 | ), 491 | ( 492 | r##"{{ json_str_query "foo" "{\"foo\":{\"bar\":{\"baz\":true}}}" format="json"}}"##, 493 | r##"{"bar":{"baz":true}}"##, 494 | ), 495 | ( 496 | r##"{{ json_str_query "foo.bar.baz" "{\"foo\":{\"bar\":{\"baz\":true}}}" }}"##, 497 | "true", 498 | ) 499 | ] 500 | } 501 | 502 | #[test] 503 | fn test_helper_json_str_query_on_yaml() -> Result<(), Box> { 504 | assert_renders![ 505 | ( 506 | r##"{{ json_str_query "foo" "{\"foo\":{\"bar\":{\"baz\":true}}}" format="yaml"}}"##, 507 | &normalize_nl( 508 | " 509 | bar: 510 | baz: true 511 | " 512 | ) 513 | ), 514 | ( 515 | r##"{{ json_str_query "foo" "foo:\n bar:\n baz: true\n" format="yaml"}}"##, 516 | &normalize_nl( 517 | " 518 | bar: 519 | baz: true 520 | " 521 | ) 522 | ), 523 | ( 524 | r##"{{ json_str_query "foo.bar.baz" "foo:\n bar:\n baz: true\n" format="yaml"}}"##, 525 | "true", 526 | ) 527 | ] 528 | } 529 | 530 | #[test] 531 | fn test_helper_json_str_query_on_toml() -> Result<(), Box> { 532 | assert_renders![ 533 | ( 534 | r##"{{ json_str_query "foo" "[foo.bar]\nbaz=true\n" format="toml"}}"##, 535 | &normalize_nl( 536 | "[bar] 537 | baz = true 538 | " 539 | ), 540 | ), 541 | // returning a single value is not a valid toml 542 | ( 543 | r##"{{ json_str_query "foo.bar.baz" "[foo.bar]\nbaz=true\n" format="toml"}}"##, 544 | "true", 545 | ) 546 | ] 547 | } 548 | 549 | #[test] 550 | fn test_block_to_json() -> Result<(), Box> { 551 | assert_renders![ 552 | (r##"{{#to_json}}{{/to_json}}"##, r##""##), 553 | ( 554 | r##"{{#to_json}}{"foo":{"bar":{"baz":true}}}{{/to_json}}"##, 555 | &normalize_nl( 556 | r##"{ 557 | "foo": { 558 | "bar": { 559 | "baz": true 560 | } 561 | } 562 | }"## 563 | ), 564 | ), 565 | ( 566 | &normalize_nl( 567 | r##"{{#to_json format="yaml"}} 568 | foo: 569 | bar: 570 | baz: true 571 | {{/to_json}}"## 572 | ), 573 | &normalize_nl( 574 | r##"{ 575 | "foo": { 576 | "bar": { 577 | "baz": true 578 | } 579 | } 580 | }"## 581 | ), 582 | ), 583 | ( 584 | &normalize_nl( 585 | r##"{{#to_json format="toml"}} 586 | [foo] 587 | bar = { baz = true } 588 | hello = "1.2.3" 589 | {{/to_json}}"## 590 | ), 591 | &normalize_nl( 592 | r##"{ 593 | "foo": { 594 | "bar": { 595 | "baz": true 596 | }, 597 | "hello": "1.2.3" 598 | } 599 | }"## 600 | ), 601 | ), 602 | ] 603 | } 604 | 605 | #[test] 606 | fn test_block_from_json() -> Result<(), Box> { 607 | assert_renders![ 608 | (r##"{{#from_json}}{{/from_json}}"##, r##""##), 609 | ( 610 | r##"{{#from_json}}{"foo":{"bar":{"baz":true}}}{{/from_json}}"##, 611 | r##"{"foo":{"bar":{"baz":true}}}"## 612 | ), 613 | ( 614 | r##"{{#from_json format="json_pretty"}}{"foo":{"bar":{"baz":true}}}{{/from_json}}"##, 615 | &normalize_nl( 616 | r##"{ 617 | "foo": { 618 | "bar": { 619 | "baz": true 620 | } 621 | } 622 | }"## 623 | ), 624 | ), 625 | ( 626 | r##"{{#from_json format="yaml"}}{"foo":{"bar":{"baz":true}}}{{/from_json}}"##, 627 | &normalize_nl( 628 | r##" 629 | foo: 630 | bar: 631 | baz: true 632 | "## 633 | ) 634 | ), 635 | ( 636 | r##"{{#from_json format="toml"}}{"foo":{"bar":{"baz":true}}}{{/from_json}}"##, 637 | &normalize_nl( 638 | r##" 639 | [foo.bar] 640 | baz = true 641 | "## 642 | ), 643 | ), 644 | ( 645 | r##"{{#from_json format="toml"}}{"foo":{"hello":"1.2.3", "bar":{"baz":true} }}{{/from_json}}"##, 646 | &normalize_nl( 647 | r##" 648 | [foo] 649 | hello = "1.2.3" 650 | 651 | [foo.bar] 652 | baz = true 653 | "## 654 | ), 655 | ), 656 | ( 657 | r##"{{#from_json format="toml_pretty"}}{"foo":{"hello":"1.2.4", "bar":{"baz":true} }}{{/from_json}}"##, 658 | &normalize_nl( 659 | r##" 660 | [foo] 661 | hello = "1.2.4" 662 | 663 | [foo.bar] 664 | baz = true 665 | "## 666 | ), 667 | ), 668 | ] 669 | } 670 | 671 | #[test] 672 | fn test_block_from_json_to_toml_with_tables_at_anyplace() -> Result<(), Box> { 673 | assert_renders![( 674 | r##"{{#from_json format="toml"}} 675 | {"foo":{ 676 | "f0": null, 677 | "f1":"1.2.3", 678 | "f2":{ 679 | "f20": true, 680 | "f21": { "f210": false} 681 | }, 682 | "f3": [1,2,3], 683 | "f4":"1.2.3", 684 | "f5":{ 685 | "f50": true, 686 | "f51": { "f510": false} 687 | } 688 | }} 689 | {{/from_json}}"##, 690 | &normalize_nl( 691 | r##" 692 | [foo] 693 | f1 = "1.2.3" 694 | f4 = "1.2.3" 695 | f3 = [1, 2, 3] 696 | 697 | [foo.f2] 698 | f20 = true 699 | 700 | [foo.f2.f21] 701 | f210 = false 702 | 703 | [foo.f5] 704 | f50 = true 705 | 706 | [foo.f5.f51] 707 | f510 = false 708 | "## 709 | ), 710 | ),] 711 | } 712 | 713 | #[test] 714 | fn test_sort_toml_map() { 715 | let mut actual = toml::map::Map::new(); 716 | actual.insert("f1".to_string(), toml::Value::String("s1".to_owned())); 717 | actual.insert("f2".to_string(), toml::Value::Table(toml::map::Map::new())); 718 | actual.insert("f3".to_string(), toml::Value::Boolean(true)); 719 | actual.insert("f4".to_string(), toml::Value::Table(toml::map::Map::new())); 720 | let mut expected = toml::map::Map::new(); 721 | expected.insert("f1".to_string(), toml::Value::String("s1".to_owned())); 722 | expected.insert("f3".to_string(), toml::Value::Boolean(true)); 723 | expected.insert("f2".to_string(), toml::Value::Table(toml::map::Map::new())); 724 | expected.insert("f4".to_string(), toml::Value::Table(toml::map::Map::new())); 725 | assert_eq!(sort_toml_map(actual), expected) 726 | } 727 | } 728 | -------------------------------------------------------------------------------- /src/jsonnet_helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::outputs::StringOutput; 2 | use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, Renderable}; 3 | use jsonnet::JsonnetVm; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Error)] 7 | enum JsonnetError { 8 | #[error("jsonnet evaluation failed: {source_str}")] 9 | EvaluateError { source_str: String }, 10 | } 11 | 12 | #[allow(clippy::let_and_return)] 13 | fn jsonnet_block<'reg, 'rc>( 14 | h: &Helper<'rc>, 15 | r: &'reg Handlebars, 16 | ctx: &'rc Context, 17 | rc: &mut RenderContext<'reg, 'rc>, 18 | out: &mut dyn Output, 19 | ) -> HelperResult { 20 | let mut content = StringOutput::default(); 21 | h.template() 22 | .map(|t| t.render(r, ctx, rc, &mut content)) 23 | .unwrap_or(Ok(()))?; 24 | let input = content.into_string()?; 25 | let res = if input.is_empty() { 26 | input 27 | } else { 28 | let mut vm = JsonnetVm::new(); 29 | vm.string_output( 30 | h.hash_get("string_output") 31 | .and_then(|v| v.value().as_bool()) 32 | .unwrap_or(false), 33 | ); 34 | // vm.fmt_indent( 35 | // h.hash_get("indent") 36 | // .and_then(|v| v.value().as_u64()) 37 | // .unwrap_or(4) as u32, 38 | // ); 39 | let s = vm 40 | .evaluate_snippet("snippet", &input) 41 | .map_err(|e| { 42 | crate::to_nested_error(JsonnetError::EvaluateError { 43 | source_str: format!("{:?}", e), 44 | }) 45 | })? 46 | .to_string(); 47 | s 48 | }; 49 | 50 | out.write(&res).map_err(crate::to_nested_error) 51 | } 52 | 53 | pub fn register(handlebars: &mut Handlebars) { 54 | handlebars.register_helper("jsonnet", Box::new(jsonnet_block)); 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | //use super::*; 60 | use crate::assert_renders; 61 | use crate::tests::normalize_nl; 62 | use std::error::Error; 63 | 64 | #[test] 65 | fn test_jsonnet_block() -> Result<(), Box> { 66 | assert_renders![ 67 | (r##"{{#jsonnet}}{{/jsonnet}}"##, r##""##), 68 | ( 69 | r##"{{#jsonnet}}{"foo":{"bar":{"baz":true}}}{{/jsonnet}}"##, 70 | &normalize_nl( 71 | r##"{ 72 | "foo": { 73 | "bar": { 74 | "baz": true 75 | } 76 | } 77 | } 78 | "## 79 | ) 80 | ), 81 | ( 82 | r##"{{#jsonnet}} 83 | local v = {"foo":{"bar":{"baz":false}}}; 84 | v 85 | {{/jsonnet}}"##, 86 | &normalize_nl( 87 | r##"{ 88 | "foo": { 89 | "bar": { 90 | "baz": false 91 | } 92 | } 93 | } 94 | "## 95 | ) 96 | ), 97 | ( 98 | r##"{{#jsonnet}} 99 | local v = {"foo":{"bar":{"baz":false}}}; 100 | v { 101 | "v": 3, 102 | "vv" +: { 103 | "vvv": 333 104 | } 105 | } 106 | {{/jsonnet}}"##, 107 | &normalize_nl( 108 | r##"{ 109 | "foo": { 110 | "bar": { 111 | "baz": false 112 | } 113 | }, 114 | "v": 3, 115 | "vv": { 116 | "vvv": 333 117 | } 118 | } 119 | "## 120 | ) 121 | ), 122 | ( 123 | r##"{{#jsonnet}} 124 | local v = {"foo":{"bar":{"baz":false}}}; 125 | v { 126 | "foo" +: { 127 | "bar" +: { 128 | "baz": true 129 | } 130 | } 131 | } 132 | {{/jsonnet}}"##, 133 | &normalize_nl( 134 | r##"{ 135 | "foo": { 136 | "bar": { 137 | "baz": true 138 | } 139 | } 140 | } 141 | "## 142 | ) 143 | ), 144 | ( 145 | r##"{{#jsonnet}} 146 | local v = {"foo":{"bar":{"baz":false}}}; 147 | v { 148 | "foo" +: { 149 | "bar" +: { 150 | "baz2": true 151 | } 152 | } 153 | } 154 | {{/jsonnet}}"##, 155 | &normalize_nl( 156 | r##"{ 157 | "foo": { 158 | "bar": { 159 | "baz": false, 160 | "baz2": true 161 | } 162 | } 163 | } 164 | "## 165 | ) 166 | ), 167 | ] 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_code)] 2 | //#![doc(html_root_url = "https://docs.rs/tower-service/0.3.0")] 3 | // #![warn( 4 | // missing_debug_implementations, 5 | // missing_docs, 6 | // rust_2018_idioms, 7 | // unreachable_pub 8 | // )] 9 | 10 | use handlebars::no_escape; 11 | use handlebars::Handlebars; 12 | 13 | #[cfg(feature = "jsontype")] 14 | pub mod assign_helpers; 15 | pub mod env_helpers; 16 | pub mod file_helpers; 17 | #[cfg(any(feature = "http_reqwest", feature = "http_attohttpc"))] 18 | pub mod http_helpers; 19 | #[cfg(feature = "json")] 20 | pub mod json_helpers; 21 | #[cfg(feature = "jsonnet")] 22 | pub mod jsonnet_helpers; 23 | pub mod outputs; 24 | pub mod path_helpers; 25 | #[cfg(feature = "regex")] 26 | pub mod regex_helpers; 27 | #[cfg(feature = "jsontype")] 28 | pub mod region_helpers; 29 | #[cfg(feature = "string")] 30 | pub mod string_helpers; 31 | #[cfg(feature = "uuid")] 32 | pub mod uuid_helpers; 33 | 34 | pub fn new_hbs<'reg>() -> Handlebars<'reg> { 35 | let mut handlebars = Handlebars::new(); 36 | setup_handlebars(&mut handlebars); 37 | handlebars 38 | } 39 | 40 | pub fn setup_handlebars(handlebars: &mut Handlebars) { 41 | handlebars.set_strict_mode(true); 42 | handlebars.register_escape_fn(no_escape); //html escaping is the default and cause issue 43 | register(handlebars); 44 | } 45 | 46 | pub fn register(handlebars: &mut Handlebars) { 47 | #[cfg(feature = "string")] 48 | string_helpers::register(handlebars); 49 | #[cfg(any(feature = "http_reqwest", feature = "http_attohttpc"))] 50 | http_helpers::register(handlebars); 51 | path_helpers::register(handlebars); 52 | env_helpers::register(handlebars); 53 | #[cfg(feature = "json")] 54 | json_helpers::register(handlebars); 55 | #[cfg(feature = "jsonnet")] 56 | jsonnet_helpers::register(handlebars); 57 | #[cfg(feature = "jsontype")] 58 | assign_helpers::register(handlebars); 59 | file_helpers::register(handlebars); 60 | #[cfg(feature = "jsontype")] 61 | region_helpers::register(handlebars); 62 | #[cfg(feature = "regex")] 63 | regex_helpers::register(handlebars); 64 | #[cfg(feature = "uuid")] 65 | uuid_helpers::register(handlebars); 66 | } 67 | 68 | #[allow(dead_code)] 69 | pub(crate) fn to_nested_error(cause: E) -> handlebars::RenderError 70 | where 71 | E: std::error::Error + Send + Sync + 'static, 72 | { 73 | handlebars::RenderErrorReason::NestedError(Box::new(cause)).into() 74 | } 75 | 76 | #[allow(dead_code)] 77 | pub fn to_other_error>(desc: T) -> handlebars::RenderError { 78 | handlebars::RenderErrorReason::Other(desc.as_ref().to_string()).into() 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use pretty_assertions::assert_eq; 85 | use std::collections::HashMap; 86 | use std::error::Error; 87 | use unindent::unindent; 88 | 89 | pub(crate) fn assert_helpers( 90 | input: &str, 91 | helper_expected: Vec<(&str, &str)>, 92 | ) -> Result<(), Box> { 93 | let mut vs: HashMap = HashMap::new(); 94 | vs.insert("var".into(), input.into()); 95 | let hbs = new_hbs(); 96 | for sample in helper_expected { 97 | let tmpl = format!("{{{{ {} var}}}}", sample.0); 98 | assert_eq!( 99 | hbs.render_template(&tmpl, &vs)?, 100 | sample.1.to_owned(), 101 | "name: {}", 102 | sample.0 103 | ); 104 | } 105 | Ok(()) 106 | } 107 | 108 | #[allow(dead_code)] 109 | pub(crate) fn normalize_nl(s: &str) -> String { 110 | unindent(s).replace("\r\n", "\n").replace('\r', "") 111 | } 112 | 113 | #[macro_export] 114 | macro_rules! assert_renders { 115 | ($($arg:expr),+$(,)?) => {{ 116 | use std::collections::HashMap; 117 | use pretty_assertions::assert_eq; 118 | let vs: HashMap = HashMap::new(); 119 | let mut hbs = $crate::new_hbs(); 120 | $({ 121 | let sample: (&str, &str) = $arg; 122 | hbs.register_template_string(&sample.0, &sample.0).expect("register_template_string"); 123 | // assert_that!(hbs.render(&sample.0, &vs).expect("render")) 124 | // .named(sample.0) 125 | // .is_equal_to(sample.1.to_owned()); 126 | assert_eq!(hbs.render(&sample.0, &vs).expect("render"), sample.1.to_owned()); 127 | })* 128 | Ok(()) 129 | }} 130 | } 131 | 132 | // pub(crate) fn assert_renders( 133 | // samples_expected: Vec<(&str, &str)>, 134 | // ) -> Result<(), Box> { 135 | // let vs: HashMap = HashMap::new(); 136 | // let hbs = new_hbs(); 137 | // for sample in samples_expected { 138 | // let tmpl = sample.0; 139 | // assert_that!(hbs.render_template(&tmpl, &vs)?) 140 | // .named(sample.0) 141 | // .is_equal_to(sample.1.to_owned()); 142 | // } 143 | // Ok(()) 144 | // } 145 | 146 | #[test] 147 | #[cfg(feature = "string")] 148 | fn test_chain_of_helpers_with_1_param() -> Result<(), Box> { 149 | let vs: HashMap = HashMap::new(); 150 | let hbs = new_hbs(); 151 | let tmpl = r#"{{ to_upper_case (to_singular "Hello foo-bars")}}"#.to_owned(); 152 | let actual = hbs.render_template(&tmpl, &vs)?; 153 | assert_eq!(&actual, "BAR"); 154 | Ok(()) 155 | } 156 | 157 | #[test] 158 | #[cfg(all(feature = "string", feature = "json"))] 159 | fn test_chain_of_default() -> Result<(), Box> { 160 | std::env::set_var("MY_VERSION_1", "1.0.0"); 161 | assert_renders![ 162 | ( 163 | r##"{{ first_non_empty (env_var "MY_VERSION") "0.0.0" }}"##, 164 | r##"0.0.0"## 165 | ), 166 | ( 167 | r##"{{ first_non_empty (env_var "MY_VERSION_1") "0.0.0" }}"##, 168 | r##"1.0.0"## 169 | ), 170 | ( 171 | r##"{{ first_non_empty (unquote (json_str_query "package.edition" (read_to_str "Cargo.toml") format="toml")) (env_var "MY_VERSION") "0.0.0" }}"##, 172 | r##"2021"## 173 | ), 174 | ] 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/outputs.rs: -------------------------------------------------------------------------------- 1 | use handlebars::Output; 2 | use std::string::FromUtf8Error; 3 | 4 | /// Copy of the (private) StringOutput from handlebars 5 | pub struct StringOutput { 6 | buf: Vec, 7 | } 8 | 9 | impl Output for StringOutput { 10 | fn write(&mut self, seg: &str) -> Result<(), std::io::Error> { 11 | self.buf.extend_from_slice(seg.as_bytes()); 12 | Ok(()) 13 | } 14 | } 15 | 16 | impl Default for StringOutput { 17 | fn default() -> Self { 18 | StringOutput { 19 | buf: Vec::with_capacity(8 * 1024), 20 | } 21 | } 22 | } 23 | impl StringOutput { 24 | pub fn into_string(self) -> Result { 25 | String::from_utf8(self.buf) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/path_helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{handlebars_helper, Handlebars}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | fn expand(s: &str) -> PathBuf { 5 | let p = PathBuf::from(s); 6 | // canonicalize to be able to extract file_name, parent, extension from path like '.' 7 | // without requested template author to call canonicalize in every place 8 | if p.exists() { 9 | p.canonicalize().unwrap_or(p) 10 | } else { 11 | p 12 | } 13 | } 14 | 15 | pub fn register(handlebars: &mut Handlebars) { 16 | { 17 | handlebars_helper!(parent: |v: str| { 18 | expand(v).parent().and_then(|s| s.to_str()).unwrap_or("").to_owned() 19 | }); 20 | handlebars.register_helper("parent", Box::new(parent)) 21 | } 22 | { 23 | handlebars_helper!(file_name: |v: str| { 24 | expand(v).file_name().and_then(|s| s.to_str()).unwrap_or("").to_owned() 25 | }); 26 | handlebars.register_helper("file_name", Box::new(file_name)) 27 | } 28 | { 29 | handlebars_helper!(extension: |v: str| expand(v).extension().and_then(|s| s.to_str()).unwrap_or("").to_owned()); 30 | handlebars.register_helper("extension", Box::new(extension)) 31 | } 32 | { 33 | handlebars_helper!(canonicalize: |v: str| { 34 | Path::new(v).canonicalize().ok().and_then(|s| s.to_str().map(|v| v.to_owned())).unwrap_or_else(|| "".into()) 35 | }); 36 | handlebars.register_helper("canonicalize", Box::new(canonicalize)) 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use crate::tests::assert_helpers; 43 | use std::error::Error; 44 | 45 | #[test] 46 | fn test_register_path_helpers() -> Result<(), Box> { 47 | assert_helpers( 48 | "/hello/bar/foo", 49 | vec![ 50 | ("file_name", "foo"), 51 | ("parent", "/hello/bar"), 52 | ("extension", ""), 53 | ("canonicalize", ""), 54 | ], 55 | )?; 56 | assert_helpers( 57 | "foo", 58 | vec![("file_name", "foo"), ("parent", ""), ("extension", "")], 59 | )?; 60 | assert_helpers( 61 | "bar/foo", 62 | vec![("file_name", "foo"), ("parent", "bar"), ("extension", "")], 63 | )?; 64 | assert_helpers( 65 | "bar/foo.txt", 66 | vec![ 67 | ("file_name", "foo.txt"), 68 | ("parent", "bar"), 69 | ("extension", "txt"), 70 | ], 71 | )?; 72 | assert_helpers( 73 | "./foo", 74 | vec![ 75 | ("file_name", "foo"), 76 | ("parent", "."), 77 | ("extension", ""), 78 | ("canonicalize", ""), 79 | ], 80 | )?; 81 | assert_helpers( 82 | "/hello/bar/../foo", 83 | vec![ 84 | ("file_name", "foo"), 85 | ("parent", "/hello/bar/.."), 86 | ("extension", ""), 87 | ("canonicalize", ""), 88 | ], 89 | )?; 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/regex_helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{ 2 | Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, RenderErrorReason, 3 | ScopedJson, 4 | }; 5 | use regex::Regex; 6 | 7 | #[allow(non_camel_case_types)] 8 | pub struct regex_captures_fct; 9 | 10 | impl HelperDef for regex_captures_fct { 11 | fn call_inner<'reg: 'rc, 'rc>( 12 | &self, 13 | h: &Helper<'rc>, 14 | _: &'reg Handlebars, 15 | _: &'rc Context, 16 | _: &mut RenderContext<'reg, 'rc>, 17 | ) -> Result, RenderError> { 18 | let on = h.hash_get("on").and_then(|v| v.value().as_str()).ok_or( 19 | RenderErrorReason::ParamNotFoundForName("regex_captures", "on".to_string()), 20 | )?; 21 | let pattern = h 22 | .hash_get("pattern") 23 | .and_then(|v| v.value().as_str()) 24 | .ok_or(RenderErrorReason::ParamNotFoundForName( 25 | "regex_captures", 26 | "pattern".to_string(), 27 | ))?; 28 | let re = Regex::new(pattern).map_err(|err| crate::to_other_error(err.to_string()))?; 29 | if let Some(caps) = re.captures(on) { 30 | let collected = re 31 | .capture_names() 32 | .filter_map(|v| { 33 | v.and_then(|name| { 34 | caps.name(name).map(|m| { 35 | ( 36 | name.to_string(), 37 | serde_json::Value::String(m.as_str().to_string()), 38 | ) 39 | }) 40 | }) 41 | }) 42 | .chain(caps.iter().enumerate().filter_map(|(i, mm)| { 43 | mm.map(|m| { 44 | ( 45 | format!("_{}", i), 46 | serde_json::Value::String(m.as_str().to_string()), 47 | ) 48 | }) 49 | })) 50 | .collect::>(); 51 | Ok(ScopedJson::Derived(serde_json::Value::Object(collected))) 52 | } else { 53 | Ok(ScopedJson::Derived(serde_json::Value::Null)) 54 | } 55 | } 56 | } 57 | 58 | #[allow(non_camel_case_types)] 59 | pub struct regex_is_match_fct; 60 | 61 | impl HelperDef for regex_is_match_fct { 62 | fn call_inner<'reg: 'rc, 'rc>( 63 | &self, 64 | h: &Helper<'rc>, 65 | _: &'reg Handlebars, 66 | _: &'rc Context, 67 | _: &mut RenderContext<'reg, 'rc>, 68 | ) -> Result, RenderError> { 69 | let on = h.hash_get("on").and_then(|v| v.value().as_str()).ok_or( 70 | RenderErrorReason::ParamNotFoundForName("regex_is_match", "on".to_string()), 71 | )?; 72 | 73 | let pattern = h 74 | .hash_get("pattern") 75 | .and_then(|v| v.value().as_str()) 76 | .ok_or(RenderErrorReason::ParamNotFoundForName( 77 | "regex_is_match", 78 | "pattern".to_string(), 79 | ))?; 80 | let re = Regex::new(pattern).map_err(|err| crate::to_other_error(err.to_string()))?; 81 | Ok(ScopedJson::Derived(serde_json::Value::Bool( 82 | re.is_match(on), 83 | ))) 84 | } 85 | } 86 | 87 | pub fn register(handlebars: &mut Handlebars) { 88 | handlebars.register_helper("regex_captures", Box::new(regex_captures_fct)); 89 | handlebars.register_helper("regex_is_match", Box::new(regex_is_match_fct)); 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use crate::assert_renders; 95 | use std::error::Error; 96 | 97 | #[test] 98 | fn test_regex_captures() -> Result<(), Box> { 99 | assert_renders![ 100 | (r##"{{ regex_captures pattern="foo" on="" }}"##, r##""##), 101 | ( 102 | r##"{{ regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}"##, 103 | r##"[object]"## 104 | ), 105 | ( 106 | r##"{{ json_to_str( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}"##, 107 | r##"{"_0":"today","_1":"t","_2":"o","_3":"y","first":"t","last":"y"}"## 108 | ), 109 | ( 110 | r##"{{ set captures=( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}{{ captures.last }}"##, 111 | r##"y"## 112 | ), 113 | ] 114 | } 115 | 116 | #[test] 117 | fn test_regex_is_match() -> Result<(), Box> { 118 | assert_renders![ 119 | ( 120 | r##"{{ regex_is_match pattern="foo" on="" }}"##, 121 | r##"false"## 122 | ), 123 | ( 124 | r##"{{ regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}"##, 125 | r##"true"## 126 | ), 127 | ( 128 | r##"{{#if (regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}ok{{/if}}"##, 129 | r##"ok"## 130 | ), 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/region_helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{ 2 | Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, Renderable, 3 | }; 4 | use log::warn; 5 | 6 | pub fn register(handlebars: &mut Handlebars) { 7 | handlebars.register_helper("replace_section", Box::new(ReplaceSectionHelper)) 8 | } 9 | 10 | #[derive(Clone, Copy)] 11 | pub struct ReplaceSectionHelper; 12 | 13 | impl HelperDef for ReplaceSectionHelper { 14 | fn call<'reg: 'rc, 'rc>( 15 | &self, 16 | h: &Helper<'rc>, 17 | hbs: &'reg Handlebars<'reg>, 18 | ctx: &'rc Context, 19 | rc: &mut RenderContext<'reg, 'rc>, 20 | out: &mut dyn Output, 21 | ) -> HelperResult { 22 | let Some(tmpl) = h.template() else { 23 | warn!("`replace_section` helper require a template"); 24 | return Ok(()); 25 | }; 26 | 27 | let Some(input) = h.param(0).and_then(|it| it.value().as_str()) else { 28 | warn!("`replace_section` helper require an string parameter"); 29 | return Ok(()); 30 | }; 31 | 32 | let remove_boundaries = h 33 | .hash_get("remove_boundaries") 34 | .and_then(|it| it.value().as_bool()) 35 | .unwrap_or_default(); 36 | 37 | let Some(begin) = h.hash_get("begin").and_then(|it| it.value().as_str()) else { 38 | warn!("`replace_section` helper require a 'begin' string value"); 39 | return Ok(()); 40 | }; 41 | let Some((before, inner)) = input.split_once(begin) else { 42 | warn!("Begin region '{begin}' not found in '{input}'"); 43 | return Ok(()); 44 | }; 45 | 46 | let Some(end) = h.hash_get("end").and_then(|it| it.value().as_str()) else { 47 | warn!("`replace_section` helper require a 'end' string value "); 48 | return Ok(()); 49 | }; 50 | let Some((_, after)) = inner.split_once(end) else { 51 | warn!("End region '{end}' not found in '{inner}'"); 52 | return Ok(()); 53 | }; 54 | 55 | out.write(before)?; 56 | if !remove_boundaries { 57 | out.write(begin)?; 58 | } 59 | tmpl.render(hbs, ctx, rc, out)?; 60 | if !remove_boundaries { 61 | out.write(end)?; 62 | } 63 | out.write(after)?; 64 | 65 | Ok(()) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use std::error::Error; 72 | 73 | use serde_json::json; 74 | use similar_asserts::assert_eq; 75 | 76 | use crate::new_hbs; 77 | 78 | const INPUT: &str = r#" 79 | 80 | 81 | 82 | 83 | 84 | 85 | Document 86 | 87 | 88 | 89 | Something by default 90 | 91 | 92 | 93 | "#; 94 | 95 | #[test] 96 | fn test_helper_replace_section() -> Result<(), Box> { 97 | let data = json!({ 98 | "content": INPUT, 99 | }); 100 | let mut hbs = new_hbs(); 101 | hbs.register_template_string( 102 | "test", 103 | r#"{{#replace_section begin="" end="" content }}This is the new content of the block{{/replace_section}}"#, 104 | )?; 105 | 106 | let result = hbs.render("test", &data)?; 107 | 108 | assert_eq!( 109 | result, 110 | r#" 111 | 112 | 113 | 114 | 115 | 116 | 117 | Document 118 | 119 | 120 | This is the new content of the block 121 | 122 | 123 | "#, 124 | ); 125 | 126 | Ok(()) 127 | } 128 | 129 | #[test] 130 | fn test_helper_replace_section_remove_remove_boundaries() -> Result<(), Box> { 131 | let data = json!({ 132 | "content": INPUT, 133 | }); 134 | let mut hbs = new_hbs(); 135 | hbs.register_template_string( 136 | "test", 137 | r#"{{~#replace_section begin="" end="" remove_boundaries=true content }} 138 | This is the new content of the block 139 | {{~/replace_section}}"#, 140 | )?; 141 | 142 | let result = hbs.render("test", &data)?; 143 | 144 | assert_eq!( 145 | result, 146 | r#" 147 | 148 | 149 | 150 | 151 | 152 | 153 | Document 154 | 155 | 156 | This is the new content of the block 157 | 158 | 159 | "#, 160 | ); 161 | 162 | Ok(()) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/string_helpers.rs: -------------------------------------------------------------------------------- 1 | use cruet::Inflector; 2 | use handlebars::{ 3 | handlebars_helper, Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, 4 | ScopedJson, 5 | }; 6 | 7 | #[macro_export] 8 | macro_rules! handlebars_register_cruet { 9 | ($engine:ident, $fct_name:ident) => { 10 | { 11 | handlebars_helper!($fct_name: |v: str| v.$fct_name()); 12 | $engine.register_helper(stringify!($fct_name), Box::new($fct_name)) 13 | } 14 | } 15 | } 16 | 17 | #[allow(non_camel_case_types)] 18 | pub struct first_non_empty_fct; 19 | 20 | impl HelperDef for first_non_empty_fct { 21 | fn call_inner<'reg: 'rc, 'rc>( 22 | &self, 23 | h: &Helper<'rc>, 24 | _: &'reg Handlebars, 25 | _: &'rc Context, 26 | _: &mut RenderContext<'reg, 'rc>, 27 | ) -> Result, RenderError> { 28 | let params = h.params(); 29 | Ok(params 30 | .iter() 31 | .filter_map(|p| p.value().as_str().filter(|s| !s.is_empty())) 32 | // .filter_map(|p| { 33 | // serde_json::to_string(p.value()) 34 | // .ok() 35 | // .filter(|s| !s.is_empty()) 36 | // }) 37 | .next() 38 | .map(|v| ScopedJson::Derived(serde_json::Value::String(v.to_owned()))) 39 | .unwrap_or_else(|| ScopedJson::Derived(serde_json::Value::String("".to_owned())))) 40 | } 41 | } 42 | 43 | pub fn register(handlebars: &mut Handlebars) { 44 | { 45 | handlebars_helper!(to_lower_case: |v: str| v.to_lowercase()); 46 | handlebars.register_helper("to_lower_case", Box::new(to_lower_case)) 47 | } 48 | { 49 | handlebars_helper!(to_upper_case: |v: str| v.to_uppercase()); 50 | handlebars.register_helper("to_upper_case", Box::new(to_upper_case)) 51 | } 52 | { 53 | handlebars_helper!(trim: |v: str| v.trim()); 54 | handlebars.register_helper("trim", Box::new(trim)) 55 | } 56 | { 57 | handlebars_helper!(trim_start: |v: str| v.trim_start()); 58 | handlebars.register_helper("trim_start", Box::new(trim_start)) 59 | } 60 | { 61 | handlebars_helper!(trim_end: |v: str| v.trim_end()); 62 | handlebars.register_helper("trim_end", Box::new(trim_end)) 63 | } 64 | { 65 | handlebars_helper!(replace: |v: str, from: str, to: str| v.replace(from, to)); 66 | handlebars.register_helper("replace", Box::new(replace)) 67 | } 68 | handlebars_register_cruet!(handlebars, is_class_case); 69 | handlebars_register_cruet!(handlebars, to_class_case); 70 | handlebars_register_cruet!(handlebars, is_camel_case); 71 | handlebars_register_cruet!(handlebars, to_camel_case); 72 | handlebars_register_cruet!(handlebars, is_pascal_case); 73 | handlebars_register_cruet!(handlebars, to_pascal_case); 74 | handlebars_register_cruet!(handlebars, is_snake_case); 75 | handlebars_register_cruet!(handlebars, to_snake_case); 76 | handlebars_register_cruet!(handlebars, is_screaming_snake_case); 77 | handlebars_register_cruet!(handlebars, to_screaming_snake_case); 78 | handlebars_register_cruet!(handlebars, is_kebab_case); 79 | handlebars_register_cruet!(handlebars, to_kebab_case); 80 | handlebars_register_cruet!(handlebars, is_train_case); 81 | handlebars_register_cruet!(handlebars, to_train_case); 82 | handlebars_register_cruet!(handlebars, is_sentence_case); 83 | handlebars_register_cruet!(handlebars, to_sentence_case); 84 | handlebars_register_cruet!(handlebars, is_title_case); 85 | handlebars_register_cruet!(handlebars, to_title_case); 86 | handlebars_register_cruet!(handlebars, is_table_case); 87 | handlebars_register_cruet!(handlebars, to_table_case); 88 | handlebars_register_cruet!(handlebars, deordinalize); 89 | handlebars_register_cruet!(handlebars, ordinalize); 90 | handlebars_register_cruet!(handlebars, is_foreign_key); 91 | handlebars_register_cruet!(handlebars, to_foreign_key); 92 | handlebars_register_cruet!(handlebars, deconstantize); 93 | handlebars_register_cruet!(handlebars, demodulize); 94 | handlebars_register_cruet!(handlebars, to_plural); 95 | handlebars_register_cruet!(handlebars, to_singular); 96 | { 97 | handlebars_helper!(quote: |quote_symbol: str, v: str| enquote::enquote(quote_symbol.chars().next().unwrap_or('"'), v)); 98 | handlebars.register_helper("quote", Box::new(quote)) 99 | } 100 | { 101 | handlebars_helper!(unquote: |v: str| match enquote::unquote(v){ 102 | Err(e) => { 103 | log::warn!( 104 | "helper: unquote failed for string '{:?}' with error '{:?}'", 105 | v, e 106 | ); 107 | v.to_owned() 108 | } 109 | Ok(s) => s, 110 | }); 111 | handlebars.register_helper("unquote", Box::new(unquote)) 112 | } 113 | handlebars.register_helper("first_non_empty", Box::new(first_non_empty_fct)); 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use crate::assert_renders; 119 | use crate::tests::assert_helpers; 120 | use std::error::Error; 121 | 122 | #[test] 123 | fn test_register_string_helpers() -> Result<(), Box> { 124 | assert_helpers( 125 | "Hello foo-bars", 126 | vec![ 127 | ("to_lower_case", "hello foo-bars"), 128 | ("to_upper_case", "HELLO FOO-BARS"), 129 | ("to_camel_case", "helloFooBars"), 130 | ("to_pascal_case", "HelloFooBars"), 131 | ("to_snake_case", "hello_foo_bars"), 132 | ("to_screaming_snake_case", "HELLO_FOO_BARS"), 133 | ("to_kebab_case", "hello-foo-bars"), 134 | ("to_train_case", "Hello-Foo-Bars"), 135 | ("to_sentence_case", "Hello foo bars"), 136 | ("to_title_case", "Hello Foo Bars"), 137 | ("to_class_case", "HelloFooBar"), 138 | ("to_table_case", "hello_foo_bars"), 139 | ("to_plural", "bars"), 140 | ("to_singular", "bar"), 141 | ], 142 | )?; 143 | Ok(()) 144 | } 145 | 146 | #[test] 147 | fn test_helper_trim() -> Result<(), Box> { 148 | assert_renders![ 149 | (r##"{{ trim "foo" }}"##, r##"foo"##), 150 | (r##"{{ trim " foo" }}"##, r##"foo"##), 151 | (r##"{{ trim "foo " }}"##, r##"foo"##), 152 | (r##"{{ trim " foo " }}"##, r##"foo"##) 153 | ] 154 | } 155 | 156 | #[test] 157 | fn test_helper_trim_start() -> Result<(), Box> { 158 | assert_renders![ 159 | (r##"{{ trim_start "foo" }}"##, r##"foo"##), 160 | (r##"{{ trim_start " foo" }}"##, r##"foo"##), 161 | (r##"{{ trim_start "foo " }}"##, r##"foo "##), 162 | (r##"{{ trim_start " foo " }}"##, r##"foo "##) 163 | ] 164 | } 165 | 166 | #[test] 167 | fn test_helper_trim_end() -> Result<(), Box> { 168 | assert_renders![ 169 | (r##"{{ trim_end "foo" }}"##, r##"foo"##), 170 | (r##"{{ trim_end " foo" }}"##, r##" foo"##), 171 | (r##"{{ trim_end "foo " }}"##, r##"foo"##), 172 | (r##"{{ trim_end " foo " }}"##, r##" foo"##) 173 | ] 174 | } 175 | 176 | #[test] 177 | fn test_helper_quote() -> Result<(), Box> { 178 | assert_renders![ 179 | (r##"{{ quote "'" "''" }}"##, r##"'\'\''"##), 180 | (r##"{{ quote "'" "foo" }}"##, r##"'foo'"##), 181 | (r##"{{ quote "\"" "foo" }}"##, r##""foo""##), 182 | (r##"{{ quote "" "foo" }}"##, r##""foo""##), 183 | ] 184 | } 185 | 186 | #[test] 187 | fn test_helper_unquote() -> Result<(), Box> { 188 | assert_renders![ 189 | (r##"{{ unquote "''" }}"##, r##""##), 190 | (r##"{{ unquote "'f'" }}"##, r##"f"##), 191 | (r##"{{ unquote "foo" }}"##, r##"foo"##), 192 | (r##"{{ unquote "'foo'" }}"##, r##"foo"##), 193 | (r##"{{ unquote "\"foo\"" }}"##, r##"foo"##), 194 | (r##"{{ unquote "foo'" }}"##, r##"foo'"##), 195 | (r##"{{ unquote "'foo" }}"##, r##"'foo"##), 196 | ] 197 | } 198 | 199 | #[test] 200 | fn test_helper_replace() -> Result<(), Box> { 201 | assert_renders![(r##"{{ replace "foo" "oo" "aa"}}"##, r##"faa"##)] 202 | } 203 | 204 | #[test] 205 | fn test_helper_first_non_empty() -> Result<(), Box> { 206 | assert_renders![ 207 | (r##"{{ first_non_empty ""}}"##, r##""##), 208 | (r##"{{ first_non_empty "foo"}}"##, r##"foo"##), 209 | (r##"{{ first_non_empty "foo" "bar"}}"##, r##"foo"##), 210 | (r##"{{ first_non_empty "" "foo"}}"##, r##"foo"##), 211 | (r##"{{ first_non_empty "" "foo" "bar"}}"##, r##"foo"##), 212 | (r##"{{ first_non_empty "" null}}"##, r##""##), 213 | (r##"{{ first_non_empty "" null 33}}"##, r##""##), 214 | (r##"{{ first_non_empty "" null "foo" "bar"}}"##, r##"foo"##), 215 | ] 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/uuid_helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext}; 2 | use uuid::Uuid; 3 | 4 | fn uuid_new_v4_fct( 5 | _: &Helper, 6 | _: &Handlebars, 7 | _: &Context, 8 | _: &mut RenderContext, 9 | out: &mut dyn Output, 10 | ) -> HelperResult { 11 | out.write(Uuid::new_v4().to_string().as_str())?; 12 | Ok(()) 13 | } 14 | 15 | fn uuid_new_v7_fct( 16 | _: &Helper, 17 | _: &Handlebars, 18 | _: &Context, 19 | _: &mut RenderContext, 20 | out: &mut dyn Output, 21 | ) -> HelperResult { 22 | out.write(Uuid::new_v4().to_string().as_str())?; 23 | Ok(()) 24 | } 25 | 26 | pub fn register(handlebars: &mut Handlebars) { 27 | handlebars.register_helper("uuid_new_v4", Box::new(uuid_new_v4_fct)); 28 | handlebars.register_helper("uuid_new_v7", Box::new(uuid_new_v7_fct)); 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use crate::assert_renders; 34 | use std::error::Error; 35 | 36 | #[test] 37 | fn test_regex_captures() -> Result<(), Box> { 38 | assert_renders![ 39 | // (r##"{{ uuid_new_v4 }}"##, r##""##), 40 | (r##"{{ len (uuid_new_v4) }}"##, r##"36"##), 41 | // (r##"{{ uuid_new_v7 }}"##, r##""##), 42 | (r##"{{ len( uuid_new_v7 )}}"##, r##"36"##), 43 | ] 44 | } 45 | } 46 | --------------------------------------------------------------------------------