├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── bench.rs ├── examples ├── custom.rs ├── github.rs ├── simple.rs └── using_structs.rs └── src ├── action.rs ├── actions ├── constant.rs ├── getter │ ├── mod.rs │ └── namespace │ │ ├── errors.rs │ │ └── mod.rs ├── join.rs ├── len.rs ├── mod.rs ├── setter │ ├── errors.rs │ ├── mod.rs │ └── namespace │ │ ├── errors.rs │ │ └── mod.rs ├── strip.rs ├── sum.rs └── trim.rs ├── errors.rs ├── lib.rs ├── parser ├── action_parsers.rs ├── errors.rs └── mod.rs └── transformer.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | on: 3 | pull_request: 4 | types: [opened, edited, reopened, synchronize] 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | platform: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{ matrix.platform }} 11 | steps: 12 | - name: Install Rust Stable 13 | uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | 17 | - name: Install Cargo 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | components: clippy 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | - name: Cache cargo registry 27 | uses: actions/cache@v1 28 | with: 29 | path: ~/.cargo/registry 30 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 31 | 32 | - name: Cache cargo index 33 | uses: actions/cache@v1 34 | with: 35 | path: ~/.cargo/git 36 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 37 | 38 | - name: Cache cargo build 39 | uses: actions/cache@v1 40 | with: 41 | path: target 42 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 43 | 44 | - name: Lint 45 | uses: actions-rs/clippy@master 46 | with: 47 | args: --all-features --all-targets 48 | 49 | - name: Test 50 | run: cargo test --all-features -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.5.0] - 2021-10-23 10 | ### Added 11 | - New `len`, `strip`, `sum` and `trim` Actions. 12 | - Actions reference to README. 13 | 14 | ### Changed 15 | - Fixed README comments. 16 | - `Action` trait to return `Result>>` which allows passing of Value data by reference. 17 | - Benchmarks to use Criterion benchmark Groups. 18 | 19 | ## [0.4.0] - 2020-12-30 20 | ### Added 21 | - `Parser::add_action_parser` adding the ability to register `ActionParserFn` for custom `Actions`. 22 | 23 | ### Changed 24 | - Exposed internal `COMMA_SEP_RE` and `QUOTED_STR_RE` helper regexes for use in custom `ActionParserFn`'s. 25 | - Changed from `lazy_static!` to `once_cell`. 26 | 27 | 28 | ## [0.3.0] - 2020-12-14 29 | ### Added 30 | - `apply_from_slice` transform function. 31 | 32 | ### Changed 33 | - Updated to latest dependencies. 34 | - Made linter suggested improvements. 35 | 36 | ### Fixed 37 | - Added `Send + Sync` bounds to the `Action` trait allowing usage across threads. 38 | 39 | ## [0.2.0] - 2020-05-25 40 | ### Changed 41 | - Converted to use `thiserror`. 42 | - Reorganized code and exports. 43 | 44 | ## [0.1.1] - 2020-01-08 45 | ### Added 46 | - Repository to Cargo.toml 47 | 48 | ## [0.1.0] - 2020-01-08 49 | ### Added 50 | - Initial Release 51 | 52 | [Unreleased]: https://github.com/rust-playground/proteus/compare/v0.5.0...HEAD 53 | [0.5.0]: https://github.com/rust-playground/proteus/compare/v0.4.0...v0.5.0 54 | [0.4.0]: https://github.com/rust-playground/proteus/compare/v0.3.0...v0.4.0 55 | [0.3.0]: https://github.com/rust-playground/proteus/compare/da422a5dd82c9cca612c864a7d9905992bce8281...v0.3.0 56 | [0.2.0]: https://github.com/rust-playground/proteus/compare/e6563929efc6cefab9a7fc086a0b129f4690b94f...da422a5dd82c9cca612c864a7d9905992bce8281 57 | [0.1.1]: https://github.com/rust-playground/proteus/compare/606709bc2d10236b8bb59da7034c98a6f4fc1f3f...e6563929efc6cefab9a7fc086a0b129f4690b94f 58 | [0.1.0]: https://github.com/rust-playground/proteus/commit/606709bc2d10236b8bb59da7034c98a6f4fc1f3f -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Dean Karn "] 3 | description = "Proteus is intended to make dynamic transformation of data using serde serializable, deserialize using JSON and a JSON transformation syntax similar to Javascript JSON syntax. It also supports registering custom Actions to be used in the syntax." 4 | edition = "2018" 5 | keywords = [ 6 | "json", 7 | "transform", 8 | "transformation", 9 | ] 10 | license = "MIT OR Apache-2.0" 11 | name = "proteus" 12 | readme = "README.md" 13 | repository = "https://github.com/rust-playground/proteus" 14 | version = "0.5.0" 15 | 16 | [badges.travis-ci] 17 | repository = "rust-playground/proteus" 18 | 19 | [[bench]] 20 | harness = false 21 | name = "bench" 22 | 23 | [dependencies] 24 | regex = "1.5.4" 25 | serde_json = "1.0.68" 26 | typetag = "0.1.7" 27 | thiserror = "1.0.30" 28 | once_cell = "1.8.0" 29 | 30 | [dependencies.serde] 31 | features = ["derive"] 32 | version = "1.0.130" 33 | 34 | [dev-dependencies] 35 | criterion = "0.3.5" 36 | 37 | [lib] 38 | bench = false 39 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proteus   [![Build Status]][travis] [![Latest Version]][crates.io] 2 | 3 | [Build Status]: https://api.travis-ci.org/rust-playground/proteus.svg?branch=master 4 | [travis]: https://travis-ci.org/rust-playground/proteus 5 | [Latest Version]: https://img.shields.io/crates/v/proteus.svg 6 | [crates.io]: https://crates.io/crates/proteus 7 | 8 | **This library is intended to make dynamic transformation of data using serde serializable, deserialize using JSON and a 9 | JSON transformation syntax similar to Javascript JSON syntax. It supports registering custom Actions for use in syntax** 10 | 11 | --- 12 | 13 | ```toml 14 | [dependencies] 15 | proteus = "0.1" 16 | ``` 17 | 18 | ## Getter/Setter Syntax 19 | The Getter and Setter syntax is custom to support custom/dynamic Actions and nearly identical with the Setter having 20 | additional options. If other parsing syntax is desired it can be used to build the Transformation in the same way that 21 | is done internally. 22 | 23 | The transformation syntax is very similar to access JSON data in Javascript. 24 | To handle special characters such as `""`(blank), `[`, `]`, `"` and `.` you can use the explicit 25 | key syntax `["example[].blah"]` which would represent the key in the following JSON: 26 | ```json 27 | { 28 | "example[].blah" : "my value" 29 | } 30 | ``` 31 | 32 | **IMPORTANT:** order of operations is important. 33 | 34 | #### Getter 35 | | syntax | description | 36 | ---------|-------------| 37 | | | this will grab the top-level value which could be any valid type: Object, array, ... | 38 | | id | Gets a JSON Object's name. eg. key in HashMap | 39 | | [0] | Gets a JSON Arrays index at the specified index. | 40 | | profile.first_name | Combine Object names with dot notation. | 41 | | profile.address[0].street | Combinations using dot notation and indexes is also supported. | 42 | 43 | #### Setter 44 | | syntax | description | 45 | ---------|-------------| 46 | | | this will set the top-level value in the destination | 47 | | id | By itself any text is considered to be a JSON Object's name. | 48 | | [] | This appends the source **data** to an array, creating it if it doesn't exist and is only valid at the end of set syntax eg. profile.address[] | 49 | | [\+] | The source Array should append all of it's values into the destination Array and is only valid at the end of set syntax eg. profile.address[] | 50 | | [\-] | The source Array values should replace the destination Array's values at the overlapping indexes and is only valid at the end of set syntax eg. profile.address[] | 51 | | {} | This merges the supplied Object overtop of the existing and is only valid at the end of set syntax eg. profile{} | 52 | | profile.first_name | Combine Object names with dot notation. | 53 | | profile.address[0].street | Combinations using dot notation and indexes is also supported. | 54 | 55 | ## Example usages 56 | ```rust 57 | use proteus::{actions, TransformBuilder}; 58 | use std::error::Error; 59 | 60 | // This example show the basic usage of transformations 61 | fn main() -> Result<(), Box> { 62 | let input = r#" 63 | { 64 | "user_id":"111", 65 | "first_name":"Dean", 66 | "last_name":"Karn", 67 | "addresses": [ 68 | { "street":"26 Here Blvd", "postal":"123456", "country":"Canada", "primary":true }, 69 | { "street":"26 Lakeside Cottage Lane.", "postal":"654321", "country":"Canada" } 70 | ], 71 | "nested": { 72 | "inner":{ 73 | "key":"value" 74 | }, 75 | "my_arr":[null,"arr_value",null] 76 | } 77 | }"#; 78 | let trans = TransformBuilder::default() 79 | .add_actions(actions!( 80 | ("user_id", "id"), 81 | ( 82 | r#"join(" ", const("Mr."), first_name, last_name)"#, 83 | "full-name" 84 | ), 85 | ( 86 | r#"join(", ", addresses[0].street, addresses[0].postal, addresses[0].country)"#, 87 | "address" 88 | ), 89 | ("nested.inner.key", "prev_nested"), 90 | ("nested.my_arr", "my_arr"), 91 | (r#"const("arr_value_2")"#, "my_arr[]") 92 | )?) 93 | .build()?; 94 | let res = trans.apply_from_str(input)?; 95 | println!("{}", serde_json::to_string_pretty(&res)?); 96 | Ok(()) 97 | } 98 | ``` 99 | 100 | or when you want to do struct to struct transformations 101 | 102 | ```rust 103 | use proteus::{actions, TransformBuilder}; 104 | use serde::{Deserialize, Serialize}; 105 | use std::error::Error; 106 | 107 | #[derive(Serialize)] 108 | struct KV { 109 | pub key: String, 110 | } 111 | 112 | #[derive(Serialize)] 113 | struct Nested { 114 | pub inner: KV, 115 | pub my_arr: Vec>, 116 | } 117 | 118 | #[derive(Serialize)] 119 | struct Address { 120 | pub street: String, 121 | pub postal: String, 122 | pub country: String, 123 | } 124 | 125 | #[derive(Serialize)] 126 | struct RawUserInfo { 127 | pub user_id: String, 128 | pub first_name: String, 129 | pub last_name: String, 130 | pub addresses: Vec
, 131 | pub nested: Nested, 132 | } 133 | 134 | #[derive(Serialize, Deserialize)] 135 | struct User { 136 | pub id: String, 137 | #[serde(rename = "full-name")] 138 | pub full_name: String, 139 | pub address: String, 140 | pub prev_nested: String, 141 | pub my_arr: Vec>, 142 | } 143 | 144 | // This example show the basic usage of transformations 145 | fn main() -> Result<(), Box> { 146 | let input = RawUserInfo { 147 | user_id: "111".to_string(), 148 | first_name: "Dean".to_string(), 149 | last_name: "Karn".to_string(), 150 | addresses: vec![ 151 | Address { 152 | street: "26 Here Blvd".to_string(), 153 | postal: "123456".to_string(), 154 | country: "Canada".to_string(), 155 | }, 156 | Address { 157 | street: "26 Lakeside Cottage Lane.".to_string(), 158 | postal: "654321".to_string(), 159 | country: "Canada".to_string(), 160 | }, 161 | ], 162 | nested: Nested { 163 | inner: KV { 164 | key: "value".to_string(), 165 | }, 166 | my_arr: vec![None, Some("arr_value".to_owned()), None], 167 | }, 168 | }; 169 | let trans = TransformBuilder::default() 170 | .add_actions(actions!( 171 | ("user_id", "id"), 172 | ( 173 | r#"join(" ", const("Mr."), first_name, last_name)"#, 174 | "full-name" 175 | ), 176 | ( 177 | r#"join(", ", addresses[0].street, addresses[0].postal, addresses[0].country)"#, 178 | "address" 179 | ), 180 | ("nested.inner.key", "prev_nested"), 181 | ("nested.my_arr", "my_arr"), 182 | (r#"const("arr_value_2")"#, "my_arr[]") 183 | )?) 184 | .build()?; 185 | let res: User = trans.apply_to(input)?; 186 | println!("{}", serde_json::to_string_pretty(&res)?); 187 | Ok(()) 188 | } 189 | ``` 190 | 191 | #### Actions 192 | The following are the supported actions. 193 | 194 | |action|description| 195 | |------|-----------| 196 | |const("Mr.")|Is used to define a constant value.| 197 | |join(",", const("Mr."), first_name, last_name)|Joins one or more using the provided separator| 198 | |len(array_field)|Returns the length of a string, array or an object(by number of keys).| 199 | |strip_start("v", key)|Strips the provided prefix from string values.| 200 | |strip_end("v", key)|Strips the provided suffix from string values.| 201 | |sum(cost, taxes, const(1))|Sums one or more provided values.| 202 | |trim(key)|Trim the start and end whitespace from strings.| 203 | |trim_start(key)|Trim the start whitespace from strings.| 204 | |trim_end(key)|Trim the end whitespace from strings.| 205 | 206 | 207 | #### License 208 | 209 | 210 | Licensed under either of Apache License, Version 211 | 2.0 or MIT license at your option. 212 | 213 | 214 |
215 | 216 | 217 | Unless you explicitly state otherwise, any contribution intentionally submitted 218 | for inclusion in Proteus by you, as defined in the Apache-2.0 license, shall be 219 | dual licensed as above, without any additional terms or conditions. 220 | 221 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use criterion::{Criterion, Throughput}; 5 | use proteus::{actions, TransformBuilder}; 6 | use serde_json::Value; 7 | 8 | fn criterion_benchmark(c: &mut Criterion) { 9 | let mut group = c.benchmark_group("top_level"); 10 | for (name, input, trans) in [ 11 | ( 12 | "1", 13 | r#"{"top": "value"}"#, 14 | TransformBuilder::default() 15 | .add_actions(actions!(("top", "new")).unwrap()) 16 | .build() 17 | .unwrap(), 18 | ), 19 | ( 20 | "10", 21 | r#" 22 | { 23 | "top1": "value", 24 | "top2": "value", 25 | "top3": "value", 26 | "top4": "value", 27 | "top5": "value", 28 | "top6": "value", 29 | "top7": "value", 30 | "top8": "value", 31 | "top9": "value", 32 | "top10": "value" 33 | }"#, 34 | TransformBuilder::default() 35 | .add_actions( 36 | actions!( 37 | ("top1", "new1"), 38 | ("top2", "new2"), 39 | ("top3", "new3"), 40 | ("top4", "new4"), 41 | ("top5", "new5"), 42 | ("top6", "new6"), 43 | ("top7", "new7"), 44 | ("top8", "new8"), 45 | ("top9", "new9"), 46 | ("top10", "new10") 47 | ) 48 | .unwrap(), 49 | ) 50 | .build() 51 | .unwrap(), 52 | ), 53 | ( 54 | "20", 55 | r#" 56 | { 57 | "top1": "value", 58 | "top2": "value", 59 | "top3": "value", 60 | "top4": "value", 61 | "top5": "value", 62 | "top6": "value", 63 | "top7": "value", 64 | "top8": "value", 65 | "top9": "value", 66 | "top10": "value", 67 | "top11": "value", 68 | "top12": "value", 69 | "top13": "value", 70 | "top14": "value", 71 | "top15": "value", 72 | "top16": "value", 73 | "top17": "value", 74 | "top18": "value", 75 | "top19": "value", 76 | "top20": "value" 77 | }"#, 78 | TransformBuilder::default() 79 | .add_actions( 80 | actions!( 81 | ("top1", "new1"), 82 | ("top2", "new2"), 83 | ("top3", "new3"), 84 | ("top4", "new4"), 85 | ("top5", "new5"), 86 | ("top6", "new6"), 87 | ("top7", "new7"), 88 | ("top8", "new8"), 89 | ("top9", "new9"), 90 | ("top10", "new10"), 91 | ("top11", "new11"), 92 | ("top12", "new12"), 93 | ("top13", "new13"), 94 | ("top14", "new14"), 95 | ("top15", "new15"), 96 | ("top16", "new16"), 97 | ("top17", "new17"), 98 | ("top18", "new18"), 99 | ("top19", "new19"), 100 | ("top20", "new20") 101 | ) 102 | .unwrap(), 103 | ) 104 | .build() 105 | .unwrap(), 106 | ), 107 | ] 108 | .iter() 109 | { 110 | let source: Value = serde_json::from_str(input).unwrap(); 111 | group.throughput(Throughput::Bytes(input.len() as u64)); 112 | group.bench_function(*name, |b| { 113 | b.iter(|| { 114 | let _res = trans.apply(&source); 115 | }) 116 | }); 117 | } 118 | group.finish(); 119 | 120 | let mut group = c.benchmark_group("constant"); 121 | for (name, input, trans) in [ 122 | ( 123 | "string", 124 | "null", 125 | TransformBuilder::default() 126 | .add_actions(actions!((r#"const("Dean Karn")"#, "string")).unwrap()) 127 | .build() 128 | .unwrap(), 129 | ), 130 | ( 131 | "number", 132 | "null", 133 | TransformBuilder::default() 134 | .add_actions(actions!((r#"const(1)"#, "number")).unwrap()) 135 | .build() 136 | .unwrap(), 137 | ), 138 | ( 139 | "array", 140 | "null", 141 | TransformBuilder::default() 142 | .add_actions(actions!((r#"const([1, 2, 3])"#, "array")).unwrap()) 143 | .build() 144 | .unwrap(), 145 | ), 146 | ( 147 | "object", 148 | "null", 149 | TransformBuilder::default() 150 | .add_actions(actions!((r#"const({"key": "value"})"#, "object")).unwrap()) 151 | .build() 152 | .unwrap(), 153 | ), 154 | ] 155 | .iter() 156 | { 157 | let source: Value = serde_json::from_str(input).unwrap(); 158 | group.throughput(Throughput::Bytes(input.len() as u64)); 159 | group.bench_function(*name, |b| { 160 | b.iter(|| { 161 | let _res = trans.apply(&source); 162 | }) 163 | }); 164 | } 165 | group.finish(); 166 | 167 | let mut group = c.benchmark_group("join"); 168 | for (name, input, trans) in [ 169 | ( 170 | "2", 171 | r#" 172 | { 173 | "first_name": "Dean", 174 | "last_name": "Karn", 175 | "meta": { 176 | "middle_name":"Peter" 177 | } 178 | }"#, 179 | TransformBuilder::default() 180 | .add_actions( 181 | actions!((r#"join(" ", first_name, last_name)"#, "full_name")).unwrap(), 182 | ) 183 | .build() 184 | .unwrap(), 185 | ), 186 | ( 187 | "3", 188 | r#" 189 | { 190 | "first_name": "Dean", 191 | "last_name": "Karn", 192 | "meta": { 193 | "middle_name":"Peter" 194 | } 195 | }"#, 196 | TransformBuilder::default() 197 | .add_actions( 198 | actions!(( 199 | r#"join(" ", const("Mr."), first_name, last_name)"#, 200 | "full_name" 201 | )) 202 | .unwrap(), 203 | ) 204 | .build() 205 | .unwrap(), 206 | ), 207 | ( 208 | "4", 209 | r#" 210 | { 211 | "first_name": "Dean", 212 | "last_name": "Karn", 213 | "meta": { 214 | "middle_name":"Peter" 215 | } 216 | }"#, 217 | TransformBuilder::default() 218 | .add_actions( 219 | actions!(( 220 | r#"join(" ", const("Mr."), first_name, meta.middle_name, last_name)"#, 221 | "full_name" 222 | )) 223 | .unwrap(), 224 | ) 225 | .build() 226 | .unwrap(), 227 | ), 228 | ] 229 | .iter() 230 | { 231 | let source: Value = serde_json::from_str(input).unwrap(); 232 | group.throughput(Throughput::Bytes(input.len() as u64)); 233 | group.bench_function(*name, |b| { 234 | b.iter(|| { 235 | let _res = trans.apply(&source); 236 | }) 237 | }); 238 | } 239 | group.finish(); 240 | 241 | let mut group = c.benchmark_group("len"); 242 | for (name, input, trans) in [ 243 | ( 244 | "string", 245 | r#""Dean Karn""#, 246 | TransformBuilder::default() 247 | .add_actions(actions!(("len()", "string")).unwrap()) 248 | .build() 249 | .unwrap(), 250 | ), 251 | ( 252 | "array", 253 | r#"[1, 2, 3]"#, 254 | TransformBuilder::default() 255 | .add_actions(actions!(("len()", "array")).unwrap()) 256 | .build() 257 | .unwrap(), 258 | ), 259 | ( 260 | "object", 261 | r#"{"key": "value"}"#, 262 | TransformBuilder::default() 263 | .add_actions(actions!(("len()", "object")).unwrap()) 264 | .build() 265 | .unwrap(), 266 | ), 267 | ] 268 | .iter() 269 | { 270 | let source: Value = serde_json::from_str(input).unwrap(); 271 | group.throughput(Throughput::Bytes(input.len() as u64)); 272 | group.bench_function(*name, |b| { 273 | b.iter(|| { 274 | let _res = trans.apply(&source); 275 | }) 276 | }); 277 | } 278 | group.finish(); 279 | 280 | let mut group = c.benchmark_group("sum"); 281 | for (name, input, trans) in [ 282 | ( 283 | "constant", 284 | "null", 285 | TransformBuilder::default() 286 | .add_actions(actions!(("sum(const(1))", "sum")).unwrap()) 287 | .build() 288 | .unwrap(), 289 | ), 290 | ( 291 | "two_numbers", 292 | r#"{"key1": 1, "key2": 1}"#, 293 | TransformBuilder::default() 294 | .add_actions(actions!(("sum(key1, key2)", "sum")).unwrap()) 295 | .build() 296 | .unwrap(), 297 | ), 298 | ( 299 | "array", 300 | "[1, 2, 3]", 301 | TransformBuilder::default() 302 | .add_actions(actions!(("sum()", "sum")).unwrap()) 303 | .build() 304 | .unwrap(), 305 | ), 306 | ] 307 | .iter() 308 | { 309 | let source: Value = serde_json::from_str(input).unwrap(); 310 | group.throughput(Throughput::Bytes(input.len() as u64)); 311 | group.bench_function(*name, |b| { 312 | b.iter(|| { 313 | let _res = trans.apply(&source); 314 | }) 315 | }); 316 | } 317 | group.finish(); 318 | 319 | let mut group = c.benchmark_group("trim"); 320 | for (name, input, trans) in [ 321 | ( 322 | "start_and_end", 323 | r#"{"key":" value "}"#, 324 | TransformBuilder::default() 325 | .add_actions(actions!(("trim(key)", "trim")).unwrap()) 326 | .build() 327 | .unwrap(), 328 | ), 329 | ( 330 | "start", 331 | r#"{"key":" value "}"#, 332 | TransformBuilder::default() 333 | .add_actions(actions!(("trim_start(key)", "trim_start")).unwrap()) 334 | .build() 335 | .unwrap(), 336 | ), 337 | ( 338 | "end", 339 | r#"{"key":" value "}"#, 340 | TransformBuilder::default() 341 | .add_actions(actions!(("trim_end(key)", "trim_end")).unwrap()) 342 | .build() 343 | .unwrap(), 344 | ), 345 | ] 346 | .iter() 347 | { 348 | let source: Value = serde_json::from_str(input).unwrap(); 349 | group.throughput(Throughput::Bytes(input.len() as u64)); 350 | group.bench_function(*name, |b| { 351 | b.iter(|| { 352 | let _res = trans.apply(&source); 353 | }) 354 | }); 355 | } 356 | group.finish(); 357 | 358 | let mut group = c.benchmark_group("strip"); 359 | for (name, input, trans) in [ 360 | ( 361 | "prefix", 362 | r#"{"key":"value"}"#, 363 | TransformBuilder::default() 364 | .add_actions(actions!((r#"strip_prefix("v", key)"#, "prefix")).unwrap()) 365 | .build() 366 | .unwrap(), 367 | ), 368 | ( 369 | "suffix", 370 | r#"{"key":"value"}"#, 371 | TransformBuilder::default() 372 | .add_actions(actions!((r#"strip_suffix("e", key)"#, "suffix")).unwrap()) 373 | .build() 374 | .unwrap(), 375 | ), 376 | ] 377 | .iter() 378 | { 379 | let source: Value = serde_json::from_str(input).unwrap(); 380 | group.throughput(Throughput::Bytes(input.len() as u64)); 381 | group.bench_function(*name, |b| { 382 | b.iter(|| { 383 | let _res = trans.apply(&source); 384 | }) 385 | }); 386 | } 387 | group.finish(); 388 | } 389 | 390 | criterion_group!(benches, criterion_benchmark); 391 | criterion_main!(benches); 392 | -------------------------------------------------------------------------------- /examples/custom.rs: -------------------------------------------------------------------------------- 1 | use proteus::action::Action; 2 | use proteus::parser::Error; 3 | use proteus::{actions, Parser, TransformBuilder}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use std::borrow::Cow; 7 | use std::ops::Deref; 8 | 9 | // This example shows how to create, register and use a custom Action 10 | fn main() -> Result<(), Box> { 11 | proteus::Parser::add_action_parser("custom", &parse_custom)?; 12 | 13 | let input = get_input(); 14 | let trans = TransformBuilder::default() 15 | .add_actions(actions!((r#"custom(id)"#, "custom_id"))?) 16 | .build()?; 17 | let res = trans.apply_from_str(input)?; 18 | println!("{}", serde_json::to_string_pretty(&res)?); 19 | Ok(()) 20 | } 21 | 22 | fn get_input() -> &'static str { 23 | r#"{"id": "01234"}"# 24 | } 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | pub struct CustomAction { 28 | action: Box, 29 | } 30 | 31 | impl CustomAction { 32 | pub fn new(action: Box) -> Self { 33 | Self { action } 34 | } 35 | } 36 | 37 | #[typetag::serde] 38 | impl Action for CustomAction { 39 | fn apply<'a>( 40 | &self, 41 | _source: &'a Value, 42 | _destination: &mut Value, 43 | ) -> Result>, proteus::Error> { 44 | match self.action.apply(_source, _destination) { 45 | Ok(v) => match v { 46 | None => Ok(None), 47 | Some(v) => match v.deref() { 48 | Value::String(s) => Ok(Some(Cow::Owned(Value::String( 49 | s.to_owned() + " from my custom function", 50 | )))), 51 | _ => Ok(Some(Cow::Owned(Value::String( 52 | v.to_string() + " from my custom function", 53 | )))), 54 | }, 55 | }, 56 | Err(e) => Err(e), 57 | } 58 | } 59 | } 60 | 61 | fn parse_custom(val: &str) -> Result, Error> { 62 | if val.is_empty() { 63 | Err(Error::MissingActionValue("custom".to_owned())) 64 | } else { 65 | let inner_action = Parser::parse_action(val)?; 66 | Ok(Box::new(CustomAction::new(inner_action))) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/github.rs: -------------------------------------------------------------------------------- 1 | use proteus::{actions, TransformBuilder}; 2 | use std::error::Error; 3 | 4 | // This example parses a real GitHub PR example into a succinct action 5 | fn main() -> Result<(), Box> { 6 | let github_pull_request = get_github_pr(); 7 | let trans = TransformBuilder::default() 8 | .add_actions(actions!( 9 | ("pull_request.user.login", "username"), 10 | ("pull_request.user.url", "user"), 11 | ("repository.url", "repo"), 12 | ("pull_request.url", "pr"), 13 | ("action", "pr_action"), 14 | ("pull_request.body", "description"), 15 | ("pull_request.created_at", "timestamp"), 16 | ("pull_request.requested_reviewers", "persons_involved"), 17 | ("pull_request.user.login", "persons_involved[]") 18 | )?) 19 | .build()?; 20 | let res = trans.apply_from_str(github_pull_request)?; 21 | println!("{}", serde_json::to_string_pretty(&res)?); 22 | Ok(()) 23 | } 24 | 25 | fn get_github_pr() -> &'static str { 26 | r#"{ 27 | "action": "opened", 28 | "number": 2, 29 | "pull_request": { 30 | "url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2", 31 | "id": 279147437, 32 | "node_id": "MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3", 33 | "html_url": "https://github.com/Codertocat/Hello-World/pull/2", 34 | "diff_url": "https://github.com/Codertocat/Hello-World/pull/2.diff", 35 | "patch_url": "https://github.com/Codertocat/Hello-World/pull/2.patch", 36 | "issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2", 37 | "number": 2, 38 | "state": "open", 39 | "locked": false, 40 | "title": "Update the README with new information.", 41 | "user": { 42 | "login": "Codertocat", 43 | "id": 21031067, 44 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 45 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 46 | "gravatar_id": "", 47 | "url": "https://api.github.com/users/Codertocat", 48 | "html_url": "https://github.com/Codertocat", 49 | "followers_url": "https://api.github.com/users/Codertocat/followers", 50 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 51 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 52 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 53 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 54 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 55 | "repos_url": "https://api.github.com/users/Codertocat/repos", 56 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 57 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 58 | "type": "User", 59 | "site_admin": false 60 | }, 61 | "body": "This is a pretty simple change that we need to pull into master.", 62 | "created_at": "2019-05-15T15:20:33Z", 63 | "updated_at": "2019-05-15T15:20:33Z", 64 | "closed_at": null, 65 | "merged_at": null, 66 | "merge_commit_sha": null, 67 | "assignee": null, 68 | "assignees": [ 69 | 70 | ], 71 | "requested_reviewers": [ 72 | 73 | ], 74 | "requested_teams": [ 75 | 76 | ], 77 | "labels": [ 78 | 79 | ], 80 | "milestone": null, 81 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits", 82 | "review_comments_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments", 83 | "review_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}", 84 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments", 85 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821", 86 | "head": { 87 | "label": "Codertocat:changes", 88 | "ref": "changes", 89 | "sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", 90 | "user": { 91 | "login": "Codertocat", 92 | "id": 21031067, 93 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 94 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 95 | "gravatar_id": "", 96 | "url": "https://api.github.com/users/Codertocat", 97 | "html_url": "https://github.com/Codertocat", 98 | "followers_url": "https://api.github.com/users/Codertocat/followers", 99 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 100 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 101 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 102 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 103 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 104 | "repos_url": "https://api.github.com/users/Codertocat/repos", 105 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 106 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 107 | "type": "User", 108 | "site_admin": false 109 | }, 110 | "repo": { 111 | "id": 186853002, 112 | "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", 113 | "name": "Hello-World", 114 | "full_name": "Codertocat/Hello-World", 115 | "private": false, 116 | "owner": { 117 | "login": "Codertocat", 118 | "id": 21031067, 119 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 120 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 121 | "gravatar_id": "", 122 | "url": "https://api.github.com/users/Codertocat", 123 | "html_url": "https://github.com/Codertocat", 124 | "followers_url": "https://api.github.com/users/Codertocat/followers", 125 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 126 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 127 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 128 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 129 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 130 | "repos_url": "https://api.github.com/users/Codertocat/repos", 131 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 132 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 133 | "type": "User", 134 | "site_admin": false 135 | }, 136 | "html_url": "https://github.com/Codertocat/Hello-World", 137 | "description": null, 138 | "fork": false, 139 | "url": "https://api.github.com/repos/Codertocat/Hello-World", 140 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 141 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 142 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 143 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 144 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 145 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 146 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 147 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 148 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 149 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 150 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 151 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 152 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 153 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 154 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 155 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 156 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 157 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 158 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 159 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 160 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 161 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 162 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 163 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 164 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 165 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 166 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 167 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 168 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 169 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 170 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 171 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 172 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 173 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 174 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 175 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 176 | "created_at": "2019-05-15T15:19:25Z", 177 | "updated_at": "2019-05-15T15:19:27Z", 178 | "pushed_at": "2019-05-15T15:20:32Z", 179 | "git_url": "git://github.com/Codertocat/Hello-World.git", 180 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 181 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 182 | "svn_url": "https://github.com/Codertocat/Hello-World", 183 | "homepage": null, 184 | "size": 0, 185 | "stargazers_count": 0, 186 | "watchers_count": 0, 187 | "language": null, 188 | "has_issues": true, 189 | "has_projects": true, 190 | "has_downloads": true, 191 | "has_wiki": true, 192 | "has_pages": true, 193 | "forks_count": 0, 194 | "mirror_url": null, 195 | "archived": false, 196 | "disabled": false, 197 | "open_issues_count": 2, 198 | "license": null, 199 | "forks": 0, 200 | "open_issues": 2, 201 | "watchers": 0, 202 | "default_branch": "master" 203 | } 204 | }, 205 | "base": { 206 | "label": "Codertocat:master", 207 | "ref": "master", 208 | "sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e", 209 | "user": { 210 | "login": "Codertocat", 211 | "id": 21031067, 212 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 213 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 214 | "gravatar_id": "", 215 | "url": "https://api.github.com/users/Codertocat", 216 | "html_url": "https://github.com/Codertocat", 217 | "followers_url": "https://api.github.com/users/Codertocat/followers", 218 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 219 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 220 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 221 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 222 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 223 | "repos_url": "https://api.github.com/users/Codertocat/repos", 224 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 225 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 226 | "type": "User", 227 | "site_admin": false 228 | }, 229 | "repo": { 230 | "id": 186853002, 231 | "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", 232 | "name": "Hello-World", 233 | "full_name": "Codertocat/Hello-World", 234 | "private": false, 235 | "owner": { 236 | "login": "Codertocat", 237 | "id": 21031067, 238 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 239 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 240 | "gravatar_id": "", 241 | "url": "https://api.github.com/users/Codertocat", 242 | "html_url": "https://github.com/Codertocat", 243 | "followers_url": "https://api.github.com/users/Codertocat/followers", 244 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 245 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 246 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 247 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 248 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 249 | "repos_url": "https://api.github.com/users/Codertocat/repos", 250 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 251 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 252 | "type": "User", 253 | "site_admin": false 254 | }, 255 | "html_url": "https://github.com/Codertocat/Hello-World", 256 | "description": null, 257 | "fork": false, 258 | "url": "https://api.github.com/repos/Codertocat/Hello-World", 259 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 260 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 261 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 262 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 263 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 264 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 265 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 266 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 267 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 268 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 269 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 270 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 271 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 272 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 273 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 274 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 275 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 276 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 277 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 278 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 279 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 280 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 281 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 282 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 283 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 284 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 285 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 286 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 287 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 288 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 289 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 290 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 291 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 292 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 293 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 294 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 295 | "created_at": "2019-05-15T15:19:25Z", 296 | "updated_at": "2019-05-15T15:19:27Z", 297 | "pushed_at": "2019-05-15T15:20:32Z", 298 | "git_url": "git://github.com/Codertocat/Hello-World.git", 299 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 300 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 301 | "svn_url": "https://github.com/Codertocat/Hello-World", 302 | "homepage": null, 303 | "size": 0, 304 | "stargazers_count": 0, 305 | "watchers_count": 0, 306 | "language": null, 307 | "has_issues": true, 308 | "has_projects": true, 309 | "has_downloads": true, 310 | "has_wiki": true, 311 | "has_pages": true, 312 | "forks_count": 0, 313 | "mirror_url": null, 314 | "archived": false, 315 | "disabled": false, 316 | "open_issues_count": 2, 317 | "license": null, 318 | "forks": 0, 319 | "open_issues": 2, 320 | "watchers": 0, 321 | "default_branch": "master" 322 | } 323 | }, 324 | "_links": { 325 | "self": { 326 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2" 327 | }, 328 | "html": { 329 | "href": "https://github.com/Codertocat/Hello-World/pull/2" 330 | }, 331 | "issue": { 332 | "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2" 333 | }, 334 | "comments": { 335 | "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments" 336 | }, 337 | "review_comments": { 338 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments" 339 | }, 340 | "review_comment": { 341 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}" 342 | }, 343 | "commits": { 344 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits" 345 | }, 346 | "statuses": { 347 | "href": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821" 348 | } 349 | }, 350 | "author_association": "OWNER", 351 | "draft": false, 352 | "merged": false, 353 | "mergeable": null, 354 | "rebaseable": null, 355 | "mergeable_state": "unknown", 356 | "merged_by": null, 357 | "comments": 0, 358 | "review_comments": 0, 359 | "maintainer_can_modify": false, 360 | "commits": 1, 361 | "additions": 1, 362 | "deletions": 1, 363 | "changed_files": 1 364 | }, 365 | "repository": { 366 | "id": 186853002, 367 | "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", 368 | "name": "Hello-World", 369 | "full_name": "Codertocat/Hello-World", 370 | "private": false, 371 | "owner": { 372 | "login": "Codertocat", 373 | "id": 21031067, 374 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 375 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 376 | "gravatar_id": "", 377 | "url": "https://api.github.com/users/Codertocat", 378 | "html_url": "https://github.com/Codertocat", 379 | "followers_url": "https://api.github.com/users/Codertocat/followers", 380 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 381 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 382 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 383 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 384 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 385 | "repos_url": "https://api.github.com/users/Codertocat/repos", 386 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 387 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 388 | "type": "User", 389 | "site_admin": false 390 | }, 391 | "html_url": "https://github.com/Codertocat/Hello-World", 392 | "description": null, 393 | "fork": false, 394 | "url": "https://api.github.com/repos/Codertocat/Hello-World", 395 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", 396 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", 397 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", 398 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", 399 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", 400 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", 401 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", 402 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", 403 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", 404 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", 405 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", 406 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", 407 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", 408 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", 409 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", 410 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", 411 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", 412 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", 413 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", 414 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", 415 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", 416 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", 417 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", 418 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", 419 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", 420 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", 421 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", 422 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", 423 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", 424 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", 425 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", 426 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", 427 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", 428 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", 429 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", 430 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", 431 | "created_at": "2019-05-15T15:19:25Z", 432 | "updated_at": "2019-05-15T15:19:27Z", 433 | "pushed_at": "2019-05-15T15:20:32Z", 434 | "git_url": "git://github.com/Codertocat/Hello-World.git", 435 | "ssh_url": "git@github.com:Codertocat/Hello-World.git", 436 | "clone_url": "https://github.com/Codertocat/Hello-World.git", 437 | "svn_url": "https://github.com/Codertocat/Hello-World", 438 | "homepage": null, 439 | "size": 0, 440 | "stargazers_count": 0, 441 | "watchers_count": 0, 442 | "language": null, 443 | "has_issues": true, 444 | "has_projects": true, 445 | "has_downloads": true, 446 | "has_wiki": true, 447 | "has_pages": true, 448 | "forks_count": 0, 449 | "mirror_url": null, 450 | "archived": false, 451 | "disabled": false, 452 | "open_issues_count": 2, 453 | "license": null, 454 | "forks": 0, 455 | "open_issues": 2, 456 | "watchers": 0, 457 | "default_branch": "master" 458 | }, 459 | "sender": { 460 | "login": "Codertocat", 461 | "id": 21031067, 462 | "node_id": "MDQ6VXNlcjIxMDMxMDY3", 463 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", 464 | "gravatar_id": "", 465 | "url": "https://api.github.com/users/Codertocat", 466 | "html_url": "https://github.com/Codertocat", 467 | "followers_url": "https://api.github.com/users/Codertocat/followers", 468 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", 469 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", 470 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", 471 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", 472 | "organizations_url": "https://api.github.com/users/Codertocat/orgs", 473 | "repos_url": "https://api.github.com/users/Codertocat/repos", 474 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", 475 | "received_events_url": "https://api.github.com/users/Codertocat/received_events", 476 | "type": "User", 477 | "site_admin": false 478 | } 479 | }"# 480 | } 481 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use proteus::{actions, TransformBuilder}; 2 | use std::error::Error; 3 | 4 | // This example show the basic usage of transformations 5 | fn main() -> Result<(), Box> { 6 | let input = r#" 7 | { 8 | "user_id":"111", 9 | "first_name":"Dean", 10 | "last_name":"Karn", 11 | "addresses": [ 12 | { "street":"26 Here Blvd", "postal":"123456", "country":"Canada", "primary":true }, 13 | { "street":"26 Lakeside Cottage Lane.", "postal":"654321", "country":"Canada" } 14 | ], 15 | "nested": { 16 | "inner":{ 17 | "key":"value" 18 | }, 19 | "my_arr":[null,"arr_value",null] 20 | } 21 | }"#; 22 | let trans = TransformBuilder::default() 23 | .add_actions(actions!( 24 | ("user_id", "id"), 25 | ( 26 | r#"join(" ", const("Mr."), first_name, last_name)"#, 27 | "full-name" 28 | ), 29 | ( 30 | r#"join(", ", addresses[0].street, addresses[0].postal, addresses[0].country)"#, 31 | "address" 32 | ), 33 | ("nested.inner.key", "prev_nested"), 34 | ("nested.my_arr", "my_arr"), 35 | (r#"const("arr_value_2")"#, "my_arr[]"), 36 | (r#"len(nested)"#, "z_amount_of_nested_data"), 37 | (r#"sum(const(1.1), len(nested))"#, "zz_sum_nested") 38 | )?) 39 | .build()?; 40 | let res = trans.apply_from_str(input)?; 41 | println!("{}", serde_json::to_string_pretty(&res)?); 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/using_structs.rs: -------------------------------------------------------------------------------- 1 | use proteus::{actions, TransformBuilder}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::error::Error; 4 | 5 | #[derive(Serialize)] 6 | struct KV { 7 | pub key: String, 8 | } 9 | 10 | #[derive(Serialize)] 11 | struct Nested { 12 | pub inner: KV, 13 | pub my_arr: Vec>, 14 | } 15 | 16 | #[derive(Serialize)] 17 | struct Address { 18 | pub street: String, 19 | pub postal: String, 20 | pub country: String, 21 | } 22 | 23 | #[derive(Serialize)] 24 | struct RawUserInfo { 25 | pub user_id: String, 26 | pub first_name: String, 27 | pub last_name: String, 28 | pub addresses: Vec
, 29 | pub nested: Nested, 30 | } 31 | 32 | #[derive(Serialize, Deserialize)] 33 | struct User { 34 | pub id: String, 35 | #[serde(rename = "full-name")] 36 | pub full_name: String, 37 | pub address: String, 38 | pub prev_nested: String, 39 | pub my_arr: Vec>, 40 | } 41 | 42 | // This example show the basic usage of transformations 43 | fn main() -> Result<(), Box> { 44 | let input = RawUserInfo { 45 | user_id: "111".to_string(), 46 | first_name: "Dean".to_string(), 47 | last_name: "Karn".to_string(), 48 | addresses: vec![ 49 | Address { 50 | street: "26 Here Blvd".to_string(), 51 | postal: "123456".to_string(), 52 | country: "Canada".to_string(), 53 | }, 54 | Address { 55 | street: "26 Lakeside Cottage Lane.".to_string(), 56 | postal: "654321".to_string(), 57 | country: "Canada".to_string(), 58 | }, 59 | ], 60 | nested: Nested { 61 | inner: KV { 62 | key: "value".to_string(), 63 | }, 64 | my_arr: vec![None, Some("arr_value".to_owned()), None], 65 | }, 66 | }; 67 | let trans = TransformBuilder::default() 68 | .add_actions(actions!( 69 | ("user_id", "id"), 70 | ( 71 | r#"join(" ", const("Mr."), first_name, last_name)"#, 72 | "full-name" 73 | ), 74 | ( 75 | r#"join(", ", addresses[0].street, addresses[0].postal, addresses[0].country)"#, 76 | "address" 77 | ), 78 | ("nested.inner.key", "prev_nested"), 79 | ("nested.my_arr", "my_arr"), 80 | (r#"const("arr_value_2")"#, "my_arr[]") 81 | )?) 82 | .build()?; 83 | let res: User = trans.apply_to(input)?; 84 | println!("{}", serde_json::to_string_pretty(&res)?); 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | //! Action trait and definitions. 2 | 3 | use crate::errors::Error; 4 | use serde_json::Value; 5 | use std::borrow::Cow; 6 | use std::fmt::Debug; 7 | 8 | /// An action represents an operation to be carried out on a serde_json::Value object. 9 | #[typetag::serde(tag = "type")] 10 | pub trait Action: Send + Sync + Debug { 11 | fn apply<'a>( 12 | &'a self, 13 | source: &'a Value, 14 | destination: &mut Value, 15 | ) -> Result>, Error>; 16 | } 17 | -------------------------------------------------------------------------------- /src/actions/constant.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::errors::Error; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::borrow::Cow; 6 | 7 | /// This type represents an [Action](../action/trait.Action.html) which returns a constant Value 8 | /// instead of it originating from the source JSON data. 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct Constant { 11 | value: Value, 12 | } 13 | 14 | impl Constant { 15 | pub const fn new(value: Value) -> Self { 16 | Self { value } 17 | } 18 | } 19 | 20 | #[typetag::serde] 21 | impl Action for Constant { 22 | fn apply<'a>( 23 | &'a self, 24 | _source: &'a Value, 25 | _destination: &mut Value, 26 | ) -> Result>, Error> { 27 | Ok(Some(Cow::Borrowed(&self.value))) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/actions/getter/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod namespace; 2 | 3 | use crate::action::Action; 4 | use crate::errors::Error; 5 | use namespace::Namespace; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use std::borrow::Cow; 9 | 10 | /// This type represents an [Action](../action/trait.Action.html) which extracts data from the 11 | /// source JSON Value. 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct Getter { 14 | namespace: Vec, 15 | } 16 | 17 | impl Getter { 18 | pub fn new(namespace: Vec) -> Self { 19 | Self { namespace } 20 | } 21 | } 22 | 23 | #[typetag::serde] 24 | impl Action for Getter { 25 | fn apply<'a>( 26 | &self, 27 | source: &'a Value, 28 | _destination: &mut Value, 29 | ) -> Result>, Error> { 30 | let mut current = source; 31 | for ns in &self.namespace { 32 | current = match expand(ns, current)? { 33 | Some(value) => value, 34 | None => return Ok(None), 35 | }; 36 | } 37 | Ok(Some(Cow::Borrowed(current))) 38 | } 39 | } 40 | 41 | #[inline] 42 | fn expand<'a>(ns: &Namespace, current: &'a Value) -> Result, Error> { 43 | match current { 44 | Value::Object(o) => match ns { 45 | Namespace::Object { id } => Ok(o.get(id)), 46 | _ => Ok(None), 47 | }, 48 | Value::Array(arr) => match ns { 49 | Namespace::Array { index } => Ok(arr.get(*index)), 50 | _ => Ok(None), 51 | }, 52 | _ => Ok(None), 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | use serde_json::{json, Map}; 60 | 61 | #[test] 62 | fn key_value() -> Result<(), Box> { 63 | let ns = Namespace::parse("key")?; 64 | let input = json!({"key":"value"}); 65 | let mut output = Value::Object(Map::new()); 66 | let getter = Getter::new(ns); 67 | let res = getter.apply(&input, &mut output)?; 68 | assert_eq!(res, Some(Cow::Owned(Value::String("value".into())))); 69 | Ok(()) 70 | } 71 | 72 | #[test] 73 | fn array_value_in_object() -> Result<(), Box> { 74 | let ns = Namespace::parse("my_array[0]")?; 75 | let input = json!({ 76 | "existing_key":"my_val1", 77 | "my_array":["value"] 78 | }); 79 | let mut output = Value::Object(Map::new()); 80 | let getter = Getter::new(ns); 81 | let res = getter.apply(&input, &mut output)?; 82 | assert_eq!(res, Some(Cow::Owned(Value::String("value".into())))); 83 | Ok(()) 84 | } 85 | 86 | #[test] 87 | fn array_value_in_array() -> Result<(), Box> { 88 | let ns = Namespace::parse("[0][0]")?; 89 | let input = json!([["value"]]); 90 | let mut output = Value::Object(Map::new()); 91 | let getter = Getter::new(ns); 92 | let res = getter.apply(&input, &mut output)?; 93 | assert_eq!(res, Some(Cow::Owned(Value::String("value".into())))); 94 | Ok(()) 95 | } 96 | 97 | #[test] 98 | fn array_in_array() -> Result<(), Box> { 99 | let ns = Namespace::parse("[0]")?; 100 | let input = json!([["value"]]); 101 | let mut output = Value::Object(Map::new()); 102 | let getter = Getter::new(ns); 103 | let res = getter.apply(&input, &mut output)?; 104 | assert_eq!(res, Some(Cow::Owned(json!(["value"])))); 105 | Ok(()) 106 | } 107 | 108 | #[test] 109 | fn object_value_in_array() -> Result<(), Box> { 110 | let ns = Namespace::parse("[0].key")?; 111 | let input = json!([{"key":"value"}]); 112 | let mut output = Value::Object(Map::new()); 113 | let getter = Getter::new(ns); 114 | let res = getter.apply(&input, &mut output)?; 115 | assert_eq!(res, Some(Cow::Owned(json!("value")))); 116 | Ok(()) 117 | } 118 | 119 | #[test] 120 | fn array_value_in_object_in_array() -> Result<(), Box> { 121 | let ns = Namespace::parse("[0].key[1]")?; 122 | let input = json!([{"key":[null,"value"]}]); 123 | let mut output = Value::Object(Map::new()); 124 | let getter = Getter::new(ns); 125 | let res = getter.apply(&input, &mut output)?; 126 | assert_eq!(res, Some(Cow::Owned(json!("value")))); 127 | Ok(()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/actions/getter/namespace/errors.rs: -------------------------------------------------------------------------------- 1 | use std::num::ParseIntError; 2 | use thiserror::Error; 3 | 4 | /// This type represents all possible errors that an occur while parsing transformation syntax to generate a [Namespace](enum.Namespace.html) to be used in [Getter](../struct.Getter.html). 5 | #[derive(Error, Debug)] 6 | pub enum Error { 7 | #[error("Invalid '.' notation for namespace: {}. {}", ns, err)] 8 | InvalidDotNotation { err: String, ns: String }, 9 | 10 | #[error(transparent)] 11 | InvalidNamespaceArrayIndex(#[from] ParseIntError), 12 | 13 | #[error("Missing end bracket ']' in array index for namespace: {0}")] 14 | MissingArrayIndexBracket(String), 15 | 16 | #[error("Invalid Explicit Key Syntax for namespace {0}. Explicit Key Syntax must start with '[\"' and end with '\"]' with any enclosed '\"' escaped.")] 17 | InvalidExplicitKeySyntax(String), 18 | } 19 | -------------------------------------------------------------------------------- /src/actions/getter/namespace/mod.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | 3 | pub use errors::Error; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use std::fmt::{Display, Formatter}; 7 | 8 | /// Represents a single group/level of JSON structures used for traversing JSON structures. 9 | /// 10 | /// # Example 11 | /// ```json 12 | /// { 13 | /// "test" : { "value" : "my value" } 14 | /// } 15 | /// ``` 16 | /// `test.value` would be represented by two Namespace Object's `test` and `value` as a way to 17 | /// traverse the JSON data to point at `my value`. 18 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 19 | pub enum Namespace { 20 | /// Represents an id/location within the source data's Object 21 | Object { id: String }, 22 | 23 | /// Represents an index/location within the source data's JSON Array. 24 | Array { index: usize }, 25 | } 26 | 27 | impl Display for Namespace { 28 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 29 | match self { 30 | Namespace::Object { id } => write!(f, "{}", id), 31 | Namespace::Array { index } => write!(f, "[{}]", index), 32 | } 33 | } 34 | } 35 | 36 | impl Namespace { 37 | /// parses a transformation syntax string into an Vec of [Namespace](enum.Namespace.html)'s for 38 | /// use in the [Getter](../struct.Getter.html). 39 | /// 40 | /// The transformation syntax is very similar to access JSON data in Javascript. 41 | /// 42 | /// To handle special characters such as ``(blank), `[`, `]`, `"` and `.` you can use the explicit 43 | /// key syntax `["example[].blah"]` which would represent the key in the following JSON: 44 | /// ```json 45 | /// { 46 | /// "example[].blah" : "my value" 47 | /// } 48 | /// ``` 49 | pub fn parse(input: &str) -> Result, Error> { 50 | if input.is_empty() { 51 | return Ok(Vec::new()); 52 | } 53 | 54 | let bytes = input.as_bytes(); 55 | let mut namespaces = Vec::new(); 56 | let mut idx = 0; 57 | let mut s = Vec::with_capacity(10); 58 | 59 | 'outer: while idx < bytes.len() { 60 | let b = bytes[idx]; 61 | match b { 62 | b'.' => { 63 | if s.is_empty() { 64 | // empty values must be via explicit key 65 | // might also be ending to other types eg. array. 66 | if idx == 0 || idx + 1 == bytes.len() { 67 | // cannot start with '.', if want a blank key must use explicit key syntax 68 | return Err(Error::InvalidDotNotation { 69 | ns: input.to_owned(), 70 | err: r#"Namespace cannot start or end with '.', explicit key syntax of '[""]' must be used to denote a blank key."#.to_owned(), 71 | }); 72 | } 73 | idx += 1; 74 | continue; 75 | } 76 | namespaces.push(Namespace::Object { 77 | id: unsafe { String::from_utf8_unchecked(s.clone()) }, 78 | }); 79 | s.clear(); 80 | idx += 1; 81 | continue; 82 | } 83 | b'[' => { 84 | if !s.is_empty() { 85 | // this syntax named[..] lets create the object 86 | namespaces.push(Namespace::Object { 87 | id: unsafe { String::from_utf8_unchecked(s.clone()) }, 88 | }); 89 | s.clear(); 90 | } 91 | idx += 1; 92 | if idx >= bytes.len() { 93 | // error incomplete namespace 94 | return Err(Error::MissingArrayIndexBracket(input.to_owned())); 95 | } 96 | return match bytes[idx] { 97 | b'"' => { 98 | // parse explicit key 99 | idx += 1; 100 | while idx < bytes.len() { 101 | let b = bytes[idx]; 102 | match b { 103 | b'"' if bytes[idx - 1] != b'\\' => { 104 | idx += 1; 105 | if bytes[idx] != b']' { 106 | // error invalid explicit key syntax 107 | return Err(Error::InvalidExplicitKeySyntax( 108 | input.to_owned(), 109 | )); 110 | } 111 | namespaces.push(Namespace::Object { 112 | id: unsafe { String::from_utf8_unchecked(s.clone()) } 113 | .replace("\\", ""), // unescape required escaped double quotes 114 | }); 115 | s.clear(); 116 | idx += 1; 117 | continue 'outer; 118 | } 119 | _ => { 120 | idx += 1; 121 | s.push(b) 122 | } 123 | }; 124 | } 125 | // error never reached the end bracket of explicit key 126 | Err(Error::InvalidExplicitKeySyntax(input.to_owned())) 127 | } 128 | _ => { 129 | // parse array index 130 | while idx < bytes.len() { 131 | let b = bytes[idx]; 132 | match b { 133 | b']' => { 134 | namespaces.push(Namespace::Array { 135 | index: unsafe { 136 | String::from_utf8_unchecked(s.clone()) 137 | } 138 | .parse()?, 139 | }); 140 | s.clear(); 141 | idx += 1; 142 | continue 'outer; 143 | } 144 | _ => { 145 | idx += 1; 146 | s.push(b) 147 | } 148 | }; 149 | } 150 | // error no end bracket 151 | Err(Error::MissingArrayIndexBracket(input.to_owned())) 152 | } 153 | }; 154 | } 155 | _ => { 156 | s.push(b); 157 | idx += 1; 158 | } 159 | }; 160 | } 161 | 162 | if !s.is_empty() { 163 | namespaces.push(Namespace::Object { 164 | id: unsafe { String::from_utf8_unchecked(s) }, 165 | }); 166 | } 167 | Ok(namespaces) 168 | } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::*; 174 | 175 | #[test] 176 | fn test_namespace() { 177 | let ns = "embedded.array[0][1]"; 178 | let results = Namespace::parse(ns).unwrap(); 179 | let expected = vec![ 180 | Namespace::Object { 181 | id: String::from("embedded"), 182 | }, 183 | Namespace::Object { 184 | id: String::from("array"), 185 | }, 186 | Namespace::Array { index: 0 }, 187 | Namespace::Array { index: 1 }, 188 | ]; 189 | assert_eq!(results, expected); 190 | } 191 | 192 | #[test] 193 | fn test_simple() { 194 | let ns = "field"; 195 | let results = Namespace::parse(ns).unwrap(); 196 | let expected = vec![Namespace::Object { 197 | id: String::from("field"), 198 | }]; 199 | assert_eq!(results, expected); 200 | 201 | let ns = "array-field[0]"; 202 | let results = Namespace::parse(ns).unwrap(); 203 | let expected = vec![ 204 | Namespace::Object { 205 | id: String::from("array-field"), 206 | }, 207 | Namespace::Array { index: 0 }, 208 | ]; 209 | assert_eq!(results, expected); 210 | } 211 | 212 | #[test] 213 | fn test_blank() { 214 | let ns = ""; 215 | let results = Namespace::parse(ns).unwrap(); 216 | let expected: Vec = Vec::new(); 217 | assert_eq!(results, expected); 218 | } 219 | 220 | #[test] 221 | fn test_blank_field() { 222 | let ns = ".field"; 223 | let results = Namespace::parse(ns); 224 | assert!(results.is_err()); 225 | let actual = matches!(results.err().unwrap(), Error::InvalidDotNotation { .. }); 226 | assert!(actual); 227 | 228 | let ns = r#"[""].field"#; 229 | let results = Namespace::parse(ns).unwrap(); 230 | let expected = vec![ 231 | Namespace::Object { 232 | id: String::from(""), 233 | }, 234 | Namespace::Object { 235 | id: String::from("field"), 236 | }, 237 | ]; 238 | assert_eq!(results, expected); 239 | } 240 | 241 | #[test] 242 | fn test_blank_array() { 243 | let ns = ".[0]"; 244 | let results = Namespace::parse(ns); 245 | assert!(results.is_err()); 246 | let actual = matches!(results.err().unwrap(), Error::InvalidDotNotation { .. }); 247 | assert!(actual); 248 | 249 | let ns = r#"[""].[0]"#; 250 | let results = Namespace::parse(ns).unwrap(); 251 | let expected = vec![ 252 | Namespace::Object { 253 | id: String::from(""), 254 | }, 255 | Namespace::Array { index: 0 }, 256 | ]; 257 | assert_eq!(expected, results); 258 | 259 | let ns = r#"[""][0]"#; 260 | let results = Namespace::parse(ns).unwrap(); 261 | let expected = vec![ 262 | Namespace::Object { 263 | id: String::from(""), 264 | }, 265 | Namespace::Array { index: 0 }, 266 | ]; 267 | assert_eq!(expected, results); 268 | 269 | let ns = ".named[0]"; 270 | let results = Namespace::parse(ns); 271 | assert!(results.is_err()); 272 | let actual = matches!(results.err().unwrap(), Error::InvalidDotNotation { .. }); 273 | assert!(actual); 274 | 275 | let ns = r#"[""].named[0]"#; 276 | let results = Namespace::parse(ns).unwrap(); 277 | let expected = vec![ 278 | Namespace::Object { 279 | id: String::from(""), 280 | }, 281 | Namespace::Object { 282 | id: String::from("named"), 283 | }, 284 | Namespace::Array { index: 0 }, 285 | ]; 286 | assert_eq!(expected, results); 287 | } 288 | 289 | #[test] 290 | fn test_array_blank() { 291 | let ns = "[0]."; 292 | let results = Namespace::parse(ns); 293 | assert!(results.is_err()); 294 | let actual = matches!(results.err().unwrap(), Error::InvalidDotNotation { .. }); 295 | assert!(actual); 296 | 297 | let ns = "[0]"; 298 | let results = Namespace::parse(ns).unwrap(); 299 | let expected = vec![Namespace::Array { index: 0 }]; 300 | assert_eq!(expected, results); 301 | } 302 | 303 | #[test] 304 | fn test_array_named() { 305 | let ns = "[0].named"; 306 | let results = Namespace::parse(ns).unwrap(); 307 | let expected = vec![ 308 | Namespace::Array { index: 0 }, 309 | Namespace::Object { 310 | id: String::from("named"), 311 | }, 312 | ]; 313 | assert_eq!(expected, results); 314 | } 315 | 316 | #[test] 317 | fn test_explicit_key() { 318 | let ns = r#"["embedded.array[0][1]"]"#; 319 | let results = Namespace::parse(ns).unwrap(); 320 | let expected = vec![Namespace::Object { 321 | id: String::from("embedded.array[0][1]"), 322 | }]; 323 | assert_eq!(expected, results); 324 | } 325 | 326 | #[test] 327 | fn test_explicit_key_array() { 328 | let ns = r#"["embedded.array[0][1]"][0]"#; 329 | let results = Namespace::parse(ns).unwrap(); 330 | let expected = vec![ 331 | Namespace::Object { 332 | id: String::from("embedded.array[0][1]"), 333 | }, 334 | Namespace::Array { index: 0 }, 335 | ]; 336 | assert_eq!(expected, results); 337 | } 338 | 339 | #[test] 340 | fn test_explicit_key_nested() { 341 | let ns = r#"name.["embedded.array[0][1]"]"#; 342 | let results = Namespace::parse(ns).unwrap(); 343 | let expected = vec![ 344 | Namespace::Object { 345 | id: "name".to_owned(), 346 | }, 347 | Namespace::Object { 348 | id: String::from("embedded.array[0][1]"), 349 | }, 350 | ]; 351 | assert_eq!(expected, results); 352 | 353 | let ns = r#"name.["embedded.array[0][1]"][0]"#; 354 | let results = Namespace::parse(ns).unwrap(); 355 | let expected = vec![ 356 | Namespace::Object { 357 | id: "name".to_owned(), 358 | }, 359 | Namespace::Object { 360 | id: "embedded.array[0][1]".to_owned(), 361 | }, 362 | Namespace::Array { index: 0 }, 363 | ]; 364 | assert_eq!(expected, results); 365 | 366 | let ns = r#"["embedded.array[0][1]"][0]"#; 367 | let results = Namespace::parse(ns).unwrap(); 368 | let expected = vec![ 369 | Namespace::Object { 370 | id: "embedded.array[0][1]".to_owned(), 371 | }, 372 | Namespace::Array { index: 0 }, 373 | ]; 374 | assert_eq!(expected, results); 375 | 376 | let ns = r#"[1].["embedded.array[0][1]"][0]"#; 377 | let results = Namespace::parse(ns).unwrap(); 378 | let expected = vec![ 379 | Namespace::Array { index: 1 }, 380 | Namespace::Object { 381 | id: "embedded.array[0][1]".to_owned(), 382 | }, 383 | Namespace::Array { index: 0 }, 384 | ]; 385 | assert_eq!(expected, results); 386 | 387 | let ns = r#"named[1].["embedded.array[0][1]"][0]"#; 388 | let results = Namespace::parse(ns).unwrap(); 389 | let expected = vec![ 390 | Namespace::Object { 391 | id: "named".to_owned(), 392 | }, 393 | Namespace::Array { index: 1 }, 394 | Namespace::Object { 395 | id: "embedded.array[0][1]".to_owned(), 396 | }, 397 | Namespace::Array { index: 0 }, 398 | ]; 399 | assert_eq!(expected, results); 400 | 401 | let ns = r#"named[1].["embedded.array[0][1]"]"#; 402 | let results = Namespace::parse(ns).unwrap(); 403 | let expected = vec![ 404 | Namespace::Object { 405 | id: "named".to_owned(), 406 | }, 407 | Namespace::Array { index: 1 }, 408 | Namespace::Object { 409 | id: "embedded.array[0][1]".to_owned(), 410 | }, 411 | ]; 412 | assert_eq!(expected, results); 413 | 414 | let ns = r#"["name()"].name"#; 415 | let results = Namespace::parse(ns).unwrap(); 416 | let expected = vec![ 417 | Namespace::Object { 418 | id: "name()".to_owned(), 419 | }, 420 | Namespace::Object { 421 | id: String::from("name"), 422 | }, 423 | ]; 424 | assert_eq!(expected, results); 425 | } 426 | 427 | #[test] 428 | fn test_explicit_key_quotes() { 429 | let ns = r#"["""]"#; 430 | let results = Namespace::parse(ns); 431 | assert!(results.is_err()); 432 | let actual = matches!(results.err().unwrap(), Error::InvalidExplicitKeySyntax { .. }); 433 | assert!(actual); 434 | 435 | let ns = r#"["\""]"#; 436 | let results = Namespace::parse(ns).unwrap(); 437 | let expected = vec![Namespace::Object { 438 | id: r#"""#.to_owned(), 439 | }]; 440 | assert_eq!(expected, results); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/actions/join.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::errors::Error; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::borrow::Cow; 6 | use std::ops::Deref; 7 | 8 | /// This type represents an [Action](../action/trait.Action.html) which joins two or more Value's 9 | /// separated by the provided `sep` and returns a Value::String(String). 10 | /// 11 | /// This also works with non-string types but they will be converted into a string prior to joining. 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct Join { 14 | sep: String, 15 | values: Vec>, 16 | } 17 | 18 | impl Join { 19 | pub fn new(sep: String, values: Vec>) -> Self { 20 | Self { sep, values } 21 | } 22 | } 23 | 24 | #[typetag::serde] 25 | impl Action for Join { 26 | fn apply<'a>( 27 | &self, 28 | source: &'a Value, 29 | destination: &mut Value, 30 | ) -> Result>, Error> { 31 | let l = self.values.len() - 1; 32 | let mut result = String::new(); 33 | for (i, v) in self.values.iter().enumerate() { 34 | match v.apply(source, destination)? { 35 | Some(v) => { 36 | match v.deref() { 37 | Value::String(s) => { 38 | if s.is_empty() { 39 | continue; 40 | } 41 | result.push_str(s); 42 | } 43 | _ => { 44 | let s = v.to_string(); 45 | if s.is_empty() { 46 | continue; 47 | } 48 | result.push_str(&s); 49 | } 50 | }; 51 | if i != l { 52 | result.push_str(&self.sep); 53 | } 54 | } 55 | None => continue, 56 | }; 57 | } 58 | 59 | if result.is_empty() { 60 | return Ok(None); 61 | } 62 | Ok(Some(Cow::Owned(Value::String(result)))) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/actions/len.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::errors::Error; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::borrow::Cow; 6 | use std::ops::Deref; 7 | 8 | /// This type represents an [Action](../action/trait.Action.html) which returns the length of a 9 | /// String, Array or Object.. 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub struct Len { 12 | action: Box, 13 | } 14 | 15 | impl Len { 16 | pub fn new(action: Box) -> Self { 17 | Len { action } 18 | } 19 | } 20 | 21 | #[typetag::serde] 22 | impl Action for Len { 23 | fn apply<'a>( 24 | &'a self, 25 | source: &'a Value, 26 | destination: &mut Value, 27 | ) -> Result>, Error> { 28 | match self.action.apply(source, destination)? { 29 | Some(v) => match v.deref() { 30 | Value::String(s) => Ok(Some(Cow::Owned(Value::Number(s.len().into())))), 31 | Value::Array(arr) => Ok(Some(Cow::Owned(Value::Number(arr.len().into())))), 32 | Value::Object(o) => Ok(Some(Cow::Owned(Value::Number(o.len().into())))), 33 | _ => Ok(None), 34 | }, 35 | None => Ok(None), 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Actions that impl the [Action](action/trait.Action.html) trait. 2 | 3 | mod constant; 4 | pub mod getter; 5 | mod join; 6 | mod len; 7 | pub mod setter; 8 | mod strip; 9 | mod sum; 10 | mod trim; 11 | 12 | #[doc(inline)] 13 | pub use constant::Constant; 14 | 15 | #[doc(inline)] 16 | pub use getter::Getter; 17 | 18 | #[doc(inline)] 19 | pub use join::Join; 20 | 21 | #[doc(inline)] 22 | pub use len::Len; 23 | 24 | #[doc(inline)] 25 | pub use sum::Sum; 26 | 27 | #[doc(inline)] 28 | pub use trim::{Trim, Type as TrimType}; 29 | 30 | #[doc(inline)] 31 | pub use strip::{Strip, Type as StripType}; 32 | 33 | #[doc(inline)] 34 | pub use setter::Setter; 35 | -------------------------------------------------------------------------------- /src/actions/setter/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// This type represents all possible errors that an occur while applying a transformation. 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | #[error("Invalid destination type. {0}")] 7 | InvalidDestinationType(String), 8 | } 9 | -------------------------------------------------------------------------------- /src/actions/setter/mod.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | pub mod namespace; 3 | 4 | pub use errors::Error; 5 | 6 | use crate::action::Action; 7 | use crate::actions::setter::namespace::Namespace; 8 | use crate::actions::setter::Error as SetterError; 9 | use crate::errors::Error as CrateErr; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_json::{Map, Value}; 12 | use std::borrow::Cow; 13 | 14 | /// This type represents an [Action](../action/trait.Action.html) which sets data to the 15 | /// destination JSON Value. 16 | #[derive(Debug, Serialize, Deserialize)] 17 | pub struct Setter { 18 | namespace: Vec, 19 | child: Box, 20 | } 21 | 22 | impl Setter { 23 | pub fn new(namespace: Vec, child: Box) -> Self { 24 | Self { namespace, child } 25 | } 26 | } 27 | 28 | #[typetag::serde] 29 | impl Action for Setter { 30 | fn apply<'a>( 31 | &self, 32 | source: &'a Value, 33 | destination: &mut Value, 34 | ) -> Result>, CrateErr> { 35 | if let Some(field) = self.child.apply(source, destination)? { 36 | let field = field.into_owned(); 37 | let mut current = destination; 38 | for ns in &self.namespace { 39 | match ns { 40 | Namespace::Object { id } => { 41 | match current { 42 | Value::Object(o) => { 43 | current = o.entry(id.clone()).or_insert(Value::Null); 44 | } 45 | Value::Null => { 46 | let mut o = Map::new(); 47 | o.insert(id.clone(), Value::Null); 48 | *current = Value::Object(o); 49 | current = current.as_object_mut().unwrap().get_mut(id).unwrap(); 50 | } 51 | _ => { 52 | return Err(SetterError::InvalidDestinationType(format!( 53 | "Attempting to set an Object by id to an {:?}", 54 | current 55 | )) 56 | .into()) 57 | } 58 | }; 59 | } 60 | Namespace::Array { index } => { 61 | let index = *index; 62 | match current { 63 | Value::Array(arr) => { 64 | if index >= arr.len() { 65 | arr.resize_with(index + 1, Value::default); 66 | arr[index] = Value::Null; 67 | } 68 | current = &mut arr[index]; 69 | } 70 | Value::Null => { 71 | *current = Value::Array(vec![Value::Null; index + 1]); 72 | current = &mut current.as_array_mut().unwrap()[index]; 73 | } 74 | _ => { 75 | return Err(SetterError::InvalidDestinationType(format!( 76 | "Attempting to set an Array by index to an {:?}", 77 | current 78 | )) 79 | .into()) 80 | } 81 | }; 82 | } 83 | Namespace::AppendArray => { 84 | match current { 85 | Value::Array(arr) => { 86 | arr.push(Value::Null); 87 | current = arr.last_mut().unwrap(); 88 | } 89 | Value::Null => { 90 | let arr = vec![Value::Null]; 91 | *current = Value::Array(arr); 92 | current = current.as_array_mut().unwrap().last_mut().unwrap(); 93 | } 94 | _ => { 95 | return Err(SetterError::InvalidDestinationType(format!( 96 | "Attempting to append an {:?} to an Array", 97 | current 98 | )) 99 | .into()) 100 | } 101 | }; 102 | } 103 | Namespace::MergeObject => { 104 | return match field { 105 | Value::Object(mut o) => match current { 106 | Value::Object(existing) => { 107 | existing.append(&mut o); 108 | Ok(None) 109 | } 110 | Value::Null => { 111 | let mut new = Map::new(); 112 | new.append(&mut o); 113 | *current = Value::Object(new); 114 | Ok(None) 115 | } 116 | _ => Err(SetterError::InvalidDestinationType(format!( 117 | "Attempting to merge an Object with and {:?}", 118 | current 119 | )) 120 | .into()), 121 | }, 122 | _ => Err(SetterError::InvalidDestinationType(format!( 123 | "Attempting to merge {:?} with an Object", 124 | field 125 | )) 126 | .into()), 127 | }; 128 | } 129 | Namespace::MergeArray => { 130 | return match field { 131 | Value::Array(arr) => match current { 132 | Value::Array(existing) => { 133 | if arr.len() > existing.len() { 134 | *existing = arr; 135 | return Ok(None); 136 | } 137 | for (i, v) in arr.into_iter().enumerate() { 138 | existing[i] = v.clone(); 139 | } 140 | Ok(None) 141 | } 142 | Value::Null => { 143 | *current = Value::Array(arr); 144 | Ok(None) 145 | } 146 | _ => Err(SetterError::InvalidDestinationType(format!( 147 | "Attempting to merge an Array with and {:?}", 148 | current 149 | )) 150 | .into()), 151 | }, 152 | _ => Err(SetterError::InvalidDestinationType(format!( 153 | "Attempting to merge {:?} with an Array", 154 | field 155 | )) 156 | .into()), 157 | }; 158 | } 159 | Namespace::CombineArray => { 160 | return match field { 161 | Value::Array(mut arr) => match current { 162 | Value::Array(existing) => { 163 | existing.append(&mut arr); 164 | Ok(None) 165 | } 166 | Value::Null => { 167 | *current = Value::Array(arr); 168 | Ok(None) 169 | } 170 | _ => Err(SetterError::InvalidDestinationType(format!( 171 | "Attempting to combine an Array with and {:?}", 172 | current 173 | )) 174 | .into()), 175 | }, 176 | _ => Err(SetterError::InvalidDestinationType(format!( 177 | "Attempting to merge {:?} with an Array", 178 | field 179 | )) 180 | .into()), 181 | }; 182 | } 183 | }; 184 | } 185 | *current = field; 186 | } 187 | Ok(None) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/actions/setter/namespace/errors.rs: -------------------------------------------------------------------------------- 1 | use std::num::ParseIntError; 2 | use thiserror::Error; 3 | 4 | /// This type represents all possible errors that an occur while parsing transformation syntax to generate a [Namespace](enum.Namespace.html) to be used in [Setter](../struct.Setter.html). 5 | #[derive(Error, Debug)] 6 | pub enum Error { 7 | #[error("Invalid '.' notation for namespace: {}. {}", ns, err)] 8 | InvalidDotNotation { err: String, ns: String }, 9 | 10 | #[error(transparent)] 11 | InvalidNamespaceArrayIndex(#[from] ParseIntError), 12 | 13 | #[error("Missing end bracket ']' in array index for namespace: {0}")] 14 | MissingArrayIndexBracket(String), 15 | 16 | #[error("Invalid Merge Object Syntax for namespace: {0}. Merge Object Syntax must be exactly '{{}}' and is only valid at the end of the namespace.")] 17 | InvalidMergeObjectSyntax(String), 18 | 19 | #[error("Invalid Merge Array Syntax for namespace: {0}. Merge Array Syntax must be exactly '[-]' and is only valid at the end of the namespace.")] 20 | InvalidMergeArraySyntax(String), 21 | 22 | #[error("Invalid Combine Array Syntax for namespace: {0}. Combine Array Syntax must be exactly '[+]' and is only valid at the end of the namespace.")] 23 | InvalidCombineArraySyntax(String), 24 | 25 | #[error("Invalid Explicit Key Syntax for namespace {0}. Explicit Key Syntax must start with '[\"' and end with '\"]' with any enclosed '\"' escaped.")] 26 | InvalidExplicitKeySyntax(String), 27 | } 28 | -------------------------------------------------------------------------------- /src/actions/setter/namespace/mod.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | 3 | pub use errors::Error; 4 | 5 | use crate::actions::setter::namespace::Error as SetterErr; 6 | use serde::{Deserialize, Serialize}; 7 | use std::fmt::{Display, Formatter}; 8 | 9 | /// Represents a single group/level of JSON structures used for traversing JSON structures. 10 | /// 11 | /// # Example 12 | /// ```json 13 | /// { 14 | /// "test" : { "value" : "my value" } 15 | /// } 16 | /// ``` 17 | /// `test.value` would be represented by two Namespace Object's `test` and `value` as a way to 18 | /// traverse the JSON data to point at `my value`. 19 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 20 | pub enum Namespace { 21 | /// Represents an id/location for an Object within the destination data. 22 | Object { id: String }, 23 | 24 | /// Represents that the [Setter](../struct.Setter.html) should merge the source and destination 25 | /// JSON Objects. 26 | MergeObject, 27 | 28 | /// Represents an index/location for an Array within the destination data. 29 | Array { index: usize }, 30 | 31 | /// Represents that the [Setter](../struct.Setter.html) should append the source data to the 32 | /// destination JSON Array. 33 | AppendArray, 34 | 35 | /// Represents that the [Setter](../struct.Setter.html) should merge the source and destination 36 | /// JSON Arrays. 37 | MergeArray, 38 | 39 | /// Represents that the [Setter](../struct.Setter.html) should combine the source JSON Array to 40 | /// the destination JSON Array by appending all array elements from the source Array to the 41 | /// destinations. 42 | CombineArray, 43 | } 44 | 45 | impl Display for Namespace { 46 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 47 | match self { 48 | Namespace::Object { id } => write!(f, "{}", id), 49 | Namespace::MergeObject => write!(f, "{{}}"), 50 | Namespace::AppendArray => write!(f, "[]"), 51 | Namespace::MergeArray => write!(f, "[-]"), 52 | Namespace::CombineArray => write!(f, "[+]"), 53 | Namespace::Array { index } => write!(f, "[{}]", index), 54 | } 55 | } 56 | } 57 | 58 | impl Namespace { 59 | /// parses a transformation syntax string into an Vec of [Namespace](enum.Namespace.html)'s for 60 | /// use in the [Setter](../struct.Setter.html). 61 | /// 62 | /// The transformation syntax is very similar to access JSON data in Javascript with a few additions: 63 | /// * `{}` eg. test.value{} which denotes that the source Object and destination Object `value` should merge their data instead of the source replace the destination value 64 | /// * `[]` eg. test.value[] which denotes that the source data should be appended to the Array `value` rather than replacing the destination value. 65 | /// * `[+]` eg. test.value[+] which denotes that the source Array should append all of it's values onto the destination Array. 66 | /// * `[-]` eg. test.value[-] which denotes that the source Array values should replace the destination Array's values at the overlapping indexes. 67 | /// NOTE: `{}`, `[+]` and `[-]` can only be used on the last element of the Namespace syntax. 68 | /// 69 | /// To handle special characters such as ``(blank), `[`, `]`, `"` and `.` you can use the explicit 70 | /// key syntax `["example[].blah"]` which would represent the key in the following JSON: 71 | /// ```json 72 | /// { 73 | /// "example[].blah" : "my value" 74 | /// } 75 | /// ``` 76 | pub fn parse(input: &str) -> Result, SetterErr> { 77 | if input.is_empty() { 78 | return Ok(Vec::new()); 79 | } 80 | 81 | let bytes = input.as_bytes(); 82 | let mut namespaces = Vec::new(); 83 | let mut idx = 0; 84 | let mut s = Vec::with_capacity(10); 85 | 86 | 'outer: while idx < bytes.len() { 87 | let b = bytes[idx]; 88 | match b { 89 | b'.' => { 90 | if s.is_empty() { 91 | // empty values must be via explicit key 92 | // might also be ending to other types eg. array. 93 | if idx == 0 || idx + 1 == bytes.len() { 94 | // cannot start with '.', if want a blank key must use explicit key syntax 95 | return Err(Error::InvalidDotNotation { 96 | ns: input.to_owned(), 97 | err: r#"Namespace cannot start or end with '.', explicit key syntax of '[""]' must be used to denote a blank key."#.to_owned(), 98 | }); 99 | } 100 | idx += 1; 101 | continue; 102 | } 103 | namespaces.push(Namespace::Object { 104 | id: unsafe { String::from_utf8_unchecked(s.clone()) }, 105 | }); 106 | s.clear(); 107 | idx += 1; 108 | continue; 109 | } 110 | b'{' => { 111 | if !s.is_empty() { 112 | namespaces.push(Namespace::Object { 113 | id: unsafe { String::from_utf8_unchecked(s.clone()) }, 114 | }); 115 | s.clear(); 116 | } 117 | // merge object syntax 118 | idx += 1; 119 | if idx < bytes.len() && bytes[idx] != b'}' { 120 | // error invalid merge object syntax 121 | return Err(Error::InvalidMergeObjectSyntax(input.to_owned())); 122 | } 123 | idx += 1; 124 | if idx != bytes.len() { 125 | // error merge object must be the last part in the namespace. 126 | return Err(Error::InvalidMergeObjectSyntax(input.to_owned())); 127 | } 128 | namespaces.push(Namespace::MergeObject); 129 | } 130 | b'[' => { 131 | if !s.is_empty() { 132 | // this syntax named[..] lets create the object 133 | namespaces.push(Namespace::Object { 134 | id: unsafe { String::from_utf8_unchecked(s.clone()) }, 135 | }); 136 | s.clear(); 137 | } 138 | idx += 1; 139 | if idx >= bytes.len() { 140 | // error incomplete namespace 141 | return Err(Error::MissingArrayIndexBracket(input.to_owned())); 142 | } 143 | match bytes[idx] { 144 | b'"' => { 145 | // parse explicit key 146 | idx += 1; 147 | while idx < bytes.len() { 148 | let b = bytes[idx]; 149 | match b { 150 | b'"' if bytes[idx - 1] != b'\\' => { 151 | idx += 1; 152 | if bytes[idx] != b']' { 153 | // error invalid explicit key syntax 154 | return Err(Error::InvalidExplicitKeySyntax( 155 | input.to_owned(), 156 | )); 157 | } 158 | namespaces.push(Namespace::Object { 159 | id: unsafe { String::from_utf8_unchecked(s.clone()) } 160 | .replace("\\", ""), // unescape required escaped double quotes 161 | }); 162 | s.clear(); 163 | idx += 1; 164 | continue 'outer; 165 | } 166 | _ => { 167 | idx += 1; 168 | s.push(b) 169 | } 170 | }; 171 | } 172 | // error never reached the end bracket of explicit key 173 | return Err(Error::InvalidExplicitKeySyntax(input.to_owned())); 174 | } 175 | b']' => { 176 | // append array index 177 | namespaces.push(Namespace::AppendArray); 178 | idx += 1; 179 | continue 'outer; 180 | } 181 | b'-' => { 182 | // merge array 183 | idx += 1; 184 | if idx < bytes.len() && bytes[idx] != b']' { 185 | // error invalid merge object syntax 186 | return Err(Error::InvalidMergeArraySyntax(input.to_owned())); 187 | } 188 | idx += 1; 189 | if idx != bytes.len() { 190 | // error merge object must be the last part in the namespace. 191 | return Err(Error::InvalidMergeArraySyntax(input.to_owned())); 192 | } 193 | namespaces.push(Namespace::MergeArray); 194 | } 195 | b'+' => { 196 | // merge array 197 | idx += 1; 198 | if idx < bytes.len() && bytes[idx] != b']' { 199 | // error invalid merge object syntax 200 | return Err(Error::InvalidCombineArraySyntax(input.to_owned())); 201 | } 202 | idx += 1; 203 | if idx != bytes.len() { 204 | // error merge object must be the last part in the namespace. 205 | return Err(Error::InvalidCombineArraySyntax(input.to_owned())); 206 | } 207 | namespaces.push(Namespace::CombineArray); 208 | } 209 | _ => { 210 | // parse array index 211 | while idx < bytes.len() { 212 | let b = bytes[idx]; 213 | match b { 214 | b']' => { 215 | namespaces.push(Namespace::Array { 216 | index: unsafe { 217 | String::from_utf8_unchecked(s.clone()) 218 | } 219 | .parse()?, 220 | }); 221 | s.clear(); 222 | idx += 1; 223 | continue 'outer; 224 | } 225 | _ => { 226 | idx += 1; 227 | s.push(b) 228 | } 229 | }; 230 | } 231 | // error no end bracket 232 | return Err(Error::MissingArrayIndexBracket(input.to_owned())); 233 | } 234 | } 235 | } 236 | _ => { 237 | s.push(b); 238 | idx += 1; 239 | } 240 | }; 241 | } 242 | if !s.is_empty() { 243 | namespaces.push(Namespace::Object { 244 | id: unsafe { String::from_utf8_unchecked(s) }, 245 | }); 246 | } 247 | Ok(namespaces) 248 | } 249 | } 250 | 251 | #[cfg(test)] 252 | mod tests { 253 | use super::*; 254 | 255 | #[test] 256 | fn test_direct_set() { 257 | let ns = ""; 258 | let results = Namespace::parse(ns).unwrap(); 259 | let expected: Vec = Vec::new(); 260 | assert_eq!(expected, results); 261 | } 262 | 263 | #[test] 264 | fn test_object_merge() { 265 | let ns = "person{}"; 266 | let results = Namespace::parse(ns).unwrap(); 267 | let expected = vec![ 268 | Namespace::Object { 269 | id: "person".into(), 270 | }, 271 | Namespace::MergeObject, 272 | ]; 273 | assert_eq!(expected, results); 274 | } 275 | 276 | #[test] 277 | fn test_array_merge() { 278 | let ns = "person[-]"; 279 | let results = Namespace::parse(ns).unwrap(); 280 | let expected = vec![ 281 | Namespace::Object { 282 | id: "person".into(), 283 | }, 284 | Namespace::MergeArray, 285 | ]; 286 | assert_eq!(expected, results); 287 | } 288 | 289 | #[test] 290 | fn test_array_combine() { 291 | let ns = "person[+]"; 292 | let results = Namespace::parse(ns).unwrap(); 293 | let expected = vec![ 294 | Namespace::Object { 295 | id: "person".into(), 296 | }, 297 | Namespace::CombineArray, 298 | ]; 299 | assert_eq!(expected, results); 300 | } 301 | 302 | #[test] 303 | fn test_append_array() { 304 | let ns = "person[]"; 305 | let results = Namespace::parse(ns).unwrap(); 306 | let expected = vec![ 307 | Namespace::Object { 308 | id: "person".into(), 309 | }, 310 | Namespace::AppendArray, 311 | ]; 312 | assert_eq!(expected, results); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/actions/strip.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::errors::Error; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::borrow::Cow; 6 | use std::ops::Deref; 7 | 8 | /// This represents the trim operation type 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub enum Type { 11 | StripPrefix, 12 | StripSuffix, 13 | } 14 | 15 | /// This type represents an [Action](../action/trait.Action.html) which trims the whitespace from 16 | /// the left and right of a string. 17 | #[derive(Debug, Serialize, Deserialize)] 18 | pub struct Strip { 19 | r#type: Type, 20 | trim: String, 21 | action: Box, 22 | } 23 | 24 | impl Strip { 25 | pub fn new(r#type: Type, trim: String, action: Box) -> Self { 26 | Self { 27 | r#type, 28 | trim, 29 | action, 30 | } 31 | } 32 | } 33 | 34 | #[typetag::serde] 35 | impl Action for Strip { 36 | fn apply<'a>( 37 | &'a self, 38 | source: &'a Value, 39 | destination: &mut Value, 40 | ) -> Result>, Error> { 41 | let res: Option> = self.action.apply(source, destination)?; 42 | match &res { 43 | Some(v) => match v.deref() { 44 | Value::String(s) => { 45 | let stripped = match self.r#type { 46 | Type::StripPrefix => s.strip_prefix(&self.trim), 47 | Type::StripSuffix => s.strip_suffix(&self.trim), 48 | }; 49 | match stripped { 50 | Some(s) => Ok(Some(Cow::Owned(Value::String(s.to_owned())))), 51 | None => Ok(res), 52 | } 53 | } 54 | _ => Ok(None), 55 | }, 56 | None => Ok(None), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/actions/sum.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::errors::Error; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::borrow::Cow; 6 | use std::ops::Deref; 7 | 8 | /// This type represents an [Action](../action/trait.Action.html) which sums two or more Value's 9 | /// and returns a Value::Number. 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub struct Sum { 12 | values: Vec>, 13 | } 14 | 15 | impl Sum { 16 | pub fn new(values: Vec>) -> Self { 17 | Self { values } 18 | } 19 | } 20 | 21 | #[typetag::serde] 22 | impl Action for Sum { 23 | fn apply<'a>( 24 | &self, 25 | source: &'a Value, 26 | destination: &mut Value, 27 | ) -> Result>, Error> { 28 | let mut result: f64 = 0.0; 29 | let mut has_f64_value = false; 30 | 31 | for v in self.values.iter() { 32 | match v.apply(source, destination)? { 33 | Some(v) => { 34 | match v.deref() { 35 | Value::Number(num) => { 36 | if num.is_f64() { 37 | has_f64_value = true; 38 | } 39 | if let Some(n) = num.as_f64() { 40 | result += n; 41 | } 42 | } 43 | Value::Array(arr) => { 44 | for v in arr { 45 | match v { 46 | Value::Number(num) => { 47 | if num.is_f64() { 48 | has_f64_value = true; 49 | } 50 | if let Some(n) = num.as_f64() { 51 | result += n; 52 | } 53 | } 54 | _ => continue, 55 | } 56 | } 57 | } 58 | _ => continue, 59 | }; 60 | } 61 | None => continue, 62 | }; 63 | } 64 | 65 | if has_f64_value { 66 | Ok(Some(Cow::Owned(result.into()))) 67 | } else { 68 | Ok(Some(Cow::Owned((result as i64).into()))) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/actions/trim.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::errors::Error; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::borrow::Cow; 6 | use std::ops::Deref; 7 | 8 | /// This represents the trim operation type 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub enum Type { 11 | Trim, 12 | TrimStart, 13 | TrimEnd, 14 | } 15 | 16 | /// This type represents an [Action](../action/trait.Action.html) which trims the whitespace from 17 | /// the left and right of a string. 18 | #[derive(Debug, Serialize, Deserialize)] 19 | pub struct Trim { 20 | r#type: Type, 21 | action: Box, 22 | } 23 | 24 | impl Trim { 25 | pub fn new(r#type: Type, action: Box) -> Self { 26 | Self { r#type, action } 27 | } 28 | } 29 | 30 | #[typetag::serde] 31 | impl Action for Trim { 32 | fn apply<'a>( 33 | &self, 34 | source: &'a Value, 35 | destination: &mut Value, 36 | ) -> Result>, Error> { 37 | match self.action.apply(source, destination)? { 38 | Some(v) => match v.deref() { 39 | Value::String(s) => { 40 | let s = match self.r#type { 41 | Type::Trim => s.trim(), 42 | Type::TrimStart => s.trim_start(), 43 | Type::TrimEnd => s.trim_end(), 44 | } 45 | .to_owned(); 46 | Ok(Some(Cow::Owned(Value::String(s)))) 47 | } 48 | _ => Ok(None), 49 | }, 50 | None => Ok(None), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Errors that can occur applying transformations. 2 | 3 | use crate::actions::setter::Error as SetterError; 4 | use thiserror::Error; 5 | 6 | /// This type represents all possible errors that an occur while building and applying a Transformation. 7 | #[derive(Error, Debug)] 8 | pub enum Error { 9 | #[error(transparent)] 10 | Setter(#[from] SetterError), 11 | 12 | #[error(transparent)] 13 | JSONError(#[from] serde_json::Error), 14 | } 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Proteus 2 | //! 3 | //! Proteus is intended to make dynamic transformation of data using serde serializable, deserialize 4 | //! using JSON and a JSON transformation syntax similar to Javascript JSON syntax. 5 | //! 6 | //! ```rust 7 | //! use proteus::{actions, TransformBuilder}; 8 | //! use std::error::Error; 9 | //! 10 | //! // This example show the basic usage of transformations 11 | //! fn main() -> Result<(), Box> { 12 | //! let input = r#" 13 | //! { 14 | //! "user_id":"111", 15 | //! "first_name":"Dean", 16 | //! "last_name":"Karn", 17 | //! "addresses": [ 18 | //! { "street":"26 Here Blvd", "postal":"123456", "country":"Canada", "primary":true }, 19 | //! { "street":"26 Lakeside Cottage Lane.", "postal":"654321", "country":"Canada" } 20 | //! ], 21 | //! "nested": { 22 | //! "inner":{ 23 | //! "key":"value" 24 | //! }, 25 | //! "my_arr":[null,"arr_value",null] 26 | //! } 27 | //! }"#; 28 | //! let trans = TransformBuilder::default() 29 | //! .add_actions(actions!( 30 | //! ("user_id", "id"), 31 | //! ( 32 | //! r#"join(" ", const("Mr."), first_name, last_name)"#, 33 | //! "full-name" 34 | //! ), 35 | //! ( 36 | //! r#"join(", ", addresses[0].street, addresses[0].postal, addresses[0].country)"#, 37 | //! "address" 38 | //! ), 39 | //! ("nested.inner.key", "prev_nested"), 40 | //! ("nested.my_arr", "my_arr"), 41 | //! (r#"const("arr_value_2")"#, "my_arr[]") 42 | //! )?) 43 | //! .build()?; 44 | //! let res = trans.apply_from_str(input)?; 45 | //! println!("{}", serde_json::to_string_pretty(&res)?); 46 | //! Ok(()) 47 | //! } 48 | //! ``` 49 | //! 50 | //! or direct from struct to struct 51 | //! 52 | //! ```rust 53 | //! use proteus::{actions, TransformBuilder}; 54 | //! use serde::{Deserialize, Serialize}; 55 | //! use std::error::Error; 56 | //! 57 | //! #[derive(Serialize)] 58 | //! struct KV { 59 | //! pub key: String, 60 | //! } 61 | //! 62 | //! #[derive(Serialize)] 63 | //! struct Nested { 64 | //! pub inner: KV, 65 | //! pub my_arr: Vec>, 66 | //! } 67 | //! 68 | //! #[derive(Serialize)] 69 | //! struct Address { 70 | //! pub street: String, 71 | //! pub postal: String, 72 | //! pub country: String, 73 | //! } 74 | //! 75 | //! #[derive(Serialize)] 76 | //! struct RawUserInfo { 77 | //! pub user_id: String, 78 | //! pub first_name: String, 79 | //! pub last_name: String, 80 | //! pub addresses: Vec
, 81 | //! pub nested: Nested, 82 | //! } 83 | //! 84 | //! #[derive(Serialize, Deserialize)] 85 | //! struct User { 86 | //! pub id: String, 87 | //! #[serde(rename = "full-name")] 88 | //! pub full_name: String, 89 | //! pub address: String, 90 | //! pub prev_nested: String, 91 | //! pub my_arr: Vec>, 92 | //! } 93 | //! 94 | //! // This example show the basic usage of transformations 95 | //! fn main() -> Result<(), Box> { 96 | //! let input = RawUserInfo { 97 | //! user_id: "111".to_string(), 98 | //! first_name: "Dean".to_string(), 99 | //! last_name: "Karn".to_string(), 100 | //! addresses: vec![ 101 | //! Address { 102 | //! street: "26 Here Blvd".to_string(), 103 | //! postal: "123456".to_string(), 104 | //! country: "Canada".to_string(), 105 | //! }, 106 | //! Address { 107 | //! street: "26 Lakeside Cottage Lane.".to_string(), 108 | //! postal: "654321".to_string(), 109 | //! country: "Canada".to_string(), 110 | //! }, 111 | //! ], 112 | //! nested: Nested { 113 | //! inner: KV { 114 | //! key: "value".to_string(), 115 | //! }, 116 | //! my_arr: vec![None, Some("arr_value".to_owned()), None], 117 | //! }, 118 | //! }; 119 | //! let trans = TransformBuilder::default() 120 | //! .add_actions(actions!( 121 | //! ("user_id", "id"), 122 | //! ( 123 | //! r#"join(" ", const("Mr."), first_name, last_name)"#, 124 | //! "full-name" 125 | //! ), 126 | //! ( 127 | //! r#"join(", ", addresses[0].street, addresses[0].postal, addresses[0].country)"#, 128 | //! "address" 129 | //! ), 130 | //! ("nested.inner.key", "prev_nested"), 131 | //! ("nested.my_arr", "my_arr"), 132 | //! (r#"const("arr_value_2")"#, "my_arr[]") 133 | //! )?) 134 | //! .build()?; 135 | //! let res: User = trans.apply_to(input)?; 136 | //! println!("{}", serde_json::to_string_pretty(&res)?); 137 | //! Ok(()) 138 | //! } 139 | //! ``` 140 | //! 141 | pub mod action; 142 | pub mod actions; 143 | pub mod errors; 144 | pub mod parser; 145 | pub mod transformer; 146 | 147 | #[doc(inline)] 148 | pub use parser::{Parsable, Parser, COMMA_SEP_RE, QUOTED_STR_RE}; 149 | 150 | #[doc(inline)] 151 | pub use transformer::TransformBuilder; 152 | 153 | #[doc(inline)] 154 | pub use errors::Error; 155 | 156 | /// This macros is shorthand for creating a set of actions to be added to [TransformBuilder](struct.TransformBuilder.html). 157 | #[macro_export] 158 | macro_rules! actions { 159 | ($($p:expr),*) => { 160 | { 161 | let mut parsables = Vec::new(); 162 | $( 163 | parsables.push(proteus::Parsable::new($p.0, $p.1)); 164 | )* 165 | proteus::Parser::parse_multi(&parsables) 166 | } 167 | }; 168 | } 169 | -------------------------------------------------------------------------------- /src/parser/action_parsers.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::actions::{Constant, Join, Len, Strip, StripType, Sum, Trim, TrimType}; 3 | use crate::parser::Error; 4 | use crate::{Parser, COMMA_SEP_RE, QUOTED_STR_RE}; 5 | use serde_json::Value; 6 | 7 | pub(super) fn parse_const(val: &str) -> Result, Error> { 8 | if val.is_empty() { 9 | Err(Error::MissingActionValue("const".to_owned())) 10 | } else { 11 | let value: Value = serde_json::from_str(val)?; 12 | Ok(Box::new(Constant::new(value))) 13 | } 14 | } 15 | 16 | pub(super) fn parse_join(val: &str) -> Result, Error> { 17 | let sep_len; 18 | let sep = match QUOTED_STR_RE.find(val) { 19 | Some(cap) => { 20 | let s = cap.as_str(); 21 | sep_len = s.len(); 22 | let s = s[..s.len() - 1].trim(); // strip ',' and trim any whitespace 23 | s[1..s.len() - 1].to_string() // remove '"" double quotes from beginning and end. 24 | } 25 | None => { 26 | return Err(Error::InvalidQuotedValue(format!("join({})", val))); 27 | } 28 | }; 29 | 30 | let sub_matches = COMMA_SEP_RE.captures_iter(&val[sep_len..]); 31 | let mut values = Vec::new(); 32 | for m in sub_matches { 33 | match m.get(0) { 34 | Some(m) => values.push(Parser::parse_action(m.as_str().trim())?), 35 | None => continue, 36 | }; 37 | } 38 | 39 | if values.is_empty() { 40 | return Err(Error::InvalidNumberOfProperties("join".to_owned())); 41 | } 42 | Ok(Box::new(Join::new(sep, values))) 43 | } 44 | 45 | pub(super) fn parse_len(val: &str) -> Result, Error> { 46 | let action = Parser::parse_action(val)?; 47 | Ok(Box::new(Len::new(action))) 48 | } 49 | 50 | pub(super) fn parse_sum(val: &str) -> Result, Error> { 51 | let sub_matches = COMMA_SEP_RE.captures_iter(val); 52 | let mut values = Vec::new(); 53 | for m in sub_matches { 54 | match m.get(0) { 55 | Some(m) => values.push(Parser::parse_action(m.as_str().trim())?), 56 | None => continue, 57 | }; 58 | } 59 | 60 | if values.is_empty() { 61 | return Err(Error::InvalidNumberOfProperties("sum".to_owned())); 62 | } 63 | Ok(Box::new(Sum::new(values))) 64 | } 65 | 66 | pub(super) fn parse_trim(val: &str) -> Result, Error> { 67 | let action = Parser::parse_action(val)?; 68 | Ok(Box::new(Trim::new(TrimType::Trim, action))) 69 | } 70 | 71 | pub(super) fn parse_trim_start(val: &str) -> Result, Error> { 72 | let action = Parser::parse_action(val)?; 73 | Ok(Box::new(Trim::new(TrimType::TrimStart, action))) 74 | } 75 | 76 | pub(super) fn parse_trim_end(val: &str) -> Result, Error> { 77 | let action = Parser::parse_action(val)?; 78 | Ok(Box::new(Trim::new(TrimType::TrimEnd, action))) 79 | } 80 | 81 | pub(super) fn parse_strip_prefix(val: &str) -> Result, Error> { 82 | let sep_len; 83 | let strip = match QUOTED_STR_RE.find(val) { 84 | Some(cap) => { 85 | let s = cap.as_str(); 86 | sep_len = s.len(); 87 | let s = s[..s.len() - 1].trim(); // strip ',' and trim any whitespace 88 | s[1..s.len() - 1].to_string() // remove '"" double quotes from beginning and end. 89 | } 90 | None => { 91 | return Err(Error::InvalidQuotedValue(format!("strip_prefix({})", val))); 92 | } 93 | }; 94 | 95 | let action = Parser::parse_action(val[sep_len..].trim())?; 96 | Ok(Box::new(Strip::new(StripType::StripPrefix, strip, action))) 97 | } 98 | 99 | pub(super) fn parse_strip_suffix(val: &str) -> Result, Error> { 100 | let sep_len; 101 | let strip = match QUOTED_STR_RE.find(val) { 102 | Some(cap) => { 103 | let s = cap.as_str(); 104 | sep_len = s.len(); 105 | let s = s[..s.len() - 1].trim(); // strip ',' and trim any whitespace 106 | s[1..s.len() - 1].to_string() // remove '"" double quotes from beginning and end. 107 | } 108 | None => { 109 | return Err(Error::InvalidQuotedValue(format!("strip_suffix({})", val))); 110 | } 111 | }; 112 | 113 | let action = Parser::parse_action(val[sep_len..].trim())?; 114 | Ok(Box::new(Strip::new(StripType::StripSuffix, strip, action))) 115 | } 116 | -------------------------------------------------------------------------------- /src/parser/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::actions::getter::namespace::Error as GetterNamespaceError; 2 | use crate::actions::setter::namespace::Error as SetterNamespaceError; 3 | use crate::errors::Error as JSONError; 4 | use thiserror::Error; 5 | 6 | /// This type represents all possible errors that an occur while parsing the transformation syntax. 7 | #[derive(Error, Debug)] 8 | pub enum Error { 9 | #[error(transparent)] 10 | ParseError(#[from] JSONError), 11 | 12 | #[error("Brackets: () must always be preceded by and action name.")] 13 | MissingActionName, 14 | 15 | #[error("Action Name: '{0}' is invalid.")] 16 | InvalidActionName(String), 17 | 18 | #[error( 19 | "Action Value missing for key:{0}. An action Value must be set in brackets eg. const(null)" 20 | )] 21 | MissingActionValue(String), 22 | 23 | #[error("Issue parsing Action Value: {0}")] 24 | ValueParseError(#[from] serde_json::Error), 25 | 26 | #[error("Invalid number of properties supplied to Action: '{0}'")] 27 | InvalidNumberOfProperties(String), 28 | 29 | #[error("Invalid quoted value supplied for Action: '{0}'")] 30 | InvalidQuotedValue(String), 31 | 32 | #[error("Setter namespace parsing error: {0}")] 33 | GetterNamespace(#[from] GetterNamespaceError), 34 | 35 | #[error("Setter namespace parsing error: {0}")] 36 | SetterNamespace(#[from] SetterNamespaceError), 37 | 38 | #[error("{0}")] 39 | CustomActionParseError(String), 40 | } 41 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | //! Parser of transformation syntax into [Action(s)](action/trait.Action.html). 2 | 3 | mod action_parsers; 4 | mod errors; 5 | 6 | pub use errors::Error; 7 | 8 | use crate::action::Action; 9 | use crate::actions::getter::namespace::Namespace as GetterNamespace; 10 | use crate::actions::setter::namespace::Namespace as SetterNamespace; 11 | use crate::actions::{Getter, Setter}; 12 | use once_cell::sync::Lazy; 13 | use regex::Regex; 14 | use serde::{Deserialize, Serialize}; 15 | use std::borrow::Cow; 16 | use std::collections::HashMap; 17 | use std::sync::{Arc, Mutex}; 18 | 19 | /// This is a Regex used to parse comma separated values and is used as a helper within custom 20 | /// Action Parsers. 21 | pub static COMMA_SEP_RE: Lazy = 22 | Lazy::new(|| Regex::new(r#"[^,(]*(?:\([^)]*\))*[^,]*"#).unwrap()); 23 | 24 | /// This is a Regex used to get content within quoted strings and is used as a helper within custom 25 | /// Action Parsers. 26 | pub static QUOTED_STR_RE: Lazy = Lazy::new(|| Regex::new(r#"^"(.*?[^\\])"\s*,"#).unwrap()); 27 | 28 | static ACTION_RE: Lazy = Lazy::new(|| { 29 | let r = format!(r#"(?P{})\((?P.*)\)"#, ACTION_NAME_BASE_REGEX); 30 | Regex::new(&r).unwrap() 31 | }); 32 | 33 | static ACTION_PARSERS: Lazy>>> = Lazy::new(|| { 34 | let mut m: HashMap> = HashMap::new(); 35 | m.insert("join".to_string(), Arc::new(action_parsers::parse_join)); 36 | m.insert("const".to_string(), Arc::new(action_parsers::parse_const)); 37 | m.insert("len".to_string(), Arc::new(action_parsers::parse_len)); 38 | m.insert("sum".to_string(), Arc::new(action_parsers::parse_sum)); 39 | m.insert("trim".to_string(), Arc::new(action_parsers::parse_trim)); 40 | m.insert( 41 | "trim_start".to_string(), 42 | Arc::new(action_parsers::parse_trim_start), 43 | ); 44 | m.insert( 45 | "trim_end".to_string(), 46 | Arc::new(action_parsers::parse_trim_end), 47 | ); 48 | m.insert( 49 | "strip_prefix".to_string(), 50 | Arc::new(action_parsers::parse_strip_prefix), 51 | ); 52 | m.insert( 53 | "strip_suffix".to_string(), 54 | Arc::new(action_parsers::parse_strip_suffix), 55 | ); 56 | Mutex::new(m) 57 | }); 58 | 59 | static ACTION_NAME_RE: Lazy = Lazy::new(|| { 60 | let r = format!("^{}$", ACTION_NAME_BASE_REGEX); 61 | Regex::new(&r).unwrap() 62 | }); 63 | 64 | const ACTION_NAME_BASE_REGEX: &str = "[a-zA-Z0-9_]+"; 65 | const ACTION_NAME: &str = "action"; 66 | const ACTION_VALUE: &str = "value"; 67 | 68 | /// ActionParserFn is function signature used for adding dynamic actions to the parser 69 | pub type ActionParserFn = dyn Fn(&str) -> Result, Error> + 'static + Send + Sync; 70 | 71 | /// This type represents a single transformation action to be taken containing the source and 72 | /// destination syntax to be parsed into an [Action](action/trait.Action.html). 73 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 74 | pub struct Parsable<'a> { 75 | source: Cow<'a, str>, 76 | destination: Cow<'a, str>, 77 | } 78 | 79 | impl<'a> Parsable<'a> { 80 | pub fn new(source: S, destination: S) -> Self 81 | where 82 | S: Into>, 83 | { 84 | Parsable { 85 | source: source.into(), 86 | destination: destination.into(), 87 | } 88 | } 89 | } 90 | 91 | /// This type represents a set of static methods for parsing transformation syntax into 92 | /// [Action](action/trait.Action.html)'s. 93 | /// 94 | /// The parser is responsible for parsing the transformation action specific syntax, take the 95 | /// following source syntax: `join(" ", const("Mr."), first_name, last_name)` 96 | /// the parser knows how to breakdown the syntax into a `join` action which calls the `const` 97 | /// action, and 2 getter actions and joins those actions results. 98 | /// 99 | /// Actions currently supported include: 100 | /// * const eg. `const()` 101 | /// * join eg. `join() 102 | /// 103 | pub struct Parser {} 104 | 105 | impl Parser { 106 | /// add_action_parser adds an Action parsing function to dynamically be parsed. 107 | /// NOTE: this WILL overwrite any pre-existing functions with the same name. 108 | /// 109 | /// name only accepts ASCII letters, numbers and _ equivalent to [a-zA-Z0-9_]. 110 | pub fn add_action_parser(name: &str, f: &'static ActionParserFn) -> Result<(), Error> { 111 | if !ACTION_NAME_RE.is_match(name) { 112 | return Err(Error::InvalidActionName(name.to_owned())); 113 | } 114 | ACTION_PARSERS 115 | .lock() 116 | .unwrap() 117 | .insert(name.to_owned(), Arc::new(f)); 118 | Ok(()) 119 | } 120 | 121 | /// parses a single transformation action to be taken with the provided source & destination. 122 | pub fn parse(source: &str, destination: &str) -> Result, Error> { 123 | let set = SetterNamespace::parse(destination)?; 124 | let action = Parser::parse_action(source)?; 125 | Ok(Box::new(Setter::new(set, action))) 126 | } 127 | 128 | /// parses a set of transformation actions into [Action](action/trait.Action.html)'s. 129 | pub fn parse_multi(parsables: &[Parsable]) -> Result>, Error> { 130 | let mut vec = Vec::new(); 131 | for p in parsables.iter() { 132 | vec.push(Parser::parse(&p.source, &p.destination)?); 133 | } 134 | Ok(vec) 135 | } 136 | 137 | /// parses a set of transformation actions into [Action](action/trait.Action.html)'s from a JSON 138 | /// string of serialized [Parsable](struct.Parsable.html) structs. 139 | pub fn parse_multi_from_str(s: &str) -> Result>, Error> { 140 | let parsables: Vec = serde_json::from_str(s)?; 141 | Parser::parse_multi(&parsables) 142 | } 143 | 144 | /// parses an [Action](action/trait.Action.html) given the provided str. This is primarily used 145 | /// as a helper in custom Action Parsers. 146 | pub fn parse_action(source: &str) -> Result, Error> { 147 | // edge case where there is no action but it looks like there's one inside of an 148 | // explicit key eg. '["const()"]' 149 | if source.starts_with(r#"[""#) { 150 | let get = GetterNamespace::parse(source)?; 151 | return Ok(Box::new(Getter::new(get))); 152 | } 153 | match ACTION_RE.captures(source) { 154 | Some(caps) => match caps.name(ACTION_NAME) { 155 | None => Err(Error::MissingActionName {}), 156 | Some(key) => { 157 | let key = key.as_str(); 158 | let parse_fn; 159 | match ACTION_PARSERS.lock().unwrap().get(key) { 160 | None => return Err(Error::InvalidActionName(key.to_owned())), 161 | Some(f) => { 162 | parse_fn = f.clone(); 163 | } 164 | }; 165 | parse_fn(caps.name(ACTION_VALUE).unwrap().as_str()) // unwrap safe, has value or never would have match ACTION_RE regex 166 | } 167 | }, 168 | None => { 169 | let get = GetterNamespace::parse(source)?; 170 | Ok(Box::new(Getter::new(get))) 171 | } 172 | } 173 | } 174 | } 175 | 176 | #[cfg(test)] 177 | mod tests { 178 | use super::*; 179 | use crate::actions::Constant; 180 | 181 | #[test] 182 | fn direct_getter() -> Result<(), Box> { 183 | let action = Parser::parse("key", "new")?; 184 | let expected = Box::new(Setter::new( 185 | SetterNamespace::parse("new")?, 186 | Box::new(Getter::new(GetterNamespace::parse("key")?)), 187 | )); 188 | assert_eq!(format!("{:?}", action), format!("{:?}", expected)); 189 | Ok(()) 190 | } 191 | 192 | #[test] 193 | fn constant() -> Result<(), Box> { 194 | let action = Parser::parse(r#"const("value")"#, "new")?; 195 | let expected = Box::new(Setter::new( 196 | SetterNamespace::parse("new")?, 197 | Box::new(Constant::new("value".into())), 198 | )); 199 | assert_eq!(format!("{:?}", action), format!("{:?}", expected)); 200 | Ok(()) 201 | } 202 | 203 | #[test] 204 | fn parser_serialize_deserialize() -> Result<(), Box> { 205 | let parsables = vec![ 206 | Parsable::new(r#"const("value")"#, "new"), 207 | Parsable::new(r#"const("value2")"#, "new2"), 208 | ]; 209 | let serialized = serde_json::to_string(&parsables)?; 210 | let expected = "[{\"source\":\"const(\\\"value\\\")\",\"destination\":\"new\"},{\"source\":\"const(\\\"value2\\\")\",\"destination\":\"new2\"}]"; 211 | assert_eq!(expected, serialized); 212 | 213 | let deserialized: Vec = serde_json::from_str(&serialized)?; 214 | assert_eq!(parsables, deserialized); 215 | Ok(()) 216 | } 217 | 218 | #[test] 219 | fn parser_from_str() -> Result<(), Box> { 220 | let parsables = vec![ 221 | Parsable::new(r#"const("value")"#, "new"), 222 | Parsable::new(r#"const("value2")"#, "new2"), 223 | ]; 224 | let expected = Parser::parse_multi(&parsables)?; 225 | let deserialized = Parser::parse_multi_from_str("[{\"source\":\"const(\\\"value\\\")\",\"destination\":\"new\"},{\"source\":\"const(\\\"value2\\\")\",\"destination\":\"new2\"}]")?; 226 | assert_eq!(format!("{:?}", expected), format!("{:?}", deserialized)); 227 | Ok(()) 228 | } 229 | 230 | #[test] 231 | fn join() -> Result<(), Box> { 232 | let action = Parser::parse( 233 | r#"join(",_" , first_name, last_name, const("Dean Karn"))"#, 234 | "full_name", 235 | )?; 236 | let expected = "Setter { namespace: [Object { id: \"full_name\" }], child: Join { sep: \",_\", values: [Getter { namespace: [Object { id: \"first_name\" }] }, Getter { namespace: [Object { id: \"last_name\" }] }, Constant { value: String(\"Dean Karn\") }] } }"; 237 | assert_eq!(format!("{:?}", action), expected.to_string()); 238 | Ok(()) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/transformer.rs: -------------------------------------------------------------------------------- 1 | //! builder and finalized transformer representations.. 2 | 3 | use crate::action::Action; 4 | use crate::errors::Error; 5 | use serde::de::DeserializeOwned; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::Value; 8 | use std::borrow::Cow; 9 | 10 | /// This type provides the ability to create a [Transformer](struct.Transformer.html) for use. 11 | #[derive(Debug)] 12 | pub struct TransformBuilder { 13 | actions: Vec>, 14 | } 15 | 16 | impl Default for TransformBuilder { 17 | fn default() -> Self { 18 | TransformBuilder { 19 | actions: Vec::new(), 20 | } 21 | } 22 | } 23 | 24 | impl TransformBuilder { 25 | /// adds a single [Action](action/trait.Action.html) to be applied during the transformation. 26 | pub fn add_action(mut self, action: Box) -> Self { 27 | self.actions.push(action); 28 | self 29 | } 30 | 31 | /// adds multiple [Action](action/trait.Action.html) to be applied during the transformation. 32 | pub fn add_actions(mut self, mut actions: Vec>) -> Self { 33 | self.actions.append(&mut actions); 34 | self 35 | } 36 | 37 | /// creates the final [Transformer](struct.Transformer.html) representation. 38 | pub fn build(self) -> Result { 39 | // Error return value is reserved for future optimization during the build phase. 40 | Ok(Transformer { 41 | actions: self.actions, 42 | }) 43 | } 44 | } 45 | 46 | /// This type represents a realized transformation which can be used on data. 47 | #[derive(Debug, Serialize, Deserialize)] 48 | pub struct Transformer { 49 | actions: Vec>, 50 | } 51 | 52 | impl Transformer { 53 | /// directly applies the transform actions, in order, on the source and sets directly on the 54 | /// provided destination. 55 | /// 56 | /// The destination in question can be an existing Object and the data set on it at any level. 57 | #[inline] 58 | pub fn apply_to_destination( 59 | &self, 60 | source: &Value, 61 | destination: &mut Value, 62 | ) -> Result<(), Error> { 63 | for a in self.actions.iter() { 64 | a.apply(source, destination)?; 65 | } 66 | Ok(()) 67 | } 68 | 69 | /// applies the transform actions, in order, on the source and returns a final Value. 70 | #[inline] 71 | pub fn apply(&self, source: &Value) -> Result { 72 | let mut value = Value::Null; 73 | self.apply_to_destination(source, &mut value)?; 74 | Ok(value) 75 | } 76 | 77 | /// applies the transform actions, in order, on the source slice. 78 | /// 79 | /// The source string MUST be valid utf-8 JSON. 80 | #[inline] 81 | pub fn apply_from_slice(&self, source: &[u8]) -> Result { 82 | self.apply(&serde_json::from_slice(source)?) 83 | } 84 | 85 | /// applies the transform actions, in order, on the source string. 86 | /// 87 | /// The source string MUST be valid JSON. 88 | #[inline] 89 | pub fn apply_from_str<'a, S>(&self, source: S) -> Result 90 | where 91 | S: Into>, 92 | { 93 | self.apply(&serde_json::from_str(&source.into())?) 94 | } 95 | 96 | /// applies the transform actions, in order, on the source string and returns the type 97 | /// represented by D. 98 | /// 99 | /// The source string MUST be valid JSON. 100 | #[inline] 101 | pub fn apply_from_str_to<'a, S, D>(&self, source: S) -> Result 102 | where 103 | S: Into>, 104 | D: DeserializeOwned, 105 | { 106 | let value = self.apply(&serde_json::from_str(&source.into())?)?; 107 | Ok(serde_json::from_value::(value)?) 108 | } 109 | 110 | /// applies the transform actions, in order, on the serializable source and returns the type 111 | /// represented by D. 112 | #[inline] 113 | pub fn apply_to(&self, source: S) -> Result 114 | where 115 | S: Serialize, 116 | D: DeserializeOwned, 117 | { 118 | let value = self.apply(&serde_json::to_value(source)?)?; 119 | Ok(serde_json::from_value::(value)?) 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use crate::{Parsable, Parser, TransformBuilder}; 126 | use serde_json::{json, Value}; 127 | 128 | #[test] 129 | fn constant() -> Result<(), Box> { 130 | let action = Parser::parse(r#"const("Dean Karn")"#, "full_name")?; 131 | let trans = TransformBuilder::default().add_action(action).build()?; 132 | let source = "".into(); 133 | let destination = trans.apply(&source)?; 134 | let expected = json!({"full_name":"Dean Karn"}); 135 | assert_eq!(expected, destination); 136 | Ok(()) 137 | } 138 | 139 | #[test] 140 | fn array_of_array_to_array() -> Result<(), Box> { 141 | let action = Parser::parse(r#"const("Dean Karn")"#, "[2][1]")?; 142 | let trans = TransformBuilder::default().add_action(action).build()?; 143 | let source = "".into(); 144 | let destination = trans.apply(&source)?; 145 | assert!(destination.is_array()); 146 | 147 | let expected = json!([null, null, [null, "Dean Karn"]]); 148 | 149 | assert_eq!(expected, destination); 150 | 151 | let action = Parser::parse(r#"const("Dean Karn")"#, "[2][1].name")?; 152 | let trans = TransformBuilder::default().add_action(action).build()?; 153 | let source = "".into(); 154 | let destination = trans.apply(&source)?; 155 | assert!(destination.is_array()); 156 | 157 | let expected = json!([null, null, [null, {"name":"Dean Karn"}]]); 158 | assert_eq!(expected, destination); 159 | Ok(()) 160 | } 161 | 162 | #[test] 163 | fn push_array() -> Result<(), Box> { 164 | let action = Parser::parse(r#"const("Dean Karn")"#, "[2][]")?; 165 | let trans = TransformBuilder::default().add_action(action).build()?; 166 | let source = "".into(); 167 | let destination = trans.apply(&source)?; 168 | assert!(destination.is_array()); 169 | 170 | let expected = json!([null, null, ["Dean Karn"]]); 171 | 172 | assert_eq!(expected, destination); 173 | 174 | let action = Parser::parse(r#"const("Dean Karn")"#, "[2][]")?; 175 | let trans = TransformBuilder::default().add_action(action).build()?; 176 | let source = "".into(); 177 | let mut destination = json!([null, null, [null]]); 178 | 179 | let res = trans.apply_to_destination(&source, &mut destination); 180 | assert!(!res.is_err()); 181 | assert!(destination.is_array()); 182 | 183 | let expected = json!([null, null, [null, "Dean Karn"]]); 184 | 185 | assert_eq!(expected, destination); 186 | 187 | let action = Parser::parse(r#"const("Dean Karn")"#, "[2]")?; 188 | let trans = TransformBuilder::default().add_action(action).build()?; 189 | let source = "".into(); 190 | let destination = trans.apply(&source)?; 191 | assert!(destination.is_array()); 192 | 193 | let expected = json!([null, null, "Dean Karn"]); 194 | 195 | assert_eq!(expected, destination); 196 | 197 | // testing replace 198 | let action = Parser::parse(r#"const("Dean Karn")"#, "[2]")?; 199 | let trans = TransformBuilder::default().add_action(action).build()?; 200 | let source = "".into(); 201 | let mut destination = json!([null, null, {"id":"id"}]); 202 | let res = trans.apply_to_destination(&source, &mut destination); 203 | assert!(!res.is_err()); 204 | assert!(destination.is_array()); 205 | 206 | let expected = json!([null, null, "Dean Karn"]); 207 | 208 | assert_eq!(expected, destination); 209 | 210 | let action = Parser::parse(r#"const("Dean Karn")"#, "[1].key.key2")?; 211 | let trans = TransformBuilder::default().add_action(action).build()?; 212 | let source = "".into(); 213 | let destination = trans.apply(&source)?; 214 | assert!(destination.is_array()); 215 | 216 | let expected = json!([null, {"key": {"key2":"Dean Karn"}}]); 217 | 218 | assert_eq!(expected, destination); 219 | Ok(()) 220 | } 221 | 222 | #[test] 223 | fn append_array_top_level() -> Result<(), Box> { 224 | let action = Parser::parse(r#"const([null,"Dean Karn"])"#, "[]")?; 225 | let trans = TransformBuilder::default().add_action(action).build()?; 226 | let source = "".into(); 227 | let mut destination = Value::Array(vec!["test".into()]); 228 | let res = trans.apply_to_destination(&source, &mut destination); 229 | assert!(!res.is_err()); 230 | assert!(destination.is_array()); 231 | 232 | let expected = json!(["test", [null, "Dean Karn"]]); 233 | 234 | assert_eq!(expected, destination); 235 | Ok(()) 236 | } 237 | 238 | #[test] 239 | fn test_top_level() -> Result<(), Box> { 240 | let actions = Parser::parse_multi(&[ 241 | Parsable::new("existing_key", "rename_from_existing_key"), 242 | Parsable::new("my_array[0]", "used_to_be_array"), 243 | Parsable::new(r#"const("consant_value")"#, "const"), 244 | ])?; 245 | let trans = TransformBuilder::default().add_actions(actions).build()?; 246 | let input = json!({ 247 | "existing_key":"my_val1", 248 | "my_array":["idx_0_value"] 249 | }); 250 | let expected = json!({"const":"consant_value","rename_from_existing_key":"my_val1","used_to_be_array":"idx_0_value"}); 251 | let output = trans.apply(&input)?; 252 | assert_eq!(expected, output); 253 | Ok(()) 254 | } 255 | 256 | #[test] 257 | fn test_10_top_level() -> Result<(), Box> { 258 | let actions = Parser::parse_multi(&[ 259 | Parsable::new("top1", "new1"), 260 | Parsable::new("top2", "new2"), 261 | Parsable::new("top3", "new3"), 262 | Parsable::new("top4", "new4"), 263 | Parsable::new("top5", "new5"), 264 | Parsable::new("top6", "new6"), 265 | Parsable::new("top7", "new7"), 266 | Parsable::new("top8", "new8"), 267 | Parsable::new("top9", "new9"), 268 | Parsable::new("top10", "new10"), 269 | ])?; 270 | 271 | let trans = TransformBuilder::default().add_actions(actions).build()?; 272 | 273 | let input = json!({ 274 | "top1": "value", 275 | "top2": "value", 276 | "top3": "value", 277 | "top4": "value", 278 | "top5": "value", 279 | "top6": "value", 280 | "top7": "value", 281 | "top8": "value", 282 | "top9": "value", 283 | "top10": "value" 284 | }); 285 | let expected = json!({"new1":"value","new10":"value","new2":"value","new3":"value","new4":"value","new5":"value","new6":"value","new7":"value","new8":"value","new9":"value"}); 286 | let output = trans.apply(&input)?; 287 | assert_eq!(expected, output); 288 | Ok(()) 289 | } 290 | 291 | #[test] 292 | fn test_join() -> Result<(), Box> { 293 | let action = Parser::parse( 294 | r#"join(" ", const("Mr."), first_name, meta.middle_name, last_name)"#, 295 | "full_name", 296 | )?; 297 | let trans = TransformBuilder::default().add_action(action).build()?; 298 | 299 | let input = json!({ 300 | "first_name": "Dean", 301 | "last_name": "Karn", 302 | "meta": { 303 | "middle_name":"Peter" 304 | } 305 | }); 306 | let expected = json!({"full_name":"Mr. Dean Peter Karn"}); 307 | let output = trans.apply(&input)?; 308 | assert_eq!(expected, output); 309 | Ok(()) 310 | } 311 | 312 | #[test] 313 | fn test_explicit_key() -> Result<(), Box> { 314 | let action = Parser::parse(r#"["name(1)"]"#, r#"["my name is ([2][])"]"#)?; 315 | let trans = TransformBuilder::default().add_action(action).build()?; 316 | let source = json!({"name(1)":"Dean Karn"}); 317 | let destination = trans.apply(&source)?; 318 | assert!(destination.is_object()); 319 | 320 | let expected = json!({"my name is ([2][])": "Dean Karn"}); 321 | 322 | assert_eq!(expected, destination); 323 | 324 | let action = Parser::parse(r#"["name(1)"].name"#, r#"["my name is ([2][])"]"#)?; 325 | let trans = TransformBuilder::default().add_action(action).build()?; 326 | let source = json!({"name(1)":{"name":"Dean Karn"}}); 327 | let destination = trans.apply(&source)?; 328 | assert!(destination.is_object()); 329 | 330 | let expected = json!({"my name is ([2][])": "Dean Karn"}); 331 | assert_eq!(expected, destination); 332 | Ok(()) 333 | } 334 | 335 | #[test] 336 | fn merge_object() -> Result<(), Box> { 337 | let actions = Parser::parse_multi(&[ 338 | Parsable::new("person.name", "person.full_name"), 339 | Parsable::new("person.metadata", "person{}"), 340 | ])?; 341 | let trans = TransformBuilder::default().add_actions(actions).build()?; 342 | let source = json!({"person":{"name":"Dean Karn", "metadata":{"age":1}}}); 343 | let destination = trans.apply(&source)?; 344 | let expected = json!({"person":{"full_name":"Dean Karn", "age":1}}); 345 | assert_eq!(expected, destination); 346 | Ok(()) 347 | } 348 | 349 | #[test] 350 | fn combine_array() -> Result<(), Box> { 351 | let actions = Parser::parse_multi(&[ 352 | Parsable::new("person.name", "person[0]"), 353 | Parsable::new("person.metadata", "person[+]"), // CombineArray = [+], MergeArray = [-] 354 | ])?; 355 | let trans = TransformBuilder::default().add_actions(actions).build()?; 356 | let source = json!({"person":{"name":"Dean Karn", "metadata":[1]}}); 357 | let destination = trans.apply(&source)?; 358 | let expected = json!({"person":["Dean Karn", 1]}); 359 | assert_eq!(expected, destination); 360 | 361 | let actions = Parser::parse_multi(&[ 362 | Parsable::new("person.name", "[0]"), 363 | Parsable::new("person.metadata", "[+]"), 364 | ])?; 365 | let trans = TransformBuilder::default().add_actions(actions).build()?; 366 | let source = json!({"person":{"name":"Dean Karn", "metadata":[1]}}); 367 | let mut destination = Value::Array(vec![1.into()]); 368 | let _ = trans.apply_to_destination(&source, &mut destination); 369 | let expected = json!(["Dean Karn", 1]); 370 | assert_eq!(expected, destination); 371 | Ok(()) 372 | } 373 | 374 | #[test] 375 | fn replace_array() -> Result<(), Box> { 376 | let actions = Parser::parse_multi(&[ 377 | Parsable::new("person.name", "person[0]"), 378 | Parsable::new("person.metadata", "person[0]"), 379 | ])?; 380 | let trans = TransformBuilder::default().add_actions(actions).build()?; 381 | let source = json!({"person":{"name":"Dean Karn", "metadata":[1]}}); 382 | let destination = trans.apply(&source)?; 383 | let expected = json!({"person":[[1]]}); 384 | assert_eq!(expected, destination); 385 | Ok(()) 386 | } 387 | 388 | #[test] 389 | fn merge_array() -> Result<(), Box> { 390 | let actions = Parser::parse_multi(&[ 391 | Parsable::new("person.name", "person[0]"), 392 | Parsable::new("person.metadata", "person[-]"), 393 | ])?; 394 | let trans = TransformBuilder::default().add_actions(actions).build()?; 395 | let source = json!({"person":{"name":"Dean Karn", "metadata":[1]}}); 396 | let destination = trans.apply(&source)?; 397 | let expected = json!({"person":[1]}); 398 | assert_eq!(expected, destination); 399 | 400 | // test source len > existing 401 | let actions = Parser::parse_multi(&[ 402 | Parsable::new("person.name", "person[0]"), 403 | Parsable::new("person.metadata", "person[-]"), 404 | ])?; 405 | let trans = TransformBuilder::default().add_actions(actions).build()?; 406 | let source = json!({"person":{"name":"Dean Karn", "metadata":[1, "blah", 45.6]}}); 407 | let destination = trans.apply(&source)?; 408 | let expected = json!({"person":[1,"blah",45.6]}); 409 | assert_eq!(expected, destination); 410 | 411 | // test source len < existing 412 | let actions = Parser::parse_multi(&[ 413 | Parsable::new("person.name", "person[5]"), 414 | Parsable::new("person.metadata", "person[-]"), 415 | ])?; 416 | let trans = TransformBuilder::default().add_actions(actions).build()?; 417 | let source = json!({"person":{"name":"Dean Karn", "metadata":[1, "blah", 45.6]}}); 418 | let destination = trans.apply(&source)?; 419 | let expected = json!({"person":[1, "blah", 45.6, null, null, "Dean Karn"]}); 420 | assert_eq!(expected, destination); 421 | Ok(()) 422 | } 423 | 424 | #[test] 425 | fn transformer_serialization() -> Result<(), Box> { 426 | let actions = Parser::parse_multi(&[ 427 | Parsable::new("person.name", "person[0]"), 428 | Parsable::new("person.metadata", "person[0]"), 429 | ])?; 430 | let trans = TransformBuilder::default().add_actions(actions).build()?; 431 | let res = serde_json::to_string(&trans)?; 432 | assert_eq!(res, "{\"actions\":[{\"type\":\"Setter\",\"namespace\":[{\"Object\":{\"id\":\"person\"}},{\"Array\":{\"index\":0}}],\"child\":{\"type\":\"Getter\",\"namespace\":[{\"Object\":{\"id\":\"person\"}},{\"Object\":{\"id\":\"name\"}}]}},{\"type\":\"Setter\",\"namespace\":[{\"Object\":{\"id\":\"person\"}},{\"Array\":{\"index\":0}}],\"child\":{\"type\":\"Getter\",\"namespace\":[{\"Object\":{\"id\":\"person\"}},{\"Object\":{\"id\":\"metadata\"}}]}}]}"); 433 | Ok(()) 434 | } 435 | 436 | #[test] 437 | fn test_set_and_get_top_level() -> Result<(), Box> { 438 | let actions = Parser::parse_multi(&[Parsable::new("", "")])?; 439 | let trans = TransformBuilder::default().add_actions(actions).build()?; 440 | let input = json!({ 441 | "existing_key":"my_val1", 442 | "my_array":["idx_0_value"] 443 | }); 444 | let expected = json!({"existing_key":"my_val1","my_array":["idx_0_value"]}); 445 | let output = trans.apply(&input)?; 446 | assert_eq!(expected, output); 447 | Ok(()) 448 | } 449 | 450 | #[test] 451 | fn test_sum() -> Result<(), Box> { 452 | let actions = Parser::parse_multi(&[ 453 | Parsable::new(r#"sum(const(1.1), arr, len(obj))"#, "sum"), 454 | Parsable::new("sum(len(arr))", "sum2"), 455 | ])?; 456 | let trans = TransformBuilder::default().add_actions(actions).build()?; 457 | 458 | let input = json!({ 459 | "arr": [1, 2, 3], 460 | "obj": {"key":"value"} 461 | }); 462 | let expected = json!({"sum":8.1, "sum2": 3}); 463 | let output = trans.apply(&input)?; 464 | assert_eq!(expected, output); 465 | 466 | let actions = Parser::parse_multi(&[Parsable::new("sum()", "sum")])?; 467 | let trans = TransformBuilder::default().add_actions(actions).build()?; 468 | 469 | let input = json!([1, 2, 3]); 470 | let expected = json!({"sum":6}); 471 | let output = trans.apply(&input)?; 472 | assert_eq!(expected, output); 473 | 474 | Ok(()) 475 | } 476 | 477 | #[test] 478 | fn test_len() -> Result<(), Box> { 479 | let actions = Parser::parse_multi(&[ 480 | Parsable::new("len()", "len1"), 481 | Parsable::new("len(arr)", "len2"), 482 | Parsable::new("len(obj)", "len3"), 483 | Parsable::new("len(obj.key)", "len4"), 484 | ])?; 485 | let trans = TransformBuilder::default().add_actions(actions).build()?; 486 | 487 | let input = json!({ 488 | "arr": [1, 2, 3], 489 | "obj": {"key":"value"} 490 | }); 491 | let expected = json!({"len1": 2, "len2": 3, "len3": 1, "len4": 5}); 492 | let output = trans.apply(&input)?; 493 | assert_eq!(expected, output); 494 | Ok(()) 495 | } 496 | 497 | #[test] 498 | fn test_trim() -> Result<(), Box> { 499 | let actions = Parser::parse_multi(&[ 500 | Parsable::new("trim(key)", "res1"), 501 | Parsable::new("trim_start(key)", "res2"), 502 | Parsable::new("trim_end(key)", "res3"), 503 | ])?; 504 | let trans = TransformBuilder::default().add_actions(actions).build()?; 505 | 506 | let input = json!({"key": " value "}); 507 | let expected = json!({"res1": "value", "res2": "value ", "res3": " value"}); 508 | let output = trans.apply(&input)?; 509 | assert_eq!(expected, output); 510 | Ok(()) 511 | } 512 | 513 | #[test] 514 | fn test_strip() -> Result<(), Box> { 515 | let actions = Parser::parse_multi(&[ 516 | Parsable::new(r#"strip_prefix("v", key)"#, "res1"), 517 | Parsable::new(r#"strip_suffix("e", key)"#, "res2"), 518 | ])?; 519 | let trans = TransformBuilder::default().add_actions(actions).build()?; 520 | 521 | let input = json!({"key": "value"}); 522 | let expected = json!({"res1": "alue", "res2": "valu"}); 523 | let output = trans.apply(&input)?; 524 | assert_eq!(expected, output); 525 | Ok(()) 526 | } 527 | } 528 | --------------------------------------------------------------------------------