Skip to content

Commit

Permalink
Support BTreeMap for feature json (#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
jayvdb authored Nov 10, 2023
1 parent bb4a560 commit 8f4f396
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 15 deletions.
90 changes: 83 additions & 7 deletions butane/tests/json.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![allow(clippy::disallowed_names)]

use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};

use butane::model;
use butane::prelude::*;
Expand Down Expand Up @@ -28,7 +28,7 @@ impl FooJJ {
}

fn json_null(conn: Connection) {
//create
// create
let id = 4;
let mut foo = FooJJ::new(id);
foo.save(&conn).unwrap();
Expand All @@ -46,7 +46,7 @@ fn json_null(conn: Connection) {
testall!(json_null);

fn basic_json(conn: Connection) {
//create
// create
let id = 4;
let mut foo = FooJJ::new(id);
let data = r#"
Expand Down Expand Up @@ -83,15 +83,15 @@ struct FooHH {
}
impl FooHH {
fn new(id: i64) -> Self {
FooHH {
Self {
id,
val: HashMap::<String, String>::default(),
bar: 0,
}
}
}
fn basic_hashmap(conn: Connection) {
//create
// create
let id = 4;
let mut foo = FooHH::new(id);
let mut data = HashMap::<String, String>::new();
Expand All @@ -112,6 +112,82 @@ fn basic_hashmap(conn: Connection) {
}
testall!(basic_hashmap);

#[model]
#[derive(PartialEq, Eq, Debug, Clone)]
struct FooFullPrefixHashMap {
id: i64,
val: std::collections::HashMap<String, String>,
bar: u32,
}
impl FooFullPrefixHashMap {
fn new(id: i64) -> Self {
Self {
id,
val: HashMap::<String, String>::default(),
bar: 0,
}
}
}
fn basic_hashmap_full_prefix(conn: Connection) {
// create
let id = 4;
let mut foo = FooFullPrefixHashMap::new(id);
let mut data = HashMap::<String, String>::new();
data.insert("a".to_string(), "1".to_string());

foo.val = data;
foo.save(&conn).unwrap();

// read
let mut foo2 = FooFullPrefixHashMap::get(&conn, id).unwrap();
assert_eq!(foo, foo2);

// update
foo2.bar = 43;
foo2.save(&conn).unwrap();
let foo3 = FooFullPrefixHashMap::get(&conn, id).unwrap();
assert_eq!(foo2, foo3);
}
testall!(basic_hashmap_full_prefix);

#[model]
#[derive(PartialEq, Eq, Debug, Clone)]
struct FooBTreeMap {
id: i64,
val: BTreeMap<String, String>,
bar: u32,
}
impl FooBTreeMap {
fn new(id: i64) -> Self {
Self {
id,
val: BTreeMap::<String, String>::default(),
bar: 0,
}
}
}
fn basic_btreemap(conn: Connection) {
// create
let id = 4;
let mut foo = FooBTreeMap::new(id);
let mut data = BTreeMap::<String, String>::new();
data.insert("a".to_string(), "1".to_string());

foo.val = data;
foo.save(&conn).unwrap();

// read
let mut foo2 = FooBTreeMap::get(&conn, id).unwrap();
assert_eq!(foo, foo2);

// update
foo2.bar = 43;
foo2.save(&conn).unwrap();
let foo3 = FooBTreeMap::get(&conn, id).unwrap();
assert_eq!(foo2, foo3);
}
testall!(basic_btreemap);

#[derive(PartialEq, Eq, Debug, Default, Clone, serde::Deserialize, serde::Serialize)]
struct HashedObject {
x: i64,
Expand All @@ -135,7 +211,7 @@ impl FooHHO {
}
}
fn hashmap_with_object_values(conn: Connection) {
//create
// create
let id = 4;
let mut foo = FooHHO::new(id);
let mut data = HashMap::<String, HashedObject>::new();
Expand Down Expand Up @@ -181,7 +257,7 @@ impl OuterFoo {
}

fn inline_json(conn: Connection) {
//create
// create
let id = 4;
let mut foo = OuterFoo::new(id, InlineFoo::new(4, 8));
foo.save(&conn).unwrap();
Expand Down
4 changes: 2 additions & 2 deletions butane_core/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use syn::{
MetaNameValue,
};

const OPTION_TYNAMES: [&str; 2] = ["Option", "std::option::Option"];
const OPTION_TYNAMES: [&str; 3] = ["Option", "option::Option", "std::option::Option"];
const MANY_TYNAMES: [&str; 2] = ["Many", "butane::Many"];
const FKEY_TYNAMES: [&str; 2] = ["ForeignKey", "butane::ForeignKey"];
const AUTOPK_TYNAMES: [&str; 2] = ["AutoPk", "butane::AutoPk"];
Expand Down Expand Up @@ -404,7 +404,7 @@ pub fn get_deferred_sql_type(ty: &syn::Type) -> DeferredSqlType {
.or_else(|| get_autopk_sql_type(ty))
.unwrap_or_else(|| {
DeferredSqlType::Deferred(TypeKey::CustomType(
ty.clone().into_token_stream().to_string(),
ty.clone().into_token_stream().to_string().replace(' ', ""),
))
})
}
Expand Down
31 changes: 29 additions & 2 deletions butane_core/src/migrations/adb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
//! CLI tool, there is no need to use this module. Even if applying
//! migrations without this tool, you are unlikely to need this module.
#[cfg(feature = "json")]
use once_cell::sync::Lazy;

use crate::{Error, Result, SqlType, SqlVal};
use serde::{de::Deserializer, de::Visitor, ser::Serializer, Deserialize, Serialize};
use std::cmp::Ordering;
Expand All @@ -10,6 +13,27 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
/// Suffix added to [`crate::many::Many`] tables.
pub const MANY_SUFFIX: &str = "_Many";

#[cfg(feature = "json")]
static JSON_MAP_PREFIXES: Lazy<Vec<String>> = Lazy::new(|| {
let map_type_names: [&str; 6] = [
"HashMap",
"collections::HashMap",
"std::collections::HashMap",
"BTreeMap",
"collections::BTreeMap",
"std::collections::BTreeMap",
];
let string_tynames: [&str; 3] = ["String", "string::String", "std::string::String"];

let mut prefixes = Vec::new();
for map_type_name in map_type_names {
for string_type_name in string_tynames {
prefixes.push(format!("{map_type_name}<{string_type_name},"));
}
}
prefixes
});

/// Identifier for a type as used in a database column. Supports both
/// [`SqlType`] and identifiers known only by name.
/// The latter is used for custom types. `SqlType::Custom` cannot easily be used
Expand Down Expand Up @@ -120,10 +144,13 @@ impl TypeResolver {
fn find_type(&self, key: &TypeKey) -> Option<TypeIdentifier> {
#[cfg(feature = "json")]
if let TypeKey::CustomType(ct) = key {
if ct.starts_with("HashMap < String,") {
return Some(TypeIdentifier::from(SqlType::Json));
for prefix in JSON_MAP_PREFIXES.iter() {
if ct.starts_with(prefix) {
return Some(TypeIdentifier::from(SqlType::Json));
}
}
}

self.types.get(key).cloned()
}
fn insert(&mut self, key: TypeKey, ty: TypeIdentifier) -> bool {
Expand Down
42 changes: 41 additions & 1 deletion butane_core/src/sqlval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{DataObject, Error::CannotConvertSqlVal, Result, SqlType};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
#[cfg(feature = "json")]
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::fmt;

#[cfg(feature = "pg")]
Expand Down Expand Up @@ -485,6 +485,7 @@ impl FieldType for serde_json::Value {
#[cfg(feature = "json")]
impl PrimaryKeyType for serde_json::Value {}

// JSON HashMap support
#[cfg(feature = "json")]
impl<T> ToSql for HashMap<String, T>
where
Expand Down Expand Up @@ -513,6 +514,7 @@ where
sql_conv_err!(val, Json)
}
}

#[cfg(feature = "json")]
impl<T> FieldType for HashMap<String, T>
where
Expand All @@ -522,6 +524,44 @@ where
const SQLTYPE: SqlType = SqlType::Json;
}

// JSON BTreeMap support
#[cfg(feature = "json")]
impl<T> ToSql for BTreeMap<String, T>
where
T: Clone + PartialEq + Serialize,
{
fn to_sql(&self) -> SqlVal {
self.to_sql_ref().into()
}
fn to_sql_ref(&self) -> SqlValRef<'_> {
SqlValRef::Json(serde_json::to_value(self).unwrap())
}
}

#[cfg(feature = "json")]
impl<T> FromSql for BTreeMap<String, T>
where
T: Clone + PartialEq + for<'a> serde::Deserialize<'a>,
{
fn from_sql_ref(val: SqlValRef) -> Result<Self> {
if let SqlValRef::Json(serde_json::Value::Object(m)) = val {
return Ok(m
.iter()
.map(|(k, v)| (k.to_owned(), T::deserialize(v).unwrap()))
.collect::<BTreeMap<String, T>>());
}
sql_conv_err!(val, Json)
}
}
#[cfg(feature = "json")]
impl<T> FieldType for BTreeMap<String, T>
where
T: Clone + PartialEq + for<'a> serde::Deserialize<'a> + serde::Serialize,
{
type RefType = Self;
const SQLTYPE: SqlType = SqlType::Json;
}

#[cfg(feature = "datetime")]
impl_basic_from_sql!(NaiveDateTime, Timestamp, Timestamp);
#[cfg(feature = "datetime")]
Expand Down
6 changes: 3 additions & 3 deletions butane_core/tests/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fn test_get_deferred_sql_type() {
let typ = syn::Type::Path(type_path);
let rv = get_deferred_sql_type(&typ);
if let DeferredSqlType::Deferred(TypeKey::CustomType(typ)) = rv {
assert_eq!(typ, "butane :: ForeignType < Foo >");
assert_eq!(typ, "butane::ForeignType<Foo>");
} else {
panic!()
}
Expand All @@ -72,7 +72,7 @@ fn test_get_deferred_sql_type() {
let typ = syn::Type::Path(type_path);
let rv = get_deferred_sql_type(&typ);
if let DeferredSqlType::Deferred(TypeKey::CustomType(typ)) = rv {
assert_eq!(typ, "butane :: ForeignType < Foo >");
assert_eq!(typ, "butane::ForeignType<Foo>");
} else {
panic!()
}
Expand All @@ -81,7 +81,7 @@ fn test_get_deferred_sql_type() {
let typ = syn::Type::Path(type_path);
let rv = get_deferred_sql_type(&typ);
if let DeferredSqlType::Deferred(TypeKey::CustomType(typ)) = rv {
assert_eq!(typ, "butane :: Many < Foo >");
assert_eq!(typ, "butane::Many<Foo>");
} else {
panic!()
}
Expand Down

0 comments on commit 8f4f396

Please sign in to comment.