From 585e04d0e0da18011671d1f640dd6efb15c63285 Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Wed, 17 Dec 2025 23:04:29 +0900 Subject: [PATCH 1/3] add transpose_tuple utility macro --- src/error.rs | 2 ++ src/lib.rs | 4 ++- src/util.rs | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/util.rs diff --git a/src/error.rs b/src/error.rs index b902649..275d9db 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,3 +35,5 @@ impl std::error::Error for Error { } } } + +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 607053f..5928479 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,9 @@ //! [`query_value_result!`]: crate::query_value_result mod error; -pub use error::Error; +pub use error::{Error, Result}; + +mod util; #[doc(hidden)] pub use paste::paste as __paste; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..b73abbd --- /dev/null +++ b/src/util.rs @@ -0,0 +1,87 @@ +/// Transposes an arbitrary length of tuple of `Option`s/`Result`s into an `Option`/`Result` of a tuple. +/// +/// Syntax: +/// +/// - `Option`-variant: `transpose_tuple!(Option; e1, e2, ..., eN)` ... transposes a tuple of `Option`s. +/// - You can omit `Option;` part, like: `transpose_tuple!(e1, e2, ..., eN)`. +/// - `Result`-variant: `transpose_tuple!(Result; e1, e2, ..., eN)`... transposes a tuple of [`valq::Result`]s. +/// +/// Note: `Result`-variant is meant to be used with [`valq::Result`] (return type of `query_value_result!` macro). +/// It doesn't support other error types for now. +/// +/// [`valq::Result`]: crate::Result +/// +/// ## Examples +/// +/// ``` +/// // Options +/// use serde_json::json; +/// use valq::{query_value, transpose_tuple}; +/// +/// let valq = json!({ +/// "name": "valq", +/// "keywords": ["macro", "query", "serde"], +/// "author": { +/// "name": "jiftechnify", +/// "age": 31 +/// } +/// }); +/// +/// let picks = transpose_tuple!( +/// query_value!(valq.name -> str), +/// query_value!(valq.keywords[1] -> str), +/// query_value!(valq.author.age -> u64), +/// ); +/// assert_eq!(picks, Some(("valq", "query", 31u64))); +/// +/// let wrong_picks = transpose_tuple!( +/// query_value!(valq.name -> str), +/// query_value!(valq.keywords[10] -> str), // out of bounds +/// ); +/// assert_eq!(wrong_picks, None); +/// ``` +/// +/// ``` +/// // Results +/// # use serde_json::json; +/// use valq::{query_value_result, transpose_tuple}; +/// # let valq = json!({ +/// # "name": "valq", +/// # "keywords": ["macro", "query", "serde"], +/// # "author": { +/// # "name": "jiftechnify", +/// # "age": 31 +/// # } +/// # }); +/// +/// let picks = transpose_tuple!( +/// Result; // don't forget this! +/// query_value_result!(valq.name -> str), +/// query_value_result!(valq.keywords[1] -> str), +/// query_value_result!(valq.author.age -> u64), +/// ); +/// assert!(matches!(picks, Ok(("valq", "query", 31u64)))); +/// +/// let wrong_picks = transpose_tuple!( +/// Result; +/// query_value_result!(valq.name -> str), +/// query_value_result!(valq.secrets), // non-existent path +/// ); +/// assert!(matches!(wrong_picks, Err(valq::Error::ValueNotFoundAtPath(_)))); +/// ``` +#[macro_export] +macro_rules! transpose_tuple { + (Option; $first:expr, $($rest:expr),+ $(,)?) => { + (|| { + Some(( $first?, $($rest?),+ )) + })() + }; + (Result; $first:expr, $($rest:expr),+ $(,)?) => { + (|| { + Ok(( $first?, $($rest?),+ )) as $crate::Result<_> + })() + }; + ($first:expr, $($rest:expr),+ $(,)?) => { + transpose_tuple!(Option; $first, $($rest),+) + }; +} From d490ea3911a51675f1309daa518e5182cf5cef2c Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Wed, 17 Dec 2025 23:09:39 +0900 Subject: [PATCH 2/3] =?UTF-8?q?transpose=5Ftuple=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/transpose_tuple.rs | 217 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 tests/transpose_tuple.rs diff --git a/tests/transpose_tuple.rs b/tests/transpose_tuple.rs new file mode 100644 index 0000000..b2c117f --- /dev/null +++ b/tests/transpose_tuple.rs @@ -0,0 +1,217 @@ +use serde_json::json; +use valq::{query_value, query_value_result, transpose_tuple, Error}; + +#[test] +fn test_option_variant_all_some() { + let data = json!({ + "name": "valq", + "version": "0.2.0", + "stars": 100 + }); + + let result = transpose_tuple!( + query_value!(data.name -> str), + query_value!(data.version -> str), + query_value!(data.stars -> u64), + ); + + assert_eq!(result, Some(("valq", "0.2.0", 100u64))); +} + +#[test] +fn test_option_variant_with_none() { + let data = json!({ + "name": "valq", + "version": "0.2.0", + }); + + let result = transpose_tuple!( + query_value!(data.name -> str), + query_value!(data.nonexistent -> str), + query_value!(data.version -> str), + ); + + assert_eq!(result, None); +} + +#[test] +fn test_option_variant_explicit_option_prefix() { + let data = json!({ + "a": 1, + "b": 2, + "c": 3 + }); + + let result = transpose_tuple!( + Option; + query_value!(data.a -> u64), + query_value!(data.b -> u64), + query_value!(data.c -> u64), + ); + + assert_eq!(result, Some((1u64, 2u64, 3u64))); +} + +#[test] +fn test_option_variant_many_elements() { + let data = json!({ + "v1": 1, + "v2": 2, + "v3": 3, + "v4": 4, + "v5": 5, + "v6": 6 + }); + + let result = transpose_tuple!( + query_value!(data.v1 -> u64), + query_value!(data.v2 -> u64), + query_value!(data.v3 -> u64), + query_value!(data.v4 -> u64), + query_value!(data.v5 -> u64), + query_value!(data.v6 -> u64), + ); + + assert_eq!(result, Some((1u64, 2u64, 3u64, 4u64, 5u64, 6u64))); +} + +#[test] +fn test_option_variant_trailing_comma() { + let data = json!({ + "a": "foo", + "b": "bar", + }); + + let result = transpose_tuple!(query_value!(data.a -> str), query_value!(data.b -> str),); + + assert_eq!(result, Some(("foo", "bar"))); +} + +#[test] +fn test_result_variant_all_ok() { + let data = json!({ + "name": "valq", + "keywords": ["macro", "query", "serde"], + "author": { + "name": "jiftechnify", + "age": 31 + } + }); + + let result = transpose_tuple!( + Result; + query_value_result!(data.name -> str), + query_value_result!(data.keywords[1] -> str), + query_value_result!(data.author.age -> u64), + ); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), ("valq", "query", 31u64)); +} + +#[test] +fn test_result_variant_with_error() { + let data = json!({ + "name": "valq", + "version": "0.2.0" + }); + + let result = transpose_tuple!( + Result; + query_value_result!(data.name -> str), + query_value_result!(data.nonexistent), + ); + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::ValueNotFoundAtPath(_))); +} + +#[test] +fn test_result_variant_type_mismatch_error() { + let data = json!({ + "value": "not a number", + "other": 123 + }); + + let result = transpose_tuple!( + Result; + query_value_result!(data.value -> u64), + query_value_result!(data.other -> u64), + ); + + assert!(result.is_err()); +} + +#[test] +fn test_result_variant_array_out_of_bounds() { + let data = json!({ + "items": [1, 2, 3] + }); + + let result = transpose_tuple!( + Result; + query_value_result!(data.items[0] -> u64), + query_value_result!(data.items[10] -> u64), + ); + + assert!(result.is_err()); +} + +#[test] +fn test_result_variant_trailing_comma() { + let data = json!({ + "a": 1.5, + "b": 2.5, + }); + + let result = transpose_tuple!( + Result; + query_value_result!(data.a -> f64), + query_value_result!(data.b -> f64), + ); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), (1.5f64, 2.5f64)); +} + +#[test] +fn test_mixed_types() { + let data = json!({ + "string": "hello", + "number": 42, + "boolean": true, + "float": 3.14 + }); + + let result = transpose_tuple!( + query_value!(data.string -> str), + query_value!(data.number -> u64), + query_value!(data.boolean -> bool), + query_value!(data.float -> f64), + ); + + assert_eq!(result, Some(("hello", 42u64, true, 3.14f64))); +} + +#[test] +fn test_nested_object_access() { + let data = json!({ + "level1": { + "level2": { + "level3": { + "value": "deep", + "number": 99 + } + } + } + }); + + let result = transpose_tuple!( + Result; + query_value_result!(data.level1.level2.level3.value -> str), + query_value_result!(data.level1.level2.level3.number -> u64), + ); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), ("deep", 99u64)); +} From d6c339f30b9296454812127ef077ac607c1db26a Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Wed, 17 Dec 2025 23:10:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Cargo.toml=E3=81=A7=E3=82=AF=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index d2fc16c..3e58aa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "MIT" description = "macros for querying semi-structured data with the JavaScript-like syntax" readme = "README.md" repository = "https://github.com/jiftechnify/valq" +categories = ["rust-patterns"] keywords = ["macro", "query", "extract", "json", "serde"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html