├── .gitignore ├── res ├── sample.yaml └── sample.toml ├── .github ├── workflows │ ├── publish.yml │ └── ci.yml └── dependabot.yml ├── Cargo.toml ├── src ├── error.rs ├── util.rs └── lib.rs ├── CHANGELOG.md ├── tests ├── transpose_tuple.rs ├── query_value.rs └── query_value_result.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | .vscode 5 | -------------------------------------------------------------------------------- /res/sample.yaml: -------------------------------------------------------------------------------- 1 | str: s 2 | num: 123 3 | map: 4 | first: zzz 5 | second: yyy 6 | seq: 7 | - first 8 | - 42 9 | - hidden: tale 10 | author: 11 | name: jiftechnify 12 | age: 31 13 | -------------------------------------------------------------------------------- /res/sample.toml: -------------------------------------------------------------------------------- 1 | str = "s" 2 | int = 123 3 | float = 1.23 4 | date = 2021-12-18T12:15:12+09:00 5 | arr = ["first", "second", "third" ] 6 | 7 | [table] 8 | first = "zzz" 9 | second = "yyy" 10 | 11 | [[arr_of_tables]] 12 | hidden = "tale" 13 | 14 | [[arr_of_tables]] 15 | hoge = 1 16 | fuga = 2 17 | 18 | [[arr_of_tables]] 19 | inner_arr = [1, 2, 3] 20 | 21 | [author] 22 | name = "jiftechnify" 23 | age = 31 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to crates.io 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - closed 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | if: ${{ github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/') }} 15 | steps: 16 | - uses: actions/checkout@v6 17 | - uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec # v1.0.3 18 | id: auth 19 | - run: cargo publish 20 | env: 21 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "valq" 3 | version = "0.3.0" 4 | authors = ["jiftechnify "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "macros for querying semi-structured data with the JavaScript-like syntax" 8 | readme = "README.md" 9 | repository = "https://github.com/jiftechnify/valq" 10 | categories = ["rust-patterns"] 11 | keywords = ["macro", "query", "extract", "json", "serde"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | paste = "1.0.15" 17 | 18 | [dev-dependencies] 19 | serde = { version = "1.0.228", features = ["derive"] } 20 | serde_json = "1.0.146" 21 | serde_yaml = "0.9.34" 22 | toml = "0.9.10" 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | ci: 8 | name: CI 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v6 13 | 14 | - name: Set up Rust toolchain 15 | uses: dtolnay/rust-toolchain@stable 16 | 17 | - name: Use dependencies cache 18 | uses: Swatinem/rust-cache@v2 19 | 20 | - name: Lint by clippy 21 | uses: giraffate/clippy-action@v1 22 | 23 | - name: Run tests 24 | run: cargo test 25 | 26 | typos: 27 | name: Detect typos 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout repo 31 | uses: actions/checkout@v6 32 | 33 | - name: Run typos 34 | uses: crate-ci/typos@v1.40.0 35 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /// An error type returned from `Result`-returning macros provided by `valq` crate. 2 | #[derive(Debug)] 3 | pub enum Error { 4 | /// No value found at the specified path. 5 | ValueNotFoundAtPath(String), 6 | /// Casting a value with `->` operator (translates to `as_***()`/`as_***_mut()`) failed. 7 | AsCastFailed(String), 8 | /// Deserialization with `>>` operator failed. 9 | DeserializationFailed(Box), 10 | } 11 | 12 | impl std::fmt::Display for Error { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | use Error; 15 | match self { 16 | Error::ValueNotFoundAtPath(path) => { 17 | write!(f, "value not found at the path: {}", path) 18 | } 19 | Error::AsCastFailed(conv_name) => { 20 | write!(f, "casting with {}() failed", conv_name) 21 | } 22 | Error::DeserializationFailed(err) => { 23 | write!(f, "failed to deserialize the queried value: {}", err) 24 | } 25 | } 26 | } 27 | } 28 | 29 | impl std::error::Error for Error { 30 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 31 | use Error; 32 | match self { 33 | Error::DeserializationFailed(err) => Some(err.as_ref()), 34 | _ => None, 35 | } 36 | } 37 | } 38 | 39 | pub type Result = std::result::Result; 40 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | /// Transposes an arbitrary length of tuple of `Option`s/`Result`s into an `Option`/`Result` of a tuple. 2 | /// 3 | /// Syntax: 4 | /// 5 | /// - `Option`-variant: `transpose_tuple!(Option; e1, e2, ..., eN)` ... transposes a tuple of `Option`s. 6 | /// - You can omit `Option;` part, like: `transpose_tuple!(e1, e2, ..., eN)`. 7 | /// - `Result`-variant: `transpose_tuple!(Result; e1, e2, ..., eN)`... transposes a tuple of [`valq::Result`]s. 8 | /// 9 | /// Note: `Result`-variant is meant to be used with [`valq::Result`] (return type of `query_value_result!` macro). 10 | /// It doesn't support other error types for now. 11 | /// 12 | /// [`valq::Result`]: crate::Result 13 | /// 14 | /// ## Examples 15 | /// 16 | /// ``` 17 | /// // Options 18 | /// use serde_json::json; 19 | /// use valq::{query_value, transpose_tuple}; 20 | /// 21 | /// let valq = json!({ 22 | /// "name": "valq", 23 | /// "keywords": ["macro", "query", "serde"], 24 | /// "author": { 25 | /// "name": "jiftechnify", 26 | /// "age": 31 27 | /// } 28 | /// }); 29 | /// 30 | /// let picks = transpose_tuple!( 31 | /// query_value!(valq.name -> str), 32 | /// query_value!(valq.keywords[1] -> str), 33 | /// query_value!(valq.author.age -> u64), 34 | /// ); 35 | /// assert_eq!(picks, Some(("valq", "query", 31u64))); 36 | /// 37 | /// let wrong_picks = transpose_tuple!( 38 | /// query_value!(valq.name -> str), 39 | /// query_value!(valq.keywords[10] -> str), // out of bounds 40 | /// ); 41 | /// assert_eq!(wrong_picks, None); 42 | /// ``` 43 | /// 44 | /// ``` 45 | /// // Results 46 | /// # use serde_json::json; 47 | /// use valq::{query_value_result, transpose_tuple}; 48 | /// # let valq = json!({ 49 | /// # "name": "valq", 50 | /// # "keywords": ["macro", "query", "serde"], 51 | /// # "author": { 52 | /// # "name": "jiftechnify", 53 | /// # "age": 31 54 | /// # } 55 | /// # }); 56 | /// 57 | /// let picks = transpose_tuple!( 58 | /// Result; // don't forget this! 59 | /// query_value_result!(valq.name -> str), 60 | /// query_value_result!(valq.keywords[1] -> str), 61 | /// query_value_result!(valq.author.age -> u64), 62 | /// ); 63 | /// assert!(matches!(picks, Ok(("valq", "query", 31u64)))); 64 | /// 65 | /// let wrong_picks = transpose_tuple!( 66 | /// Result; 67 | /// query_value_result!(valq.name -> str), 68 | /// query_value_result!(valq.secrets), // non-existent path 69 | /// ); 70 | /// assert!(matches!(wrong_picks, Err(valq::Error::ValueNotFoundAtPath(_)))); 71 | /// ``` 72 | #[macro_export] 73 | macro_rules! transpose_tuple { 74 | (Option; $first:expr, $($rest:expr),+ $(,)?) => { 75 | (|| { 76 | Some(( $first?, $($rest?),+ )) 77 | })() 78 | }; 79 | (Result; $first:expr, $($rest:expr),+ $(,)?) => { 80 | (|| { 81 | Ok(( $first?, $($rest?),+ )) as $crate::Result<_> 82 | })() 83 | }; 84 | ($first:expr, $($rest:expr),+ $(,)?) => { 85 | transpose_tuple!(Option; $first, $($rest),+) 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.0] - 2025-12-17 9 | 10 | ### Added 11 | 12 | - **`query_value_result!` macro** for Result-returning queries ([#56](https://github.com/jiftechnify/valq/pull/56)) 13 | - A new flavor of `query_value!` that returns a `valq::Result` instead of an `Option` 14 | - Enables better error handling for queries where keys or indices might not exist 15 | - Supports all the same query syntax as `query_value!` 16 | 17 | - **`transpose_tuple!` helper macro** for transposing tuples of Results/Options ([#58](https://github.com/jiftechnify/valq/pull/58)) 18 | - Convert `(Option, Option, ...)` into `Option<(A, B, ...)>` 19 | - Convert `(Result, Result, ...)` into `Result<(A, B, ...)>` 20 | - Particularly useful when combining multiple `query_value!`/`query_value_result!` calls 21 | 22 | ## [0.2.0] - 2025-12-12 23 | 24 | ### Added 25 | 26 | - **Deserialization operator (`>>`)** for deserializing queried values into arbitrary types ([#46](https://github.com/jiftechnify/valq/pull/46)) 27 | - Deserialize queried values into any type implementing `serde::Deserialize` 28 | - Syntax: `query_value!(obj.field >> (Type))` 29 | - Don't forget to wrap the destination type with parentheses! 30 | 31 | - **Null coalescing operator (`??`)** for unwrapping query results with default values ([#50](https://github.com/jiftechnify/valq/pull/50)) 32 | - `... ?? ` to provide a custom default value 33 | - `... ?? default` to use `Default::default()` as the fallback 34 | 35 | ### Changed 36 | 37 | - **Arbitrary type casting with `->` operator** ([#46](https://github.com/jiftechnify/valq/pull/46)) 38 | - No longer limited to hard-coded conversions! 39 | - Any `as_xxx()` method available on the value type can be used 40 | 41 | - **Made syntax more JS-like** ([#47](https://github.com/jiftechnify/valq/pull/47)) 42 | - More flexible bracket notation `[...]` 43 | - Put arbitrary Rust expressions and enjoy unlimited dynamic queries! 44 | - Removed `."key"` syntax in favor of revamped bracket notation 45 | - `??` operator for unwrapping `Option`s 46 | 47 | ### Documentation 48 | 49 | - Removed tedious enumeration of macro matching patterns from documentation ([#49](https://github.com/jiftechnify/valq/pull/49)) 50 | - Added comprehensive examples for all major features 51 | - Clarified query syntax specification 52 | 53 | ## [0.1.0] - 2021-12-19 54 | 55 | Initial release with basic query functionality. 56 | 57 | ### Added 58 | 59 | - `query_value!` macro for querying semi-structured data 60 | - Dot notation for accessing object properties (`.field`) 61 | - Bracket notation for array/object indexing (`[index]`) 62 | - Mutable reference extraction with `mut` prefix 63 | - Basic type casting using `as_***()` methods with `->` operator 64 | 65 | [0.3.0]: https://github.com/jiftechnify/valq/compare/0.2.0...0.3.0 66 | [0.2.0]: https://github.com/jiftechnify/valq/compare/0.1.0...0.2.0 67 | [0.1.0]: https://github.com/jiftechnify/valq/releases/tag/0.1.0 68 | -------------------------------------------------------------------------------- /tests/transpose_tuple.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use valq::{query_value, query_value_result, transpose_tuple, Error}; 3 | 4 | #[test] 5 | fn test_option_variant_all_some() { 6 | let data = json!({ 7 | "name": "valq", 8 | "version": "0.2.0", 9 | "stars": 100 10 | }); 11 | 12 | let result = transpose_tuple!( 13 | query_value!(data.name -> str), 14 | query_value!(data.version -> str), 15 | query_value!(data.stars -> u64), 16 | ); 17 | 18 | assert_eq!(result, Some(("valq", "0.2.0", 100u64))); 19 | } 20 | 21 | #[test] 22 | fn test_option_variant_with_none() { 23 | let data = json!({ 24 | "name": "valq", 25 | "version": "0.2.0", 26 | }); 27 | 28 | let result = transpose_tuple!( 29 | query_value!(data.name -> str), 30 | query_value!(data.nonexistent -> str), 31 | query_value!(data.version -> str), 32 | ); 33 | 34 | assert_eq!(result, None); 35 | } 36 | 37 | #[test] 38 | fn test_option_variant_explicit_option_prefix() { 39 | let data = json!({ 40 | "a": 1, 41 | "b": 2, 42 | "c": 3 43 | }); 44 | 45 | let result = transpose_tuple!( 46 | Option; 47 | query_value!(data.a -> u64), 48 | query_value!(data.b -> u64), 49 | query_value!(data.c -> u64), 50 | ); 51 | 52 | assert_eq!(result, Some((1u64, 2u64, 3u64))); 53 | } 54 | 55 | #[test] 56 | fn test_option_variant_many_elements() { 57 | let data = json!({ 58 | "v1": 1, 59 | "v2": 2, 60 | "v3": 3, 61 | "v4": 4, 62 | "v5": 5, 63 | "v6": 6 64 | }); 65 | 66 | let result = transpose_tuple!( 67 | query_value!(data.v1 -> u64), 68 | query_value!(data.v2 -> u64), 69 | query_value!(data.v3 -> u64), 70 | query_value!(data.v4 -> u64), 71 | query_value!(data.v5 -> u64), 72 | query_value!(data.v6 -> u64), 73 | ); 74 | 75 | assert_eq!(result, Some((1u64, 2u64, 3u64, 4u64, 5u64, 6u64))); 76 | } 77 | 78 | #[test] 79 | fn test_option_variant_trailing_comma() { 80 | let data = json!({ 81 | "a": "foo", 82 | "b": "bar", 83 | }); 84 | 85 | let result = transpose_tuple!(query_value!(data.a -> str), query_value!(data.b -> str),); 86 | 87 | assert_eq!(result, Some(("foo", "bar"))); 88 | } 89 | 90 | #[test] 91 | fn test_result_variant_all_ok() { 92 | let data = json!({ 93 | "name": "valq", 94 | "keywords": ["macro", "query", "serde"], 95 | "author": { 96 | "name": "jiftechnify", 97 | "age": 31 98 | } 99 | }); 100 | 101 | let result = transpose_tuple!( 102 | Result; 103 | query_value_result!(data.name -> str), 104 | query_value_result!(data.keywords[1] -> str), 105 | query_value_result!(data.author.age -> u64), 106 | ); 107 | 108 | assert!(result.is_ok()); 109 | assert_eq!(result.unwrap(), ("valq", "query", 31u64)); 110 | } 111 | 112 | #[test] 113 | fn test_result_variant_with_error() { 114 | let data = json!({ 115 | "name": "valq", 116 | "version": "0.2.0" 117 | }); 118 | 119 | let result = transpose_tuple!( 120 | Result; 121 | query_value_result!(data.name -> str), 122 | query_value_result!(data.nonexistent), 123 | ); 124 | 125 | assert!(result.is_err()); 126 | assert!(matches!(result.unwrap_err(), Error::ValueNotFoundAtPath(_))); 127 | } 128 | 129 | #[test] 130 | fn test_result_variant_type_mismatch_error() { 131 | let data = json!({ 132 | "value": "not a number", 133 | "other": 123 134 | }); 135 | 136 | let result = transpose_tuple!( 137 | Result; 138 | query_value_result!(data.value -> u64), 139 | query_value_result!(data.other -> u64), 140 | ); 141 | 142 | assert!(result.is_err()); 143 | } 144 | 145 | #[test] 146 | fn test_result_variant_array_out_of_bounds() { 147 | let data = json!({ 148 | "items": [1, 2, 3] 149 | }); 150 | 151 | let result = transpose_tuple!( 152 | Result; 153 | query_value_result!(data.items[0] -> u64), 154 | query_value_result!(data.items[10] -> u64), 155 | ); 156 | 157 | assert!(result.is_err()); 158 | } 159 | 160 | #[test] 161 | fn test_result_variant_trailing_comma() { 162 | let data = json!({ 163 | "a": 1.5, 164 | "b": 2.5, 165 | }); 166 | 167 | let result = transpose_tuple!( 168 | Result; 169 | query_value_result!(data.a -> f64), 170 | query_value_result!(data.b -> f64), 171 | ); 172 | 173 | assert!(result.is_ok()); 174 | assert_eq!(result.unwrap(), (1.5f64, 2.5f64)); 175 | } 176 | 177 | #[test] 178 | fn test_mixed_types() { 179 | let data = json!({ 180 | "string": "hello", 181 | "number": 42, 182 | "boolean": true, 183 | "float": 3.14 184 | }); 185 | 186 | let result = transpose_tuple!( 187 | query_value!(data.string -> str), 188 | query_value!(data.number -> u64), 189 | query_value!(data.boolean -> bool), 190 | query_value!(data.float -> f64), 191 | ); 192 | 193 | assert_eq!(result, Some(("hello", 42u64, true, 3.14f64))); 194 | } 195 | 196 | #[test] 197 | fn test_nested_object_access() { 198 | let data = json!({ 199 | "level1": { 200 | "level2": { 201 | "level3": { 202 | "value": "deep", 203 | "number": 99 204 | } 205 | } 206 | } 207 | }); 208 | 209 | let result = transpose_tuple!( 210 | Result; 211 | query_value_result!(data.level1.level2.level3.value -> str), 212 | query_value_result!(data.level1.level2.level3.number -> u64), 213 | ); 214 | 215 | assert!(result.is_ok()); 216 | assert_eq!(result.unwrap(), ("deep", 99u64)); 217 | } 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # valq   [![Docs.rs shield]][Docs.rs link] [![crates.io shield]][crates.io link] 2 | 3 | [Docs.rs shield]: https://img.shields.io/docsrs/valq/latest 4 | [Docs.rs link]: https://docs.rs/valq/latest 5 | [crates.io shield]: https://img.shields.io/crates/v/valq 6 | [crates.io link]: https://crates.io/crates/valq 7 | 8 | `valq` provides macros for querying semi-structured ("JSON-ish") data **with the JavaScript-like syntax**. 9 | 10 | Look & Feel: 11 | 12 | ```rust 13 | let obj: serde_json::Value = ...; 14 | 15 | // without valq: tedious and_then() chain... 16 | let deep = obj 17 | .get("path") 18 | .and_then(|v| v.get("to")) 19 | .and_then(|v| v.get("value")) 20 | .and_then(|v| v.get("at")) 21 | .and_then(|v| v.get("deep")); 22 | 23 | // with valq: very concise and readable! 24 | use valq::query_value; 25 | let deep = query_value!(obj.path.to.value.at.deep); 26 | ``` 27 | 28 | ## Installation 29 | 30 | Add this to the `Cargo.toml` in your project: 31 | 32 | ```toml 33 | [dependencies] 34 | valq = "*" 35 | ``` 36 | 37 | ## What's provided 38 | 39 | The principal macro provided by this crate is `query_value!`. 40 | Also, there is a `Result`-returning variant of `query_value!`, called `query_value_result!`. 41 | 42 | ### `query_value!` macro 43 | A macro for querying, extracting and converting inner value of semi-structured data. 44 | 45 | #### Basic Queries 46 | ```rust 47 | // get field `foo` from JSON object `obj` 48 | let foo = query_value!(obj.foo); 49 | 50 | // get the first item of the nested JSON array `arr` in `obj` 51 | let head = query_value!(obj.arr[0]); 52 | 53 | // more complex query, just works! 54 | let abyss = query_value!(obj.path.to.matrix[0][1].abyss); 55 | ``` 56 | 57 | #### Extracting Mutable Reference to Inner Value 58 | ```rust 59 | use serde_json::{json, Value} 60 | 61 | let mut obj = json!({"foo": { "bar": { "x": 1, "y": 2 }}}); 62 | { 63 | // prefixed `mut` means extracting mutable reference 64 | let bar: &mut Value = query_value!(mut obj.foo.bar).unwrap(); 65 | *bar = json!({"x": 100, "y": 200}); 66 | } 67 | // with `->` syntax, you can cast `Value` as typed value (see below) 68 | assert_eq!(query_value!(obj.foo.bar.x -> u64), Some(100)); 69 | assert_eq!(query_value!(obj.foo.bar.y -> u64), Some(200)); 70 | ``` 71 | 72 | #### Casting & Deserializing to Specified Type 73 | ```rust 74 | // try to cast the queried value into `u64` using `as_u64()` method on that value. 75 | // results in `None` in case of type mismatch 76 | let foo_u64: Option = query_value!(obj.foo -> u64); 77 | 78 | // in the context of mutable reference extraction (see below), `as_xxx_mut()` method is used instead. 79 | let arr_vec: Option<&mut Vec> = query_value!(mut obj.arr -> array); 80 | ``` 81 | 82 | ```rust 83 | use serde::Deserialize; 84 | use serde_json::json; 85 | use valq::query_value; 86 | 87 | #[derive(Deserialize)] 88 | struct Person { 89 | name: String, 90 | age: u8, 91 | } 92 | 93 | let j = json!({"author": {"name": "jiftechnify", "age": 31}}); 94 | 95 | // try to deserialize the queried value into a value of type `Person`. 96 | let author: Option = query_value!(j.author >> (Person)); 97 | ``` 98 | 99 | #### Unwrapping Query Results with Default Values 100 | ```rust 101 | use serde_json::json; 102 | use valq::query_value; 103 | 104 | let obj = json!({"foo": {"bar": "not a number"}}); 105 | assert_eq!(query_value!(obj.foo.bar -> str ?? "failed!"), "not a number"); 106 | assert_eq!(query_value!(obj.foo.bar -> u64 ?? 42), 42); // explicitly provided default 107 | assert_eq!(query_value!(obj.foo.bar -> u64 ?? default), 0u64); // using u64::default() 108 | ``` 109 | 110 | ### `query_value_result!` macro 111 | A variant of `query_value!` that returns `Result` instead of `Option`. 112 | 113 | ```rust 114 | use serde::Deserialize; 115 | use serde_json::json; 116 | use valq::{query_value_result, Error}; 117 | 118 | let obj = json!({"foo": {"bar": 42}}); 119 | 120 | // Error::ValueNotFoundAtPath: querying non-existent path 121 | let result = query_value_result!(obj.foo.baz); 122 | assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); 123 | 124 | // Error::AsCastFailed: type casting failure 125 | let result = query_value_result!(obj.foo.bar -> str); 126 | assert!(matches!(result, Err(Error::AsCastFailed(_)))); 127 | 128 | // Error::DeserializationFailed: deserialization failure 129 | let result = query_value_result!(obj.foo >> (Vec)); 130 | assert!(matches!(result, Err(Error::DeserializationFailed(_)))); 131 | ``` 132 | 133 | ### Helper: `transpose_tuple!` macro 134 | Transposes a tuple of `Option`s/`Result`s into an `Option`/`Result` of a tuple. 135 | This is meant to be used with `query_value!`/`query_value_result!` macros, for "cherry-picking" deep value from data. 136 | 137 | ```rust 138 | use serde_json::json; 139 | use valq::{query_value, transpose_tuple}; 140 | 141 | let data = json!({"name": "valq", "version": "0.2.0"}); 142 | 143 | // Combine multiple Option results into Option 144 | let picks = transpose_tuple!( 145 | query_value!(data.name -> str), 146 | query_value!(data.version -> str), 147 | ); 148 | assert_eq!(picks, Some(("valq", "0.2.0"))); 149 | 150 | // For Result variant, "Result;" prefix is needed 151 | let picks = transpose_tuple!( 152 | Result; 153 | query_value_result!(data.name -> str), 154 | query_value_result!(data.version -> str), 155 | ); 156 | assert!(picks.is_ok()); 157 | ``` 158 | 159 | ## Compatibility 160 | The `query_value!` macro can be used with arbitrary data structure(to call, `Value`) that supports `get(&self, idx) -> Option<&Value>` method that retrieves a value at `idx`. 161 | 162 | Extracting mutable reference is also supported if your `Value` supports `get_mut(&mut self, idx) -> Option<&Value>`. 163 | 164 | Instances of compatible data structures: 165 | - [`serde_json::Value`](https://docs.rs/serde_json/latest/serde_json/enum.Value.html) 166 | - [`serde_yaml::Value`](https://docs.rs/serde_yaml/latest/serde_yaml/enum.Value.html) 167 | - [`toml::Value`](https://docs.rs/toml/latest/toml/value/enum.Value.html) 168 | - and more... 169 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "equivalent" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 10 | 11 | [[package]] 12 | name = "hashbrown" 13 | version = "0.16.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 16 | 17 | [[package]] 18 | name = "indexmap" 19 | version = "2.12.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 22 | dependencies = [ 23 | "equivalent", 24 | "hashbrown", 25 | ] 26 | 27 | [[package]] 28 | name = "itoa" 29 | version = "1.0.15" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 32 | 33 | [[package]] 34 | name = "memchr" 35 | version = "2.7.6" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 38 | 39 | [[package]] 40 | name = "paste" 41 | version = "1.0.15" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 44 | 45 | [[package]] 46 | name = "proc-macro2" 47 | version = "1.0.103" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 50 | dependencies = [ 51 | "unicode-ident", 52 | ] 53 | 54 | [[package]] 55 | name = "quote" 56 | version = "1.0.41" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 59 | dependencies = [ 60 | "proc-macro2", 61 | ] 62 | 63 | [[package]] 64 | name = "ryu" 65 | version = "1.0.20" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 68 | 69 | [[package]] 70 | name = "serde" 71 | version = "1.0.228" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 74 | dependencies = [ 75 | "serde_core", 76 | "serde_derive", 77 | ] 78 | 79 | [[package]] 80 | name = "serde_core" 81 | version = "1.0.228" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 84 | dependencies = [ 85 | "serde_derive", 86 | ] 87 | 88 | [[package]] 89 | name = "serde_derive" 90 | version = "1.0.228" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 93 | dependencies = [ 94 | "proc-macro2", 95 | "quote", 96 | "syn", 97 | ] 98 | 99 | [[package]] 100 | name = "serde_json" 101 | version = "1.0.146" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8" 104 | dependencies = [ 105 | "itoa", 106 | "memchr", 107 | "ryu", 108 | "serde", 109 | "serde_core", 110 | ] 111 | 112 | [[package]] 113 | name = "serde_spanned" 114 | version = "1.0.4" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" 117 | dependencies = [ 118 | "serde_core", 119 | ] 120 | 121 | [[package]] 122 | name = "serde_yaml" 123 | version = "0.9.34+deprecated" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 126 | dependencies = [ 127 | "indexmap", 128 | "itoa", 129 | "ryu", 130 | "serde", 131 | "unsafe-libyaml", 132 | ] 133 | 134 | [[package]] 135 | name = "syn" 136 | version = "2.0.108" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" 139 | dependencies = [ 140 | "proc-macro2", 141 | "quote", 142 | "unicode-ident", 143 | ] 144 | 145 | [[package]] 146 | name = "toml" 147 | version = "0.9.10+spec-1.1.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" 150 | dependencies = [ 151 | "indexmap", 152 | "serde_core", 153 | "serde_spanned", 154 | "toml_datetime", 155 | "toml_parser", 156 | "toml_writer", 157 | "winnow", 158 | ] 159 | 160 | [[package]] 161 | name = "toml_datetime" 162 | version = "0.7.5+spec-1.1.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" 165 | dependencies = [ 166 | "serde_core", 167 | ] 168 | 169 | [[package]] 170 | name = "toml_parser" 171 | version = "1.0.6+spec-1.1.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" 174 | dependencies = [ 175 | "winnow", 176 | ] 177 | 178 | [[package]] 179 | name = "toml_writer" 180 | version = "1.0.6+spec-1.1.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" 183 | 184 | [[package]] 185 | name = "unicode-ident" 186 | version = "1.0.12" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 189 | 190 | [[package]] 191 | name = "unsafe-libyaml" 192 | version = "0.2.11" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 195 | 196 | [[package]] 197 | name = "valq" 198 | version = "0.3.0" 199 | dependencies = [ 200 | "paste", 201 | "serde", 202 | "serde_json", 203 | "serde_yaml", 204 | "toml", 205 | ] 206 | 207 | [[package]] 208 | name = "winnow" 209 | version = "0.7.14" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 212 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # valq 2 | //! `valq` provides macros for querying and extracting an inner value from a structured data **with the JavaScript-like syntax**. 3 | //! 4 | //! ``` 5 | //! # use serde_json::json; 6 | //! use serde_json::Value; 7 | //! use valq::{query_value, query_value_result}; 8 | //! 9 | //! // let obj: Value = ...; 10 | //! # let obj = json!({}); 11 | //! let deep_val: Option<&Value> = query_value!(obj.path.to.value.at.deep); 12 | //! let deep_val_res: Result<&Value, valq::Error> = query_value_result!(obj.path.to.value.at.deep); 13 | //! ``` 14 | //! 15 | //! The principal macro provided by this crate is `query_value!`. Read [the `query_value` doc] for detailed usage. 16 | //! 17 | //! Also, there is a `Result`-returning variant of `query_value!`, called [`query_value_result!`]. 18 | //! 19 | //! [the `query_value` doc]: crate::query_value 20 | //! [`query_value_result!`]: crate::query_value_result 21 | 22 | mod error; 23 | pub use error::{Error, Result}; 24 | 25 | mod util; 26 | 27 | #[doc(hidden)] 28 | pub use paste::paste as __paste; 29 | 30 | macro_rules! doc_query_value { 31 | ($query_value:item) => { 32 | /// A macro for querying an inner value of a structured ("JSON-ish") data. 33 | /// 34 | /// ``` 35 | /// use valq::query_value; 36 | /// # use serde_json::{json, Value}; 37 | /// # 38 | /// # let obj = json!({"foo":{"bar":"bar!"},"arr":[1,2,3],"path":{"to":{"matrix":[[{},{"abyss":"I gaze into you."}],[{},{}]]}}}); 39 | /// # let arr = json!([1,2,3]); 40 | /// 41 | /// // let obj = json!({ ... }); 42 | /// // let arr = json!([ ... ]); 43 | /// 44 | /// // get the field `foo` from the JSON-ish object `obj` 45 | /// let foo: Option<&Value> = query_value!(obj.foo); 46 | /// 47 | /// // get the first item of the nested JSON array `arr` in `obj` 48 | /// let head = query_value!(obj.arr[0]); 49 | /// 50 | /// // more complex query, just works! 51 | /// let abyss = query_value!(obj.path.to.matrix[0][1].abyss); 52 | /// 53 | /// ``` 54 | /// 55 | /// ## Query Notations 56 | /// 57 | /// You can traverse through semi-structured ("JSON-ish") data by chaining JavaScript-like accessor notaions: 58 | /// 59 | /// - **Dot notation** (`.field`): Access a property of an "object" (key-value structure) by name 60 | /// - **Bracket notation** (`["field"]`): Access a property of an "object", or an element of an "array"-like value by index 61 | /// - With string index, you get access to object properties, similar to dot notation. 62 | /// This is especially useful for keys that are not valid Rust identifiers (e.g. `"1st"`, `"foo-bar"`). 63 | /// - With integer index, you get access to array elements. 64 | /// - Dynamic query: you can place a Rust expression that evaluate to string or integer in the brackets. 65 | /// 66 | /// ## Query Result 67 | /// 68 | /// `query_value!` returns an `Option` as the result of the query (except when unwrapping operator `??` is used; see below for details). 69 | /// 70 | /// Queries can fail for the following reasons. In that case, `query_value!` returns `None`: 71 | /// 72 | /// - The specified key or index does not exist in the target value 73 | /// - Indexing an object (key-value structure) with an integer 74 | /// - Indexing an array with a string key 75 | /// 76 | /// Otherwise, i.e. if your query succeeds, `query_value!` returns the queried value wrapped in `Some`. 77 | /// 78 | /// With basic queries, `query_value!` extracts a shared reference (`&`) to the inner value by default. Think of it as a function that has following signature: 79 | /// 80 | /// ```txt 81 | /// query_value!(query...) -> Option(&Value) 82 | /// ``` 83 | /// 84 | /// ## `mut`: Extracting Mutable Reference to Inner Value 85 | /// 86 | /// Queries start with `mut` extract the mutable reference (`&mut`) to the inner value instead: 87 | /// 88 | /// ```txt 89 | /// query_value!(mut query...) -> Option(&mut Value) 90 | /// ``` 91 | /// 92 | /// Example: 93 | /// 94 | /// ``` 95 | /// use serde_json::{json, Value}; 96 | /// use valq::query_value; 97 | /// 98 | /// let mut obj = json!({"foo": { "bar": { "x": 1, "y": 2 }}}); 99 | /// { 100 | /// let bar: &mut Value = query_value!(mut obj.foo.bar).unwrap(); 101 | /// *bar = json!({"x": 100, "y": 200}); 102 | /// } 103 | /// // see below for `->` syntax 104 | /// assert_eq!(query_value!(obj.foo.bar.x -> u64), Some(100)); 105 | /// assert_eq!(query_value!(obj.foo.bar.y -> u64), Some(200)); 106 | /// ``` 107 | /// 108 | /// ## `->`: Cast Value with `as_***()` 109 | /// 110 | /// Queries end with `-> ***` try to cast the extracted value with `as_***()` method. 111 | /// In the `mut` context, `as_***_mut()` method is used instead. 112 | /// 113 | /// ```txt 114 | /// // assuming your value has the method `as_str(&self) -> Option(&str)` 115 | /// query_value!(query... -> str) -> Option(&str) 116 | /// 117 | /// // assuming your value has the method `as_array_mut(&mut self) -> Option(&mut Vec)` 118 | /// query_value!(mut query... -> array) -> Option(&mut Vec) 119 | /// ``` 120 | /// 121 | /// ``` 122 | /// use serde_json::{json, Value}; 123 | /// use valq::query_value; 124 | /// 125 | /// let mut obj = json!({"foo": "hello", "arr": [1, 2]}); 126 | /// 127 | /// // try to cast extracted value with `as_u64` method on that value 128 | /// // results in `None` in case of type mismatch 129 | /// let foo_str: Option<&str> = query_value!(obj.foo -> str); 130 | /// assert_eq!(foo_str, Some("hello")); 131 | /// 132 | /// // `mut` example 133 | /// let arr_vec: Option<&mut Vec> = query_value!(mut obj.arr -> array); 134 | /// assert_eq!(arr_vec, Some(&mut vec![json!(1), json!(2)])); 135 | /// ``` 136 | /// 137 | /// ## `>>`: Deserializing Value into Any Types Implement `serde::Deserialize` trait 138 | /// 139 | /// Queries end with `>> (Type)` try to deserialize the extracted value using `deserialize()` method on the `Type`; 140 | /// i.e. you can get a value of your `Type` out of the queried value, assuming the `Type` implements `serde::Deserialize`. 141 | /// 142 | /// ```txt 143 | /// // assuming `Type` implements `serde::Deserialize` 144 | /// query_value!(query... >> (Type)) -> Option(Type) 145 | /// ``` 146 | /// 147 | /// ``` 148 | /// use serde::Deserialize; 149 | /// use serde_json::json; 150 | /// use valq::query_value; 151 | /// 152 | /// #[derive(Debug, PartialEq, Deserialize)] 153 | /// struct Person { 154 | /// name: String, 155 | /// age: u8, 156 | /// } 157 | /// 158 | /// let j = json!({"author": {"name": "jiftechnify", "age": 31}}); 159 | /// assert_eq!( 160 | /// query_value!(j.author >> (Person)), 161 | /// Some(Person { 162 | /// name: "jiftechnify".into(), 163 | /// age: 31u8, 164 | /// }), 165 | /// ); 166 | /// ``` 167 | /// 168 | /// A few notes on the `>>` operator: 169 | /// 170 | /// - Basically, the type name after `>>` must be wrapped with parentheses. As a special case, you can omit that parens only if your type name consists of *a single identifier*, for simplicity. 171 | /// + For example, the query above can be simplified to `j.author >> Person`. 172 | /// - Deserialization with `>>` involves cloning of the queried value. You may want to use `->` type casting if possible. 173 | /// 174 | /// ## `??`: Unwarp Query Result with Default Value 175 | /// 176 | /// You put `?? ...` at the very end of the query to unwrap the query result with providing a default value in case of query failure. 177 | /// 178 | /// - `?? `: Use the value of`` as the default. 179 | /// - `?? default`: Use `Default::default()` as the default. 180 | /// 181 | /// This is especilly useful together with `->` or `>>` conversions: 182 | /// 183 | /// ``` 184 | /// use serde_json::{json, Value}; 185 | /// use valq::query_value; 186 | /// 187 | /// let obj = json!({"foo": {"bar": "not a number"}}); 188 | /// assert_eq!(query_value!(obj.foo.bar -> str ?? "failed!"), "not a number"); 189 | /// assert_eq!(query_value!(obj.foo.bar -> u64 ?? 42), 42); 190 | /// assert_eq!(query_value!(obj.foo.bar -> u64 ?? default), 0u64); // u64::default() 191 | /// ``` 192 | /// 193 | /// ## Query Syntax Specification 194 | /// 195 | /// ```txt 196 | /// query_value!( 197 | /// ("mut")? 198 | /// ("." | "[" "]")* 199 | /// ("->" | ">>" "(" ")")? 200 | /// ("??" ("default" | ))? 201 | /// ) 202 | /// ``` 203 | /// 204 | /// where: 205 | /// 206 | /// - ``: An expression evaluates to a structured data to be queried 207 | /// - ``: A property/field key to extract value from a key-value structure 208 | /// - ``: An index to extract value from structure 209 | /// + For an array-like structure, any expressions evaluates to an integer can be used 210 | /// + For a key-value structure, any expressions evaluates to a string can be used 211 | /// - ``: A destination type of type casting with `as_***()` / `as_***_mut()` methods 212 | /// - ``: A type name into which the queried value is deserialized 213 | /// + The specified type *MUST* implement the `serde::Deserialize` trait 214 | /// + If the type name contains only a single identifier, you can omit parentheses around it 215 | /// - ``: An expression for a default value in case of query failure 216 | /// + Instead, you can put `default` keyword in this place to use `Default::default()` as the default value 217 | /// 218 | /// ## Compatibility 219 | /// 220 | /// `query_value!` can be used with arbitrary data structure(to call, `Value`) that supports `get(&self, idx) -> Option<&Value>` method that retrieves a value at `idx`. 221 | /// 222 | /// Extracting mutable reference is also supported if your `Value` supports `get_mut(&mut self, idx) -> Option<&Value>`. 223 | /// 224 | /// Instances of compatible data structures: 225 | /// 226 | /// - [`serde_json::Value`](https://docs.rs/serde_json/latest/serde_json/enum.Value.html) 227 | /// - [`serde_yaml::Value`](https://docs.rs/serde_yaml/latest/serde_yaml/enum.Value.html) 228 | /// - [`toml::Value`](https://docs.rs/toml/latest/toml/value/enum.Value.html) 229 | /// - and more... 230 | #[macro_export] 231 | $query_value 232 | }; 233 | } 234 | 235 | // fake implementation illustrates the macro syntax for docs 236 | #[cfg(doc)] 237 | doc_query_value! {macro_rules! query_value { 238 | ($(mut)? $value:tt $(query:tt)* $(?? $default:expr)?) => {}; 239 | ($(mut)? $value:tt $(query:tt)* -> $as:ident $(?? $default:expr)?) => {}; 240 | ($(mut)? $value:tt $(query:tt)* >> ($deser_to:ty) $(?? $default:expr)?) => {}; 241 | }} 242 | 243 | // actual implementation 244 | #[cfg(not(doc))] 245 | doc_query_value! {macro_rules! query_value { 246 | /* non-mut traversal */ 247 | // traversal step 248 | (@trv { $vopt:expr } . $key:ident $($rest:tt)*) => { 249 | $crate::query_value!(@trv { $vopt.and_then(|v| v.get(stringify!($key))) } $($rest)*) 250 | }; 251 | (@trv { $vopt:expr } [ $idx:expr ] $($rest:tt)*) => { 252 | $crate::query_value!(@trv { $vopt.and_then(|v| v.get($idx)) } $($rest)*) 253 | }; 254 | // conversion step -> convert then jump to finalization step 255 | (@trv { $vopt:expr } -> $dest:ident $($rest:tt)*) => { 256 | $crate::__paste! { 257 | $crate::query_value!(@fin { $vopt.and_then(|v| v.[]()) } $($rest)*) 258 | } 259 | }; 260 | (@trv { $vopt:expr } >> $dest:ident $($rest:tt)*) => { 261 | $crate::query_value!(@fin { $vopt.and_then(|v| <$dest>::deserialize(v.clone()).ok()) } $($rest)*) 262 | }; 263 | (@trv { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { 264 | $crate::query_value!(@fin { $vopt.and_then(|v| <$dest>::deserialize(v.clone()).ok()) } $($rest)*) 265 | }; 266 | // no conversion -> just jump to finalization step 267 | (@trv { $vopt:expr } $($rest:tt)*) => { 268 | $crate::query_value!(@fin { $vopt } $($rest)*) 269 | }; 270 | 271 | /* mut traversal */ 272 | // traversal step 273 | (@trv_mut { $vopt:expr } . $key:ident $($rest:tt)*) => { 274 | $crate::query_value!(@trv_mut { $vopt.and_then(|v| v.get_mut(stringify!($key))) } $($rest)*) 275 | }; 276 | (@trv_mut { $vopt:expr } [ $idx:expr ] $($rest:tt)*) => { 277 | $crate::query_value!(@trv_mut { $vopt.and_then(|v| v.get_mut($idx)) } $($rest)*) 278 | }; 279 | // conversion step -> convert then jump to finalization step 280 | (@trv_mut { $vopt:expr } -> $dest:ident $($rest:tt)*) => { 281 | $crate::__paste! { 282 | $crate::query_value!(@fin { $vopt.and_then(|v| v.[]()) } $($rest)*) 283 | } 284 | }; 285 | (@trv_mut { $vopt:expr } >> $dest:ident $($rest:tt)*) => { 286 | $crate::query_value!(@fin { $vopt.and_then(|v| <$dest>::deserialize(v.clone()).ok()) } $($rest)*) 287 | }; 288 | (@trv_mut { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { 289 | $crate::query_value!(@fin { $vopt.and_then(|v| <$dest>::deserialize(v.clone()).ok()) } $($rest)*) 290 | }; 291 | // no conversion -> just jump to finalization step 292 | (@trv_mut { $vopt:expr } $($rest:tt)*) => { 293 | $crate::query_value!(@fin { $vopt } $($rest)*) 294 | }; 295 | 296 | /* finalize: handle unwrapping operator */ 297 | (@fin { $vopt:expr } ?? default) => { 298 | $vopt.unwrap_or_default() 299 | }; 300 | (@fin { $vopt:expr } ?? $default:expr) => { 301 | $vopt.unwrap_or_else(|| $default) 302 | }; 303 | // no unwrapping operator 304 | (@fin { $vopt:expr }) => { 305 | $vopt 306 | }; 307 | // unreachable branch -> report syntax error 308 | (@fin $($_:tt)*) => { 309 | compile_error!("invalid query syntax for query_value!()") 310 | }; 311 | 312 | /* entry points */ 313 | (mut $v:tt $($rest:tt)*) => { 314 | $crate::query_value!(@trv_mut { Some(&mut $v) } $($rest)*) 315 | }; 316 | ($v:tt $($rest:tt)*) => { 317 | $crate::query_value!(@trv { Some(&$v) } $($rest)*) 318 | }; 319 | }} 320 | 321 | macro_rules! doc_query_value_result { 322 | ($query_value_result:item) => { 323 | /// A `Result`-returning variant of [`query_value!`]. 324 | /// 325 | /// See the documentation of [`query_value!`] macro for detailed usage. 326 | /// 327 | /// If your query fails, this macro returns a [`valq::Error`] describing the failure reason. 328 | /// 329 | /// ``` 330 | /// use serde::Deserialize; 331 | /// use serde_json::json; 332 | /// use valq::{query_value_result, Error}; 333 | /// 334 | /// let obj = json!({"foo": {"bar": 42}}); 335 | /// 336 | /// // Error::ValueNotFoundAtPath: querying non-existent path 337 | /// let result = query_value_result!(obj.foo.baz); 338 | /// assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); 339 | /// 340 | /// // Error::AsCastFailed: type casting failure 341 | /// let result = query_value_result!(obj.foo.bar -> str); 342 | /// assert!(matches!(result, Err(Error::AsCastFailed(_)))); 343 | /// 344 | /// // Error::DeserializationFailed: deserialization failure 345 | /// let result = query_value_result!(obj.foo >> (Vec)); 346 | /// assert!(matches!(result, Err(Error::DeserializationFailed(_)))); 347 | /// ``` 348 | /// 349 | /// [`query_value!`]: crate::query_value 350 | /// [`valq::Error`]: crate::Error 351 | #[macro_export] 352 | $query_value_result 353 | }; 354 | } 355 | 356 | // fake implementation illustrates the macro syntax for docs 357 | #[cfg(doc)] 358 | doc_query_value_result! {macro_rules! query_value_result { 359 | ($(mut)? $value:tt $(query:tt)* $(?? $default:expr)?) => {}; 360 | ($(mut)? $value:tt $(query:tt)* -> $as:ident $(?? $default:expr)?) => {}; 361 | ($(mut)? $value:tt $(query:tt)* >> ($deser_to:ty) $(?? $default:expr)?) => {}; 362 | }} 363 | 364 | // actual implementation 365 | #[cfg(not(doc))] 366 | doc_query_value_result! {macro_rules! query_value_result { 367 | /* non-mut traversal */ 368 | // traversal step 369 | (@trv [$trace:ident] { $vopt:expr } . $key:ident $($rest:tt)*) => { 370 | $crate::query_value_result!(@trv [$trace] { 371 | $vopt.and_then(|v| { 372 | $trace.push_str(stringify!(.$key)); 373 | v.get(stringify!($key)).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) 374 | }) 375 | } $($rest)*) 376 | }; 377 | (@trv [$trace:ident] { $vopt:expr } [ $idx:expr ] $($rest:tt)*) => { 378 | $crate::query_value_result!(@trv [$trace] { 379 | $vopt.and_then(|v| { 380 | $trace.push_str(format!("[{}]", stringify!($idx)).as_str()); 381 | v.get($idx).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) 382 | }) 383 | } $($rest)*) 384 | }; 385 | // conversion step -> convert then jump to finalization step 386 | (@trv [$trace:ident] { $vopt:expr } -> $dest:ident $($rest:tt)*) => { 387 | $crate::__paste! { 388 | $crate::query_value_result!(@fin [$trace] { 389 | $vopt.and_then(|v| { 390 | let conv_name = format!("as_{}", stringify!($dest)); 391 | v.[]() .ok_or_else(|| $crate::Error::AsCastFailed(conv_name)) 392 | }) 393 | } $($rest)*) 394 | } 395 | }; 396 | (@trv [$trace:ident] { $vopt:expr } >> $dest:ident $($rest:tt)*) => { 397 | $crate::query_value_result!(@fin [$trace] { 398 | $vopt.and_then(|v| { 399 | <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) 400 | }) 401 | } $($rest)*) 402 | }; 403 | (@trv [$trace:ident] { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { 404 | $crate::query_value_result!(@fin [$trace] { 405 | $vopt.and_then(|v| { 406 | <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) 407 | }) 408 | } $($rest)*) 409 | }; 410 | // no conversion -> just jump to finalization step 411 | (@trv [$trace:ident] { $vopt:expr } $($rest:tt)*) => { 412 | $crate::query_value_result!(@fin [$trace] { $vopt } $($rest)*) 413 | }; 414 | 415 | /* mut traversal */ 416 | // traversal step 417 | (@trv_mut [$trace:ident] { $vopt:expr } . $key:ident $($rest:tt)*) => { 418 | $crate::query_value_result!(@trv_mut [$trace] { 419 | $vopt.and_then(|v| { 420 | $trace.push_str(stringify!(.$key)); 421 | v.get_mut(stringify!($key)).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) 422 | }) 423 | } $($rest)*) 424 | }; 425 | (@trv_mut [$trace:ident] { $vopt:expr } [ $idx:expr ] $($rest:tt)*) => { 426 | $crate::query_value_result!(@trv_mut [$trace] { 427 | $vopt.and_then(|v| { 428 | $trace.push_str(format!("[{}]", stringify!($idx)).as_str()); 429 | v.get_mut($idx).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) 430 | }) 431 | } $($rest)*) 432 | }; 433 | // conversion step -> convert then jump to finalization step 434 | (@trv_mut [$trace:ident] { $vopt:expr } -> $dest:ident $($rest:tt)*) => { 435 | $crate::__paste! { 436 | $crate::query_value_result!(@fin [$trace] { 437 | $vopt.and_then(|v| { 438 | let conv_name = format!("as_{}_mut", stringify!($dest)); 439 | v.[]().ok_or_else(|| $crate::Error::AsCastFailed(conv_name)) 440 | }) 441 | } $($rest)*) 442 | } 443 | }; 444 | (@trv_mut [$trace:ident] { $vopt:expr } >> $dest:ident $($rest:tt)*) => { 445 | $crate::query_value_result!(@fin [$trace] { 446 | $vopt.and_then(|v| { 447 | <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) 448 | }) 449 | } $($rest)*) 450 | }; 451 | (@trv_mut [$trace:ident] { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { 452 | $crate::query_value_result!(@fin [$trace] { 453 | $vopt.and_then(|v| { 454 | <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) 455 | }) 456 | } $($rest)*) 457 | }; 458 | // no conversion -> just jump to finalization step 459 | (@trv_mut [$trace:ident] { $vopt:expr } $($rest:tt)*) => { 460 | $crate::query_value_result!(@fin [$trace] { $vopt } $($rest)*) 461 | }; 462 | 463 | /* finalize: handle unwrapping operator */ 464 | (@fin [$trace:ident] { $vopt:expr } ?? default) => { 465 | { 466 | use $crate::Error; 467 | let mut $trace = String::new(); 468 | $vopt.unwrap_or_default() 469 | } 470 | }; 471 | (@fin [$trace:ident] { $vopt:expr } ?? $default:expr) => { 472 | { 473 | use $crate::Error; 474 | let mut $trace = String::new(); 475 | $vopt.unwrap_or_else(|_| $default) 476 | } 477 | }; 478 | // no unwrapping operator 479 | (@fin [$trace:ident] { $vopt:expr }) => { 480 | { 481 | use $crate::Error; 482 | let mut $trace = String::new(); 483 | $vopt 484 | } 485 | }; 486 | // unreachable branch -> report syntax error 487 | (@fin $($_:tt)*) => { 488 | compile_error!("invalid query syntax for query_value_result!()") 489 | }; 490 | 491 | /* entry points */ 492 | (mut $v:tt $($rest:tt)*) => { 493 | $crate::query_value_result!(@trv_mut [trace] { Ok::<_, $crate::Error>(&mut $v) } $($rest)*) 494 | }; 495 | ($v:tt $($rest:tt)*) => { 496 | $crate::query_value_result!(@trv [trace] { Ok::<_, $crate::Error>(&$v) } $($rest)*) 497 | }; 498 | }} 499 | -------------------------------------------------------------------------------- /tests/query_value.rs: -------------------------------------------------------------------------------- 1 | use valq::query_value; 2 | 3 | macro_rules! test_is_some_of_expected_val { 4 | ($tests:expr) => { 5 | for (res, exp) in $tests { 6 | if let Some(act) = res { 7 | assert_eq!(act, &exp) 8 | } 9 | else { 10 | panic!("expect Some(...) but actually None") 11 | } 12 | } 13 | }; 14 | } 15 | 16 | macro_rules! test_all_true_or_failed_idx { 17 | ($test_res:expr) => { 18 | if let Some(failed_idx) = $test_res.iter().position(|&r| !r) { 19 | panic!("test idx: {} failed", failed_idx) 20 | } 21 | }; 22 | } 23 | 24 | mod json { 25 | use super::query_value; 26 | use serde_json::{json, Value}; 27 | 28 | fn make_sample_json() -> Value { 29 | json!({ 30 | "str": "s", 31 | "nums": { 32 | "u64": 123, 33 | "i64": -123, 34 | "f64": 1.23, 35 | }, 36 | "bool": true, 37 | "null": null, 38 | "obj": { 39 | "inner": "value", 40 | "more": "item" 41 | }, 42 | "arr": [ 43 | "first", 44 | 42, 45 | { "hidden": "tale" }, 46 | [0] 47 | ], 48 | "num_arr": [0, 1, 2], 49 | "1st": "prop starts with digit!" 50 | }) 51 | } 52 | 53 | #[test] 54 | fn test_query_with_dot_syntax() { 55 | let j = make_sample_json(); 56 | 57 | let tests = [ 58 | (query_value!(j.str), json!("s")), 59 | (query_value!(j.nums.u64), json!(123)), 60 | (query_value!(j.nums.i64), json!(-123)), 61 | (query_value!(j.nums.f64), json!(1.23)), 62 | (query_value!(j.bool), json!(true)), 63 | (query_value!(j.null), json!(null)), 64 | ( 65 | query_value!(j.obj), 66 | json!({"inner": "value", "more": "item"}), 67 | ), 68 | (query_value!(j.obj.inner), json!("value")), 69 | ( 70 | query_value!(j.arr), 71 | json!(["first", 42, {"hidden": "tale"}, [0]]), 72 | ), 73 | (query_value!(j["1st"]), json!("prop starts with digit!")), 74 | ]; 75 | 76 | test_is_some_of_expected_val!(tests); 77 | } 78 | 79 | #[test] 80 | fn test_query_with_bracket_syntax() { 81 | let j = make_sample_json(); 82 | 83 | let tests = [ 84 | (query_value!(j["str"]), json!("s")), 85 | (query_value!(j["nums"]["u64"]), json!(123)), 86 | (query_value!(j["nums"].i64), json!(-123)), // mixed query 87 | (query_value!(j["1st"]), json!("prop starts with digit!")), 88 | ]; 89 | 90 | test_is_some_of_expected_val!(tests); 91 | } 92 | 93 | #[test] 94 | fn test_indexing_array() { 95 | let j = make_sample_json(); 96 | let tests = [ 97 | (query_value!(j.arr[0]), json!("first")), 98 | (query_value!(j.arr[1]), json!(42)), 99 | (query_value!(j.arr[2].hidden), json!("tale")), // more complex query! 100 | (query_value!(j.arr[3][0]), json!(0)), // successive indexing 101 | ]; 102 | 103 | test_is_some_of_expected_val!(tests); 104 | } 105 | 106 | #[test] 107 | fn test_query_mut() { 108 | let mut j = make_sample_json(); 109 | 110 | // rewriting value of prop 111 | { 112 | let obj_inner = query_value!(mut j.obj.inner).unwrap(); 113 | *obj_inner = json!("just woke up!"); 114 | } 115 | assert_eq!( 116 | query_value!(j.obj), 117 | Some(&json!({"inner": "just woke up!", "more": "item"})) 118 | ); 119 | 120 | // get inner object as Map, then add new prop via insert() 121 | { 122 | let obj = query_value!(mut j.obj -> object).unwrap(); 123 | obj.insert("new_prop".to_string(), json!("yeah")); 124 | } 125 | assert_eq!(query_value!(j.obj.new_prop -> str), Some("yeah")); 126 | 127 | // get inner array as Vec, then append new value via push() 128 | { 129 | let arr = query_value!(mut j.arr -> array).unwrap(); 130 | arr.push(json!("appended!")); 131 | } 132 | assert_eq!(query_value!(j.arr[4] -> str), Some("appended!")); 133 | } 134 | 135 | #[test] 136 | fn test_query_with_ref_value() { 137 | let j = make_sample_json(); 138 | let j_ref = &j; 139 | 140 | let tests = [ 141 | (query_value!(j_ref.str), json!("s")), 142 | (query_value!(j_ref.nums.u64), json!(123)), 143 | (query_value!(j_ref.obj.inner), json!("value")), 144 | ]; 145 | 146 | test_is_some_of_expected_val!(tests); 147 | } 148 | 149 | #[test] 150 | fn test_query_with_refmut_value() { 151 | let mut j = make_sample_json(); 152 | let mut j_ref = &mut j; 153 | 154 | assert_eq!( 155 | query_value!(mut j_ref.obj.inner).unwrap(), 156 | &mut json!("value") 157 | ); 158 | } 159 | 160 | #[test] 161 | fn test_query_and_cast() { 162 | let j = make_sample_json(); 163 | 164 | let tests = [ 165 | query_value!(j.str -> str) == Some("s"), 166 | query_value!(j.nums.u64 -> u64) == Some(123), 167 | query_value!(j.nums.i64 -> i64) == Some(-123), 168 | query_value!(j.nums.f64 -> f64) == Some(1.23), 169 | query_value!(j.bool -> bool) == Some(true), 170 | query_value!(j.null -> null) == Some(()), 171 | query_value!(j.obj -> object).unwrap().get("inner").unwrap() == "value", 172 | query_value!(j.arr -> array).unwrap() 173 | == &vec![ 174 | json!("first"), 175 | json!(42), 176 | json!({"hidden": "tale"}), 177 | json!([0]), 178 | ], 179 | ]; 180 | 181 | test_all_true_or_failed_idx!(tests); 182 | } 183 | 184 | #[test] 185 | fn test_query_and_deserialize() { 186 | use serde::Deserialize; 187 | 188 | let j = make_sample_json(); 189 | 190 | let tests = [ 191 | query_value!(j.str >> (String)) == Some("s".into()), 192 | query_value!(j.str >> (std::string::String)) == Some("s".into()), 193 | query_value!(j.str >> String) == Some("s".into()), // parens around type name can be omitted if single identifier 194 | query_value!(j.nums.u64 >> u8) == Some(123u8), 195 | query_value!(j.nums.i64 >> i8) == Some(-123i8), 196 | query_value!(j.nums.i64 >> u8) == None, // fails since can't deserialize negative value into u8 197 | query_value!(j.nums.f64 >> f32) == Some(1.23f32), 198 | query_value!(j.null >> (())) == Some(()), 199 | ]; 200 | 201 | test_all_true_or_failed_idx!(tests); 202 | } 203 | 204 | #[test] 205 | fn test_deserialize_into_custom_struct() { 206 | use serde::Deserialize; 207 | 208 | #[derive(Debug, PartialEq, Deserialize)] 209 | struct Person { 210 | name: String, 211 | age: u8, 212 | } 213 | 214 | let j = json!({ "author": {"name": "jiftechnify", "age": 31 } }); 215 | assert_eq!( 216 | query_value!(j.author >> Person), 217 | Some(Person { 218 | name: "jiftechnify".into(), 219 | age: 31u8, 220 | }), 221 | ); 222 | } 223 | 224 | #[test] 225 | fn test_query_with_unwrapping() { 226 | let j = make_sample_json(); 227 | 228 | let default_str = &json!("default"); 229 | 230 | // basic query with ?? 231 | assert_eq!(query_value!(j.str ?? default_str), &json!("s")); 232 | assert_eq!(query_value!(j.unknown ?? default_str), &json!("default")); 233 | 234 | // `?? default` 235 | assert_eq!(query_value!(j.nums.u64 -> u64 ?? default), 123u64); 236 | assert_eq!(query_value!(j.unknown -> u64 ?? default), 0u64); // u64::default() 237 | assert_eq!(query_value!(j.unknown -> str ?? default), ""); // &str::default() 238 | 239 | // with casting (->) 240 | assert_eq!(query_value!(j.str -> str ?? "default"), "s"); 241 | assert_eq!(query_value!(j.nums.u64 -> u64 ?? 999), 123); 242 | assert_eq!( 243 | query_value!(j.nums.u64 -> str ?? "not a string"), 244 | "not a string" 245 | ); // type mismatch 246 | assert_eq!(query_value!(j.unknown -> str ?? "default"), "default"); 247 | assert_eq!(query_value!(j.unknown -> str ?? default), ""); // &str::default() 248 | 249 | // with deserialization (>>) 250 | use serde::Deserialize; 251 | 252 | #[derive(Debug, PartialEq, Deserialize, Default)] 253 | struct Nums { 254 | u64: u64, 255 | i64: i64, 256 | f64: f64, 257 | } 258 | 259 | let expected_nums = Nums { 260 | u64: 123, 261 | i64: -123, 262 | f64: 1.23, 263 | }; 264 | 265 | assert_eq!(query_value!(j.nums >> Nums ?? default), expected_nums); 266 | assert_eq!( 267 | query_value!(j.unknown >> Nums ?? Nums { u64: 999, i64: -999, f64: 9.99 }), 268 | Nums { 269 | u64: 999, 270 | i64: -999, 271 | f64: 9.99 272 | } 273 | ); 274 | assert_eq!( 275 | query_value!(j.unknown >> Nums ?? default), 276 | Nums { 277 | u64: 0, 278 | i64: 0, 279 | f64: 0.0, 280 | } 281 | ); 282 | 283 | assert_eq!( 284 | query_value!(j.num_arr >> (Vec) ?? default), 285 | vec![0, 1, 2] 286 | ); 287 | assert_eq!( 288 | query_value!(j.arr >> (Vec) ?? default), 289 | Vec::::new() 290 | ); 291 | assert_eq!(query_value!(j.arr >> (Vec) ?? vec![42]), vec![42]); 292 | } 293 | 294 | #[test] 295 | fn test_deserialize_into_vec() { 296 | use serde::Deserialize; 297 | 298 | let j = make_sample_json(); 299 | 300 | let tests = [ 301 | query_value!(j.num_arr >> (Vec)) == Some(vec![0, 1, 2]), 302 | query_value!(j.num_arr >> (Vec) ?? default) == vec![0, 1, 2], 303 | query_value!(j.arr >> (Vec)) == None, 304 | query_value!(j.arr >> (Vec) ?? default) == Vec::::new(), 305 | query_value!(j.arr >> (Vec) ?? vec![42]) == vec![42], 306 | ]; 307 | test_all_true_or_failed_idx!(tests); 308 | } 309 | 310 | #[test] 311 | fn test_deserialize_into_hash_map() { 312 | use serde::Deserialize; 313 | use serde_json::{json, Value}; 314 | use std::collections::HashMap; 315 | 316 | let j = make_sample_json(); 317 | 318 | let exp_json: HashMap = HashMap::from([ 319 | ("inner".into(), json!("value")), 320 | ("more".into(), json!("item")), 321 | ]); 322 | 323 | let exp_string: HashMap = HashMap::from([ 324 | ("inner".into(), "value".into()), 325 | ("more".into(), "item".into()), 326 | ]); 327 | 328 | let tests = [ 329 | query_value!(j.obj >> (HashMap)) == Some(exp_json), 330 | query_value!(j.obj >> (HashMap)) == Some(exp_string), 331 | query_value!(j.obj >> (HashMap)) == None, 332 | ]; 333 | test_all_true_or_failed_idx!(tests); 334 | } 335 | 336 | #[test] 337 | fn test_query_fail() { 338 | let j = make_sample_json(); 339 | 340 | let tests = [ 341 | query_value!(j.unknown), // non existent property 342 | query_value!(j.nums.i128), // non existent property of nested object 343 | query_value!(j.obj[0]), // indexing against non-array value 344 | query_value!(j.arr[100]), // indexing out of bound 345 | ] 346 | .iter() 347 | .map(|res| res.is_none()) 348 | .collect::>(); 349 | 350 | test_all_true_or_failed_idx!(tests); 351 | } 352 | 353 | #[test] 354 | fn test_query_fail_mut() { 355 | let mut j = make_sample_json(); 356 | 357 | let tests = [ 358 | { query_value!(mut j.unknown).is_none() }, 359 | { query_value!(mut j.nums.i128).is_none() }, 360 | { query_value!(mut j.obj[0]).is_none() }, 361 | { query_value!(mut j.arr[100]).is_none() }, 362 | ]; 363 | 364 | test_all_true_or_failed_idx!(tests); 365 | } 366 | 367 | #[test] 368 | fn test_query_with_dynamic_indices() { 369 | let j = make_sample_json(); 370 | 371 | // Dynamic string key 372 | let key = "str"; 373 | assert_eq!(query_value!(j[key]), Some(&json!("s"))); 374 | 375 | let obj_key = "obj"; 376 | let inner_key = "inner"; 377 | assert_eq!(query_value!(j[obj_key][inner_key]), Some(&json!("value"))); 378 | 379 | // Dynamic integer index 380 | let index = 0; 381 | assert_eq!(query_value!(j.arr[index]), Some(&json!("first"))); 382 | 383 | let arr_index = 1; 384 | assert_eq!(query_value!(j.arr[arr_index]), Some(&json!(42))); 385 | 386 | // Mix of static and dynamic 387 | let key2 = "nums"; 388 | assert_eq!(query_value!(j[key2].u64), Some(&json!(123))); 389 | 390 | // Dynamic expression 391 | let base_index = 1; 392 | assert_eq!( 393 | query_value!(j.arr[base_index + 1].hidden), 394 | Some(&json!("tale")) 395 | ); 396 | 397 | // With casting (->) 398 | assert_eq!(query_value!(j[key] -> str), Some("s")); 399 | assert_eq!(query_value!(j.arr[index] -> str), Some("first")); 400 | 401 | // With unwrapping operator 402 | let missing_key = "missing"; 403 | let fallback = json!("fallback"); 404 | assert_eq!( 405 | query_value!(j[missing_key]?? & fallback), 406 | &json!("fallback") 407 | ); 408 | 409 | let out_of_bounds = 999; 410 | let oob_val = json!("oob"); 411 | assert_eq!( 412 | query_value!(j.arr[out_of_bounds]?? & oob_val), 413 | &json!("oob") 414 | ); 415 | } 416 | 417 | #[test] 418 | fn test_query_with_dynamic_indices_mut() { 419 | let mut j = make_sample_json(); 420 | 421 | // Dynamic string key (mut) 422 | let key = "str"; 423 | { 424 | let val = query_value!(mut j[key]).unwrap(); 425 | *val = json!("modified"); 426 | } 427 | assert_eq!(query_value!(j.str), Some(&json!("modified"))); 428 | 429 | // Dynamic integer index (mut) 430 | let index = 1; 431 | { 432 | let val = query_value!(mut j.arr[index]).unwrap(); 433 | *val = json!(100); 434 | } 435 | assert_eq!(query_value!(j.arr[1]), Some(&json!(100))); 436 | 437 | // With casting (mut) 438 | let obj_key = "obj"; 439 | let dynamic_key = "dynamic_key"; 440 | { 441 | let obj = query_value!(mut j[obj_key] -> object).unwrap(); 442 | obj.insert("dynamic_key".to_string(), json!("added")); 443 | } 444 | assert_eq!(query_value!(j.obj[dynamic_key]), Some(&json!("added"))); 445 | } 446 | 447 | #[test] 448 | fn test_query_complex_expressions() { 449 | use serde::Deserialize; 450 | 451 | fn gen_value() -> serde_json::Value { 452 | json!({ "x": 1 }) 453 | } 454 | 455 | let tuple = (json!({ "x": 1 }),); 456 | 457 | struct S { 458 | value: serde_json::Value, 459 | } 460 | impl S { 461 | fn new() -> Self { 462 | Self { 463 | value: json!({ "x": 1 }), 464 | } 465 | } 466 | fn gen_value(&self) -> serde_json::Value { 467 | json!({ "x": 1 }) 468 | } 469 | } 470 | let s = S::new(); 471 | 472 | let v = vec![json!({ "x": 1 })]; 473 | 474 | let tests = [ 475 | // querying immediate value 476 | query_value!((json!({ "x": 1 })).x) == Some(&json!(1)), 477 | query_value!((json!({ "x": 1 })).y ?? default) == &json!(null), 478 | query_value!((json!({ "x": 1 })).x -> u64) == Some(1u64), 479 | query_value!((json!({ "x": 1 })).x -> str ?? default) == "", 480 | query_value!((json!({ "x": 1 })).x -> str ?? "not str") == "not str", 481 | query_value!((json!({ "x": 1 })).x >> u8) == Some(1u8), 482 | query_value!((json!({ "x": 1 })).x >> String ?? default) == "".to_string(), 483 | query_value!((json!({ "x": 1 })).x >> String ?? "deser failed".to_string()) 484 | == "deser failed".to_string(), 485 | // querying immediate value (mut) 486 | query_value!(mut (json!({ "x": 1 })).x) == Some(&mut json!(1)), 487 | query_value!(mut (json!({ "x": 1 })).x >> u8) == Some(1u8), 488 | query_value!(mut (json!({ "x": 1 })).x >> String ?? "deser failed".to_string()) 489 | == "deser failed".to_string(), 490 | // querying return value of function 491 | query_value!((gen_value()).x) == Some(&json!(1)), 492 | query_value!((gen_value()).x -> u64) == Some(1u64), 493 | query_value!((gen_value()).x -> str ?? "not str") == "not str", 494 | // querying element of tuple 495 | query_value!((tuple.0).x) == Some(&json!(1)), 496 | query_value!((tuple.0).x -> u64) == Some(1u64), 497 | query_value!((tuple.0).x -> str ?? "not str") == "not str", 498 | // querying field of struct 499 | query_value!((s.value).x) == Some(&json!(1)), 500 | query_value!((s.value).x -> u64) == Some(1u64), 501 | query_value!((s.value).x -> str ?? "not str") == "not str", 502 | // querying return value of method 503 | query_value!((s.gen_value()).x) == Some(&json!(1)), 504 | query_value!((s.gen_value()).x -> u64) == Some(1u64), 505 | query_value!((s.gen_value()).x -> str ?? "not str") == "not str", 506 | // querying indexed value 507 | query_value!((v[0]).x) == Some(&json!(1)), 508 | query_value!((v[0]).x -> u64) == Some(1u64), 509 | query_value!((v[0]).x -> str ?? "not str") == "not str", 510 | ]; 511 | test_all_true_or_failed_idx!(tests); 512 | } 513 | } 514 | 515 | mod yaml { 516 | use super::query_value; 517 | use serde_yaml::{from_str, Mapping, Sequence, Value}; 518 | 519 | fn make_sample_yaml() -> Value { 520 | let yaml_str = include_str!("../res/sample.yaml"); 521 | from_str(yaml_str).unwrap() 522 | } 523 | 524 | fn sample_mapping() -> Mapping { 525 | Mapping::from_iter([ 526 | ( 527 | Value::String("first".to_string()), 528 | Value::String("zzz".to_string()), 529 | ), 530 | ( 531 | Value::String("second".to_string()), 532 | Value::String("yyy".to_string()), 533 | ), 534 | ]) 535 | } 536 | fn sample_map_in_seq() -> Mapping { 537 | Mapping::from_iter([( 538 | Value::String("hidden".to_string()), 539 | Value::String("tale".to_string()), 540 | )]) 541 | } 542 | fn sample_sequence() -> Sequence { 543 | Sequence::from_iter(vec![ 544 | Value::String("first".to_string()), 545 | Value::Number(42.into()), 546 | Value::Mapping(sample_map_in_seq()), 547 | ]) 548 | } 549 | 550 | #[test] 551 | fn test_query() { 552 | let y = make_sample_yaml(); 553 | 554 | let tests = [ 555 | (query_value!(y.str), Value::String("s".to_string())), 556 | (query_value!(y.num), Value::Number(123.into())), 557 | (query_value!(y.map), Value::Mapping(sample_mapping())), 558 | (query_value!(y.map.second), Value::String("yyy".to_string())), 559 | (query_value!(y.seq), Value::Sequence(sample_sequence())), 560 | (query_value!(y.seq[0]), Value::String("first".to_string())), 561 | (query_value!(y.seq[2]), Value::Mapping(sample_map_in_seq())), 562 | ]; 563 | test_is_some_of_expected_val!(tests); 564 | } 565 | 566 | #[test] 567 | fn test_query_and_cast() { 568 | let y = make_sample_yaml(); 569 | 570 | let tests = [ 571 | query_value!(y.str -> str) == Some("s"), 572 | query_value!(y.num -> u64) == Some(123), 573 | query_value!(y.map -> mapping).unwrap().len() == 2, 574 | query_value!(y.seq -> sequence).unwrap() 575 | == &vec![ 576 | Value::String("first".to_string()), 577 | Value::Number(42.into()), 578 | Value::Mapping(sample_map_in_seq()), 579 | ], 580 | ]; 581 | 582 | test_all_true_or_failed_idx!(tests); 583 | } 584 | 585 | #[test] 586 | fn test_query_and_deserialize() { 587 | use serde::Deserialize; 588 | 589 | #[derive(Debug, PartialEq, Deserialize)] 590 | struct Person { 591 | name: String, 592 | age: u8, 593 | } 594 | 595 | let y = make_sample_yaml(); 596 | assert_eq!( 597 | query_value!(y.author >> Person), 598 | Some(Person { 599 | name: "jiftechnify".into(), 600 | age: 31u8, 601 | }), 602 | ); 603 | } 604 | } 605 | 606 | mod toml { 607 | use super::query_value; 608 | use toml::{ 609 | from_str, 610 | value::{Array, Table}, 611 | Value, 612 | }; 613 | 614 | fn make_sample_toml() -> Value { 615 | let toml_str = include_str!("../res/sample.toml"); 616 | from_str(toml_str).unwrap() 617 | } 618 | fn sample_table() -> Table { 619 | Table::from_iter([ 620 | ("first".to_string(), Value::String("zzz".to_string())), 621 | ("second".to_string(), Value::String("yyy".to_string())), 622 | ]) 623 | } 624 | fn sample_array() -> Array { 625 | vec!["first", "second", "third"] 626 | .into_iter() 627 | .map(|e| Value::String(e.to_string())) 628 | .collect() 629 | } 630 | fn sample_arr_of_tables() -> Array { 631 | let t1 = Table::from_iter([("hidden".to_string(), Value::String("tale".to_string()))]); 632 | let t2 = Table::from_iter([ 633 | ("hoge".to_string(), Value::Integer(1)), 634 | ("fuga".to_string(), Value::Integer(2)), 635 | ]); 636 | let t3 = Table::from_iter([( 637 | "inner_arr".to_string(), 638 | Value::Array(vec![ 639 | Value::Integer(1), 640 | Value::Integer(2), 641 | Value::Integer(3), 642 | ]), 643 | )]); 644 | 645 | vec![t1, t2, t3].into_iter().map(Value::Table).collect() 646 | } 647 | 648 | #[test] 649 | fn test_query() { 650 | let t = make_sample_toml(); 651 | 652 | let tests = [ 653 | (query_value!(t.str), Value::String("s".to_string())), 654 | (query_value!(t.int), Value::Integer(123)), 655 | (query_value!(t.float), Value::Float(1.23)), 656 | (query_value!(t.table), Value::Table(sample_table())), 657 | ( 658 | query_value!(t.table.second), 659 | Value::String("yyy".to_string()), 660 | ), 661 | (query_value!(t.arr), Value::Array(sample_array())), 662 | (query_value!(t.arr[2]), Value::String("third".to_string())), 663 | ( 664 | query_value!(t.arr_of_tables), 665 | Value::Array(sample_arr_of_tables()), 666 | ), 667 | ( 668 | query_value!(t.arr_of_tables[0].hidden), 669 | Value::String("tale".to_string()), 670 | ), 671 | ( 672 | query_value!(t.arr_of_tables[2].inner_arr[0]), 673 | Value::Integer(1), 674 | ), 675 | ]; 676 | test_is_some_of_expected_val!(tests); 677 | } 678 | 679 | #[test] 680 | fn test_query_and_cast() { 681 | let t = make_sample_toml(); 682 | 683 | let tests = [ 684 | query_value!(t.str -> str) == Some("s"), 685 | query_value!(t.int -> integer) == Some(123), 686 | query_value!(t.float -> float) == Some(1.23), 687 | query_value!(t.date -> datetime).unwrap().to_string() == "2021-12-18T12:15:12+09:00", 688 | query_value!(t.table -> table).unwrap().len() == 2, 689 | query_value!(t.arr -> array).unwrap() 690 | == &vec!["first", "second", "third"] 691 | .into_iter() 692 | .map(|v| Value::String(v.to_string())) 693 | .collect::>(), 694 | query_value!(t.arr_of_tables -> array).unwrap().len() == 3, 695 | ]; 696 | 697 | test_all_true_or_failed_idx!(tests); 698 | } 699 | 700 | #[test] 701 | fn test_query_and_deserialize() { 702 | use serde::Deserialize; 703 | 704 | #[derive(Debug, PartialEq, Deserialize)] 705 | struct Person { 706 | name: String, 707 | age: u8, 708 | } 709 | 710 | let t = make_sample_toml(); 711 | assert_eq!( 712 | query_value!(t.author >> Person), 713 | Some(Person { 714 | name: "jiftechnify".into(), 715 | age: 31u8, 716 | }), 717 | ); 718 | } 719 | } 720 | -------------------------------------------------------------------------------- /tests/query_value_result.rs: -------------------------------------------------------------------------------- 1 | use valq::{query_value_result, Error}; 2 | 3 | macro_rules! test_all_true_or_failed_idx { 4 | ($test_res:expr) => { 5 | if let Some(failed_idx) = $test_res.iter().position(|&r| !r) { 6 | panic!("test idx: {} failed", failed_idx) 7 | } 8 | }; 9 | } 10 | 11 | mod json { 12 | use super::{query_value_result, Error}; 13 | use serde_json::{json, Value}; 14 | 15 | fn make_sample_json() -> Value { 16 | json!({ 17 | "str": "s", 18 | "nums": { 19 | "u64": 123, 20 | "i64": -123, 21 | "f64": 1.23, 22 | }, 23 | "bool": true, 24 | "null": null, 25 | "obj": { 26 | "inner": "value", 27 | "more": "item" 28 | }, 29 | "arr": [ 30 | "first", 31 | 42, 32 | { "hidden": "tale" }, 33 | [0] 34 | ], 35 | "num_arr": [0, 1, 2], 36 | "1st": "prop starts with digit!" 37 | }) 38 | } 39 | 40 | #[test] 41 | fn test_query_with_dot_syntax() { 42 | let j = make_sample_json(); 43 | 44 | let tests = [ 45 | (query_value_result!(j.str), json!("s")), 46 | (query_value_result!(j.nums.u64), json!(123)), 47 | (query_value_result!(j.nums.i64), json!(-123)), 48 | (query_value_result!(j.nums.f64), json!(1.23)), 49 | (query_value_result!(j.bool), json!(true)), 50 | (query_value_result!(j.null), json!(null)), 51 | ( 52 | query_value_result!(j.obj), 53 | json!({"inner": "value", "more": "item"}), 54 | ), 55 | (query_value_result!(j.obj.inner), json!("value")), 56 | ( 57 | query_value_result!(j.arr), 58 | json!(["first", 42, {"hidden": "tale"}, [0]]), 59 | ), 60 | ( 61 | query_value_result!(j["1st"]), 62 | json!("prop starts with digit!"), 63 | ), 64 | ]; 65 | 66 | for (res, exp) in tests { 67 | assert_eq!(res.unwrap(), &exp); 68 | } 69 | } 70 | 71 | #[test] 72 | fn test_query_with_bracket_syntax() { 73 | let j = make_sample_json(); 74 | 75 | let tests = [ 76 | (query_value_result!(j["str"]), json!("s")), 77 | (query_value_result!(j["nums"]["u64"]), json!(123)), 78 | (query_value_result!(j["nums"].i64), json!(-123)), // mixed query 79 | ( 80 | query_value_result!(j["1st"]), 81 | json!("prop starts with digit!"), 82 | ), 83 | ]; 84 | 85 | for (res, exp) in tests { 86 | assert_eq!(res.unwrap(), &exp); 87 | } 88 | } 89 | 90 | #[test] 91 | fn test_indexing_array() { 92 | let j = make_sample_json(); 93 | let tests = [ 94 | (query_value_result!(j.arr[0]), json!("first")), 95 | (query_value_result!(j.arr[1]), json!(42)), 96 | (query_value_result!(j.arr[2].hidden), json!("tale")), // more complex query! 97 | (query_value_result!(j.arr[3][0]), json!(0)), // successive indexing 98 | ]; 99 | 100 | for (res, exp) in tests { 101 | assert_eq!(res.unwrap(), &exp); 102 | } 103 | } 104 | 105 | #[test] 106 | fn test_query_mut() { 107 | let mut j = make_sample_json(); 108 | 109 | // rewriting value of prop 110 | { 111 | let obj_inner = query_value_result!(mut j.obj.inner).unwrap(); 112 | *obj_inner = json!("just woke up!"); 113 | } 114 | assert_eq!( 115 | query_value_result!(j.obj).unwrap(), 116 | &json!({"inner": "just woke up!", "more": "item"}) 117 | ); 118 | 119 | // get inner object as Map, then add new prop via insert() 120 | { 121 | let obj = query_value_result!(mut j.obj -> object).unwrap(); 122 | obj.insert("new_prop".to_string(), json!("yeah")); 123 | } 124 | assert_eq!(query_value_result!(j.obj.new_prop -> str).unwrap(), "yeah"); 125 | 126 | // get inner array as Vec, then append new value via push() 127 | { 128 | let arr = query_value_result!(mut j.arr -> array).unwrap(); 129 | arr.push(json!("appended!")); 130 | } 131 | assert_eq!(query_value_result!(j.arr[4] -> str).unwrap(), "appended!"); 132 | } 133 | 134 | #[test] 135 | fn test_query_with_ref_value() { 136 | let j = make_sample_json(); 137 | let j_ref = &j; 138 | 139 | let tests = [ 140 | (query_value_result!(j_ref.str), json!("s")), 141 | (query_value_result!(j_ref.nums.u64), json!(123)), 142 | (query_value_result!(j_ref.obj.inner), json!("value")), 143 | ]; 144 | 145 | for (res, exp) in tests { 146 | assert_eq!(res.unwrap(), &exp); 147 | } 148 | } 149 | 150 | #[test] 151 | fn test_query_with_refmut_value() { 152 | let mut j = make_sample_json(); 153 | let mut j_ref = &mut j; 154 | 155 | assert_eq!( 156 | query_value_result!(mut j_ref.obj.inner).unwrap(), 157 | &mut json!("value") 158 | ); 159 | } 160 | 161 | #[test] 162 | fn test_query_and_cast() { 163 | let j = make_sample_json(); 164 | 165 | let tests = [ 166 | query_value_result!(j.str -> str).unwrap() == "s", 167 | query_value_result!(j.nums.u64 -> u64).unwrap() == 123, 168 | query_value_result!(j.nums.i64 -> i64).unwrap() == -123, 169 | query_value_result!(j.nums.f64 -> f64).unwrap() == 1.23, 170 | query_value_result!(j.bool -> bool).unwrap() == true, 171 | query_value_result!(j.null -> null).unwrap() == (), 172 | query_value_result!(j.obj -> object) 173 | .unwrap() 174 | .get("inner") 175 | .unwrap() 176 | == "value", 177 | query_value_result!(j.arr -> array).unwrap() 178 | == &vec![ 179 | json!("first"), 180 | json!(42), 181 | json!({"hidden": "tale"}), 182 | json!([0]), 183 | ], 184 | ]; 185 | 186 | test_all_true_or_failed_idx!(tests); 187 | } 188 | 189 | #[test] 190 | fn test_query_and_deserialize() { 191 | use serde::Deserialize; 192 | 193 | let j = make_sample_json(); 194 | 195 | let tests = [ 196 | query_value_result!(j.str >> (String)).unwrap() == "s", 197 | query_value_result!(j.str >> (std::string::String)).unwrap() == "s", 198 | query_value_result!(j.str >> String).unwrap() == "s", // parens around type name can be omitted if single identifier 199 | query_value_result!(j.nums.u64 >> u8).unwrap() == 123u8, 200 | query_value_result!(j.nums.i64 >> i8).unwrap() == -123i8, 201 | query_value_result!(j.nums.f64 >> f32).unwrap() == 1.23f32, 202 | query_value_result!(j.null >> (())).unwrap() == (), 203 | ]; 204 | 205 | test_all_true_or_failed_idx!(tests); 206 | } 207 | 208 | #[test] 209 | fn test_deserialize_into_custom_struct() { 210 | use serde::Deserialize; 211 | 212 | #[derive(Debug, PartialEq, Deserialize)] 213 | struct Person { 214 | name: String, 215 | age: u8, 216 | } 217 | 218 | let j = json!({ "author": {"name": "jiftechnify", "age": 31 } }); 219 | assert_eq!( 220 | query_value_result!(j.author >> Person).unwrap(), 221 | Person { 222 | name: "jiftechnify".into(), 223 | age: 31u8, 224 | }, 225 | ); 226 | } 227 | 228 | #[test] 229 | fn test_query_with_unwrapping() { 230 | let j = make_sample_json(); 231 | 232 | let default_str = &json!("default"); 233 | 234 | // basic query with ?? 235 | assert_eq!(query_value_result!(j.str ?? default_str), &json!("s")); 236 | assert_eq!( 237 | query_value_result!(j.unknown ?? default_str), 238 | &json!("default") 239 | ); 240 | 241 | // `?? default` 242 | assert_eq!(query_value_result!(j.nums.u64 -> u64 ?? default), 123u64); 243 | assert_eq!(query_value_result!(j.unknown -> u64 ?? default), 0u64); // u64::default() 244 | assert_eq!(query_value_result!(j.unknown -> str ?? default), ""); // &str::default() 245 | 246 | // with casting (->) 247 | assert_eq!(query_value_result!(j.str -> str ?? "default"), "s"); 248 | assert_eq!(query_value_result!(j.nums.u64 -> u64 ?? 999), 123); 249 | assert_eq!( 250 | query_value_result!(j.nums.u64 -> str ?? "not a string"), 251 | "not a string" 252 | ); // type mismatch 253 | assert_eq!( 254 | query_value_result!(j.unknown -> str ?? "default"), 255 | "default" 256 | ); 257 | assert_eq!(query_value_result!(j.unknown -> str ?? default), ""); // &str::default() 258 | 259 | // with deserialization (>>) 260 | use serde::Deserialize; 261 | 262 | #[derive(Debug, PartialEq, Deserialize, Default)] 263 | struct Nums { 264 | u64: u64, 265 | i64: i64, 266 | f64: f64, 267 | } 268 | 269 | let expected_nums = Nums { 270 | u64: 123, 271 | i64: -123, 272 | f64: 1.23, 273 | }; 274 | 275 | assert_eq!( 276 | query_value_result!(j.nums >> Nums ?? default), 277 | expected_nums 278 | ); 279 | assert_eq!( 280 | query_value_result!(j.unknown >> Nums ?? Nums { u64: 999, i64: -999, f64: 9.99 }), 281 | Nums { 282 | u64: 999, 283 | i64: -999, 284 | f64: 9.99 285 | } 286 | ); 287 | assert_eq!( 288 | query_value_result!(j.unknown >> Nums ?? default), 289 | Nums { 290 | u64: 0, 291 | i64: 0, 292 | f64: 0.0, 293 | } 294 | ); 295 | 296 | assert_eq!( 297 | query_value_result!(j.num_arr >> (Vec) ?? default), 298 | vec![0, 1, 2] 299 | ); 300 | assert_eq!( 301 | query_value_result!(j.arr >> (Vec) ?? default), 302 | Vec::::new() 303 | ); 304 | assert_eq!( 305 | query_value_result!(j.arr >> (Vec) ?? vec![42]), 306 | vec![42] 307 | ); 308 | } 309 | 310 | #[test] 311 | fn test_deserialize_into_vec() { 312 | use serde::Deserialize; 313 | 314 | let j = make_sample_json(); 315 | 316 | let tests = [ 317 | query_value_result!(j.num_arr >> (Vec)).unwrap() == vec![0, 1, 2], 318 | query_value_result!(j.num_arr >> (Vec) ?? default) == vec![0, 1, 2], 319 | query_value_result!(j.arr >> (Vec) ?? default) == Vec::::new(), 320 | query_value_result!(j.arr >> (Vec) ?? vec![42]) == vec![42], 321 | ]; 322 | test_all_true_or_failed_idx!(tests); 323 | } 324 | 325 | #[test] 326 | fn test_deserialize_into_hash_map() { 327 | use serde::Deserialize; 328 | use serde_json::{json, Value}; 329 | use std::collections::HashMap; 330 | 331 | let j = make_sample_json(); 332 | 333 | let exp_json: HashMap = HashMap::from([ 334 | ("inner".into(), json!("value")), 335 | ("more".into(), json!("item")), 336 | ]); 337 | 338 | let exp_string: HashMap = HashMap::from([ 339 | ("inner".into(), "value".into()), 340 | ("more".into(), "item".into()), 341 | ]); 342 | 343 | let tests = [ 344 | query_value_result!(j.obj >> (HashMap)).unwrap() == exp_json, 345 | query_value_result!(j.obj >> (HashMap)).unwrap() == exp_string, 346 | ]; 347 | test_all_true_or_failed_idx!(tests); 348 | } 349 | 350 | #[test] 351 | fn test_query_complex_expressions() { 352 | fn gen_value() -> serde_json::Value { 353 | json!({ "x": 1 }) 354 | } 355 | 356 | let tuple = (json!({ "x": 1 }),); 357 | 358 | struct S { 359 | value: serde_json::Value, 360 | } 361 | impl S { 362 | fn new() -> Self { 363 | Self { 364 | value: json!({ "x": 1 }), 365 | } 366 | } 367 | fn gen_value(&self) -> serde_json::Value { 368 | json!({ "x": 1 }) 369 | } 370 | } 371 | let s = S::new(); 372 | 373 | let v = vec![json!({ "x": 1 })]; 374 | 375 | let tests = [ 376 | // querying immediate value 377 | query_value_result!((json!({ "x": 1 })).x).unwrap() == &json!(1), 378 | query_value_result!((json!({ "x": 1 })).y ?? default) == &json!(null), 379 | query_value_result!((json!({ "x": 1 })).x -> u64).unwrap() == 1u64, 380 | query_value_result!((json!({ "x": 1 })).x -> str ?? default) == "", 381 | query_value_result!((json!({ "x": 1 })).x -> str ?? "not str") == "not str", 382 | // querying immediate value (mut) 383 | query_value_result!(mut (json!({ "x": 1 })).x).unwrap() == &mut json!(1), 384 | // querying return value of function 385 | query_value_result!((gen_value()).x).unwrap() == &json!(1), 386 | query_value_result!((gen_value()).x -> u64).unwrap() == 1u64, 387 | query_value_result!((gen_value()).x -> str ?? "not str") == "not str", 388 | // querying element of tuple 389 | query_value_result!((tuple.0).x).unwrap() == &json!(1), 390 | query_value_result!((tuple.0).x -> u64).unwrap() == 1u64, 391 | query_value_result!((tuple.0).x -> str ?? "not str") == "not str", 392 | // querying field of struct 393 | query_value_result!((s.value).x).unwrap() == &json!(1), 394 | query_value_result!((s.value).x -> u64).unwrap() == 1u64, 395 | query_value_result!((s.value).x -> str ?? "not str") == "not str", 396 | // querying return value of method 397 | query_value_result!((s.gen_value()).x).unwrap() == &json!(1), 398 | query_value_result!((s.gen_value()).x -> u64).unwrap() == 1u64, 399 | query_value_result!((s.gen_value()).x -> str ?? "not str") == "not str", 400 | // querying indexed value 401 | query_value_result!((v[0]).x).unwrap() == &json!(1), 402 | query_value_result!((v[0]).x -> u64).unwrap() == 1u64, 403 | query_value_result!((v[0]).x -> str ?? "not str") == "not str", 404 | ]; 405 | test_all_true_or_failed_idx!(tests); 406 | } 407 | 408 | #[test] 409 | fn test_query_with_dynamic_indices() { 410 | let j = make_sample_json(); 411 | 412 | // Dynamic string key 413 | let key = "str"; 414 | assert_eq!(query_value_result!(j[key]).unwrap(), &json!("s")); 415 | 416 | let obj_key = "obj"; 417 | let inner_key = "inner"; 418 | assert_eq!( 419 | query_value_result!(j[obj_key][inner_key]).unwrap(), 420 | &json!("value") 421 | ); 422 | 423 | // Dynamic integer index 424 | let index = 0; 425 | assert_eq!(query_value_result!(j.arr[index]).unwrap(), &json!("first")); 426 | 427 | let arr_index = 1; 428 | assert_eq!(query_value_result!(j.arr[arr_index]).unwrap(), &json!(42)); 429 | 430 | // Mix of static and dynamic 431 | let key2 = "nums"; 432 | assert_eq!(query_value_result!(j[key2].u64).unwrap(), &json!(123)); 433 | 434 | // Dynamic expression 435 | let base_index = 1; 436 | assert_eq!( 437 | query_value_result!(j.arr[base_index + 1].hidden).unwrap(), 438 | &json!("tale") 439 | ); 440 | 441 | // With casting (->) 442 | assert_eq!(query_value_result!(j[key] -> str).unwrap(), "s"); 443 | assert_eq!(query_value_result!(j.arr[index] -> str).unwrap(), "first"); 444 | 445 | // With unwrapping operator 446 | let missing_key = "missing"; 447 | let fallback = json!("fallback"); 448 | assert_eq!( 449 | query_value_result!(j[missing_key]?? & fallback), 450 | &json!("fallback") 451 | ); 452 | 453 | let out_of_bounds = 999; 454 | let oob_val = json!("oob"); 455 | assert_eq!( 456 | query_value_result!(j.arr[out_of_bounds]?? & oob_val), 457 | &json!("oob") 458 | ); 459 | } 460 | 461 | #[test] 462 | fn test_query_with_dynamic_indices_mut() { 463 | let mut j = make_sample_json(); 464 | 465 | // Dynamic string key (mut) 466 | let key = "str"; 467 | { 468 | let val = query_value_result!(mut j[key]).unwrap(); 469 | *val = json!("modified"); 470 | } 471 | assert_eq!(query_value_result!(j.str).unwrap(), &json!("modified")); 472 | 473 | // Dynamic integer index (mut) 474 | let index = 1; 475 | { 476 | let val = query_value_result!(mut j.arr[index]).unwrap(); 477 | *val = json!(100); 478 | } 479 | assert_eq!(query_value_result!(j.arr[1]).unwrap(), &json!(100)); 480 | 481 | // With casting (mut) 482 | let obj_key = "obj"; 483 | let dynamic_key = "dynamic_key"; 484 | { 485 | let obj = query_value_result!(mut j[obj_key] -> object).unwrap(); 486 | obj.insert("dynamic_key".to_string(), json!("added")); 487 | } 488 | assert_eq!( 489 | query_value_result!(j.obj[dynamic_key]).unwrap(), 490 | &json!("added") 491 | ); 492 | } 493 | 494 | // Error case tests - ValueNotFoundAtPath 495 | #[test] 496 | fn test_error_value_not_found() { 497 | let j = make_sample_json(); 498 | 499 | let tests = [ 500 | (query_value_result!(j.unknown), ".unknown"), 501 | (query_value_result!(j.nums.i128), ".nums.i128"), 502 | (query_value_result!(j.obj[0]), ".obj[0]"), // indexing against non-array value 503 | (query_value_result!(j.arr[100]), ".arr[100]"), // indexing out of bound 504 | ( 505 | query_value_result!(j.obj.inner.not_here.oh.nothing.but.pain), 506 | ".obj.inner.not_here", // make sure that it reports the shallowest non-existing path 507 | ), 508 | ]; 509 | 510 | for (result, expected_path) in tests { 511 | if let Err(Error::ValueNotFoundAtPath(path)) = result { 512 | assert_eq!(path, expected_path); 513 | } 514 | else { 515 | panic!("expected ValueNotFoundAtPath error, but got: {:?}", result); 516 | } 517 | } 518 | } 519 | 520 | // Error case tests - AsCastFailed 521 | #[test] 522 | fn test_error_as_cast_failed() { 523 | let j = make_sample_json(); 524 | 525 | let tests = [ 526 | (query_value_result!(j.str -> u64).unwrap_err(), "as_u64"), 527 | ( 528 | query_value_result!(j.nums.u64 -> str).unwrap_err(), 529 | "as_str", 530 | ), 531 | (query_value_result!(j.obj -> array).unwrap_err(), "as_array"), 532 | ( 533 | query_value_result!(j.arr -> object).unwrap_err(), 534 | "as_object", 535 | ), 536 | ]; 537 | 538 | for (result, expected_conv_name) in tests { 539 | if let Error::AsCastFailed(conv_name) = result { 540 | assert_eq!(conv_name, expected_conv_name); 541 | } 542 | else { 543 | panic!("expected AsCastFailed error, but got: {:?}", result); 544 | } 545 | } 546 | } 547 | 548 | // Error case tests - AsCastFailed (mut) 549 | #[test] 550 | fn test_error_as_cast_failed_mut() { 551 | let mut j = make_sample_json(); 552 | 553 | let tests = [ 554 | ( 555 | query_value_result!(mut j.obj -> array).unwrap_err(), 556 | "as_array_mut", 557 | ), 558 | ( 559 | query_value_result!(mut j.arr -> object).unwrap_err(), 560 | "as_object_mut", 561 | ), 562 | ]; 563 | 564 | for (result, expected_conv_name) in tests { 565 | if let Error::AsCastFailed(conv_name) = result { 566 | assert_eq!(conv_name, expected_conv_name); 567 | } 568 | else { 569 | panic!("expected AsCastFailed error, but got: {:?}", result); 570 | } 571 | } 572 | } 573 | 574 | // Error case tests - DeserializationFailed 575 | #[test] 576 | fn test_error_deserialization_failed() { 577 | use serde::Deserialize; 578 | use std::collections::HashMap; 579 | 580 | let j = make_sample_json(); 581 | 582 | #[derive(Debug, PartialEq, Deserialize)] 583 | struct Person { 584 | name: String, 585 | age: u8, 586 | } 587 | 588 | let tests = [ 589 | query_value_result!(j.nums.i64 >> u8).unwrap_err(), 590 | query_value_result!(j.str >> u8).unwrap_err(), 591 | query_value_result!(j.obj >> Person).unwrap_err(), 592 | query_value_result!(j.arr >> (Vec)).unwrap_err(), 593 | query_value_result!(j.obj >> (HashMap)).unwrap_err(), 594 | ]; 595 | 596 | for result in tests { 597 | assert!(matches!(result, Error::DeserializationFailed(_))); 598 | } 599 | } 600 | 601 | // Test error display 602 | #[test] 603 | fn test_error_display() { 604 | use serde::Deserialize; 605 | 606 | let j = make_sample_json(); 607 | 608 | // ValueNotFoundAtPath 609 | let err = query_value_result!(j.unknown).unwrap_err(); 610 | assert_eq!(err.to_string(), "value not found at the path: .unknown"); 611 | 612 | // AsCastFailed 613 | let err = query_value_result!(j.str -> u64).unwrap_err(); 614 | assert_eq!(err.to_string(), "casting with as_u64() failed"); 615 | 616 | // DeserializationFailed 617 | let err = query_value_result!(j.nums.i64 >> u8).unwrap_err(); 618 | assert!(err 619 | .to_string() 620 | .starts_with("failed to deserialize the queried value:")); 621 | } 622 | } 623 | 624 | mod yaml { 625 | use super::{query_value_result, Error}; 626 | use serde_yaml::{from_str, Mapping, Sequence, Value}; 627 | 628 | fn make_sample_yaml() -> Value { 629 | let yaml_str = include_str!("../res/sample.yaml"); 630 | from_str(yaml_str).unwrap() 631 | } 632 | 633 | fn sample_mapping() -> Mapping { 634 | Mapping::from_iter([ 635 | ( 636 | Value::String("first".to_string()), 637 | Value::String("zzz".to_string()), 638 | ), 639 | ( 640 | Value::String("second".to_string()), 641 | Value::String("yyy".to_string()), 642 | ), 643 | ]) 644 | } 645 | fn sample_map_in_seq() -> Mapping { 646 | Mapping::from_iter([( 647 | Value::String("hidden".to_string()), 648 | Value::String("tale".to_string()), 649 | )]) 650 | } 651 | fn sample_sequence() -> Sequence { 652 | Sequence::from_iter(vec![ 653 | Value::String("first".to_string()), 654 | Value::Number(42.into()), 655 | Value::Mapping(sample_map_in_seq()), 656 | ]) 657 | } 658 | 659 | #[test] 660 | fn test_query() { 661 | let y = make_sample_yaml(); 662 | 663 | let tests = [ 664 | (query_value_result!(y.str), Value::String("s".to_string())), 665 | (query_value_result!(y.num), Value::Number(123.into())), 666 | (query_value_result!(y.map), Value::Mapping(sample_mapping())), 667 | ( 668 | query_value_result!(y.map.second), 669 | Value::String("yyy".to_string()), 670 | ), 671 | ( 672 | query_value_result!(y.seq), 673 | Value::Sequence(sample_sequence()), 674 | ), 675 | ( 676 | query_value_result!(y.seq[0]), 677 | Value::String("first".to_string()), 678 | ), 679 | ( 680 | query_value_result!(y.seq[2]), 681 | Value::Mapping(sample_map_in_seq()), 682 | ), 683 | ]; 684 | 685 | for (res, exp) in tests { 686 | assert_eq!(res.unwrap(), &exp); 687 | } 688 | } 689 | 690 | #[test] 691 | fn test_query_and_cast() { 692 | let y = make_sample_yaml(); 693 | 694 | let tests = [ 695 | query_value_result!(y.str -> str).unwrap() == "s", 696 | query_value_result!(y.num -> u64).unwrap() == 123, 697 | query_value_result!(y.map -> mapping).unwrap().len() == 2, 698 | query_value_result!(y.seq -> sequence).unwrap() 699 | == &vec![ 700 | Value::String("first".to_string()), 701 | Value::Number(42.into()), 702 | Value::Mapping(sample_map_in_seq()), 703 | ], 704 | ]; 705 | 706 | test_all_true_or_failed_idx!(tests); 707 | } 708 | 709 | #[test] 710 | fn test_query_and_deserialize() { 711 | use serde::Deserialize; 712 | 713 | #[derive(Debug, PartialEq, Deserialize)] 714 | struct Person { 715 | name: String, 716 | age: u8, 717 | } 718 | 719 | let y = make_sample_yaml(); 720 | assert_eq!( 721 | query_value_result!(y.author >> Person).unwrap(), 722 | Person { 723 | name: "jiftechnify".into(), 724 | age: 31u8, 725 | }, 726 | ); 727 | } 728 | 729 | // Error case tests for YAML 730 | #[test] 731 | fn test_error_value_not_found() { 732 | let y = make_sample_yaml(); 733 | 734 | let result = query_value_result!(y.unknown); 735 | assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); 736 | if let Err(Error::ValueNotFoundAtPath(path)) = result { 737 | assert_eq!(path, ".unknown"); 738 | } 739 | } 740 | 741 | #[test] 742 | fn test_error_as_cast_failed() { 743 | let y = make_sample_yaml(); 744 | 745 | // string cannot be cast to u64 746 | let result = query_value_result!(y.str -> u64); 747 | assert!(matches!(result, Err(Error::AsCastFailed(_)))); 748 | } 749 | 750 | #[test] 751 | fn test_error_deserialization_failed() { 752 | use serde::Deserialize; 753 | 754 | let y = make_sample_yaml(); 755 | 756 | // string into u8 757 | let result = query_value_result!(y.str >> u8); 758 | assert!(matches!(result, Err(Error::DeserializationFailed(_)))); 759 | } 760 | } 761 | 762 | mod toml { 763 | use super::{query_value_result, Error}; 764 | use toml::{ 765 | from_str, 766 | value::{Array, Table}, 767 | Value, 768 | }; 769 | 770 | fn make_sample_toml() -> Value { 771 | let toml_str = include_str!("../res/sample.toml"); 772 | from_str(toml_str).unwrap() 773 | } 774 | fn sample_table() -> Table { 775 | Table::from_iter([ 776 | ("first".to_string(), Value::String("zzz".to_string())), 777 | ("second".to_string(), Value::String("yyy".to_string())), 778 | ]) 779 | } 780 | fn sample_array() -> Array { 781 | vec!["first", "second", "third"] 782 | .into_iter() 783 | .map(|e| Value::String(e.to_string())) 784 | .collect() 785 | } 786 | fn sample_arr_of_tables() -> Array { 787 | let t1 = Table::from_iter([("hidden".to_string(), Value::String("tale".to_string()))]); 788 | let t2 = Table::from_iter([ 789 | ("hoge".to_string(), Value::Integer(1)), 790 | ("fuga".to_string(), Value::Integer(2)), 791 | ]); 792 | let t3 = Table::from_iter([( 793 | "inner_arr".to_string(), 794 | Value::Array(vec![ 795 | Value::Integer(1), 796 | Value::Integer(2), 797 | Value::Integer(3), 798 | ]), 799 | )]); 800 | 801 | vec![t1, t2, t3].into_iter().map(Value::Table).collect() 802 | } 803 | 804 | #[test] 805 | fn test_query() { 806 | let t = make_sample_toml(); 807 | 808 | let tests = [ 809 | (query_value_result!(t.str), Value::String("s".to_string())), 810 | (query_value_result!(t.int), Value::Integer(123)), 811 | (query_value_result!(t.float), Value::Float(1.23)), 812 | (query_value_result!(t.table), Value::Table(sample_table())), 813 | ( 814 | query_value_result!(t.table.second), 815 | Value::String("yyy".to_string()), 816 | ), 817 | (query_value_result!(t.arr), Value::Array(sample_array())), 818 | ( 819 | query_value_result!(t.arr[2]), 820 | Value::String("third".to_string()), 821 | ), 822 | ( 823 | query_value_result!(t.arr_of_tables), 824 | Value::Array(sample_arr_of_tables()), 825 | ), 826 | ( 827 | query_value_result!(t.arr_of_tables[0].hidden), 828 | Value::String("tale".to_string()), 829 | ), 830 | ( 831 | query_value_result!(t.arr_of_tables[2].inner_arr[0]), 832 | Value::Integer(1), 833 | ), 834 | ]; 835 | 836 | for (res, exp) in tests { 837 | assert_eq!(res.unwrap(), &exp); 838 | } 839 | } 840 | 841 | #[test] 842 | fn test_query_and_cast() { 843 | let t = make_sample_toml(); 844 | 845 | let tests = [ 846 | query_value_result!(t.str -> str).unwrap() == "s", 847 | query_value_result!(t.int -> integer).unwrap() == 123, 848 | query_value_result!(t.float -> float).unwrap() == 1.23, 849 | query_value_result!(t.date -> datetime).unwrap().to_string() 850 | == "2021-12-18T12:15:12+09:00", 851 | query_value_result!(t.table -> table).unwrap().len() == 2, 852 | query_value_result!(t.arr -> array).unwrap() 853 | == &vec!["first", "second", "third"] 854 | .into_iter() 855 | .map(|v| Value::String(v.to_string())) 856 | .collect::>(), 857 | query_value_result!(t.arr_of_tables -> array).unwrap().len() == 3, 858 | ]; 859 | 860 | test_all_true_or_failed_idx!(tests); 861 | } 862 | 863 | #[test] 864 | fn test_query_and_deserialize() { 865 | use serde::Deserialize; 866 | 867 | #[derive(Debug, PartialEq, Deserialize)] 868 | struct Person { 869 | name: String, 870 | age: u8, 871 | } 872 | 873 | let t = make_sample_toml(); 874 | assert_eq!( 875 | query_value_result!(t.author >> Person).unwrap(), 876 | Person { 877 | name: "jiftechnify".into(), 878 | age: 31u8, 879 | }, 880 | ); 881 | } 882 | 883 | // Error case tests for TOML 884 | #[test] 885 | fn test_error_value_not_found() { 886 | let t = make_sample_toml(); 887 | 888 | let result = query_value_result!(t.unknown); 889 | assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); 890 | if let Err(Error::ValueNotFoundAtPath(path)) = result { 891 | assert_eq!(path, ".unknown"); 892 | } 893 | } 894 | 895 | #[test] 896 | fn test_error_as_cast_failed() { 897 | let t = make_sample_toml(); 898 | 899 | // string cannot be cast to integer 900 | let result = query_value_result!(t.str -> integer); 901 | assert!(matches!(result, Err(Error::AsCastFailed(_)))); 902 | } 903 | 904 | #[test] 905 | fn test_error_deserialization_failed() { 906 | use serde::Deserialize; 907 | 908 | let t = make_sample_toml(); 909 | 910 | // string into u8 911 | let result = query_value_result!(t.str >> u8); 912 | assert!(matches!(result, Err(Error::DeserializationFailed(_)))); 913 | } 914 | } 915 | --------------------------------------------------------------------------------