Skip to content

Commit

Permalink
feat: Json column type
Browse files Browse the repository at this point in the history
  • Loading branch information
Fyko committed Dec 14, 2023
1 parent 582fb21 commit 5d7c906
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.features": "all"
}
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions scyllax/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
142 changes: 142 additions & 0 deletions scyllax/src/json.rs
Original file line number Diff line number Diff line change
@@ -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<Json>` for `prost_types::Struct` and vice versa.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Json(pub serde_json::Map<String, serde_json::Value>);

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<u8>) -> Result<(), scylla::frame::value::ValueTooBig> {
let data = serde_json::to_vec(&self.0).unwrap();
<Vec<u8> as scylla::frame::value::Value>::serialize(&data, buf)
}
}

impl FromCqlVal<CqlValue> for Json {
fn from_cql(cql_val: CqlValue) -> Result<Self, FromCqlValError> {
let data = <String as FromCqlVal<CqlValue>>::from_cql(cql_val)?;

serde_json::from_str(&data)
.map(Json)
.ok()
.ok_or(FromCqlValError::BadCqlType)
}
}

#[cfg(feature = "grpc")]
impl From<Json> for prost_types::Struct {
fn from(json: Json) -> Self {
fn to_struct(json: serde_json::Map<String, serde_json::Value>) -> 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 {

Check failure on line 52 in scyllax/src/json.rs

View workflow job for this annotation

GitHub Actions / Check Suite

Diff in /home/runner/work/scyllax/scyllax/scyllax/src/json.rs
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<prost_types::Struct> for Json {
fn from(value: prost_types::Struct) -> Self {
fn from_struct(struct_: prost_types::Struct) -> serde_json::Map<String, serde_json::Value> {
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() {

Check failure on line 84 in scyllax/src/json.rs

View workflow job for this annotation

GitHub Actions / Check Suite

Diff in /home/runner/work/scyllax/scyllax/scyllax/src/json.rs
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<String, serde_json::Value> {
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());
}
}
2 changes: 2 additions & 0 deletions scyllax/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 5d7c906

Please sign in to comment.