From e3f41d7e0dfe37ad6efb57d44895e7c4cd2cf624 Mon Sep 17 00:00:00 2001 From: trivernis Date: Wed, 12 Oct 2022 22:09:33 +0200 Subject: [PATCH] Add IntoJson trait to convert a rusty value into json --- .helix/languages.toml | 5 + Cargo.toml | 6 +- README.md | 3 +- derive/src/lib.rs | 3 +- src/formats/into_json.rs | 307 +++++++++++++++++++++++++++++++++++++++ src/formats/mod.rs | 4 + src/lib.rs | 2 + tests/enums.rs | 1 + tests/structs.rs | 1 + 9 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 .helix/languages.toml create mode 100644 src/formats/into_json.rs create mode 100644 src/formats/mod.rs diff --git a/.helix/languages.toml b/.helix/languages.toml new file mode 100644 index 0000000..952c1be --- /dev/null +++ b/.helix/languages.toml @@ -0,0 +1,5 @@ +[[language]] +name = "rust" + +[language.config] +cargo = { features = "all" } diff --git a/Cargo.toml b/Cargo.toml index f7da9af..4316978 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,16 @@ members = [".", "derive"] [package] name = "rusty-value" -version = "0.4.2" +version = "0.5.0" edition = "2021" license = "Apache-2.0" repository = "https://github.com/Trivernis/rusty-value" description = "Create a generic inspectable value from any rust type" authors = ["trivernis "] +[dependencies] +serde_json = { version = "1.0.85", default-features = false, optional = true, features = ["std"]} + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies.rusty-value-derive] @@ -20,6 +23,7 @@ optional = true [features] default = [] derive = ["rusty-value-derive"] +json = ["serde_json"] [dev-dependencies.rusty-value-derive] path = "./derive" diff --git a/README.md b/README.md index dcc2984..020c4e1 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,7 @@ The trait `RustyValue` allows one to create a `rusty_value::Value` for any type that implements it. This trait can be derived if the `derive` **feature** is enabled. ```rust - -use rusty_value::{RustyValue, Value}; +use rusty_value::*; #[derive(RustyValue)] struct MyStruct { diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5d7f0fb..791b8dd 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -204,7 +204,8 @@ fn add_rusty_bound(generics: &Generics) -> WhereClause { fn get_rusty_value_crate() -> proc_macro2::TokenStream { use proc_macro_crate::{crate_name, FoundCrate}; match crate_name("rusty_value") { - Ok(FoundCrate::Itself) | Err(_) => quote!(rusty_value), + Ok(FoundCrate::Itself) => quote!(rusty_value), + Err(_) => quote!(crate), Ok(FoundCrate::Name(name)) => quote!(#name), } } diff --git a/src/formats/into_json.rs b/src/formats/into_json.rs new file mode 100644 index 0000000..36c4ac7 --- /dev/null +++ b/src/formats/into_json.rs @@ -0,0 +1,307 @@ +use serde_json::{Number, Value}; +use std::string::ToString; + +use crate::{Enum, HashableValue, Primitive, RustyValue, Struct}; + +/// Options for how to represent certain rust types +/// as JSON +#[derive(Clone, Debug, RustyValue, Default)] +pub struct IntoJsonOptions { + pub enum_repr: EnumRepr, +} + +/// Controls how enums should be represented +/// This works similarly to serde except that internal tagging isn't supported +/// except that internal tagging isn't supported +#[derive(Clone, Debug, RustyValue)] +pub enum EnumRepr { + Untagged, + ExternallyTagged, + AdjacentlyTagged { + type_field: String, + value_field: String, + }, +} + +impl Default for EnumRepr { + fn default() -> Self { + Self::ExternallyTagged + } +} + +/// Trait to convert a value into a json value +pub trait IntoJson { + /// Converts the value into a json value with default options + fn into_json(self) -> Value; + /// Converts the value into a json value with the given options + fn into_json_with_options(self, options: &IntoJsonOptions) -> Value; +} + +trait RustyIntoJson { + /// Converts the value into a json value with default options + fn into_json(self) -> Value; + /// Converts the value into a json value with the given options + fn into_json_with_options(self, options: &IntoJsonOptions) -> Value; +} + +impl RustyIntoJson for crate::Value { + #[inline] + fn into_json(self) -> Value { + self.into_json_with_options(&IntoJsonOptions::default()) + } + + fn into_json_with_options(self, opt: &IntoJsonOptions) -> Value { + match self { + crate::Value::Primitive(p) => p.into_json_with_options(opt), + crate::Value::Struct(s) => s.into_json_with_options(opt), + crate::Value::Enum(e) => e.into_json_with_options(opt), + crate::Value::Map(m) => Value::Object( + m.into_iter() + .map(|(k, v)| (hashable_to_string(k), v.into_json_with_options(opt))) + .collect(), + ), + crate::Value::List(l) => Value::Array( + l.into_iter() + .map(|v| v.into_json_with_options(opt)) + .collect(), + ), + crate::Value::None => Value::Null, + } + } +} + +impl IntoJson for Primitive { + fn into_json(self) -> Value { + match self { + Primitive::Integer(i) => match i { + crate::Integer::USize(n) => Value::Number(n.into()), + crate::Integer::ISize(n) => Value::Number(n.into()), + crate::Integer::U8(n) => Value::Number(n.into()), + crate::Integer::I8(n) => Value::Number(n.into()), + crate::Integer::U16(n) => Value::Number(n.into()), + crate::Integer::I16(n) => Value::Number(n.into()), + crate::Integer::U32(n) => Value::Number(n.into()), + crate::Integer::I32(n) => Value::Number(n.into()), + crate::Integer::U64(n) => Value::Number(n.into()), + crate::Integer::I64(n) => Value::Number(n.into()), + crate::Integer::U128(n) => Value::Array(vec![ + ((n >> 64) as u64).into(), + ((n & 0xFFFFFFFFFFFFFFFF) as u64).into(), + ]), + crate::Integer::I128(n) => Value::Array(vec![ + ((n >> 64) as i64).into(), + ((n & 0xFFFFFFFFFFFFFFFF) as u64).into(), + ]), + }, + Primitive::Float(f) => match f { + crate::Float::F32(f) => Number::from_f64(f as f64) + .map(Value::Number) + .unwrap_or(Value::Null), + crate::Float::F64(f) => Number::from_f64(f) + .map(Value::Number) + .unwrap_or(Value::Null), + }, + Primitive::String(s) => Value::String(s), + Primitive::OsString(o) => Value::String(o.to_string_lossy().into_owned()), + Primitive::Char(c) => Value::String(c.to_string()), + Primitive::Bool(b) => Value::Bool(b), + } + } + + #[inline] + fn into_json_with_options(self, _options: &IntoJsonOptions) -> Value { + self.into_json() + } +} + +impl IntoJson for Enum { + #[inline] + fn into_json(self) -> Value { + self.into_json_with_options(&IntoJsonOptions::default()) + } + fn into_json_with_options(self, opt: &IntoJsonOptions) -> Value { + let value = match self.fields { + crate::Fields::Named(n) => Value::Object( + n.into_iter() + .map(|(k, v)| (k, v.into_json_with_options(opt))) + .collect(), + ), + crate::Fields::Unnamed(mut u) => { + if u.len() == 1 { + u.remove(0).into_json_with_options(opt) + } else { + Value::Array( + u.into_iter() + .map(|v| v.into_json_with_options(opt)) + .collect(), + ) + } + } + crate::Fields::Unit => Value::String(self.variant.clone()), + }; + match &opt.enum_repr { + EnumRepr::Untagged => value, + EnumRepr::ExternallyTagged => { + Value::Object([(self.variant, value)].into_iter().collect()) + } + EnumRepr::AdjacentlyTagged { + type_field, + value_field, + } => Value::Object( + [ + (type_field.to_owned(), Value::String(self.variant)), + (value_field.to_owned(), value), + ] + .into_iter() + .collect(), + ), + } + } +} + +impl IntoJson for Struct { + #[inline] + fn into_json(self) -> Value { + self.into_json_with_options(&IntoJsonOptions::default()) + } + fn into_json_with_options(self, opt: &IntoJsonOptions) -> Value { + match self.fields { + crate::Fields::Named(n) => Value::Object( + n.into_iter() + .map(|(k, v)| (k, v.into_json_with_options(opt))) + .collect(), + ), + crate::Fields::Unnamed(mut u) => { + if u.len() == 1 { + u.remove(0).into_json_with_options(opt) + } else { + Value::Array( + u.into_iter() + .map(|v| v.into_json_with_options(opt)) + .collect(), + ) + } + } + crate::Fields::Unit => Value::String(self.name), + } + } +} + +impl IntoJson for R { + #[inline] + fn into_json(self) -> Value { + self.into_json_with_options(&IntoJsonOptions::default()) + } + + #[inline] + fn into_json_with_options(self, opt: &IntoJsonOptions) -> Value { + self.into_rusty_value().into_json_with_options(opt) + } +} + +fn hashable_to_string(hashable: HashableValue) -> String { + match hashable { + HashableValue::Primitive(p) => p.to_string(), + HashableValue::List(l) => l + .into_iter() + .map(hashable_to_string) + .collect::>() + .join(","), + HashableValue::None => String::new(), + } +} + +#[cfg(test)] +mod test { + #![allow(unused)] + use serde_json::json; + + use crate as rusty_value; + use crate::into_json::IntoJsonOptions; + use crate::RustyValue; + + use super::IntoJson; + + #[test] + fn it_serializes_primitives() { + assert_eq!(u8::MAX.into_json(), json!(u8::MAX)); + assert_eq!(i8::MIN.into_json(), json!(i8::MIN)); + assert_eq!(u16::MAX.into_json(), json!(u16::MAX)); + assert_eq!(i16::MIN.into_json(), json!(i16::MIN)); + assert_eq!(u32::MAX.into_json(), json!(u32::MAX)); + assert_eq!(i32::MIN.into_json(), json!(i32::MIN)); + assert_eq!(u64::MAX.into_json(), json!(u64::MAX)); + assert_eq!(i64::MIN.into_json(), json!(i64::MIN)); + assert_eq!(u128::MAX.into_json(), json!([u64::MAX, u64::MAX])); + assert_eq!(i128::MIN.into_json(), json!([i64::MIN, 0])); + } + + #[derive(Default, RustyValue)] + struct TestStruct { + foo: String, + bar: u8, + } + + #[test] + fn it_serializes_structs() { + let val = TestStruct::default(); + let value = val.into_json(); + + assert!(value.is_object()); + } + + #[derive(RustyValue)] + enum TestEnum { + Foo, + Bar(String), + } + + #[test] + fn it_serializes_unit_enums_untagged() { + let val = TestEnum::Foo; + let value = val.into_json_with_options(&IntoJsonOptions { + enum_repr: rusty_value::into_json::EnumRepr::Untagged, + }); + + assert!(value.is_string()); + assert_eq!(value.as_str(), Some("Foo")) + } + + #[test] + fn it_serializes_struct_enums_adjacently_tagged() { + let val = TestEnum::Bar(String::new()); + let value = val.into_json_with_options(&IntoJsonOptions { + enum_repr: rusty_value::into_json::EnumRepr::AdjacentlyTagged { + type_field: "type".into(), + value_field: "value".into(), + }, + }); + println!("{}", value.to_string()); + + assert!(value.is_object()); + assert_eq!(value.get("type").unwrap().as_str(), Some("Bar")); + assert!(value.get("value").unwrap().is_string()); + } + + #[test] + fn it_serializes_struct_enums_untagged() { + let val = TestEnum::Bar(String::new()); + let value = val.into_json_with_options(&IntoJsonOptions { + enum_repr: rusty_value::into_json::EnumRepr::Untagged, + }); + + assert!(value.is_string()); + assert_eq!(value.as_str(), Some("")); + } + + #[test] + fn it_serializes_struct_enums_externally_tagged() { + let val = TestEnum::Bar(String::new()); + let value = val.into_json_with_options(&IntoJsonOptions { + enum_repr: rusty_value::into_json::EnumRepr::ExternallyTagged, + }); + + assert!(value.is_object()); + assert!(value.get("Bar").unwrap().is_string()); + } +} diff --git a/src/formats/mod.rs b/src/formats/mod.rs new file mode 100644 index 0000000..7b80072 --- /dev/null +++ b/src/formats/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "json")] +#[cfg_attr(docsrs, doc(cfg(feature = "json")))] +/// Implements converting the [crate::Value] into a [serde_json::Value]. +pub mod into_json; diff --git a/src/lib.rs b/src/lib.rs index 00b3a8f..82f17f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ #![doc=include_str!("../README.md")] +pub(crate) mod formats; pub(crate) mod value; pub(crate) mod value_trait; +pub use formats::*; pub use value::*; pub use value_trait::*; diff --git a/tests/enums.rs b/tests/enums.rs index 87094b2..cfa0299 100644 --- a/tests/enums.rs +++ b/tests/enums.rs @@ -1,3 +1,4 @@ +use rusty_value::*; use rusty_value::{Fields, RustyValue, Value}; #[allow(dead_code)] diff --git a/tests/structs.rs b/tests/structs.rs index c2bb41c..eb471b6 100644 --- a/tests/structs.rs +++ b/tests/structs.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use rusty_value::*; use rusty_value::{Fields, RustyValue, Value}; #[derive(RustyValue)]