From 5d7c9065fca56698825ee685ac973f5ce4c9bdd6 Mon Sep 17 00:00:00 2001 From: Carter Himmel Date: Wed, 13 Dec 2023 23:25:39 -0700 Subject: [PATCH] feat: `Json` column type --- .vscode/settings.json | 3 + Cargo.lock | 32 ++++++++++ scyllax/Cargo.toml | 8 +++ scyllax/src/json.rs | 142 ++++++++++++++++++++++++++++++++++++++++++ scyllax/src/lib.rs | 2 + 5 files changed, 187 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 scyllax/src/json.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2decb06 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.features": "all" +} diff --git a/Cargo.lock b/Cargo.lock index fcc21cf..409f92d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,6 +1180,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.33" diff --git a/scyllax/Cargo.toml b/scyllax/Cargo.toml index 3ef6b49..9bd907f 100644 --- a/scyllax/Cargo.toml +++ b/scyllax/Cargo.toml @@ -21,3 +21,11 @@ thiserror = "1" tokio.workspace = true tracing.workspace = true uuid.workspace = true +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +prost-types = { version = "0.12", optional = true } + +[features] +default = [] +json = ["serde_json", "serde"] +grpc = ["prost-types"] diff --git a/scyllax/src/json.rs b/scyllax/src/json.rs new file mode 100644 index 0000000..87bc848 --- /dev/null +++ b/scyllax/src/json.rs @@ -0,0 +1,142 @@ +use scylla::cql_to_rust::{FromCqlVal, FromCqlValError}; +use scylla::frame::response::result::CqlValue; +use serde::{Deserialize, Serialize}; + +/// An implementation of a JSON type for ScyllaDB. +/// +/// Also implements `From` for `prost_types::Struct` and vice versa. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Json(pub serde_json::Map); + +impl std::fmt::Debug for Json { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl scylla::frame::value::Value for Json { + fn serialize(&self, buf: &mut Vec) -> Result<(), scylla::frame::value::ValueTooBig> { + let data = serde_json::to_vec(&self.0).unwrap(); + as scylla::frame::value::Value>::serialize(&data, buf) + } +} + +impl FromCqlVal for Json { + fn from_cql(cql_val: CqlValue) -> Result { + let data = >::from_cql(cql_val)?; + + serde_json::from_str(&data) + .map(Json) + .ok() + .ok_or(FromCqlValError::BadCqlType) + } +} + +#[cfg(feature = "grpc")] +impl From for prost_types::Struct { + fn from(json: Json) -> Self { + fn to_struct(json: serde_json::Map) -> prost_types::Struct { + prost_types::Struct { + fields: json + .into_iter() + .map(|(k, v)| (k, serde_json_to_prost(v))) + .collect(), + } + } + + fn serde_json_to_prost(json: serde_json::Value) -> prost_types::Value { + use prost_types::value::Kind; + use serde_json::Value; + + prost_types::Value { + kind: Some(match json { + Value::Null => Kind::NullValue(0 /* wot? */), + Value::Bool(v) => Kind::BoolValue(v), + Value::Number(n) => Kind::NumberValue(n.as_f64().expect("Non-f64-representable number")), + Value::String(s) => Kind::StringValue(s), + Value::Array(v) => Kind::ListValue(prost_types::ListValue { + values: v.into_iter().map(serde_json_to_prost).collect(), + }), + Value::Object(v) => Kind::StructValue(to_struct(v)), + }), + } + } + + to_struct(json.0) + } +} + +#[cfg(feature = "grpc")] +impl From for Json { + fn from(value: prost_types::Struct) -> Self { + fn from_struct(struct_: prost_types::Struct) -> serde_json::Map { + struct_ + .fields + .into_iter() + .map(|(k, v)| (k, prost_to_serde_json(v))) + .collect() + } + + fn prost_to_serde_json(value: prost_types::Value) -> serde_json::Value { + use prost_types::value::Kind; + use serde_json::Value; + + match value.kind.unwrap() { + Kind::NullValue(_) => Value::Null, + Kind::BoolValue(v) => Value::Bool(v), + Kind::NumberValue(n) => { + Value::Number(serde_json::Number::from_f64(n).expect("Non-f64-representable number")) + } + Kind::StringValue(s) => Value::String(s), + Kind::ListValue(v) => Value::Array(v.values.into_iter().map(prost_to_serde_json).collect()), + Kind::StructValue(v) => Value::Object(from_struct(v)), + } + } + + Json(from_struct(value)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn create_map() -> serde_json::Map { + let mut map = serde_json::Map::new(); + map.insert("a".to_string(), json!(1)); + map.insert("b".to_string(), json!("2")); + map.insert("c".to_string(), json!([1, 2, 3])); + map.insert("d".to_string(), json!({"e": "f"})); + map + } + + #[test] + fn test_json() { + let json = Json(create_map()); + let data = serde_json::to_string(&json).unwrap(); + assert_eq!(data, r#"{"a":1,"b":"2","c":[1,2,3],"d":{"e":"f"}}"#); + } + + #[test] + fn test_json_from_cql() { + let json = Json(create_map()); + let cql_val = CqlValue::Text(serde_json::to_string(&json).unwrap()); + let json2 = Json::from_cql(cql_val).unwrap(); + assert_eq!(json, json2); + } + + #[test] + fn test_json_from_cql_bad_type() { + let cql_val = CqlValue::Int(1); + let json = Json::from_cql(cql_val); + assert!(json.is_err()); + } + + #[test] + fn test_json_from_cql_bad_json() { + let cql_val = CqlValue::Text("bad json".to_string()); + let json = Json::from_cql(cql_val); + assert!(json.is_err()); + } +} diff --git a/scyllax/src/lib.rs b/scyllax/src/lib.rs index d58b90f..4301f3d 100644 --- a/scyllax/src/lib.rs +++ b/scyllax/src/lib.rs @@ -40,6 +40,8 @@ pub mod collection; pub mod entity; pub mod error; pub mod executor; +#[cfg(feature = "json")] +pub mod json; pub mod maybe_unset; // mod playground; pub mod prelude;