From d5b7f9542fb50036932bf11f683108232912dcf1 Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Tue, 16 Dec 2025 23:31:08 +0900 Subject: [PATCH 1/6] add Result-returning flavor of query_value! macro --- src/lib.rs | 169 ++++++++ tests/query_value_result.rs | 813 ++++++++++++++++++++++++++++++++++++ 2 files changed, 982 insertions(+) create mode 100644 tests/query_value_result.rs diff --git a/src/lib.rs b/src/lib.rs index 5ed8765..cd15419 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -310,3 +310,172 @@ doc! {macro_rules! query_value { query_value!(@trv { Some(&$v) } $($rest)*) }; }} + +#[derive(Debug)] +pub enum QueryValueError { + ValueNotFoundAtPath(String), + AsCastFailed(String), + DeserializationFailed(Box), +} + +impl std::fmt::Display for QueryValueError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use QueryValueError as Error; + match self { + Error::ValueNotFoundAtPath(path) => { + write!(f, "value not found at the path: {}", path) + } + Error::AsCastFailed(conv_name) => { + write!(f, "conversion with {}() failed", conv_name) + } + Error::DeserializationFailed(err) => { + write!(f, "failed to deserialize the queried value: {}", err) + } + } + } +} + +impl std::error::Error for QueryValueError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use QueryValueError as Error; + match self { + Error::DeserializationFailed(err) => Some(err.as_ref()), + _ => None, + } + } +} + +#[macro_export] +macro_rules! query_value_result { + /* non-mut traversal */ + // traversal step + (@trv [$trace:ident] { $vopt:expr } . $key:ident $($rest:tt)*) => { + query_value_result!(@trv [$trace] { + $vopt.and_then(|v| { + $trace.push_str(stringify!(.$key)); + v.get(stringify!($key)).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + }) + } $($rest)*) + }; + (@trv [$trace:ident] { $vopt:expr } [ $idx:expr ] $($rest:tt)*) => { + query_value_result!(@trv [$trace] { + $vopt.and_then(|v| { + $trace.push_str(format!("[{}]", stringify!($idx)).as_str()); + v.get($idx).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + }) + } $($rest)*) + }; + // conversion step -> convert then jump to finalization step + (@trv [$trace:ident] { $vopt:expr } -> $dest:ident $($rest:tt)*) => { + $crate::__paste! { + query_value_result!(@fin [$trace] { + $vopt.and_then(|v| { + let conv_name = format!("as_{}", stringify!($dest)); + v.[]() .ok_or_else(|| $crate::QueryValueError::AsCastFailed(conv_name)) + }) + } $($rest)*) + } + }; + (@trv [$trace:ident] { $vopt:expr } >> $dest:ident $($rest:tt)*) => { + query_value_result!(@fin [$trace] { + $vopt.and_then(|v| { + <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + }) + } $($rest)*) + }; + (@trv [$trace:ident] { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { + query_value_result!(@fin [$trace] { + $vopt.and_then(|v| { + <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + }) + } $($rest)*) + }; + // no conversion -> just jump to finalization step + (@trv [$trace:ident] { $vopt:expr } $($rest:tt)*) => { + query_value_result!(@fin [$trace] { $vopt } $($rest)*) + }; + + /* mut traversal */ + // traversal step + (@trv_mut [$trace:ident] { $vopt:expr } . $key:ident $($rest:tt)*) => { + query_value_result!(@trv_mut [$trace] { + $vopt.and_then(|v| { + $trace.push_str(stringify!(.$key)); + v.get_mut(stringify!($key)).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + }) + } $($rest)*) + }; + (@trv_mut [$trace:ident] { $vopt:expr } [ $idx:expr ] $($rest:tt)*) => { + query_value_result!(@trv_mut [$trace] { + $vopt.and_then(|v| { + $trace.push_str(format!("[{}]", stringify!($idx)).as_str()); + v.get_mut($idx).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + }) + } $($rest)*) + }; + // conversion step -> convert then jump to finalization step + (@trv_mut [$trace:ident] { $vopt:expr } -> $dest:ident $($rest:tt)*) => { + $crate::__paste! { + query_value_result!(@fin [$trace] { + $vopt.and_then(|v| { + let conv_name = format!("as_{}_mut", stringify!($dest)); + v.[]().ok_or_else(|| $crate::QueryValueError::AsCastFailed(conv_name)) + }) + } $($rest)*) + } + }; + (@trv_mut [$trace:ident] { $vopt:expr } >> $dest:ident $($rest:tt)*) => { + query_value_result!(@fin [$trace] { + $vopt.and_then(|v| { + <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + }) + } $($rest)*) + }; + (@trv_mut [$trace:ident] { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { + query_value_result!(@fin [$trace] { + $vopt.and_then(|v| { + <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + }) + } $($rest)*) + }; + // no conversion -> just jump to finalization step + (@trv_mut [$trace:ident] { $vopt:expr } $($rest:tt)*) => { + query_value_result!(@fin [$trace] { $vopt } $($rest)*) + }; + + /* finalize: handle unwrapping operator */ + (@fin [$trace:ident] { $vopt:expr } ?? default) => { + { + use $crate::QueryValueError; + let mut $trace = String::new(); + $vopt.unwrap_or_default() + } + }; + (@fin [$trace:ident] { $vopt:expr } ?? $default:expr) => { + { + use $crate::QueryValueError; + let mut $trace = String::new(); + $vopt.unwrap_or_else(|_| $default) + } + }; + // no unwrapping operator + (@fin [$trace:ident] { $vopt:expr }) => { + { + use $crate::QueryValueError; + let mut $trace = String::new(); + $vopt + } + }; + // unreachable branch -> report syntax error + (@fin $($_:tt)*) => { + compile_error!("invalid query syntax for query_value_result!()") + }; + + /* entry points */ + (mut $v:tt $($rest:tt)*) => { + query_value_result!(@trv_mut [trace] { Ok::<_, $crate::QueryValueError>(&mut $v) } $($rest)*) + }; + ($v:tt $($rest:tt)*) => { + query_value_result!(@trv [trace] { Ok::<_, $crate::QueryValueError>(&$v) } $($rest)*) + }; +} diff --git a/tests/query_value_result.rs b/tests/query_value_result.rs new file mode 100644 index 0000000..2443f7b --- /dev/null +++ b/tests/query_value_result.rs @@ -0,0 +1,813 @@ +use valq::{query_value_result, QueryValueError}; + +macro_rules! test_all_true_or_failed_idx { + ($test_res:expr) => { + if let Some(failed_idx) = $test_res.iter().position(|&r| !r) { + panic!("test idx: {} failed", failed_idx) + } + }; +} + +mod json { + use super::{query_value_result, QueryValueError}; + use serde_json::{json, Value}; + + fn make_sample_json() -> Value { + json!({ + "str": "s", + "nums": { + "u64": 123, + "i64": -123, + "f64": 1.23, + }, + "bool": true, + "null": null, + "obj": { + "inner": "value", + "more": "item" + }, + "arr": [ + "first", + 42, + { "hidden": "tale" }, + [0] + ], + "num_arr": [0, 1, 2], + "1st": "prop starts with digit!" + }) + } + + #[test] + fn test_query_with_dot_syntax() { + let j = make_sample_json(); + + let tests = [ + (query_value_result!(j.str), json!("s")), + (query_value_result!(j.nums.u64), json!(123)), + (query_value_result!(j.nums.i64), json!(-123)), + (query_value_result!(j.nums.f64), json!(1.23)), + (query_value_result!(j.bool), json!(true)), + (query_value_result!(j.null), json!(null)), + ( + query_value_result!(j.obj), + json!({"inner": "value", "more": "item"}), + ), + (query_value_result!(j.obj.inner), json!("value")), + ( + query_value_result!(j.arr), + json!(["first", 42, {"hidden": "tale"}, [0]]), + ), + ( + query_value_result!(j["1st"]), + json!("prop starts with digit!"), + ), + ]; + + for (res, exp) in tests { + assert_eq!(res.unwrap(), &exp); + } + } + + #[test] + fn test_query_with_bracket_syntax() { + let j = make_sample_json(); + + let tests = [ + (query_value_result!(j["str"]), json!("s")), + (query_value_result!(j["nums"]["u64"]), json!(123)), + (query_value_result!(j["nums"].i64), json!(-123)), // mixed query + ( + query_value_result!(j["1st"]), + json!("prop starts with digit!"), + ), + ]; + + for (res, exp) in tests { + assert_eq!(res.unwrap(), &exp); + } + } + + #[test] + fn test_indexing_array() { + let j = make_sample_json(); + let tests = [ + (query_value_result!(j.arr[0]), json!("first")), + (query_value_result!(j.arr[1]), json!(42)), + (query_value_result!(j.arr[2].hidden), json!("tale")), // more complex query! + (query_value_result!(j.arr[3][0]), json!(0)), // successive indexing + ]; + + for (res, exp) in tests { + assert_eq!(res.unwrap(), &exp); + } + } + + #[test] + fn test_query_mut() { + let mut j = make_sample_json(); + + // rewriting value of prop + { + let obj_inner = query_value_result!(mut j.obj.inner).unwrap(); + *obj_inner = json!("just woke up!"); + } + assert_eq!( + query_value_result!(j.obj).unwrap(), + &json!({"inner": "just woke up!", "more": "item"}) + ); + + // get inner object as Map, then add new prop via insert() + { + let obj = query_value_result!(mut j.obj -> object).unwrap(); + obj.insert("new_prop".to_string(), json!("yeah")); + } + assert_eq!(query_value_result!(j.obj.new_prop -> str).unwrap(), "yeah"); + + // get inner array as Vec, then append new value via push() + { + let arr = query_value_result!(mut j.arr -> array).unwrap(); + arr.push(json!("appended!")); + } + assert_eq!(query_value_result!(j.arr[4] -> str).unwrap(), "appended!"); + } + + #[test] + fn test_query_and_convert() { + let j = make_sample_json(); + + let tests = [ + query_value_result!(j.str -> str).unwrap() == "s", + query_value_result!(j.nums.u64 -> u64).unwrap() == 123, + query_value_result!(j.nums.i64 -> i64).unwrap() == -123, + query_value_result!(j.nums.f64 -> f64).unwrap() == 1.23, + query_value_result!(j.bool -> bool).unwrap() == true, + query_value_result!(j.null -> null).unwrap() == (), + query_value_result!(j.obj -> object) + .unwrap() + .get("inner") + .unwrap() + == "value", + query_value_result!(j.arr -> array).unwrap() + == &vec![ + json!("first"), + json!(42), + json!({"hidden": "tale"}), + json!([0]), + ], + ]; + + test_all_true_or_failed_idx!(tests); + } + + #[test] + fn test_query_and_deserialize() { + use serde::Deserialize; + + let j = make_sample_json(); + + let tests = [ + query_value_result!(j.str >> (String)).unwrap() == "s", + query_value_result!(j.str >> (std::string::String)).unwrap() == "s", + query_value_result!(j.str >> String).unwrap() == "s", // parens around type name can be omitted if single identifier + query_value_result!(j.nums.u64 >> u8).unwrap() == 123u8, + query_value_result!(j.nums.i64 >> i8).unwrap() == -123i8, + query_value_result!(j.nums.f64 >> f32).unwrap() == 1.23f32, + query_value_result!(j.null >> (())).unwrap() == (), + ]; + + test_all_true_or_failed_idx!(tests); + } + + #[test] + fn test_deserialize_into_custom_struct() { + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct Person { + name: String, + age: u8, + } + + let j = json!({ "author": {"name": "jiftechnify", "age": 31 } }); + assert_eq!( + query_value_result!(j.author >> Person).unwrap(), + Person { + name: "jiftechnify".into(), + age: 31u8, + }, + ); + } + + #[test] + fn test_query_with_unwrapping() { + let j = make_sample_json(); + + let default_str = &json!("default"); + + // basic query with ?? + assert_eq!(query_value_result!(j.str ?? default_str), &json!("s")); + assert_eq!( + query_value_result!(j.unknown ?? default_str), + &json!("default") + ); + + // `?? default` + assert_eq!(query_value_result!(j.nums.u64 -> u64 ?? default), 123u64); + assert_eq!(query_value_result!(j.unknown -> u64 ?? default), 0u64); // u64::default() + assert_eq!(query_value_result!(j.unknown -> str ?? default), ""); // &str::default() + + // with conversion (->) + assert_eq!(query_value_result!(j.str -> str ?? "default"), "s"); + assert_eq!(query_value_result!(j.nums.u64 -> u64 ?? 999), 123); + assert_eq!( + query_value_result!(j.nums.u64 -> str ?? "not a string"), + "not a string" + ); // type mismatch + assert_eq!( + query_value_result!(j.unknown -> str ?? "default"), + "default" + ); + assert_eq!(query_value_result!(j.unknown -> str ?? default), ""); // &str::default() + + // with deserialization (>>) + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize, Default)] + struct Nums { + u64: u64, + i64: i64, + f64: f64, + } + + let expected_nums = Nums { + u64: 123, + i64: -123, + f64: 1.23, + }; + + assert_eq!( + query_value_result!(j.nums >> Nums ?? default), + expected_nums + ); + assert_eq!( + query_value_result!(j.unknown >> Nums ?? Nums { u64: 999, i64: -999, f64: 9.99 }), + Nums { + u64: 999, + i64: -999, + f64: 9.99 + } + ); + assert_eq!( + query_value_result!(j.unknown >> Nums ?? default), + Nums { + u64: 0, + i64: 0, + f64: 0.0, + } + ); + + assert_eq!( + query_value_result!(j.num_arr >> (Vec) ?? default), + vec![0, 1, 2] + ); + assert_eq!( + query_value_result!(j.arr >> (Vec) ?? default), + Vec::::new() + ); + assert_eq!( + query_value_result!(j.arr >> (Vec) ?? vec![42]), + vec![42] + ); + } + + #[test] + fn test_deserialize_into_vec() { + use serde::Deserialize; + + let j = make_sample_json(); + + let tests = [ + query_value_result!(j.num_arr >> (Vec)).unwrap() == vec![0, 1, 2], + query_value_result!(j.num_arr >> (Vec) ?? default) == vec![0, 1, 2], + query_value_result!(j.arr >> (Vec) ?? default) == Vec::::new(), + query_value_result!(j.arr >> (Vec) ?? vec![42]) == vec![42], + ]; + test_all_true_or_failed_idx!(tests); + } + + #[test] + fn test_deserialize_into_hash_map() { + use serde::Deserialize; + use serde_json::{json, Value}; + use std::collections::HashMap; + + let j = make_sample_json(); + + let exp_json: HashMap = HashMap::from([ + ("inner".into(), json!("value")), + ("more".into(), json!("item")), + ]); + + let exp_string: HashMap = HashMap::from([ + ("inner".into(), "value".into()), + ("more".into(), "item".into()), + ]); + + let tests = [ + query_value_result!(j.obj >> (HashMap)).unwrap() == exp_json, + query_value_result!(j.obj >> (HashMap)).unwrap() == exp_string, + ]; + test_all_true_or_failed_idx!(tests); + } + + #[test] + fn test_query_complex_expressions() { + fn gen_value() -> serde_json::Value { + json!({ "x": 1 }) + } + + let tuple = (json!({ "x": 1 }),); + + struct S { + value: serde_json::Value, + } + impl S { + fn new() -> Self { + Self { + value: json!({ "x": 1 }), + } + } + fn gen_value(&self) -> serde_json::Value { + json!({ "x": 1 }) + } + } + let s = S::new(); + + let v = vec![json!({ "x": 1 })]; + + let tests = [ + // querying immediate value + query_value_result!((json!({ "x": 1 })).x).unwrap() == &json!(1), + query_value_result!((json!({ "x": 1 })).y ?? default) == &json!(null), + query_value_result!((json!({ "x": 1 })).x -> u64).unwrap() == 1u64, + query_value_result!((json!({ "x": 1 })).x -> str ?? default) == "", + query_value_result!((json!({ "x": 1 })).x -> str ?? "not str") == "not str", + // querying immediate value (mut) + query_value_result!(mut (json!({ "x": 1 })).x).unwrap() == &mut json!(1), + // querying return value of function + query_value_result!((gen_value()).x).unwrap() == &json!(1), + query_value_result!((gen_value()).x -> u64).unwrap() == 1u64, + query_value_result!((gen_value()).x -> str ?? "not str") == "not str", + // querying element of tuple + query_value_result!((tuple.0).x).unwrap() == &json!(1), + query_value_result!((tuple.0).x -> u64).unwrap() == 1u64, + query_value_result!((tuple.0).x -> str ?? "not str") == "not str", + // querying field of struct + query_value_result!((s.value).x).unwrap() == &json!(1), + query_value_result!((s.value).x -> u64).unwrap() == 1u64, + query_value_result!((s.value).x -> str ?? "not str") == "not str", + // querying return value of method + query_value_result!((s.gen_value()).x).unwrap() == &json!(1), + query_value_result!((s.gen_value()).x -> u64).unwrap() == 1u64, + query_value_result!((s.gen_value()).x -> str ?? "not str") == "not str", + // querying indexed value + query_value_result!((v[0]).x).unwrap() == &json!(1), + query_value_result!((v[0]).x -> u64).unwrap() == 1u64, + query_value_result!((v[0]).x -> str ?? "not str") == "not str", + ]; + test_all_true_or_failed_idx!(tests); + } + + // Error case tests - ValueNotFoundAtPath + #[test] + fn test_error_value_not_found() { + let j = make_sample_json(); + + let tests = [ + (query_value_result!(j.unknown), ".unknown"), + (query_value_result!(j.nums.i128), ".nums.i128"), + (query_value_result!(j.obj[0]), ".obj[0]"), // indexing against non-array value + (query_value_result!(j.arr[100]), ".arr[100]"), // indexing out of bound + ( + query_value_result!(j.obj.inner.not_here.oh.nothing.but.pain), + ".obj.inner.not_here", // make sure that it reports the shallowest non-existing path + ), + ]; + + for (result, expected_path) in tests { + if let Err(QueryValueError::ValueNotFoundAtPath(path)) = result { + assert_eq!(path, expected_path); + } + else { + panic!("expected ValueNotFoundAtPath error, but got: {:?}", result); + } + } + } + + // Error case tests - AsCastFailed + #[test] + fn test_error_as_cast_failed() { + let j = make_sample_json(); + + let tests = [ + (query_value_result!(j.str -> u64).unwrap_err(), "as_u64"), + ( + query_value_result!(j.nums.u64 -> str).unwrap_err(), + "as_str", + ), + (query_value_result!(j.obj -> array).unwrap_err(), "as_array"), + ( + query_value_result!(j.arr -> object).unwrap_err(), + "as_object", + ), + ]; + + for (result, expected_conv_name) in tests { + if let QueryValueError::AsCastFailed(conv_name) = result { + assert_eq!(conv_name, expected_conv_name); + } + else { + panic!("expected AsCastFailed error, but got: {:?}", result); + } + } + } + + // Error case tests - AsCastFailed (mut) + #[test] + fn test_error_as_cast_failed_mut() { + let mut j = make_sample_json(); + + let tests = [ + ( + query_value_result!(mut j.obj -> array).unwrap_err(), + "as_array_mut", + ), + ( + query_value_result!(mut j.arr -> object).unwrap_err(), + "as_object_mut", + ), + ]; + + for (result, expected_conv_name) in tests { + if let QueryValueError::AsCastFailed(conv_name) = result { + assert_eq!(conv_name, expected_conv_name); + } + else { + panic!("expected AsCastFailed error, but got: {:?}", result); + } + } + } + + // Error case tests - DeserializationFailed + #[test] + fn test_error_deserialization_failed() { + use serde::Deserialize; + use std::collections::HashMap; + + let j = make_sample_json(); + + #[derive(Debug, PartialEq, Deserialize)] + struct Person { + name: String, + age: u8, + } + + let tests = [ + query_value_result!(j.nums.i64 >> u8).unwrap_err(), + query_value_result!(j.str >> u8).unwrap_err(), + query_value_result!(j.obj >> Person).unwrap_err(), + query_value_result!(j.arr >> (Vec)).unwrap_err(), + query_value_result!(j.obj >> (HashMap)).unwrap_err(), + ]; + + for result in tests { + assert!(matches!(result, QueryValueError::DeserializationFailed(_))); + } + } + + // Test error display + #[test] + fn test_error_display() { + use serde::Deserialize; + + let j = make_sample_json(); + + // ValueNotFoundAtPath + let err = query_value_result!(j.unknown).unwrap_err(); + assert_eq!(err.to_string(), "value not found at the path: .unknown"); + + // AsCastFailed + let err = query_value_result!(j.str -> u64).unwrap_err(); + assert_eq!(err.to_string(), "conversion with as_u64() failed"); + + // DeserializationFailed + let err = query_value_result!(j.nums.i64 >> u8).unwrap_err(); + assert!(err + .to_string() + .starts_with("failed to deserialize the queried value:")); + } +} + +mod yaml { + use super::{query_value_result, QueryValueError}; + use serde_yaml::{from_str, Mapping, Sequence, Value}; + + fn make_sample_yaml() -> Value { + let yaml_str = include_str!("../res/sample.yaml"); + from_str(yaml_str).unwrap() + } + + fn sample_mapping() -> Mapping { + Mapping::from_iter([ + ( + Value::String("first".to_string()), + Value::String("zzz".to_string()), + ), + ( + Value::String("second".to_string()), + Value::String("yyy".to_string()), + ), + ]) + } + fn sample_map_in_seq() -> Mapping { + Mapping::from_iter([( + Value::String("hidden".to_string()), + Value::String("tale".to_string()), + )]) + } + fn sample_sequence() -> Sequence { + Sequence::from_iter(vec![ + Value::String("first".to_string()), + Value::Number(42.into()), + Value::Mapping(sample_map_in_seq()), + ]) + } + + #[test] + fn test_query() { + let y = make_sample_yaml(); + + let tests = [ + (query_value_result!(y.str), Value::String("s".to_string())), + (query_value_result!(y.num), Value::Number(123.into())), + (query_value_result!(y.map), Value::Mapping(sample_mapping())), + ( + query_value_result!(y.map.second), + Value::String("yyy".to_string()), + ), + ( + query_value_result!(y.seq), + Value::Sequence(sample_sequence()), + ), + ( + query_value_result!(y.seq[0]), + Value::String("first".to_string()), + ), + ( + query_value_result!(y.seq[2]), + Value::Mapping(sample_map_in_seq()), + ), + ]; + + for (res, exp) in tests { + assert_eq!(res.unwrap(), &exp); + } + } + + #[test] + fn test_query_and_convert() { + let y = make_sample_yaml(); + + let tests = [ + query_value_result!(y.str -> str).unwrap() == "s", + query_value_result!(y.num -> u64).unwrap() == 123, + query_value_result!(y.map -> mapping).unwrap().len() == 2, + query_value_result!(y.seq -> sequence).unwrap() + == &vec![ + Value::String("first".to_string()), + Value::Number(42.into()), + Value::Mapping(sample_map_in_seq()), + ], + ]; + + test_all_true_or_failed_idx!(tests); + } + + #[test] + fn test_query_and_deserialize() { + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct Person { + name: String, + age: u8, + } + + let y = make_sample_yaml(); + assert_eq!( + query_value_result!(y.author >> Person).unwrap(), + Person { + name: "jiftechnify".into(), + age: 31u8, + }, + ); + } + + // Error case tests for YAML + #[test] + fn test_error_value_not_found() { + let y = make_sample_yaml(); + + let result = query_value_result!(y.unknown); + assert!(matches!( + result, + Err(QueryValueError::ValueNotFoundAtPath(_)) + )); + if let Err(QueryValueError::ValueNotFoundAtPath(path)) = result { + assert_eq!(path, ".unknown"); + } + } + + #[test] + fn test_error_as_cast_failed() { + let y = make_sample_yaml(); + + // string cannot be converted to u64 + let result = query_value_result!(y.str -> u64); + assert!(matches!(result, Err(QueryValueError::AsCastFailed(_)))); + } + + #[test] + fn test_error_deserialization_failed() { + use serde::Deserialize; + + let y = make_sample_yaml(); + + // string into u8 + let result = query_value_result!(y.str >> u8); + assert!(matches!( + result, + Err(QueryValueError::DeserializationFailed(_)) + )); + } +} + +mod toml { + use super::{query_value_result, QueryValueError}; + use toml::{ + from_str, + value::{Array, Table}, + Value, + }; + + fn make_sample_toml() -> Value { + let toml_str = include_str!("../res/sample.toml"); + from_str(toml_str).unwrap() + } + fn sample_table() -> Table { + Table::from_iter([ + ("first".to_string(), Value::String("zzz".to_string())), + ("second".to_string(), Value::String("yyy".to_string())), + ]) + } + fn sample_array() -> Array { + vec!["first", "second", "third"] + .into_iter() + .map(|e| Value::String(e.to_string())) + .collect() + } + fn sample_arr_of_tables() -> Array { + let t1 = Table::from_iter([("hidden".to_string(), Value::String("tale".to_string()))]); + let t2 = Table::from_iter([ + ("hoge".to_string(), Value::Integer(1)), + ("fuga".to_string(), Value::Integer(2)), + ]); + let t3 = Table::from_iter([( + "inner_arr".to_string(), + Value::Array(vec![ + Value::Integer(1), + Value::Integer(2), + Value::Integer(3), + ]), + )]); + + vec![t1, t2, t3].into_iter().map(Value::Table).collect() + } + + #[test] + fn test_query() { + let t = make_sample_toml(); + + let tests = [ + (query_value_result!(t.str), Value::String("s".to_string())), + (query_value_result!(t.int), Value::Integer(123)), + (query_value_result!(t.float), Value::Float(1.23)), + (query_value_result!(t.table), Value::Table(sample_table())), + ( + query_value_result!(t.table.second), + Value::String("yyy".to_string()), + ), + (query_value_result!(t.arr), Value::Array(sample_array())), + ( + query_value_result!(t.arr[2]), + Value::String("third".to_string()), + ), + ( + query_value_result!(t.arr_of_tables), + Value::Array(sample_arr_of_tables()), + ), + ( + query_value_result!(t.arr_of_tables[0].hidden), + Value::String("tale".to_string()), + ), + ( + query_value_result!(t.arr_of_tables[2].inner_arr[0]), + Value::Integer(1), + ), + ]; + + for (res, exp) in tests { + assert_eq!(res.unwrap(), &exp); + } + } + + #[test] + fn test_query_and_convert() { + let t = make_sample_toml(); + + let tests = [ + query_value_result!(t.str -> str).unwrap() == "s", + query_value_result!(t.int -> integer).unwrap() == 123, + query_value_result!(t.float -> float).unwrap() == 1.23, + query_value_result!(t.date -> datetime).unwrap().to_string() + == "2021-12-18T12:15:12+09:00", + query_value_result!(t.table -> table).unwrap().len() == 2, + query_value_result!(t.arr -> array).unwrap() + == &vec!["first", "second", "third"] + .into_iter() + .map(|v| Value::String(v.to_string())) + .collect::>(), + query_value_result!(t.arr_of_tables -> array).unwrap().len() == 3, + ]; + + test_all_true_or_failed_idx!(tests); + } + + #[test] + fn test_query_and_deserialize() { + use serde::Deserialize; + + #[derive(Debug, PartialEq, Deserialize)] + struct Person { + name: String, + age: u8, + } + + let t = make_sample_toml(); + assert_eq!( + query_value_result!(t.author >> Person).unwrap(), + Person { + name: "jiftechnify".into(), + age: 31u8, + }, + ); + } + + // Error case tests for TOML + #[test] + fn test_error_value_not_found() { + let t = make_sample_toml(); + + let result = query_value_result!(t.unknown); + assert!(matches!( + result, + Err(QueryValueError::ValueNotFoundAtPath(_)) + )); + if let Err(QueryValueError::ValueNotFoundAtPath(path)) = result { + assert_eq!(path, ".unknown"); + } + } + + #[test] + fn test_error_as_cast_failed() { + let t = make_sample_toml(); + + // string cannot be converted to integer + let result = query_value_result!(t.str -> integer); + assert!(matches!(result, Err(QueryValueError::AsCastFailed(_)))); + } + + #[test] + fn test_error_deserialization_failed() { + use serde::Deserialize; + + let t = make_sample_toml(); + + // string into u8 + let result = query_value_result!(t.str >> u8); + assert!(matches!( + result, + Err(QueryValueError::DeserializationFailed(_)) + )); + } +} From 2f7e021034bfeb2675865e083569407b125b18a2 Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Tue, 16 Dec 2025 23:32:00 +0900 Subject: [PATCH 2/6] add test cases for dynamic query --- tests/query_value.rs | 99 +++++++++++++++++++++++++++++++++---- tests/query_value_result.rs | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 9 deletions(-) diff --git a/tests/query_value.rs b/tests/query_value.rs index e3f121a..2af5384 100644 --- a/tests/query_value.rs +++ b/tests/query_value.rs @@ -5,7 +5,8 @@ macro_rules! test_is_some_of_expected_val { for (res, exp) in $tests { if let Some(act) = res { assert_eq!(act, &exp) - } else { + } + else { panic!("expect Some(...) but actually None") } } @@ -53,7 +54,7 @@ mod json { fn test_query_with_dot_syntax() { let j = make_sample_json(); - let tests = vec![ + let tests = [ (query_value!(j.str), json!("s")), (query_value!(j.nums.u64), json!(123)), (query_value!(j.nums.i64), json!(-123)), @@ -79,7 +80,7 @@ mod json { fn test_query_with_bracket_syntax() { let j = make_sample_json(); - let tests = vec![ + let tests = [ (query_value!(j["str"]), json!("s")), (query_value!(j["nums"]["u64"]), json!(123)), (query_value!(j["nums"].i64), json!(-123)), // mixed query @@ -92,7 +93,7 @@ mod json { #[test] fn test_indexing_array() { let j = make_sample_json(); - let tests = vec![ + let tests = [ (query_value!(j.arr[0]), json!("first")), (query_value!(j.arr[1]), json!(42)), (query_value!(j.arr[2].hidden), json!("tale")), // more complex query! @@ -271,7 +272,7 @@ mod json { let j = make_sample_json(); - let tests = vec![ + let tests = [ query_value!(j.num_arr >> (Vec)) == Some(vec![0, 1, 2]), query_value!(j.num_arr >> (Vec) ?? default) == vec![0, 1, 2], query_value!(j.arr >> (Vec)) == None, @@ -299,7 +300,7 @@ mod json { ("more".into(), "item".into()), ]); - let tests = vec![ + let tests = [ query_value!(j.obj >> (HashMap)) == Some(exp_json), query_value!(j.obj >> (HashMap)) == Some(exp_string), query_value!(j.obj >> (HashMap)) == None, @@ -338,6 +339,86 @@ mod json { test_all_true_or_failed_idx!(tests); } + #[test] + fn test_query_with_dynamic_indices() { + let j = make_sample_json(); + + // Dynamic string key + let key = "str"; + assert_eq!(query_value!(j[key]), Some(&json!("s"))); + + let obj_key = "obj"; + let inner_key = "inner"; + assert_eq!(query_value!(j[obj_key][inner_key]), Some(&json!("value"))); + + // Dynamic integer index + let index = 0; + assert_eq!(query_value!(j.arr[index]), Some(&json!("first"))); + + let arr_index = 1; + assert_eq!(query_value!(j.arr[arr_index]), Some(&json!(42))); + + // Mix of static and dynamic + let key2 = "nums"; + assert_eq!(query_value!(j[key2].u64), Some(&json!(123))); + + // Dynamic expression + let base_index = 1; + assert_eq!( + query_value!(j.arr[base_index + 1].hidden), + Some(&json!("tale")) + ); + + // With conversion + assert_eq!(query_value!(j[key] -> str), Some("s")); + assert_eq!(query_value!(j.arr[index] -> str), Some("first")); + + // With unwrapping operator + let missing_key = "missing"; + let fallback = json!("fallback"); + assert_eq!( + query_value!(j[missing_key]?? & fallback), + &json!("fallback") + ); + + let out_of_bounds = 999; + let oob_val = json!("oob"); + assert_eq!( + query_value!(j.arr[out_of_bounds]?? & oob_val), + &json!("oob") + ); + } + + #[test] + fn test_query_with_dynamic_indices_mut() { + let mut j = make_sample_json(); + + // Dynamic string key (mut) + let key = "str"; + { + let val = query_value!(mut j[key]).unwrap(); + *val = json!("modified"); + } + assert_eq!(query_value!(j.str), Some(&json!("modified"))); + + // Dynamic integer index (mut) + let index = 1; + { + let val = query_value!(mut j.arr[index]).unwrap(); + *val = json!(100); + } + assert_eq!(query_value!(j.arr[1]), Some(&json!(100))); + + // With conversion (mut) + let obj_key = "obj"; + let dynamic_key = "dynamic_key"; + { + let obj = query_value!(mut j[obj_key] -> object).unwrap(); + obj.insert("dynamic_key".to_string(), json!("added")); + } + assert_eq!(query_value!(j.obj[dynamic_key]), Some(&json!("added"))); + } + #[test] fn test_query_complex_expressions() { use serde::Deserialize; @@ -365,7 +446,7 @@ mod json { let v = vec![json!({ "x": 1 })]; - let tests = vec![ + let tests = [ // querying immediate value query_value!((json!({ "x": 1 })).x) == Some(&json!(1)), query_value!((json!({ "x": 1 })).y ?? default) == &json!(null), @@ -445,7 +526,7 @@ mod yaml { fn test_query() { let y = make_sample_yaml(); - let tests = vec![ + let tests = [ (query_value!(y.str), Value::String("s".to_string())), (query_value!(y.num), Value::Number(123.into())), (query_value!(y.map), Value::Mapping(sample_mapping())), @@ -543,7 +624,7 @@ mod toml { fn test_query() { let t = make_sample_toml(); - let tests = vec![ + let tests = [ (query_value!(t.str), Value::String("s".to_string())), (query_value!(t.int), Value::Integer(123)), (query_value!(t.float), Value::Float(1.23)), diff --git a/tests/query_value_result.rs b/tests/query_value_result.rs index 2443f7b..b0d7955 100644 --- a/tests/query_value_result.rs +++ b/tests/query_value_result.rs @@ -378,6 +378,92 @@ mod json { test_all_true_or_failed_idx!(tests); } + #[test] + fn test_query_with_dynamic_indices() { + let j = make_sample_json(); + + // Dynamic string key + let key = "str"; + assert_eq!(query_value_result!(j[key]).unwrap(), &json!("s")); + + let obj_key = "obj"; + let inner_key = "inner"; + assert_eq!( + query_value_result!(j[obj_key][inner_key]).unwrap(), + &json!("value") + ); + + // Dynamic integer index + let index = 0; + assert_eq!(query_value_result!(j.arr[index]).unwrap(), &json!("first")); + + let arr_index = 1; + assert_eq!(query_value_result!(j.arr[arr_index]).unwrap(), &json!(42)); + + // Mix of static and dynamic + let key2 = "nums"; + assert_eq!(query_value_result!(j[key2].u64).unwrap(), &json!(123)); + + // Dynamic expression + let base_index = 1; + assert_eq!( + query_value_result!(j.arr[base_index + 1].hidden).unwrap(), + &json!("tale") + ); + + // With conversion + assert_eq!(query_value_result!(j[key] -> str).unwrap(), "s"); + assert_eq!(query_value_result!(j.arr[index] -> str).unwrap(), "first"); + + // With unwrapping operator + let missing_key = "missing"; + let fallback = json!("fallback"); + assert_eq!( + query_value_result!(j[missing_key]?? & fallback), + &json!("fallback") + ); + + let out_of_bounds = 999; + let oob_val = json!("oob"); + assert_eq!( + query_value_result!(j.arr[out_of_bounds]?? & oob_val), + &json!("oob") + ); + } + + #[test] + fn test_query_with_dynamic_indices_mut() { + let mut j = make_sample_json(); + + // Dynamic string key (mut) + let key = "str"; + { + let val = query_value_result!(mut j[key]).unwrap(); + *val = json!("modified"); + } + assert_eq!(query_value_result!(j.str).unwrap(), &json!("modified")); + + // Dynamic integer index (mut) + let index = 1; + { + let val = query_value_result!(mut j.arr[index]).unwrap(); + *val = json!(100); + } + assert_eq!(query_value_result!(j.arr[1]).unwrap(), &json!(100)); + + // With conversion (mut) + let obj_key = "obj"; + let dynamic_key = "dynamic_key"; + { + let obj = query_value_result!(mut j[obj_key] -> object).unwrap(); + obj.insert("dynamic_key".to_string(), json!("added")); + } + assert_eq!( + query_value_result!(j.obj[dynamic_key]).unwrap(), + &json!("added") + ); + } + // Error case tests - ValueNotFoundAtPath #[test] fn test_error_value_not_found() { From 2053c143bf4814cd18dab89dfbf071377af1795a Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Tue, 16 Dec 2025 23:50:03 +0900 Subject: [PATCH 3/6] separate error module, shorten name of the error type --- src/error.rs | 37 ++++++++++++++++++++ src/lib.rs | 67 ++++++++++--------------------------- tests/query_value_result.rs | 44 +++++++++--------------- 3 files changed, 71 insertions(+), 77 deletions(-) create mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8137692 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,37 @@ +/// An error type returned from `Result`-returning macros provided by `valq` crate. +#[derive(Debug)] +pub enum Error { + /// No value found at the specified path. + ValueNotFoundAtPath(String), + /// Conversion with `->` operator (translates to `as_***()`/`as_***_mut()`) failed. + AsCastFailed(String), + /// Deserialization with `>>` operator failed. + DeserializationFailed(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error; + match self { + Error::ValueNotFoundAtPath(path) => { + write!(f, "value not found at the path: {}", path) + } + Error::AsCastFailed(conv_name) => { + write!(f, "conversion with {}() failed", conv_name) + } + Error::DeserializationFailed(err) => { + write!(f, "failed to deserialize the queried value: {}", err) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use Error; + match self { + Error::DeserializationFailed(err) => Some(err.as_ref()), + _ => None, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index cd15419..ec26616 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,9 @@ //! //! [the `query_value` doc]: crate::query_value +mod error; +pub use error::Error; + #[doc(hidden)] pub use paste::paste as __paste; @@ -311,40 +314,6 @@ doc! {macro_rules! query_value { }; }} -#[derive(Debug)] -pub enum QueryValueError { - ValueNotFoundAtPath(String), - AsCastFailed(String), - DeserializationFailed(Box), -} - -impl std::fmt::Display for QueryValueError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use QueryValueError as Error; - match self { - Error::ValueNotFoundAtPath(path) => { - write!(f, "value not found at the path: {}", path) - } - Error::AsCastFailed(conv_name) => { - write!(f, "conversion with {}() failed", conv_name) - } - Error::DeserializationFailed(err) => { - write!(f, "failed to deserialize the queried value: {}", err) - } - } - } -} - -impl std::error::Error for QueryValueError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use QueryValueError as Error; - match self { - Error::DeserializationFailed(err) => Some(err.as_ref()), - _ => None, - } - } -} - #[macro_export] macro_rules! query_value_result { /* non-mut traversal */ @@ -353,7 +322,7 @@ macro_rules! query_value_result { query_value_result!(@trv [$trace] { $vopt.and_then(|v| { $trace.push_str(stringify!(.$key)); - v.get(stringify!($key)).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + v.get(stringify!($key)).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) }) } $($rest)*) }; @@ -361,7 +330,7 @@ macro_rules! query_value_result { query_value_result!(@trv [$trace] { $vopt.and_then(|v| { $trace.push_str(format!("[{}]", stringify!($idx)).as_str()); - v.get($idx).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + v.get($idx).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) }) } $($rest)*) }; @@ -371,7 +340,7 @@ macro_rules! query_value_result { query_value_result!(@fin [$trace] { $vopt.and_then(|v| { let conv_name = format!("as_{}", stringify!($dest)); - v.[]() .ok_or_else(|| $crate::QueryValueError::AsCastFailed(conv_name)) + v.[]() .ok_or_else(|| $crate::Error::AsCastFailed(conv_name)) }) } $($rest)*) } @@ -379,14 +348,14 @@ macro_rules! query_value_result { (@trv [$trace:ident] { $vopt:expr } >> $dest:ident $($rest:tt)*) => { query_value_result!(@fin [$trace] { $vopt.and_then(|v| { - <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) }) } $($rest)*) }; (@trv [$trace:ident] { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { query_value_result!(@fin [$trace] { $vopt.and_then(|v| { - <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) }) } $($rest)*) }; @@ -401,7 +370,7 @@ macro_rules! query_value_result { query_value_result!(@trv_mut [$trace] { $vopt.and_then(|v| { $trace.push_str(stringify!(.$key)); - v.get_mut(stringify!($key)).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + v.get_mut(stringify!($key)).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) }) } $($rest)*) }; @@ -409,7 +378,7 @@ macro_rules! query_value_result { query_value_result!(@trv_mut [$trace] { $vopt.and_then(|v| { $trace.push_str(format!("[{}]", stringify!($idx)).as_str()); - v.get_mut($idx).ok_or_else(|| $crate::QueryValueError::ValueNotFoundAtPath($trace.clone())) + v.get_mut($idx).ok_or_else(|| $crate::Error::ValueNotFoundAtPath($trace.clone())) }) } $($rest)*) }; @@ -419,7 +388,7 @@ macro_rules! query_value_result { query_value_result!(@fin [$trace] { $vopt.and_then(|v| { let conv_name = format!("as_{}_mut", stringify!($dest)); - v.[]().ok_or_else(|| $crate::QueryValueError::AsCastFailed(conv_name)) + v.[]().ok_or_else(|| $crate::Error::AsCastFailed(conv_name)) }) } $($rest)*) } @@ -427,14 +396,14 @@ macro_rules! query_value_result { (@trv_mut [$trace:ident] { $vopt:expr } >> $dest:ident $($rest:tt)*) => { query_value_result!(@fin [$trace] { $vopt.and_then(|v| { - <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) }) } $($rest)*) }; (@trv_mut [$trace:ident] { $vopt:expr } >> ($dest:ty) $($rest:tt)*) => { query_value_result!(@fin [$trace] { $vopt.and_then(|v| { - <$dest>::deserialize(v.clone()).map_err(|e| $crate::QueryValueError::DeserializationFailed(Box::new(e))) + <$dest>::deserialize(v.clone()).map_err(|e| $crate::Error::DeserializationFailed(Box::new(e))) }) } $($rest)*) }; @@ -446,14 +415,14 @@ macro_rules! query_value_result { /* finalize: handle unwrapping operator */ (@fin [$trace:ident] { $vopt:expr } ?? default) => { { - use $crate::QueryValueError; + use $crate::Error; let mut $trace = String::new(); $vopt.unwrap_or_default() } }; (@fin [$trace:ident] { $vopt:expr } ?? $default:expr) => { { - use $crate::QueryValueError; + use $crate::Error; let mut $trace = String::new(); $vopt.unwrap_or_else(|_| $default) } @@ -461,7 +430,7 @@ macro_rules! query_value_result { // no unwrapping operator (@fin [$trace:ident] { $vopt:expr }) => { { - use $crate::QueryValueError; + use $crate::Error; let mut $trace = String::new(); $vopt } @@ -473,9 +442,9 @@ macro_rules! query_value_result { /* entry points */ (mut $v:tt $($rest:tt)*) => { - query_value_result!(@trv_mut [trace] { Ok::<_, $crate::QueryValueError>(&mut $v) } $($rest)*) + query_value_result!(@trv_mut [trace] { Ok::<_, $crate::Error>(&mut $v) } $($rest)*) }; ($v:tt $($rest:tt)*) => { - query_value_result!(@trv [trace] { Ok::<_, $crate::QueryValueError>(&$v) } $($rest)*) + query_value_result!(@trv [trace] { Ok::<_, $crate::Error>(&$v) } $($rest)*) }; } diff --git a/tests/query_value_result.rs b/tests/query_value_result.rs index b0d7955..fe0dfa0 100644 --- a/tests/query_value_result.rs +++ b/tests/query_value_result.rs @@ -1,4 +1,4 @@ -use valq::{query_value_result, QueryValueError}; +use valq::{query_value_result, Error}; macro_rules! test_all_true_or_failed_idx { ($test_res:expr) => { @@ -9,7 +9,7 @@ macro_rules! test_all_true_or_failed_idx { } mod json { - use super::{query_value_result, QueryValueError}; + use super::{query_value_result, Error}; use serde_json::{json, Value}; fn make_sample_json() -> Value { @@ -481,7 +481,7 @@ mod json { ]; for (result, expected_path) in tests { - if let Err(QueryValueError::ValueNotFoundAtPath(path)) = result { + if let Err(Error::ValueNotFoundAtPath(path)) = result { assert_eq!(path, expected_path); } else { @@ -509,7 +509,7 @@ mod json { ]; for (result, expected_conv_name) in tests { - if let QueryValueError::AsCastFailed(conv_name) = result { + if let Error::AsCastFailed(conv_name) = result { assert_eq!(conv_name, expected_conv_name); } else { @@ -535,7 +535,7 @@ mod json { ]; for (result, expected_conv_name) in tests { - if let QueryValueError::AsCastFailed(conv_name) = result { + if let Error::AsCastFailed(conv_name) = result { assert_eq!(conv_name, expected_conv_name); } else { @@ -567,7 +567,7 @@ mod json { ]; for result in tests { - assert!(matches!(result, QueryValueError::DeserializationFailed(_))); + assert!(matches!(result, Error::DeserializationFailed(_))); } } @@ -595,7 +595,7 @@ mod json { } mod yaml { - use super::{query_value_result, QueryValueError}; + use super::{query_value_result, Error}; use serde_yaml::{from_str, Mapping, Sequence, Value}; fn make_sample_yaml() -> Value { @@ -705,11 +705,8 @@ mod yaml { let y = make_sample_yaml(); let result = query_value_result!(y.unknown); - assert!(matches!( - result, - Err(QueryValueError::ValueNotFoundAtPath(_)) - )); - if let Err(QueryValueError::ValueNotFoundAtPath(path)) = result { + assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); + if let Err(Error::ValueNotFoundAtPath(path)) = result { assert_eq!(path, ".unknown"); } } @@ -720,7 +717,7 @@ mod yaml { // string cannot be converted to u64 let result = query_value_result!(y.str -> u64); - assert!(matches!(result, Err(QueryValueError::AsCastFailed(_)))); + assert!(matches!(result, Err(Error::AsCastFailed(_)))); } #[test] @@ -731,15 +728,12 @@ mod yaml { // string into u8 let result = query_value_result!(y.str >> u8); - assert!(matches!( - result, - Err(QueryValueError::DeserializationFailed(_)) - )); + assert!(matches!(result, Err(Error::DeserializationFailed(_)))); } } mod toml { - use super::{query_value_result, QueryValueError}; + use super::{query_value_result, Error}; use toml::{ from_str, value::{Array, Table}, @@ -865,11 +859,8 @@ mod toml { let t = make_sample_toml(); let result = query_value_result!(t.unknown); - assert!(matches!( - result, - Err(QueryValueError::ValueNotFoundAtPath(_)) - )); - if let Err(QueryValueError::ValueNotFoundAtPath(path)) = result { + assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); + if let Err(Error::ValueNotFoundAtPath(path)) = result { assert_eq!(path, ".unknown"); } } @@ -880,7 +871,7 @@ mod toml { // string cannot be converted to integer let result = query_value_result!(t.str -> integer); - assert!(matches!(result, Err(QueryValueError::AsCastFailed(_)))); + assert!(matches!(result, Err(Error::AsCastFailed(_)))); } #[test] @@ -891,9 +882,6 @@ mod toml { // string into u8 let result = query_value_result!(t.str >> u8); - assert!(matches!( - result, - Err(QueryValueError::DeserializationFailed(_)) - )); + assert!(matches!(result, Err(Error::DeserializationFailed(_)))); } } From 5ae1971ef01f3dbd2d55a50445ecb29bacb2ff94 Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Wed, 17 Dec 2025 00:37:31 +0900 Subject: [PATCH 4/6] add docs for query_value_result --- README.md | 28 ++++++++++++++++++++-- src/lib.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0c3fc9e..d361e1a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ Add this to the `Cargo.toml` in your project: valq = "*" ``` -For now, there is only single macro exported: `query_value`. +The principal macro provided by this crate is `query_value!`. +Also, there is a `Result`-returning variant of `query_value!`, called `query_value_result!`. ## `query_value!` macro A macro for querying, extracting and converting inner value of semi-structured data. @@ -104,7 +105,30 @@ assert_eq!(query_value!(obj.foo.bar -> u64 ?? 42), 42); // explicitly provided d assert_eq!(query_value!(obj.foo.bar -> u64 ?? default), 0u64); // using u64::default() ``` -### Compatibility +## `query_value_result!` macro +A variant of `query_value!` that returns `Result` instead of `Option`. + +```rust +use serde::Deserialize; +use serde_json::json; +use valq::{query_value_result, Error}; + +let obj = json!({"foo": {"bar": 42}}); + +// Error::ValueNotFoundAtPath: querying non-existent path +let result = query_value_result!(obj.foo.baz); +assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); + +// Error::AsCastFailed: type conversion failure +let result = query_value_result!(obj.foo.bar -> str); +assert!(matches!(result, Err(Error::AsCastFailed(_)))); + +// Error::DeserializationFailed: deserialization failure +let result = query_value_result!(obj.foo >> (Vec)); +assert!(matches!(result, Err(Error::DeserializationFailed(_)))); +``` + +## Compatibility 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`. Extracting mutable reference is also supported if your `Value` supports `get_mut(&mut self, idx) -> Option<&Value>`. diff --git a/src/lib.rs b/src/lib.rs index ec26616..f88ac06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,21 +1,23 @@ //! # valq -//! `valq` provides a macro for querying and extracting an inner value from a structured data **with the JavaScript-like syntax**. -//! -//! look & feel: +//! `valq` provides macros for querying and extracting an inner value from a structured data **with the JavaScript-like syntax**. //! //! ``` //! # use serde_json::json; //! use serde_json::Value; -//! use valq::query_value; +//! use valq::{query_value, query_value_result}; //! //! // let obj: Value = ...; //! # let obj = json!({}); //! let deep_val: Option<&Value> = query_value!(obj.path.to.value.at.deep); +//! let deep_val_res: Result<&Value, valq::Error> = query_value_result!(obj.path.to.value.at.deep); //! ``` //! -//! For now, there is only single macro exported: `query_value`. Refer to [the `query_value` doc] for detailed usage. +//! The principal macro provided by this crate is `query_value!`. Read [the `query_value` doc] for detailed usage. +//! +//! Also, there is a `Result`-returning variant of `query_value!`, called [`query_value_result!`]. //! //! [the `query_value` doc]: crate::query_value +//! [`query_value_result!`]: crate::query_value_result mod error; pub use error::Error; @@ -23,7 +25,7 @@ pub use error::Error; #[doc(hidden)] pub use paste::paste as __paste; -macro_rules! doc { +macro_rules! doc_query_value { ($query_value:item) => { /// A macro for querying an inner value of a structured ("JSON-ish") data. /// @@ -230,7 +232,7 @@ macro_rules! doc { // fake implementation illustrates the macro syntax for docs #[cfg(doc)] -doc! {macro_rules! query_value { +doc_query_value! {macro_rules! query_value { ($(mut)? $value:tt $(query:tt)* $(?? $default:expr)?) => {}; ($(mut)? $value:tt $(query:tt)* -> $as:ident $(?? $default:expr)?) => {}; ($(mut)? $value:tt $(query:tt)* >> ($deser_to:ty) $(?? $default:expr)?) => {}; @@ -238,7 +240,7 @@ doc! {macro_rules! query_value { // actual implementation #[cfg(not(doc))] -doc! {macro_rules! query_value { +doc_query_value! {macro_rules! query_value { /* non-mut traversal */ // traversal step (@trv { $vopt:expr } . $key:ident $($rest:tt)*) => { @@ -314,8 +316,52 @@ doc! {macro_rules! query_value { }; }} -#[macro_export] -macro_rules! query_value_result { +macro_rules! doc_query_value_result { + ($query_value_result:item) => { + /// A `Result`-returning variant of [`query_value!`]. + /// + /// See the documentation of [`query_value!`] macro for detailed usage. + /// + /// If your query fails, this macro returns a [`valq::Error`] describing the failure reason. + /// + /// ``` + /// use serde::Deserialize; + /// use serde_json::json; + /// use valq::{query_value_result, Error}; + /// + /// let obj = json!({"foo": {"bar": 42}}); + /// + /// // Error::ValueNotFoundAtPath: querying non-existent path + /// let result = query_value_result!(obj.foo.baz); + /// assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); + /// + /// // Error::AsCastFailed: type conversion failure + /// let result = query_value_result!(obj.foo.bar -> str); + /// assert!(matches!(result, Err(Error::AsCastFailed(_)))); + /// + /// // Error::DeserializationFailed: deserialization failure + /// let result = query_value_result!(obj.foo >> (Vec)); + /// assert!(matches!(result, Err(Error::DeserializationFailed(_)))); + /// ``` + /// + /// [`query_value!`]: crate::query_value + /// [`valq::Error`]: crate::Error + #[macro_export] + $query_value_result + }; +} + +// fake implementation illustrates the macro syntax for docs +#[cfg(doc)] +doc_query_value_result! {macro_rules! query_value_result { + ($(mut)? $value:tt $(query:tt)* $(?? $default:expr)?) => {}; + ($(mut)? $value:tt $(query:tt)* -> $as:ident $(?? $default:expr)?) => {}; + ($(mut)? $value:tt $(query:tt)* >> ($deser_to:ty) $(?? $default:expr)?) => {}; +}} + +// actual implementation +#[cfg(not(doc))] +doc_query_value_result! {macro_rules! query_value_result { /* non-mut traversal */ // traversal step (@trv [$trace:ident] { $vopt:expr } . $key:ident $($rest:tt)*) => { @@ -447,4 +493,4 @@ macro_rules! query_value_result { ($v:tt $($rest:tt)*) => { query_value_result!(@trv [trace] { Ok::<_, $crate::Error>(&$v) } $($rest)*) }; -} +}} From adb6ca9465dc02133aa1816f3b8f6a5c35d38597 Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Wed, 17 Dec 2025 00:47:44 +0900 Subject: [PATCH 5/6] wording: call the conversion with as_***() methods "(type) casting" --- CHANGELOG.md | 4 ++-- README.md | 8 ++++---- src/error.rs | 4 ++-- src/lib.rs | 12 ++++++------ tests/query_value.rs | 12 ++++++------ tests/query_value_result.rs | 18 +++++++++--------- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9d601..211d729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Arbitrary type conversion with `->` operator** ([#46](https://github.com/jiftechnify/valq/pull/46)) +- **Arbitrary type casting with `->` operator** ([#46](https://github.com/jiftechnify/valq/pull/46)) - No longer limited to hard-coded conversions! - Any `as_xxx()` method available on the value type can be used @@ -46,7 +46,7 @@ Initial release with basic query functionality. - Dot notation for accessing object properties (`.field`) - Bracket notation for array/object indexing (`[index]`) - Mutable reference extraction with `mut` prefix - - Basic type conversion using `as_***()` methods with `->` operator + - Basic type casting using `as_***()` methods with `->` operator [0.2.0]: https://github.com/jiftechnify/valq/compare/0.1.0...0.2.0 [0.1.0]: https://github.com/jiftechnify/valq/releases/tag/0.1.0 diff --git a/README.md b/README.md index d361e1a..32201aa 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,14 @@ let mut obj = json!({"foo": { "bar": { "x": 1, "y": 2 }}}); let bar: &mut Value = query_value!(mut obj.foo.bar).unwrap(); *bar = json!({"x": 100, "y": 200}); } -// `->` syntax converts `Value` to typed value (see below) +// with `->` syntax, you can cast `Value` as typed value (see below) assert_eq!(query_value!(obj.foo.bar.x -> u64), Some(100)); assert_eq!(query_value!(obj.foo.bar.y -> u64), Some(200)); ``` -### Converting & Deserializing to Specified Type +### Casting & Deserializing to Specified Type ```rust -// try to convert the queried value into `u64` using `as_u64()` method on that value. +// try to cast the queried value into `u64` using `as_u64()` method on that value. // results in `None` in case of type mismatch let foo_u64: Option = query_value!(obj.foo -> u64); @@ -119,7 +119,7 @@ let obj = json!({"foo": {"bar": 42}}); let result = query_value_result!(obj.foo.baz); assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); -// Error::AsCastFailed: type conversion failure +// Error::AsCastFailed: type casting failure let result = query_value_result!(obj.foo.bar -> str); assert!(matches!(result, Err(Error::AsCastFailed(_)))); diff --git a/src/error.rs b/src/error.rs index 8137692..b902649 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,7 @@ pub enum Error { /// No value found at the specified path. ValueNotFoundAtPath(String), - /// Conversion with `->` operator (translates to `as_***()`/`as_***_mut()`) failed. + /// Casting a value with `->` operator (translates to `as_***()`/`as_***_mut()`) failed. AsCastFailed(String), /// Deserialization with `>>` operator failed. DeserializationFailed(Box), @@ -17,7 +17,7 @@ impl std::fmt::Display for Error { write!(f, "value not found at the path: {}", path) } Error::AsCastFailed(conv_name) => { - write!(f, "conversion with {}() failed", conv_name) + write!(f, "casting with {}() failed", conv_name) } Error::DeserializationFailed(err) => { write!(f, "failed to deserialize the queried value: {}", err) diff --git a/src/lib.rs b/src/lib.rs index f88ac06..607053f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,9 +103,9 @@ macro_rules! doc_query_value { /// assert_eq!(query_value!(obj.foo.bar.y -> u64), Some(200)); /// ``` /// - /// ## `->`: Converting Value with `as_***()` + /// ## `->`: Cast Value with `as_***()` /// - /// Queries end with `-> ***` try to convert the extracted value with `as_***()` method. + /// Queries end with `-> ***` try to cast the extracted value with `as_***()` method. /// In the `mut` context, `as_***_mut()` method is used instead. /// /// ```txt @@ -122,7 +122,7 @@ macro_rules! doc_query_value { /// /// let mut obj = json!({"foo": "hello", "arr": [1, 2]}); /// - /// // try to convert extracted value with `as_u64` method on that value + /// // try to cast extracted value with `as_u64` method on that value /// // results in `None` in case of type mismatch /// let foo_str: Option<&str> = query_value!(obj.foo -> str); /// assert_eq!(foo_str, Some("hello")); @@ -167,7 +167,7 @@ macro_rules! doc_query_value { /// /// - 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. /// + For example, the query above can be simplified to `j.author >> Person`. - /// - Deserialization with `>>` involves cloning of the queried value. You may want to use `->` conversion if possible. + /// - Deserialization with `>>` involves cloning of the queried value. You may want to use `->` type casting if possible. /// /// ## `??`: Unwarp Query Result with Default Value /// @@ -206,7 +206,7 @@ macro_rules! doc_query_value { /// - ``: An index to extract value from structure /// + For an array-like structure, any expressions evaluates to an integer can be used /// + For a key-value structure, any expressions evaluates to a string can be used - /// - ``: A destination type of conversion with `as_***()` / `as_***_mut()` methods + /// - ``: A destination type of type casting with `as_***()` / `as_***_mut()` methods /// - ``: A type name into which the queried value is deserialized /// + The specified type *MUST* implement the `serde::Deserialize` trait /// + If the type name contains only a single identifier, you can omit parentheses around it @@ -335,7 +335,7 @@ macro_rules! doc_query_value_result { /// let result = query_value_result!(obj.foo.baz); /// assert!(matches!(result, Err(Error::ValueNotFoundAtPath(_)))); /// - /// // Error::AsCastFailed: type conversion failure + /// // Error::AsCastFailed: type casting failure /// let result = query_value_result!(obj.foo.bar -> str); /// assert!(matches!(result, Err(Error::AsCastFailed(_)))); /// diff --git a/tests/query_value.rs b/tests/query_value.rs index 2af5384..3e20cf6 100644 --- a/tests/query_value.rs +++ b/tests/query_value.rs @@ -133,7 +133,7 @@ mod json { } #[test] - fn test_query_and_convert() { + fn test_query_and_cast() { let j = make_sample_json(); let tests = [ @@ -211,7 +211,7 @@ mod json { assert_eq!(query_value!(j.unknown -> u64 ?? default), 0u64); // u64::default() assert_eq!(query_value!(j.unknown -> str ?? default), ""); // &str::default() - // with conversion (->) + // with casting (->) assert_eq!(query_value!(j.str -> str ?? "default"), "s"); assert_eq!(query_value!(j.nums.u64 -> u64 ?? 999), 123); assert_eq!( @@ -369,7 +369,7 @@ mod json { Some(&json!("tale")) ); - // With conversion + // With casting (->) assert_eq!(query_value!(j[key] -> str), Some("s")); assert_eq!(query_value!(j.arr[index] -> str), Some("first")); @@ -409,7 +409,7 @@ mod json { } assert_eq!(query_value!(j.arr[1]), Some(&json!(100))); - // With conversion (mut) + // With casting (mut) let obj_key = "obj"; let dynamic_key = "dynamic_key"; { @@ -539,7 +539,7 @@ mod yaml { } #[test] - fn test_query_and_convert() { + fn test_query_and_cast() { let y = make_sample_yaml(); let tests = [ @@ -652,7 +652,7 @@ mod toml { } #[test] - fn test_query_and_convert() { + fn test_query_and_cast() { let t = make_sample_toml(); let tests = [ diff --git a/tests/query_value_result.rs b/tests/query_value_result.rs index fe0dfa0..8dfc080 100644 --- a/tests/query_value_result.rs +++ b/tests/query_value_result.rs @@ -132,7 +132,7 @@ mod json { } #[test] - fn test_query_and_convert() { + fn test_query_and_cast() { let j = make_sample_json(); let tests = [ @@ -216,7 +216,7 @@ mod json { assert_eq!(query_value_result!(j.unknown -> u64 ?? default), 0u64); // u64::default() assert_eq!(query_value_result!(j.unknown -> str ?? default), ""); // &str::default() - // with conversion (->) + // with casting (->) assert_eq!(query_value_result!(j.str -> str ?? "default"), "s"); assert_eq!(query_value_result!(j.nums.u64 -> u64 ?? 999), 123); assert_eq!( @@ -411,7 +411,7 @@ mod json { &json!("tale") ); - // With conversion + // With casting (->) assert_eq!(query_value_result!(j[key] -> str).unwrap(), "s"); assert_eq!(query_value_result!(j.arr[index] -> str).unwrap(), "first"); @@ -451,7 +451,7 @@ mod json { } assert_eq!(query_value_result!(j.arr[1]).unwrap(), &json!(100)); - // With conversion (mut) + // With casting (mut) let obj_key = "obj"; let dynamic_key = "dynamic_key"; { @@ -584,7 +584,7 @@ mod json { // AsCastFailed let err = query_value_result!(j.str -> u64).unwrap_err(); - assert_eq!(err.to_string(), "conversion with as_u64() failed"); + assert_eq!(err.to_string(), "cast with as_u64() failed"); // DeserializationFailed let err = query_value_result!(j.nums.i64 >> u8).unwrap_err(); @@ -661,7 +661,7 @@ mod yaml { } #[test] - fn test_query_and_convert() { + fn test_query_and_cast() { let y = make_sample_yaml(); let tests = [ @@ -715,7 +715,7 @@ mod yaml { fn test_error_as_cast_failed() { let y = make_sample_yaml(); - // string cannot be converted to u64 + // string cannot be cast to u64 let result = query_value_result!(y.str -> u64); assert!(matches!(result, Err(Error::AsCastFailed(_)))); } @@ -812,7 +812,7 @@ mod toml { } #[test] - fn test_query_and_convert() { + fn test_query_and_cast() { let t = make_sample_toml(); let tests = [ @@ -869,7 +869,7 @@ mod toml { fn test_error_as_cast_failed() { let t = make_sample_toml(); - // string cannot be converted to integer + // string cannot be cast to integer let result = query_value_result!(t.str -> integer); assert!(matches!(result, Err(Error::AsCastFailed(_)))); } From e44e7ef7fab229ad686f05425f2c1a1d6b088b63 Mon Sep 17 00:00:00 2001 From: jiftechnify Date: Wed, 17 Dec 2025 01:17:01 +0900 Subject: [PATCH 6/6] fix test --- tests/query_value_result.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/query_value_result.rs b/tests/query_value_result.rs index 8dfc080..e664080 100644 --- a/tests/query_value_result.rs +++ b/tests/query_value_result.rs @@ -584,7 +584,7 @@ mod json { // AsCastFailed let err = query_value_result!(j.str -> u64).unwrap_err(); - assert_eq!(err.to_string(), "cast with as_u64() failed"); + assert_eq!(err.to_string(), "casting with as_u64() failed"); // DeserializationFailed let err = query_value_result!(j.nums.i64 >> u8).unwrap_err();