Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -61,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<u64> = query_value!(obj.foo -> u64);

Expand Down Expand Up @@ -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<T, valq::Error>` instead of `Option<T>`.

```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 casting 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<u8>));
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>`.
Expand Down
37 changes: 37 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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),
/// Casting a value with `->` operator (translates to `as_***()`/`as_***_mut()`) failed.
AsCastFailed(String),
/// Deserialization with `>>` operator failed.
DeserializationFailed(Box<dyn std::error::Error>),
}

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, "casting 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,
}
}
}
210 changes: 197 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
//! # 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;

#[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.
///
Expand Down Expand Up @@ -98,9 +103,9 @@ macro_rules! doc {
/// 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
Expand All @@ -117,7 +122,7 @@ macro_rules! doc {
///
/// 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"));
Expand Down Expand Up @@ -162,7 +167,7 @@ macro_rules! doc {
///
/// - 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
///
Expand Down Expand Up @@ -201,7 +206,7 @@ macro_rules! doc {
/// - `<idx>`: 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
/// - `<as_dest>`: A destination type of conversion with `as_***()` / `as_***_mut()` methods
/// - `<as_dest>`: A destination type of type casting with `as_***()` / `as_***_mut()` methods
/// - `<deser_dest>`: 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
Expand All @@ -227,15 +232,15 @@ 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)?) => {};
}}

// 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)*) => {
Expand Down Expand Up @@ -310,3 +315,182 @@ doc! {macro_rules! query_value {
query_value!(@trv { Some(&$v) } $($rest)*)
};
}}

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 casting 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<u8>));
/// 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)*) => {
query_value_result!(@trv [$trace] {
$vopt.and_then(|v| {
$trace.push_str(stringify!(.$key));
v.get(stringify!($key)).ok_or_else(|| $crate::Error::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::Error::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.[<as_ $dest>]() .ok_or_else(|| $crate::Error::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::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::Error::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::Error::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::Error::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.[<as_ $dest _mut>]().ok_or_else(|| $crate::Error::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::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::Error::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::Error;
let mut $trace = String::new();
$vopt.unwrap_or_default()
}
};
(@fin [$trace:ident] { $vopt:expr } ?? $default:expr) => {
{
use $crate::Error;
let mut $trace = String::new();
$vopt.unwrap_or_else(|_| $default)
}
};
// no unwrapping operator
(@fin [$trace:ident] { $vopt:expr }) => {
{
use $crate::Error;
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::Error>(&mut $v) } $($rest)*)
};
($v:tt $($rest:tt)*) => {
query_value_result!(@trv [trace] { Ok::<_, $crate::Error>(&$v) } $($rest)*)
};
}}
Loading