diff --git a/Cargo.lock b/Cargo.lock index f17cb46..7ecab7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -437,7 +437,7 @@ checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" [[package]] name = "valu3" -version = "0.6.6" +version = "0.6.7" dependencies = [ "bincode", "chrono", @@ -452,7 +452,7 @@ dependencies = [ [[package]] name = "valu3-derive" -version = "0.6.6" +version = "0.6.7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0b3e68a..b08be80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,3 @@ [workspace] -members = [ - "valu3", - "valu3_derive", -] +members = ["valu3", "valu3_derive"] resolver = "2" diff --git a/VERSION.txt b/VERSION.txt index bf21f52..8b707c6 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.6.6 \ No newline at end of file +0.6.7 \ No newline at end of file diff --git a/example.json b/example.json new file mode 100644 index 0000000..0573ed2 --- /dev/null +++ b/example.json @@ -0,0 +1,22 @@ +{ + "menu": { + "id": "file", + "value": "File", + "popup": { + "menuitem": [ + { + "value": "New", + "onclick": "CreateNewDoc()" + }, + { + "value": "Open", + "onclick": "OpenDoc()" + }, + { + "value": "Close", + "onclick": "CloseDoc()" + } + ] + } + } +} \ No newline at end of file diff --git a/valu3/Cargo.toml b/valu3/Cargo.toml index c0fb830..cc79112 100644 --- a/valu3/Cargo.toml +++ b/valu3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "valu3" -version = "0.6.6" +version = "0.6.7" edition = "2021" license = "Apache-2.0" readme = "crates-io.md" @@ -17,7 +17,7 @@ pest_derive = "2.7.15" regex = "1.11.1" chrono = "0.4.39" serde = { version = "1.0.216", features = ["derive"], optional = true } -valu3-derive = { path = "../valu3_derive", optional = true, version = "0.6.6" } +valu3-derive = { path = "../valu3_derive", optional = true, version = "0.6.7" } bincode = { version = "1.3.3", optional = true } [dev-dependencies] diff --git a/valu3/README.md b/valu3/README.md index cdb1e0d..52eb4bb 100644 --- a/valu3/README.md +++ b/valu3/README.md @@ -3,11 +3,11 @@ Welcome to **Valu3** - the ultimate, flexible, and powerful library for manipulating diverse data types in your Rust projects. Say goodbye to the complexity of handling numbers, strings, arrays, objects, and datetime values. Valu3 is here to make your life easier! -[![crates.io](https://img.shields.io/crates/v/valu3?label=0.6.6)](https://crates.io/crates/valu3) -[![Documentation](https://docs.rs/valu3/badge.svg?version=0.6.6)](https://docs.rs/valu3/0.6.6) +[![crates.io](https://img.shields.io/crates/v/valu3?label=0.6.7)](https://crates.io/crates/valu3) +[![Documentation](https://docs.rs/valu3/badge.svg?version=0.6.7)](https://docs.rs/valu3/0.6.7) ![MSRV](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/valu3/0.6.6/status.svg)](https://deps.rs/crate/valu3/0.6.6) +[![Dependency Status](https://deps.rs/crate/valu3/0.6.7/status.svg)](https://deps.rs/crate/valu3/0.6.7) ![Main test](https://github.com/lowcarboncode/valu3/actions/workflows/main-test.yml/badge.svg) [![codecov](https://codecov.io/gh/lowcarboncode/valu3/branch/master/graph/badge.svg)](https://codecov.io/gh/lowcarboncode/valu3) ![downloads](https://img.shields.io/crates/d/valu3.svg) diff --git a/valu3/src/parser/json/json.pest b/valu3/src/parser/json/json.pest new file mode 100644 index 0000000..b7ced78 --- /dev/null +++ b/valu3/src/parser/json/json.pest @@ -0,0 +1,24 @@ +json = _{ SOI ~ (object | array | string | number | boolean | null) ~ EOI } +WHITESPACE = _{ " " | "\t" | "\r" | "\n" } +object = { + ("#{" | "{") ~ "}" + | ("#{" | "{") ~ key_value_pair ~ ("," ~ key_value_pair)* ~ "}" +} +key_value_pair = { string ~ ":" ~ value } +array = { + "[" ~ "]" + | "[" ~ value ~ ("," ~ value)* ~ "]" +} +value = _{ object | array | string | number | boolean | null } +boolean = { "true" | "false" } +null = { "null" } +string = ${ "\"" ~ inner ~ "\"" } +inner = @{ char* } +char = { + !("\"" | "\\") ~ ANY + | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") + | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) +} +number = @{ + "-"? ~ ("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT+)? +} diff --git a/valu3/src/parser/json/mod.rs b/valu3/src/parser/json/mod.rs new file mode 100644 index 0000000..fccab32 --- /dev/null +++ b/valu3/src/parser/json/mod.rs @@ -0,0 +1,159 @@ +use crate::prelude::*; +use pest::Parser; +use std::collections::HashMap; + +#[derive(Parser)] +#[grammar = "parser/json/json.pest"] +struct JSONParser; + +use pest::iterators::Pair; + +impl Value { + pub fn json_to_value(str: &str) -> Result { + let value = match JSONParser::parse(Rule::json, str.trim()) { + Ok(mut pairs) => match pairs.next() { + Some(pair) => Self::json_parse_value_inner(pair), + None => return Err(Error::NonParseble), + }, + Err(msg) => return Err(Error::NonParsebleMsg(msg.to_string())), + }; + Ok(value) + } + + fn json_parse_value_inner(pair: Pair) -> Self { + match pair.as_rule() { + Rule::object => { + let map = pair + .into_inner() + .map(|pair| { + let mut inner_rules = pair.into_inner(); + let name = inner_rules + .next() + .unwrap() + .into_inner() + .next() + .unwrap() + .as_str() + .to_string(); + let value = Self::json_parse_value_inner(inner_rules.next().unwrap()); + (name, value) + }) + .collect::>(); + + Self::from(map) + } + Rule::array => Self::from( + pair.into_inner() + .map(Self::json_parse_value_inner) + .collect::>(), + ), + Rule::string => Self::from(StringB::from(pair.into_inner().next().unwrap().as_str())), + Rule::number => Self::from(Number::try_from(pair.as_str()).unwrap()), + Rule::boolean => Self::Boolean(pair.as_str().parse().unwrap()), + Rule::null => Self::Null, + Rule::json + | Rule::EOI + | Rule::key_value_pair + | Rule::value + | Rule::inner + | Rule::char + | Rule::WHITESPACE => Self::Undefined, + } + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + use std::collections::HashMap; + + #[test] + fn json() { + let raw: &str = "{ + \"test\": true, + \"test2\": \"ok\", + \"test3\": [0, 1] + }"; + + let compare = Value::from({ + let mut map = HashMap::new(); + map.insert("test".to_string(), true.to_value()); + map.insert("test2".to_string(), "ok".to_value()); + map.insert("test3".to_string(), Value::from(vec![0, 1])); + map + }); + + assert_eq!(Value::json_to_value(raw), Ok(compare)); + } + + #[test] + fn array() { + let raw = "[0, true, null, \"ok\"]"; + + let compare = { + let mut list = Vec::new(); + list.push(Value::Number(Number::from(0))); + list.push(Value::Boolean(true)); + list.push(Value::Null); + list.push(Value::String(StringB::from("ok"))); + Value::from(list) + }; + + assert_eq!(Value::json_to_value(raw), Ok(compare)); + } + + #[test] + fn number() { + let int = "0"; + let float = "1.0"; + + assert_eq!( + Value::json_to_value(int), + Ok(Value::Number(Number::from(0))) + ); + assert_eq!( + Value::json_to_value(float), + Ok(Value::Number(Number::from(1.0))) + ); + } + + #[test] + fn string() { + let string = r#""string""#; + + assert_eq!( + Value::json_to_value(string), + Ok(Value::String(StringB::from("string"))) + ); + } + + #[test] + fn null() { + let null = "null"; + + assert_eq!(Value::json_to_value(null), Ok(Value::Null)); + } + #[test] + fn boolean() { + let boolean = "true"; + + assert_eq!(Value::json_to_value(boolean), Ok(Value::Boolean(true))); + } + + #[test] + fn all() { + let boolean = Value::json_to_value("true").unwrap(); + let float = Value::json_to_value("3.14").unwrap(); + let json = Value::json_to_value(r#"{"item": 3.14}"#).unwrap(); + let array = Value::json_to_value(r#"[1,2,3]"#).unwrap(); + let null = Value::json_to_value("null").unwrap(); + let string = Value::json_to_value(r#""123""#).unwrap(); + + assert_eq!(boolean, true.to_value()); + assert_eq!(float, 3.14.to_value()); + assert_eq!(json, Value::from(vec![("item", 3.14)])); + assert_eq!(array, vec![1, 2, 3].to_value()); + assert_eq!(null, Value::Null); + assert_eq!(string, "123".to_value()); + } +} diff --git a/valu3/src/parser/mod.rs b/valu3/src/parser/mod.rs index 40eda87..22fdbb3 100644 --- a/valu3/src/parser/mod.rs +++ b/valu3/src/parser/mod.rs @@ -1,156 +1 @@ -use crate::prelude::*; -use pest::Parser; -use std::collections::HashMap; - -#[derive(Parser)] -#[grammar = "parser/value.pest"] -struct JSONParser; - -use pest::iterators::Pair; - -impl Value { - pub fn payload_to_value(str: &str) -> Result { - let value = match JSONParser::parse(Rule::json, str.trim()) { - Ok(mut pairs) => match pairs.next() { - Some(pair) => Self::parse_value(pair), - None => return Err(Error::NonParseble), - }, - Err(msg) => return Err(Error::NonParsebleMsg(msg.to_string())), - }; - Ok(value) - } - - /// Parses a `Pair` from `pest` to a `Value`. - fn parse_value(pair: Pair) -> Self { - match pair.as_rule() { - Rule::object => { - let map = pair - .into_inner() - .map(|pair| { - let mut inner_rules = pair.into_inner(); - let name = inner_rules - .next() - .unwrap() - .into_inner() - .next() - .unwrap() - .as_str() - .to_string(); - let value = Self::parse_value(inner_rules.next().unwrap()); - (name, value) - }) - .collect::>(); - - Self::from(map) - } - Rule::array => Self::from(pair.into_inner().map(Self::parse_value).collect::>()), - Rule::string => Self::from(StringB::from(pair.into_inner().next().unwrap().as_str())), - Rule::number => Self::from(Number::try_from(pair.as_str()).unwrap()), - Rule::boolean => Self::Boolean(pair.as_str().parse().unwrap()), - Rule::null => Self::Null, - Rule::json - | Rule::EOI - | Rule::pair - | Rule::value - | Rule::inner - | Rule::char - | Rule::WHITESPACE => Self::Undefined, - } - } -} - -#[cfg(test)] -mod tests { - use crate::prelude::*; - use std::collections::HashMap; - - #[test] - fn json() { - let raw: &str = "{ - \"test\": true, - \"test2\": \"ok\", - \"test3\": [0, 1] - }"; - - let compare = Value::from({ - let mut map = HashMap::new(); - map.insert("test".to_string(), true.to_value()); - map.insert("test2".to_string(), "ok".to_value()); - map.insert("test3".to_string(), Value::from(vec![0, 1])); - map - }); - - assert_eq!(Value::payload_to_value(raw), Ok(compare)); - } - - #[test] - fn array() { - let raw = "[0, true, null, \"ok\"]"; - - let compare = { - let mut list = Vec::new(); - list.push(Value::Number(Number::from(0))); - list.push(Value::Boolean(true)); - list.push(Value::Null); - list.push(Value::String(StringB::from("ok"))); - Value::from(list) - }; - - assert_eq!(Value::payload_to_value(raw), Ok(compare)); - } - - #[test] - fn number() { - let int = "0"; - let float = "1.0"; - - assert_eq!( - Value::payload_to_value(int), - Ok(Value::Number(Number::from(0))) - ); - assert_eq!( - Value::payload_to_value(float), - Ok(Value::Number(Number::from(1.0))) - ); - } - - #[test] - fn string() { - let string = r#""string""#; - - assert_eq!( - Value::payload_to_value(string), - Ok(Value::String(StringB::from("string"))) - ); - } - - #[test] - fn null() { - let null = "null"; - - assert_eq!(Value::payload_to_value(null), Ok(Value::Null)); - } - #[test] - fn boolean() { - let boolean = "true"; - - assert_eq!(Value::payload_to_value(boolean), Ok(Value::Boolean(true))); - } - - #[test] - fn all() { - let boolean = Value::payload_to_value("true").unwrap(); - let float = Value::payload_to_value("3.14").unwrap(); - let json = Value::payload_to_value(r#"{"item": 3.14}"#).unwrap(); - let array = Value::payload_to_value(r#"[1,2,3]"#).unwrap(); - let null = Value::payload_to_value("null").unwrap(); - let string = Value::payload_to_value(r#""123""#).unwrap(); - - assert_eq!(boolean, true.to_value()); - assert_eq!(float, 3.14.to_value()); - assert_eq!(json, Value::from(vec![("item", 3.14)])); - assert_eq!(array, vec![1, 2, 3].to_value()); - assert_eq!(null, Value::Null); - assert_eq!(string, "123".to_value()); - } -} +pub mod json; diff --git a/valu3/src/parser/value.pest b/valu3/src/parser/value.pest deleted file mode 100644 index e613c04..0000000 --- a/valu3/src/parser/value.pest +++ /dev/null @@ -1,27 +0,0 @@ -json = _{ SOI ~ (object | array | string | number | boolean | null) ~ EOI } -WHITESPACE = _{ " " | "\t" | "\r" | "\n" } -object = { - ("#{" | "{") ~ "}" | - ("#{" | "{") ~ pair ~ ("," ~ pair)* ~ "}" -} -pair = { string ~ ":" ~ value } -array = { - "[" ~ "]" | - "[" ~ value ~ ("," ~ value)* ~ "]" -} -value = _{ object | array | string | number | boolean | null } -boolean = { "true" | "false" } -null = { "null" } -string = ${ "\"" ~ inner ~ "\"" } -inner = @{ char* } -char = { - !("\"" | "\\") ~ ANY - | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") - | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) -} -number = @{ - "-"? - ~ ("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) - ~ ("." ~ ASCII_DIGIT*)? - ~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT+)? -} diff --git a/valu3/src/serde_value/error.rs b/valu3/src/serde_value/error.rs deleted file mode 100644 index 7018dc6..0000000 --- a/valu3/src/serde_value/error.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std; -use std::fmt::{self, Display}; - -use serde::{de, ser}; - -pub type Result = std::result::Result; - -// This is a bare-bones implementation. A real library would provide additional -// information in its error type, for example the line and column at which the -// error occurred, the byte offset into the input, or the current key being -// processed. -#[derive(Debug)] -pub enum Error { - Message(String), -} - -impl ser::Error for Error { - fn custom(msg: T) -> Self { - Error::Message(msg.to_string()) - } -} - -impl de::Error for Error { - fn custom(msg: T) -> Self { - Error::Message(msg.to_string()) - } -} - -impl Display for Error { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::Message(msg) => formatter.write_str(msg), - } - } -} - -impl std::error::Error for Error {} diff --git a/valu3/src/serde_value/mod.rs b/valu3/src/serde_value/mod.rs index d319b72..aa85777 100644 --- a/valu3/src/serde_value/mod.rs +++ b/valu3/src/serde_value/mod.rs @@ -1,2 +1,164 @@ pub mod de; pub mod ser; + +#[cfg(test)] +mod tests { + use crate::prelude::*; + use std::collections::HashMap; + + #[test] + fn test_serde_number() { + let value = Value::from(42u64); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "42"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let value = Value::from(3.14); + let serialized = serde_json::to_string(&value).unwrap(); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let value = Value::from(-3.14); + let serialized = serde_json::to_string(&value).unwrap(); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let value = Value::from(3.14e10); + let serialized = serde_json::to_string(&value).unwrap(); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_serde_string() { + let value = Value::from("hello"); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "\"hello\""); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_serde_array() { + let value = Value::from(vec![ + Value::from(1u64), + Value::from(2u64), + Value::from(3u64), + ]); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "[1,2,3]"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_serde_object() { + let mut object = HashMap::new(); + object.insert("a", Value::from(1u64)); + object.insert("b", Value::from(2u64)); + object.insert("c", Value::from(3u64)); + let value = Value::from(object); + let serialized = serde_json::to_string(&value).unwrap(); + + let cases = [ + r#"{"a":1,"b":2,"c":3}"#, + r#"{"a":1,"c":3,"b":2}"#, + r#"{"b":2,"a":1,"c":3}"#, + r#"{"b":2,"c":3,"a":1}"#, + r#"{"c":3,"b":2,"a":1}"#, + r#"{"c":3,"a":1,"b":2}"#, + ]; + assert_eq!(cases.contains(&serialized.as_str()), true); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_serde_bool() { + let value = Value::from(true); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "true"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_serde_null() { + let value = Value::Null; + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "null"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + } + + #[test] + fn test_serde_value() { + let value = Value::from(42u64); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "42"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let value = Value::from("hello"); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "\"hello\""); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let value = Value::from(vec![ + Value::from(1u64), + Value::from(2u64), + Value::from(3u64), + ]); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "[1,2,3]"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let mut object = HashMap::new(); + object.insert("a", Value::from(1u64)); + object.insert("b", Value::from(2u64)); + object.insert("c", Value::from(3u64)); + let value = Value::from(object); + let serialized = serde_json::to_string(&value).unwrap(); + let cases = [ + r#"{"a":1,"b":2,"c":3}"#, + r#"{"a":1,"c":3,"b":2}"#, + r#"{"b":2,"a":1,"c":3}"#, + r#"{"b":2,"c":3,"a":1}"#, + r#"{"c":3,"b":2,"a":1}"#, + r#"{"c":3,"a":1,"b":2}"#, + ]; + assert_eq!(cases.contains(&serialized.as_str()), true); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let value = Value::from(true); + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "true"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + + let value = Value::Null; + let serialized = serde_json::to_string(&value).unwrap(); + assert_eq!(serialized, "null"); + + let deserialized: Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, value); + } +} diff --git a/valu3/src/to/json.rs b/valu3/src/to/json.rs index e78e301..90ce3fe 100644 --- a/valu3/src/to/json.rs +++ b/valu3/src/to/json.rs @@ -132,9 +132,9 @@ mod tests { #[test] fn it_should_convert_a_value_to_json_string() { - let value_str = Value::payload_to_value("{\"name\":\"John Doe\"}").unwrap(); - let value_number = Value::payload_to_value("{\"age\":30}").unwrap(); - let value_boolean = Value::payload_to_value("{\"is_active\":true}").unwrap(); + let value_str = Value::json_to_value("{\"name\":\"John Doe\"}").unwrap(); + let value_number = Value::json_to_value("{\"age\":30}").unwrap(); + let value_boolean = Value::json_to_value("{\"is_active\":true}").unwrap(); assert_eq!( "{\n\t\"name\": \"John Doe\"\n}", value_str.to_json(JsonMode::Indented) diff --git a/valu3_derive/Cargo.toml b/valu3_derive/Cargo.toml index 12a62d2..66fe8a2 100644 --- a/valu3_derive/Cargo.toml +++ b/valu3_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "valu3-derive" -version = "0.6.6" +version = "0.6.7" edition = "2021" license = "Apache-2.0" readme = "crates-io.md" diff --git a/valu3_derive/README.md b/valu3_derive/README.md index cdb1e0d..52eb4bb 100644 --- a/valu3_derive/README.md +++ b/valu3_derive/README.md @@ -3,11 +3,11 @@ Welcome to **Valu3** - the ultimate, flexible, and powerful library for manipulating diverse data types in your Rust projects. Say goodbye to the complexity of handling numbers, strings, arrays, objects, and datetime values. Valu3 is here to make your life easier! -[![crates.io](https://img.shields.io/crates/v/valu3?label=0.6.6)](https://crates.io/crates/valu3) -[![Documentation](https://docs.rs/valu3/badge.svg?version=0.6.6)](https://docs.rs/valu3/0.6.6) +[![crates.io](https://img.shields.io/crates/v/valu3?label=0.6.7)](https://crates.io/crates/valu3) +[![Documentation](https://docs.rs/valu3/badge.svg?version=0.6.7)](https://docs.rs/valu3/0.6.7) ![MSRV](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) ![Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/valu3/0.6.6/status.svg)](https://deps.rs/crate/valu3/0.6.6) +[![Dependency Status](https://deps.rs/crate/valu3/0.6.7/status.svg)](https://deps.rs/crate/valu3/0.6.7) ![Main test](https://github.com/lowcarboncode/valu3/actions/workflows/main-test.yml/badge.svg) [![codecov](https://codecov.io/gh/lowcarboncode/valu3/branch/master/graph/badge.svg)](https://codecov.io/gh/lowcarboncode/valu3) ![downloads](https://img.shields.io/crates/d/valu3.svg)