iq-0.2.2/.cargo_vcs_info.json0000644000000001360000000000100114460ustar { "git": { "sha1": "93e9ac2aa73738a9d987f8eee9a94cb26fb0ad24" }, "path_in_vcs": "" }iq-0.2.2/.gitignore000064400000000000000000000000101046102023000122150ustar 00000000000000/target iq-0.2.2/Cargo.lock0000644000000077760000000000100074420ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "iq" version = "0.2.2" dependencies = [ "lazy-regex", "serde", "serde_json", ] [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "lazy-regex" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" dependencies = [ "proc-macro2", "quote", "regex", "syn", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "syn" version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" iq-0.2.2/Cargo.toml0000644000000023560000000000100074520ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.56" name = "iq" version = "0.2.2" authors = ["dystroy "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "introspection with dynamic queries" readme = "README.md" keywords = [ "serde", "introspection", "query", ] categories = ["parsing"] license = "MIT" repository = "https://github.com/Canop/iq" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [lib] name = "iq" path = "src/lib.rs" [dependencies.lazy-regex] version = "3" optional = true [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" [features] default = [] template = ["lazy-regex"] iq-0.2.2/Cargo.toml.orig000064400000000000000000000013361046102023000131300ustar 00000000000000[package] name = "iq" version = "0.2.2" edition = "2021" authors = ["dystroy "] repository = "https://github.com/Canop/iq" description = "introspection with dynamic queries" keywords = ["serde", "introspection", "query"] license = "MIT" categories = ["parsing"] # seriously, the categories taxonomy makes no sense readme = "README.md" rust-version = "1.56" [features] template = ["lazy-regex"] default = [] [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" lazy-regex = { version = "3", optional = true } # https://users.rust-lang.org/t/how-to-document-optional-features-in-api-docs/64577 [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] iq-0.2.2/LICENSE000064400000000000000000000020461046102023000112450ustar 00000000000000MIT License Copyright (c) 2024 Canop Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. iq-0.2.2/README.md000064400000000000000000000061421046102023000115200ustar 00000000000000# Introspect Query [![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] [s1]: https://img.shields.io/crates/v/iq.svg [l1]: https://crates.io/crates/iq [s2]: https://img.shields.io/badge/license-MIT-blue.svg [l2]: LICENSE [s3]: https://docs.rs/iq/badge.svg [l3]: https://docs.rs/iq/ [s4]: https://miaou.dystroy.org/static/shields/room.svg [l4]: https://miaou.dystroy.org/3768?rust IQ (Introspect Query) lets you query standard structs, maps, enums, arrays, tuples, and nested combinations of these, to get deep values with a simple path syntax. Values jut have to implement serde's `Serialize` trait. Both values and queries are dynamic, and can be provided at runtime. IQ is efficient: the explored value isn't serialized, the `Serialize` trait is used to visit it and the visit goes only to the desired target, skipping other branches. See the [IQ](trait.IQ.html) trait for all extract functions. ```rust use iq::IQ; use serde::{ Deserialize, Serialize }; #[derive(Debug, Serialize)] struct Car { pub engine: String, pub passengers: Vec, pub driver: Dog, } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct Dog { pub name: String, pub ears: u8, } let car = Car { engine: "V8".to_string(), passengers: vec![ Dog { name: "Roverandom".to_string(), ears: 1, }, Dog { name: "Laïka".to_string(), ears: 2, }, ], driver: Dog { name: "Rex".to_string(), ears: 2, }, }; // extract "primitive" values as strings with extract_primitive assert_eq!(car.extract_primitive("driver.ears").unwrap(), "2"); assert_eq!(car.extract_primitive("driver.name").unwrap(), "Rex"); assert_eq!(car.extract_primitive("passengers.1.name").unwrap(), "Laïka"); assert_eq!(car.extract_primitive("passengers.1"), None); // it's not a primitive // extract any value as Json with extract_json assert_eq!(car.extract_json("wrong.path"), None); assert_eq!(car.extract_json("driver.ears").unwrap(), "2"); assert_eq!(car.extract_json("driver.name").unwrap(), r#""Rex""#); assert_eq!( car.extract_json("passengers.0").unwrap(), r#"{"name":"Roverandom","ears":1}"# ); assert_eq!(car.extract_json("passengers.3"), None); // extract any deserializable value with extract_value assert_eq!(car.extract_value("driver.ears").unwrap(), Some(2)); assert_eq!( car.extract_value("passengers.1").unwrap(), Some(Dog { name: "Laïka".to_string(), ears: 2 }), ); // You don't have to concat tokens if you build the path assert_eq!( car.extract_primitive(vec!["passengers", "0", "ears"]) .unwrap(), "1" ); // Extract functions are available both on the IQ trait and as standalone functions. assert_eq!(iq::extract_primitive(&car, "driver.name").unwrap(), "Rex"); // If iq is compiled with the "template" feature, you get a mini templating utility let template = iq::Template::new("{driver.name} drives a {engine} car."); assert_eq!(template.render(&car), "Rex drives a V8 car."); ``` IQ also works with enums, maps, and tuples: more tests can be found in libs.rs. iq-0.2.2/bacon.toml000064400000000000000000000020131046102023000122110ustar 00000000000000# This is a configuration file for the bacon tool # # Complete help on configuration: https://dystroy.org/bacon/config/ # # You may check the current default at # https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml default_job = "check" env.CARGO_TERM_COLOR = "always" [jobs.check] command = ["cargo", "check"] need_stdout = false # Run clippy on the default target [jobs.clippy] command = ["cargo", "clippy"] need_stdout = false [jobs.test] command = ["cargo", "test", "--features", "template"] need_stdout = true [jobs.nextest] command = [ "cargo", "nextest", "run", "--hide-progress-bar", "--failure-output", "final" ] need_stdout = true analyzer = "nextest" [jobs.doc] command = ["cargo", "doc", "--no-deps"] need_stdout = false # If the doc compiles, then it opens in your browser and bacon switches # to the previous job [jobs.doc-open] command = ["cargo", "doc", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change [keybindings] iq-0.2.2/fmt.sh000075500000000000000000000000231046102023000113560ustar 00000000000000cargo +nightly fmt iq-0.2.2/rustfmt.toml000064400000000000000000000001761046102023000126430ustar 00000000000000edition = "2021" style_edition = "2024" imports_granularity = "One" imports_layout = "Vertical" fn_params_layout = "Vertical" iq-0.2.2/src/diver.rs000064400000000000000000000316251046102023000125130ustar 00000000000000use { crate::{ errors::IqInternalError, *, }, serde::{ Serialize, ser, }, }; /// The thing wich dives into a Serialize value and goes directly /// to the searched value. pub(crate) struct Diver<'p> { keys: &'p [&'p str], next_token: usize, requested_seq_idx: usize, current_seq_idx: usize, return_next_primitive: bool, accept_next_map_value: bool, return_next_map_value: bool, format: IqFormat, } impl<'p> Diver<'p> { pub fn new( keys: &'p [&'p str], format: IqFormat, ) -> Self { Self { keys, next_token: 0, requested_seq_idx: 0, current_seq_idx: 0, return_next_primitive: false, return_next_map_value: false, accept_next_map_value: false, format, } } fn has_next_token( &self, key: &str, ) -> bool { self.next_token < self.keys.len() && key == self.keys[self.next_token] } fn on_found_with_value( &mut self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { match self.format { IqFormat::Primitive => { self.return_next_primitive = true; Ok(()) } IqFormat::Json => { let json = serde_json::to_string(value)?; Err(IqInternalError::Found(json)) } IqFormat::JsonPretty => { let json = serde_json::to_string_pretty(value)?; Err(IqInternalError::Found(json)) } } } fn incr_next_token_with_value( &mut self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { self.next_token += 1; if self.next_token >= self.keys.len() { return self.on_found_with_value(value); } Ok(()) } } impl ser::Serializer for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; type SerializeSeq = Self; type SerializeTuple = Self; type SerializeTupleStruct = Self; type SerializeTupleVariant = Self; type SerializeMap = Self; type SerializeStruct = Self; type SerializeStructVariant = Self; fn serialize_bool( self, v: bool, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_i8( self, v: i8, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_i16( self, v: i16, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_i32( self, v: i32, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_i64( self, v: i64, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_u8( self, v: u8, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_u16( self, v: u16, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_u32( self, v: u32, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_u64( self, v: u64, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_f32( self, v: f32, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_f64( self, v: f64, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_char( self, v: char, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(format!("{}", v))); } Ok(()) } fn serialize_str( self, v: &str, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(v.to_string())); } Ok(()) } fn serialize_bytes( self, _v: &[u8], ) -> Result<(), IqInternalError> { // FIXME not yet implemented Ok(()) } fn serialize_none(self) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found("none".to_string())); } Ok(()) } fn serialize_some( self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { value.serialize(self) } fn serialize_unit(self) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found("unit".to_string())); } Ok(()) } fn serialize_unit_struct( self, _name: &'static str, ) -> Result<(), IqInternalError> { self.serialize_unit() } fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, ) -> Result<(), IqInternalError> { if self.return_next_primitive { return Err(IqInternalError::Found(variant.to_string())); } self.serialize_str(variant) } fn serialize_newtype_struct( self, _name: &'static str, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { value.serialize(self) } fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { // TODO I'm not sure what this is for value.serialize(&mut *self)?; Ok(()) } fn serialize_seq( self, _len: Option, ) -> Result { self.requested_seq_idx = self.keys[self.next_token] .parse() .map_err(|_| IqInternalError::IndexExpected)?; self.current_seq_idx = 0; Ok(self) } fn serialize_tuple( self, len: usize, ) -> Result { self.serialize_seq(Some(len)) } fn serialize_tuple_struct( self, _name: &'static str, len: usize, ) -> Result { self.serialize_seq(Some(len)) } fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, _len: usize, ) -> Result { variant.serialize(&mut *self)?; Ok(self) } fn serialize_map( self, _len: Option, ) -> Result { Ok(self) } fn serialize_struct( self, _name: &'static str, len: usize, ) -> Result { self.serialize_map(Some(len)) } fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, _len: usize, ) -> Result { variant.serialize(&mut *self)?; Ok(self) } } impl ser::SerializeSeq for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; fn serialize_element( &mut self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { if self.current_seq_idx == self.requested_seq_idx { self.incr_next_token_with_value(value)?; value.serialize(&mut **self)?; } self.current_seq_idx += 1; Ok(()) } fn end(self) -> Result<(), IqInternalError> { Ok(()) } } impl ser::SerializeTuple for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; fn serialize_element( &mut self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { if self.current_seq_idx == self.requested_seq_idx { self.incr_next_token_with_value(value)?; value.serialize(&mut **self)?; } self.current_seq_idx += 1; Ok(()) } fn end(self) -> Result<(), IqInternalError> { Ok(()) } } impl ser::SerializeTupleStruct for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; fn serialize_field( &mut self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { if self.current_seq_idx == self.requested_seq_idx { self.incr_next_token_with_value(value)?; value.serialize(&mut **self)?; } self.current_seq_idx += 1; Ok(()) } fn end(self) -> Result<(), IqInternalError> { Ok(()) } } // TODO not sure I correctly handled this thing impl ser::SerializeTupleVariant for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; fn serialize_field( &mut self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { value.serialize(&mut **self) } fn end(self) -> Result<(), IqInternalError> { Ok(()) } } impl ser::SerializeMap for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; fn serialize_key( &mut self, _key: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { // The key can be anything. For the purpose of comparing with the path, // we'll take the JSON representation of the key, with quotes removed. // For complex composite keys, a specific query language might be needed. let key = serde_json::to_string(_key)?; self.accept_next_map_value = key.trim_matches('"') == self.keys[self.next_token].trim_matches('"'); if self.accept_next_map_value { self.next_token += 1; if self.next_token >= self.keys.len() { self.return_next_map_value = true; } } Ok(()) } fn serialize_value( &mut self, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { if !self.accept_next_map_value { return Ok(()); } if self.return_next_map_value { self.on_found_with_value(value)?; } value.serialize(&mut **self) } fn end(self) -> Result<(), IqInternalError> { Ok(()) } } impl ser::SerializeStruct for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; fn serialize_field( &mut self, key: &'static str, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { if self.has_next_token(key) { self.incr_next_token_with_value(value)?; value.serialize(&mut **self)?; } Ok(()) } fn end(self) -> Result<(), IqInternalError> { Ok(()) } } // Similar to `SerializeTupleVariant`, here the `end` method is responsible for // closing both of the curly braces opened by `serialize_struct_variant`. impl ser::SerializeStructVariant for &mut Diver<'_> { type Ok = (); type Error = IqInternalError; fn serialize_field( &mut self, key: &'static str, value: &T, ) -> Result<(), IqInternalError> where T: ?Sized + Serialize, { if self.has_next_token(key) { self.incr_next_token_with_value(value)?; } value.serialize(&mut **self) } fn end(self) -> Result<(), IqInternalError> { Ok(()) } } iq-0.2.2/src/errors.rs000064400000000000000000000030501046102023000127050ustar 00000000000000use { serde::ser, std::fmt, }; /// Internal error type for the extract operation. #[derive(Debug)] pub(crate) enum IqInternalError { Message(String), Json(serde_json::Error), Found(String), IndexExpected, } impl std::error::Error for IqInternalError {} impl ser::Error for IqInternalError { fn custom(msg: T) -> Self { Self::Message(msg.to_string()) } } impl From for IqInternalError { fn from(err: serde_json::Error) -> Self { Self::Json(err) } } impl fmt::Display for IqInternalError { fn fmt( &self, formatter: &mut fmt::Formatter, ) -> fmt::Result { match self { Self::Message(msg) => write!(formatter, "IQ Error: {}", msg), Self::Json(err) => write!(formatter, "IQ Error: JSON: {}", err), Self::IndexExpected => write!(formatter, "IQ Error: Index expected"), Self::Found(_) => write!(formatter, "IQ Error: Found"), } } } /// Error #[derive(Debug)] pub enum IqError { Serde(String), Json(serde_json::Error), } impl std::error::Error for IqError {} impl From for IqError { fn from(err: serde_json::Error) -> Self { Self::Json(err) } } impl fmt::Display for IqError { fn fmt( &self, formatter: &mut fmt::Formatter, ) -> fmt::Result { match self { Self::Serde(msg) => write!(formatter, "Serde Error: {}", msg), Self::Json(err) => write!(formatter, "JSON error: {}", err), } } } iq-0.2.2/src/extract.rs000064400000000000000000000062731046102023000130550ustar 00000000000000use { crate::{ diver::Diver, errors::IqInternalError, *, }, serde::{ Serialize, de::DeserializeOwned, }, }; /// Format for the extracted value #[derive(Debug, PartialEq, Eq, Hash)] pub enum IqFormat { /// Exctract as Display, but only if the value is a "primitive" Primitive, /// Extract as JSON Json, /// Extract as JSON, pretty JsonPretty, } /// Extract a string from a structure at a given path, with a given format. /// /// If the path is not found, or empty, return None. /// /// May theorethically return an error (eg if structure serialization fails), /// but most users should probably use one of the simpler other functions. pub fn extract_string_checked( source: &T, path: P, format: IqFormat, ) -> Result, IqError> { let keys: Vec<&str> = path.keys().collect(); if keys.is_empty() { // we can't return the complete structure because we // would need to determine if it's a primitive return Ok(None); } let mut diver = Diver::new(&keys, format); match source.serialize(&mut diver) { Ok(()) => Ok(None), // Not found Err(IqInternalError::Found(json)) => Ok(Some(json)), Err(IqInternalError::Message(msg)) => Err(IqError::Serde(msg)), Err(IqInternalError::Json(err)) => Err(IqError::Json(err)), Err(IqInternalError::IndexExpected) => Ok(None), // path doesn't match } } /// Extract a string from a structure at a given path, with a given format. /// /// If the path is not found, or empty, return None. /// /// This function also returns None if the the `Serialize` implementation fails, /// which should not happen with a standard implementation. pub fn extract_string( source: &T, path: P, format: IqFormat, ) -> Option { extract_string_checked(source, path, format).unwrap_or(None) } /// Extract a value as JSON pub fn extract_json( source: &T, path: P, ) -> Option { extract_string(source, path, IqFormat::Json) } /// Extract a value as JSON, pretty pub fn extract_json_pretty( source: &T, path: P, ) -> Option { extract_string(source, path, IqFormat::JsonPretty) } /// Extract a "primitive" value (including strings, simple enum variants, etc) /// as a string using the `Display` implementation of the deep value. pub fn extract_primitive( source: &T, path: P, ) -> Option { extract_string(source, path, IqFormat::Primitive) } /// Extract a value, which must implement `Deserialize`, from a value, at /// the given path. /// /// This function uses a JSON representation of the deep value as intermediate /// step, which adds some (usually light) overload but also allows to extract /// in a different type than the real type of the deep value. pub fn extract_value( source: &T, path: P, ) -> Result, IqError> { let json = extract_string_checked(source, path, IqFormat::Json)?; let value = json.map(|json| serde_json::from_str(&json)).transpose()?; Ok(value) } iq-0.2.2/src/iq.rs000064400000000000000000000033711046102023000120100ustar 00000000000000use { crate::*, serde::{ Serialize, de::DeserializeOwned, }, }; /// A trait to import if you want extract function on any `Serialize` type. pub trait IQ { /// Extract a "primitive" value (including strings, simple enum variants, etc) /// as a string using the Display implementation of the deep value. fn extract_primitive( &self, path: P, ) -> Option; /// Extract a value as JSON fn extract_json( &self, path: P, ) -> Option; /// Extract a value as JSON, pretty fn extract_json_pretty( &self, path: P, ) -> Option; /// Extract a value in a type which must implement `Deserialize`, from a value, at /// the given path. /// /// This function uses a JSON representation of the deep value as intermediate /// step, which adds some (usually light) overload but also allows to extract /// in a different type than the real type of the deep value. fn extract_value( &self, path: P, ) -> Result, IqError>; } impl IQ for T where T: Serialize, { fn extract_primitive( &self, path: P, ) -> Option { extract_primitive(self, path) } fn extract_json( &self, path: P, ) -> Option { extract_json(self, path) } fn extract_json_pretty( &self, path: P, ) -> Option { extract_json(self, path) } fn extract_value( &self, path: P, ) -> Result, IqError> { extract_value(self, path) } } iq-0.2.2/src/lib.rs000064400000000000000000000163221046102023000121450ustar 00000000000000//! IQ (Introspect Query) lets you query standard structs, maps, enums, arrays, tuples, and //! nested combinations of these, to get deep values with a simple path syntax. //! //! Values jut have to implement serde's `Serialize` trait. //! //! Both values and queries are dynamic, and can be provided at runtime. //! //! IQ is efficient: the explored value isn't serialized, the `Serialize` trait is used to visit it and the visit goes only to the desired target, skipping other branches. //! //! See the [IQ](trait.IQ.html) trait for all extract functions. //! //! ```rust //! use iq::IQ; //! use serde::{ Deserialize, Serialize }; //! //! #[derive(Debug, Serialize)] //! struct Car { //! pub engine: String, //! pub passengers: Vec, //! pub driver: Dog, //! } //! #[derive(Debug, PartialEq, Serialize, Deserialize)] //! struct Dog { //! pub name: String, //! pub ears: u8, //! } //! //! let car = Car { //! engine: "V8".to_string(), //! passengers: vec![ //! Dog { //! name: "Roverandom".to_string(), //! ears: 1, //! }, //! Dog { //! name: "Laïka".to_string(), //! ears: 2, //! }, //! ], //! driver: Dog { //! name: "Rex".to_string(), //! ears: 2, //! }, //! }; //! //! // extract "primitive" values as strings with extract_primitive //! assert_eq!(car.extract_primitive("driver.ears").unwrap(), "2"); //! assert_eq!(car.extract_primitive("driver.name").unwrap(), "Rex"); //! assert_eq!(car.extract_primitive("passengers.1.name").unwrap(), "Laïka"); //! assert_eq!(car.extract_primitive("passengers.1"), None); // it's not a primitive //! //! // extract any value as Json with extract_json //! assert_eq!(car.extract_json("wrong.path"), None); //! assert_eq!(car.extract_json("driver.ears").unwrap(), "2"); //! assert_eq!(car.extract_json("driver.name").unwrap(), r#""Rex""#); //! assert_eq!( //! car.extract_json("passengers.0").unwrap(), //! r#"{"name":"Roverandom","ears":1}"# //! ); //! assert_eq!(car.extract_json("passengers.3"), None); //! //! // extract any deserializable value with extract_value //! assert_eq!(car.extract_value("driver.ears").unwrap(), Some(2)); //! assert_eq!( //! car.extract_value("passengers.1").unwrap(), //! Some(Dog { //! name: "Laïka".to_string(), //! ears: 2 //! }), //! ); //! //! // You don't have to concat tokens if you build the path //! assert_eq!( //! car.extract_primitive(vec!["passengers", "0", "ears"]) //! .unwrap(), //! "1" //! ); //! //! // Extract functions are available both on the IQ trait and as standalone functions. //! assert_eq!(iq::extract_primitive(&car, "driver.name").unwrap(), "Rex"); //! //! //! // If iq is compiled with the "template" feature, you get a mini templating utility //! let template = iq::Template::new("{driver.name} drives a {engine} car."); //! assert_eq!(template.render(&car), "Rex drives a V8 car."); //! //! ``` //! //! IQ also works with enums, maps, and tuples: more tests can be found in libs.rs. //! #![cfg_attr(docsrs, feature(doc_cfg))] mod diver; mod errors; mod extract; mod iq; mod path; #[cfg(feature = "template")] mod template; pub use { errors::IqError, extract::*, iq::*, path::*, }; #[cfg(feature = "template")] #[cfg_attr(docsrs, doc(cfg(feature = "template")))] pub use template::*; #[cfg(test)] mod tests { use { super::IQ, serde::{ Deserialize, Serialize, }, std::collections::HashMap, }; #[test] fn structs_and_arrays() { #[derive(Debug, Serialize)] struct Car { pub engine: String, pub passengers: Vec, pub driver: Dog, } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct Dog { pub name: String, pub ears: u8, } let car = Car { engine: "V8".to_string(), passengers: vec![ Dog { name: "Roverandom".to_string(), ears: 1, }, Dog { name: "Laïka".to_string(), ears: 2, }, ], driver: Dog { name: "Rex".to_string(), ears: 2, }, }; // extract "primitive" values as strings with extract_primitive assert_eq!(car.extract_primitive("driver.ears").unwrap(), "2"); assert_eq!(car.extract_primitive("driver.name").unwrap(), "Rex"); assert_eq!(car.extract_primitive("passengers.1.name").unwrap(), "Laïka"); assert_eq!(car.extract_primitive("passengers.1"), None,); // extract any value as Json with extract_json assert_eq!(car.extract_json("wrong.path"), None); assert_eq!(car.extract_json("driver.ears").unwrap(), "2"); assert_eq!(car.extract_json("driver.name").unwrap(), r#""Rex""#); assert_eq!( car.extract_json("passengers.0").unwrap(), r#"{"name":"Roverandom","ears":1}"# ); assert_eq!(car.extract_json("passengers.3"), None); // extract any Deserializable value with extract_value assert_eq!( car.extract_value("passengers.1").unwrap(), Some(Dog { name: "Laïka".to_string(), ears: 2 }), ); // You don't have to concat tokens if you build the path assert_eq!( car.extract_primitive(vec!["passengers", "0", "ears"]) .unwrap(), "1" ); } #[test] fn structs_enums_maps_and_tuples() { #[derive(Debug, PartialEq, Serialize, Deserialize)] struct Dog { pub name: String, pub ears: u8, } #[derive(Debug, Serialize, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] enum Realm { Real, Fantasy, } #[derive(Debug, Serialize, Default)] struct World { pub targets: HashMap, pub masters: HashMap, } let mut world = World::default(); world.targets.insert("Earth".to_string(), (1, 2, 3)); world.targets.insert("Moon".to_string(), (4, 5, 6)); world.masters.insert(Realm::Fantasy, Dog { name: "Roverandom".to_string(), ears: 1, }); world.masters.insert(Realm::Real, Dog { name: "Laïka".to_string(), ears: 2, }); assert_eq!(world.extract_primitive("targets.Earth.1").unwrap(), "2"); assert_eq!(world.extract_json("targets.Moon").unwrap(), "[4,5,6]"); assert_eq!(world.extract_primitive("targets.Moon.2").unwrap(), "6"); assert_eq!(world.extract_primitive("targets.Moon.3"), None); assert_eq!( world.extract_primitive("masters.fantasy.name").unwrap(), "Roverandom" ); assert_eq!(world.extract_primitive("masters.real.ears").unwrap(), "2"); assert_eq!( world.extract_value("masters.fantasy").unwrap(), Some(Dog { name: "Roverandom".to_string(), ears: 1, }), ); } } iq-0.2.2/src/path.rs000064400000000000000000000024431046102023000123320ustar 00000000000000/// A path defining a deep destination into a value. /// /// Searching with an empty path will always return None. pub trait IqPath { fn keys(&self) -> impl Iterator; /// Build from any implementation of the trait a canonical parsed /// version which is a little faster to use (for when you need to /// use the same iq_path multiple times). fn iq_path(&self) -> Vec { self.keys().map(|s| s.to_string()).collect() } } impl IqPath for &Vec { fn keys(&self) -> impl Iterator { self.iter().map(|s| s.as_str()) } } impl IqPath for &Vec<&str> { fn keys(&self) -> impl Iterator { self.iter().copied() } } impl IqPath for &[String] { fn keys(&self) -> impl Iterator { self.iter().map(|s| s.as_str()) } } impl IqPath for &[&str; N] { fn keys(&self) -> impl Iterator { self.iter().copied() } } impl IqPath for &[&str] { fn keys(&self) -> impl Iterator { self.iter().copied() } } impl IqPath for Vec<&str> { fn keys(&self) -> impl Iterator { self.iter().copied() } } impl IqPath for &str { fn keys(&self) -> impl Iterator { self.split('.') } } iq-0.2.2/src/template.rs000064400000000000000000000054371046102023000132170ustar 00000000000000use { crate::*, lazy_regex::*, serde::{ Deserialize, Serialize, }, }; #[derive(Debug, Clone, Serialize, Deserialize)] enum Token { Literal(String), IqPath(Vec), } /// A template that can be rendered with data. /// /// ``` /// let template = iq::Template::new("test {1}"); /// let data = ('a', 'b'); /// assert_eq!(template.render(data), "test b"); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Template { tokens: Vec, } impl Template { pub fn new(template: &str) -> Self { let re = regex!(r"\{([^{} ]+)\}"); let mut tokens = Vec::new(); let mut last_end = 0; for mat in re.find_iter(template) { let start = mat.start(); let end = mat.end(); if start > last_end { tokens.push(Token::Literal(template[last_end..start].to_string())); } let iq_path = &template[start + 1..end - 1]; tokens.push(Token::IqPath(iq_path.iq_path())); last_end = end; } if last_end < template.len() { tokens.push(Token::Literal(template[last_end..].to_string())); } Self { tokens } } pub fn render( &self, data: T, ) -> String where T: Serialize, { let mut applied = String::new(); for token in &self.tokens { match token { Token::Literal(lit) => applied.push_str(lit), Token::IqPath(path) => { if let Some(s) = data.extract_primitive(path) { applied.push_str(&s); } } } } applied } } #[test] fn test_templates() { #[derive(Serialize)] struct Span<'s> { text: &'s str, age: u32, } #[derive(Serialize)] struct Diagnostic { disease: Option, diag_span: Span<'static>, } #[derive(Serialize)] struct Data<'s> { spans: Vec>, diag: Diagnostic, stuf: (u16, u16), } let data = Data { spans: vec![ Span { text: "hello", age: 1, }, Span { text: "world", age: 2, }, ], diag: Diagnostic { disease: Some("covid".to_string()), diag_span: Span { text: "diagnosis", age: 3, }, }, stuf: (4, 5), }; let template = Template::new( "spans: {spans.0.text} {spans.1.age}, diag: {diag.disease} {diag.diag_span.text}, stuf: {stuf.0} {stuf.1}", ); assert_eq!( template.render(&data), "spans: hello 2, diag: covid diagnosis, stuf: 4 5", ); }