use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::Attributes;
pub type SerializationMap = IndexMap<String, Value>;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializationOptions {
pub only: Option<Vec<String>>,
pub except: Option<Vec<String>>,
pub methods: Option<Vec<String>>,
pub include: Option<Vec<SerializationInclude>>,
pub root: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SerializationInclude {
Association(String),
Nested {
association: String,
options: SerializationOptions,
},
}
impl SerializationInclude {
pub fn named(name: impl Into<String>) -> Self {
Self::Association(name.into())
}
pub fn with_options(name: impl Into<String>, options: SerializationOptions) -> Self {
Self::Nested {
association: name.into(),
options,
}
}
fn association(&self) -> &str {
match self {
Self::Association(association) => association,
Self::Nested { association, .. } => association,
}
}
fn options(&self) -> Option<SerializationOptions> {
match self {
Self::Association(_) => None,
Self::Nested { options, .. } => Some(options.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct SerializationAssociationRequest {
association: String,
options: SerializationOptions,
}
const ASSOCIATION_REQUEST_PREFIX: &str = "__rustrails_association__=";
pub fn association_serialization_key(
association: impl Into<String>,
options: Option<SerializationOptions>,
) -> String {
let request = SerializationAssociationRequest {
association: association.into(),
options: options.unwrap_or_default(),
};
format!(
"{ASSOCIATION_REQUEST_PREFIX}{}",
serde_json::to_string(&request).expect("association request should serialize")
)
}
pub fn parse_association_serialization_key(name: &str) -> Option<(String, SerializationOptions)> {
let payload = name.strip_prefix(ASSOCIATION_REQUEST_PREFIX)?;
let request = serde_json::from_str::<SerializationAssociationRequest>(payload).ok()?;
Some((request.association, request.options))
}
impl SerializationOptions {
pub fn new() -> Self {
Self::default()
}
pub fn only(mut self, attrs: Vec<String>) -> Self {
self.only = Some(attrs);
self
}
pub fn except(mut self, attrs: Vec<String>) -> Self {
self.except = Some(attrs);
self
}
pub fn methods(mut self, methods: Vec<String>) -> Self {
self.methods = Some(methods);
self
}
pub fn include(mut self, include: Vec<SerializationInclude>) -> Self {
self.include = Some(include);
self
}
pub fn root(mut self, root: String) -> Self {
self.root = Some(root);
self
}
}
pub trait Serialization: Attributes {
fn serializable_hash(&self, options: Option<SerializationOptions>) -> SerializationMap {
let options = options.unwrap_or_default();
let attribute_names = filter_attribute_names(Self::attribute_names(), &options);
let mut serialized = SerializationMap::with_capacity(
attribute_names.len()
+ options.methods.as_ref().map_or(0, Vec::len)
+ options.include.as_ref().map_or(0, Vec::len),
);
for attribute_name in attribute_names {
let value = self.read_attribute(&attribute_name).unwrap_or(Value::Null);
serialized.insert(attribute_name, value);
}
if let Some(methods) = options.methods.as_ref() {
for method_name in methods {
if let Some(value) = self.read_attribute(method_name) {
serialized.insert(method_name.clone(), value);
}
}
}
if let Some(includes) = options.include.as_ref() {
for include in includes {
let request =
association_serialization_key(include.association(), include.options());
if let Some(value) = self.read_attribute(&request) {
serialized.insert(include.association().to_string(), value);
}
}
}
serialized
}
fn as_json(&self, options: Option<SerializationOptions>) -> Value {
let root = options.as_ref().and_then(|opts| opts.root.clone());
let object = Value::Object(hash_to_json_map(self.serializable_hash(options)));
match root {
Some(root_key) => {
let mut wrapped = Map::with_capacity(1);
wrapped.insert(root_key, object);
Value::Object(wrapped)
}
None => object,
}
}
fn to_json(&self, options: Option<SerializationOptions>) -> String {
let root = options.as_ref().and_then(|opts| opts.root.clone());
let object = self.serializable_hash(options);
match root {
Some(root_key) => serde_json::to_string(&IndexMap::from([(
root_key,
Value::Object(hash_to_json_map(object)),
)]))
.expect("root-wrapped serialization should succeed"),
None => serde_json::to_string(&object).expect("serialization should succeed"),
}
}
}
impl<T> Serialization for T where T: Attributes {}
fn filter_attribute_names(attribute_names: &[&str], options: &SerializationOptions) -> Vec<String> {
if let Some(only) = options.only.as_ref() {
let mut selected = Vec::with_capacity(only.len());
for candidate in only {
if attribute_names.contains(&candidate.as_str()) && !selected.contains(candidate) {
selected.push(candidate.clone());
}
}
return selected;
}
attribute_names
.iter()
.filter(|name| {
options
.except
.as_ref()
.is_none_or(|except| except.iter().all(|candidate| candidate != *name))
})
.map(|name| (*name).to_string())
.collect()
}
fn hash_to_json_map(hash: SerializationMap) -> Map<String, Value> {
hash.into_iter().collect()
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use serde_json::{Value, json};
use super::{
Serialization, SerializationInclude, SerializationMap, SerializationOptions,
association_serialization_key, parse_association_serialization_key,
};
use crate::{AttributeError, Attributes};
#[derive(Debug, Clone)]
struct TestUser {
id: u64,
name: String,
email: String,
}
impl Attributes for TestUser {
fn attribute_names() -> &'static [&'static str] {
&["id", "name", "email"]
}
fn read_attribute(&self, name: &str) -> Option<Value> {
match name {
"id" => Some(Value::from(self.id)),
"name" => Some(Value::String(self.name.clone())),
"email" => Some(Value::String(self.email.clone())),
"display_name" => Some(Value::String(format!("{} <{}>", self.name, self.email))),
_ => None,
}
}
fn write_attribute(&mut self, name: &str, value: Value) -> Result<(), AttributeError> {
match (name, value) {
("id", Value::Number(number)) => {
let id = number
.as_u64()
.ok_or_else(|| AttributeError::TypeMismatch {
attribute: "id".to_string(),
expected: "u64".to_string(),
actual: "number".to_string(),
})?;
self.id = id;
Ok(())
}
("name", Value::String(name)) => {
self.name = name;
Ok(())
}
("email", Value::String(email)) => {
self.email = email;
Ok(())
}
("id", other) => Err(AttributeError::TypeMismatch {
attribute: "id".to_string(),
expected: "u64".to_string(),
actual: other.to_string(),
}),
("name", other) => Err(AttributeError::TypeMismatch {
attribute: "name".to_string(),
expected: "string".to_string(),
actual: other.to_string(),
}),
("email", other) => Err(AttributeError::TypeMismatch {
attribute: "email".to_string(),
expected: "string".to_string(),
actual: other.to_string(),
}),
(unknown, _) => Err(AttributeError::UnknownAttribute(unknown.to_string())),
}
}
fn assign_attributes(
&mut self,
attrs: HashMap<String, Value>,
) -> Result<(), AttributeError> {
for (name, value) in attrs {
self.write_attribute(&name, value)?;
}
Ok(())
}
fn attributes(&self) -> HashMap<String, Value> {
let mut attributes = HashMap::new();
attributes.insert("id".to_string(), Value::from(self.id));
attributes.insert("name".to_string(), Value::String(self.name.clone()));
attributes.insert("email".to_string(), Value::String(self.email.clone()));
attributes
}
}
fn test_user() -> TestUser {
TestUser {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
}
}
#[derive(Debug, Clone)]
struct TestAddress {
street: String,
city: String,
state: String,
zip: u32,
}
impl Attributes for TestAddress {
fn attribute_names() -> &'static [&'static str] {
&["street", "city", "state", "zip"]
}
fn read_attribute(&self, name: &str) -> Option<Value> {
match name {
"street" => Some(json!(self.street)),
"city" => Some(json!(self.city)),
"state" => Some(json!(self.state)),
"zip" => Some(json!(self.zip)),
_ => None,
}
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> HashMap<String, Value> {
HashMap::from([
("street".to_string(), json!(self.street)),
("city".to_string(), json!(self.city)),
("state".to_string(), json!(self.state)),
("zip".to_string(), json!(self.zip)),
])
}
}
#[derive(Debug, Clone)]
struct FriendList {
friends: Vec<SerializationUser>,
}
#[derive(Debug, Clone)]
enum FriendsAssociation {
Direct(Vec<SerializationUser>),
Ary(FriendList),
}
#[derive(Debug, Clone)]
struct SerializationUser {
name: String,
email: String,
gender: String,
address: Option<TestAddress>,
friends: FriendsAssociation,
}
impl SerializationUser {
fn new(name: &str, email: &str, gender: &str) -> Self {
Self {
name: name.to_string(),
email: email.to_string(),
gender: gender.to_string(),
address: None,
friends: FriendsAssociation::Direct(vec![]),
}
}
}
impl Attributes for SerializationUser {
fn attribute_names() -> &'static [&'static str] {
&["name", "email", "gender"]
}
fn read_attribute(&self, name: &str) -> Option<Value> {
if let Some((association, options)) = parse_association_serialization_key(name) {
return match association.as_str() {
"address" => self
.address
.as_ref()
.map(|address| serialize_record(address, options)),
"friends" => Some(match &self.friends {
FriendsAssociation::Direct(friends) => serialize_records(friends, options),
FriendsAssociation::Ary(list) => serialize_records(&list.friends, options),
}),
_ => None,
};
}
match name {
"name" => Some(json!(self.name)),
"email" => Some(json!(self.email)),
"gender" => Some(json!(self.gender)),
"full_name" => Some(json!(format!("{} <{}>", self.name, self.email))),
_ => None,
}
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> HashMap<String, Value> {
HashMap::from([
("name".to_string(), json!(self.name)),
("email".to_string(), json!(self.email)),
("gender".to_string(), json!(self.gender)),
])
}
}
fn serialize_record<T: Serialization>(record: &T, options: SerializationOptions) -> Value {
Value::Object(super::hash_to_json_map(
record.serializable_hash(Some(options)),
))
}
fn serialize_records<T: Serialization>(records: &[T], options: SerializationOptions) -> Value {
Value::Array(
records
.iter()
.map(|record| serialize_record(record, options.clone()))
.collect(),
)
}
fn serialization_friends() -> Vec<SerializationUser> {
vec![
SerializationUser::new("Joe", "joe@example.com", "male"),
SerializationUser::new("Sue", "sue@example.com", "female"),
]
}
fn serialization_user_shallow() -> SerializationUser {
SerializationUser::new("David", "david@example.com", "male")
}
fn serialization_user() -> SerializationUser {
let mut user = serialization_user_shallow();
user.address = Some(TestAddress {
street: "123 Lane".to_string(),
city: "Springfield".to_string(),
state: "CA".to_string(),
zip: 11111,
});
user.friends = FriendsAssociation::Direct(serialization_friends());
user
}
#[test]
fn serializes_all_attributes_by_default() {
let user = test_user();
let serialized = user.serializable_hash(None);
assert_eq!(serialized.get("id"), Some(&json!(1)));
assert_eq!(serialized.get("name"), Some(&json!("Alice")));
assert_eq!(serialized.get("email"), Some(&json!("alice@example.com")));
}
#[test]
fn only_filters_attributes_and_wins_over_except() {
let user = test_user();
let options = SerializationOptions::new()
.only(vec!["name".to_string(), "email".to_string()])
.except(vec!["email".to_string()]);
let serialized = user.serializable_hash(Some(options));
assert_eq!(serialized.len(), 2);
assert!(serialized.contains_key("name"));
assert!(serialized.contains_key("email"));
assert!(!serialized.contains_key("id"));
}
#[test]
fn except_excludes_attributes() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().except(vec!["email".to_string()]),
));
assert!(serialized.contains_key("id"));
assert!(serialized.contains_key("name"));
assert!(!serialized.contains_key("email"));
}
#[test]
fn methods_add_extra_values_when_available() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().methods(vec!["display_name".to_string()]),
));
assert_eq!(
serialized.get("display_name"),
Some(&json!("Alice <alice@example.com>"))
);
}
#[test]
fn as_json_wraps_under_root_key() {
let user = test_user();
let value = user.as_json(Some(SerializationOptions::new().root("user".to_string())));
assert_eq!(
value,
json!({
"user": {
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
})
);
}
#[test]
fn to_json_returns_valid_json() {
let user = test_user();
let json = user.to_json(Some(
SerializationOptions::new().only(vec!["name".to_string()]),
));
assert_eq!(json, "{\"name\":\"Alice\"}");
}
#[test]
fn association_request_keys_round_trip_nested_options() {
let options = SerializationOptions::new()
.only(vec!["name".to_string()])
.methods(vec!["full_name".to_string()]);
let key = association_serialization_key("friends", Some(options.clone()));
assert_eq!(
parse_association_serialization_key(&key),
Some(("friends".to_string(), options))
);
}
#[test]
fn builder_methods_store_requested_options() {
let options = SerializationOptions::new()
.only(vec!["name".to_string()])
.except(vec!["email".to_string()])
.methods(vec!["display_name".to_string()])
.include(vec![SerializationInclude::named("address")])
.root("user".to_string());
assert_eq!(options.only, Some(vec!["name".to_string()]));
assert_eq!(options.except, Some(vec!["email".to_string()]));
assert_eq!(options.methods, Some(vec!["display_name".to_string()]));
assert_eq!(
options.include,
Some(vec![SerializationInclude::named("address")])
);
assert_eq!(options.root, Some("user".to_string()));
}
#[test]
fn methods_ignore_unknown_dynamic_names() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().methods(vec!["missing".to_string()]),
));
assert!(!serialized.contains_key("missing"));
}
#[test]
fn as_json_without_root_returns_plain_object() {
let user = test_user();
assert_eq!(
user.as_json(None),
json!({
"id": 1,
"name": "Alice",
"email": "alice@example.com"
})
);
}
#[test]
fn only_with_unknown_attributes_returns_empty_hash() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().only(vec!["missing".to_string()]),
));
assert!(serialized.is_empty());
}
#[test]
fn except_can_remove_every_attribute() {
let user = test_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().except(vec![
"id".to_string(),
"name".to_string(),
"email".to_string(),
])));
assert!(serialized.is_empty());
}
#[test]
fn to_json_preserves_root_wrapper() {
let user = test_user();
let json = user.to_json(Some(SerializationOptions::new().root("user".to_string())));
assert_eq!(
serde_json::from_str::<Value>(&json).expect("root-wrapped JSON should parse"),
json!({
"user": {
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
})
);
}
#[test]
fn unreadable_declared_attributes_serialize_as_null() {
#[derive(Debug, Clone)]
struct PartialRead;
impl Attributes for PartialRead {
fn attribute_names() -> &'static [&'static str] {
&["visible", "hidden"]
}
fn read_attribute(&self, name: &str) -> Option<Value> {
match name {
"visible" => Some(json!("value")),
_ => None,
}
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> HashMap<String, Value> {
HashMap::from([
("visible".to_string(), json!("value")),
("hidden".to_string(), Value::Null),
])
}
}
let value = PartialRead.as_json(None);
assert_eq!(value, json!({"visible": "value", "hidden": null}));
}
#[test]
fn serialization_options_start_empty() {
let options = SerializationOptions::new();
assert_eq!(options.only, None);
assert_eq!(options.except, None);
assert_eq!(options.methods, None);
assert_eq!(options.include, None);
assert_eq!(options.root, None);
}
#[test]
fn only_with_a_single_attribute_returns_that_attribute() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().only(vec!["name".to_string()]),
));
assert_eq!(
serialized,
SerializationMap::from([("name".to_string(), json!("Alice"))])
);
}
#[test]
fn except_with_an_unknown_attribute_keeps_all_attributes() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().except(vec!["missing".to_string()]),
));
assert_eq!(serialized, user.serializable_hash(None));
}
#[test]
fn empty_except_keeps_all_attributes() {
let user = test_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().except(vec![])));
assert_eq!(serialized, user.serializable_hash(None));
}
#[test]
fn empty_only_returns_an_empty_hash() {
let user = test_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().only(vec![])));
assert!(serialized.is_empty());
}
#[test]
fn duplicate_only_attributes_do_not_duplicate_entries() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().only(vec!["name".to_string(), "name".to_string()]),
));
assert_eq!(serialized.len(), 1);
assert_eq!(serialized.get("name"), Some(&json!("Alice")));
}
#[test]
fn methods_can_be_returned_without_base_attributes() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new()
.only(vec![])
.methods(vec!["display_name".to_string()]),
));
assert_eq!(
serialized,
SerializationMap::from([(
"display_name".to_string(),
json!("Alice <alice@example.com>"),
)])
);
}
#[test]
fn mixed_known_and_unknown_methods_include_only_known_values() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new()
.methods(vec!["display_name".to_string(), "missing".to_string()]),
));
assert_eq!(
serialized.get("display_name"),
Some(&json!("Alice <alice@example.com>"))
);
assert!(!serialized.contains_key("missing"));
}
#[test]
fn methods_can_reintroduce_attributes_filtered_out_by_except() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new()
.except(vec![
"id".to_string(),
"name".to_string(),
"email".to_string(),
])
.methods(vec!["name".to_string()]),
));
assert_eq!(
serialized,
SerializationMap::from([("name".to_string(), json!("Alice"))])
);
}
#[test]
fn as_json_without_root_can_include_method_values() {
let user = test_user();
let value = user.as_json(Some(
SerializationOptions::new().methods(vec!["display_name".to_string()]),
));
assert_eq!(
value,
json!({
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"display_name": "Alice <alice@example.com>"
})
);
}
#[test]
fn as_json_with_root_can_include_method_values() {
let user = test_user();
let value = user.as_json(Some(
SerializationOptions::new()
.methods(vec!["display_name".to_string()])
.root("user".to_string()),
));
assert_eq!(
value,
json!({
"user": {
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"display_name": "Alice <alice@example.com>"
}
})
);
}
#[test]
fn to_json_without_options_matches_as_json_output() {
let user = test_user();
assert_eq!(
serde_json::from_str::<Value>(&user.to_json(None)).expect("JSON should parse"),
user.as_json(None)
);
}
#[test]
fn to_json_with_root_and_methods_round_trips_through_json_parser() {
let user = test_user();
let json = user.to_json(Some(
SerializationOptions::new()
.methods(vec!["display_name".to_string()])
.root("user".to_string()),
));
assert_eq!(
serde_json::from_str::<Value>(&json).expect("JSON should parse"),
json!({
"user": {
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"display_name": "Alice <alice@example.com>"
}
})
);
}
#[test]
fn selected_unreadable_attributes_still_serialize_as_null() {
#[derive(Debug, Clone)]
struct HiddenOnly;
impl Attributes for HiddenOnly {
fn attribute_names() -> &'static [&'static str] {
&["hidden"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
None
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> HashMap<String, Value> {
HashMap::from([("hidden".to_string(), Value::Null)])
}
}
let serialized = HiddenOnly.serializable_hash(Some(
SerializationOptions::new().only(vec!["hidden".to_string()]),
));
assert_eq!(
serialized,
SerializationMap::from([("hidden".to_string(), Value::Null)])
);
}
#[test]
fn as_json_allows_an_empty_root_key() {
let user = test_user();
let value = user.as_json(Some(SerializationOptions::new().root(String::new())));
assert_eq!(
value,
json!({
"": {
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
})
);
}
#[test]
fn to_json_serializes_nulls_for_unreadable_declared_attributes() {
#[derive(Debug, Clone)]
struct HiddenOnly;
impl Attributes for HiddenOnly {
fn attribute_names() -> &'static [&'static str] {
&["hidden"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
None
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> HashMap<String, Value> {
HashMap::from([("hidden".to_string(), Value::Null)])
}
}
assert_eq!(
serde_json::from_str::<Value>(&HiddenOnly.to_json(None)).expect("JSON should parse"),
json!({"hidden": null})
);
}
#[test]
fn methods_can_override_existing_attribute_values() {
#[derive(Debug)]
struct OverridableUser {
name_reads: std::cell::RefCell<usize>,
}
impl Attributes for OverridableUser {
fn attribute_names() -> &'static [&'static str] {
&["name"]
}
fn read_attribute(&self, name: &str) -> Option<Value> {
match name {
"name" => {
let mut reads = self.name_reads.borrow_mut();
*reads += 1;
Some(if *reads == 1 {
json!("Alice")
} else {
json!("Alicia")
})
}
_ => None,
}
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> HashMap<String, Value> {
HashMap::from([("name".to_string(), json!("Alice"))])
}
}
let user = OverridableUser {
name_reads: std::cell::RefCell::new(0),
};
let serialized = user.serializable_hash(Some(
SerializationOptions::new().methods(vec!["name".to_string()]),
));
assert_eq!(
serialized,
SerializationMap::from([("name".to_string(), json!("Alicia"))])
);
}
#[test]
fn root_does_not_affect_serializable_hash_when_only_and_methods_are_used() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new()
.only(vec![])
.methods(vec!["display_name".to_string()])
.root("user".to_string()),
));
assert_eq!(
serialized,
SerializationMap::from([(
"display_name".to_string(),
json!("Alice <alice@example.com>"),
)])
);
assert!(!serialized.contains_key("user"));
}
#[test]
fn to_json_with_only_empty_and_root_round_trips_to_an_empty_object() {
let user = test_user();
let json = user.to_json(Some(
SerializationOptions::new()
.only(vec![])
.root("user".to_string()),
));
assert_eq!(
serde_json::from_str::<Value>(&json).expect("JSON should parse"),
json!({"user": {}})
);
}
#[test]
fn to_json_with_only_empty_methods_and_root_round_trips_to_a_methods_only_object() {
let user = test_user();
let json = user.to_json(Some(
SerializationOptions::new()
.only(vec![])
.methods(vec!["display_name".to_string()])
.root("user".to_string()),
));
assert_eq!(
serde_json::from_str::<Value>(&json).expect("JSON should parse"),
json!({
"user": {
"display_name": "Alice <alice@example.com>"
}
})
);
}
#[test]
fn test_method_serializable_hash_should_work() {
let user = test_user();
assert_eq!(
user.serializable_hash(None),
SerializationMap::from([
("id".to_string(), json!(1)),
("name".to_string(), json!("Alice")),
("email".to_string(), json!("alice@example.com")),
])
);
}
#[test]
fn test_method_serializable_hash_should_work_with_only_option() {
let user = test_user();
assert_eq!(
user.serializable_hash(Some(
SerializationOptions::new().only(vec!["name".to_string()]),
)),
SerializationMap::from([("name".to_string(), json!("Alice"))])
);
}
#[test]
fn test_method_serializable_hash_should_work_with_only_option_with_order_of_given_keys() {
let user = test_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().only(vec!["name".to_string(), "email".to_string()]),
));
assert_eq!(
serialized.keys().cloned().collect::<Vec<_>>(),
vec!["name".to_string(), "email".to_string()]
);
}
#[test]
fn test_method_serializable_hash_should_work_with_except_option() {
let user = test_user();
assert_eq!(
user.serializable_hash(Some(
SerializationOptions::new().except(vec!["name".to_string()]),
)),
SerializationMap::from([
("id".to_string(), json!(1)),
("email".to_string(), json!("alice@example.com")),
])
);
}
#[test]
fn test_method_serializable_hash_should_work_with_methods_option() {
let user = test_user();
assert_eq!(
user.serializable_hash(Some(
SerializationOptions::new().methods(vec!["display_name".to_string()]),
)),
SerializationMap::from([
("id".to_string(), json!(1)),
("name".to_string(), json!("Alice")),
("email".to_string(), json!("alice@example.com")),
(
"display_name".to_string(),
json!("Alice <alice@example.com>"),
),
])
);
}
#[test]
fn test_method_serializable_hash_should_work_with_only_and_methods() {
let user = test_user();
assert_eq!(
user.serializable_hash(Some(
SerializationOptions::new()
.only(vec![])
.methods(vec!["display_name".to_string()]),
)),
SerializationMap::from([(
"display_name".to_string(),
json!("Alice <alice@example.com>"),
)])
);
}
#[test]
fn test_method_serializable_hash_should_work_with_except_and_methods() {
let user = test_user();
assert_eq!(
user.serializable_hash(Some(
SerializationOptions::new()
.except(vec!["name".to_string()])
.methods(vec!["display_name".to_string()]),
)),
SerializationMap::from([
("id".to_string(), json!(1)),
("email".to_string(), json!("alice@example.com")),
(
"display_name".to_string(),
json!("Alice <alice@example.com>"),
),
])
);
}
#[test]
#[ignore = "Rails-specific: Rust serialization skips unknown method names instead of raising an error"]
fn test_should_raise_no_method_error_for_non_existing_method() {}
#[test]
#[ignore = "Rails-specific: the Serialization trait has no read_attribute_for_serialization override hook"]
fn test_should_use_read_attribute_for_serialization() {}
#[test]
fn test_include_option_with_singular_association() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().include(vec![SerializationInclude::named("address")]),
));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
(
"address".to_string(),
json!({
"street": "123 Lane",
"city": "Springfield",
"state": "CA",
"zip": 11111
}),
),
])
);
}
#[test]
fn test_include_option_with_plural_association() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
(
"friends".to_string(),
json!([
{"name": "Joe", "email": "joe@example.com", "gender": "male"},
{"name": "Sue", "email": "sue@example.com", "gender": "female"}
]),
),
])
);
}
#[test]
fn test_include_option_with_empty_association() {
let mut user = serialization_user();
user.friends = FriendsAssociation::Direct(vec![]);
let serialized = user.serializable_hash(Some(
SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
("friends".to_string(), json!([])),
])
);
}
#[test]
fn test_include_option_with_ary() {
let mut user = serialization_user();
user.friends = FriendsAssociation::Ary(FriendList {
friends: serialization_friends(),
});
let serialized = user.serializable_hash(Some(
SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
(
"friends".to_string(),
json!([
{"name": "Joe", "email": "joe@example.com", "gender": "male"},
{"name": "Sue", "email": "sue@example.com", "gender": "female"}
]),
),
])
);
}
#[test]
fn test_multiple_includes() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
SerializationInclude::named("address"),
SerializationInclude::named("friends"),
])));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
(
"address".to_string(),
json!({
"street": "123 Lane",
"city": "Springfield",
"state": "CA",
"zip": 11111
}),
),
(
"friends".to_string(),
json!([
{"name": "Joe", "email": "joe@example.com", "gender": "male"},
{"name": "Sue", "email": "sue@example.com", "gender": "female"}
]),
),
])
);
}
#[test]
fn test_include_with_options() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
SerializationInclude::with_options(
"address",
SerializationOptions::new().only(vec!["street".to_string()]),
),
])));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
("address".to_string(), json!({"street": "123 Lane"})),
])
);
}
#[test]
fn test_include_with_nested_methods() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
SerializationInclude::with_options(
"friends",
SerializationOptions::new()
.only(vec!["name".to_string()])
.methods(vec!["full_name".to_string()]),
),
])));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
(
"friends".to_string(),
json!([
{"name": "Joe", "full_name": "Joe <joe@example.com>"},
{"name": "Sue", "full_name": "Sue <sue@example.com>"}
]),
),
])
);
}
#[test]
fn test_nested_include() {
let mut user = serialization_user();
if let FriendsAssociation::Direct(friends) = &mut user.friends {
friends[0].friends = FriendsAssociation::Direct(vec![serialization_user_shallow()]);
}
let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
SerializationInclude::with_options(
"friends",
SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
),
])));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
(
"friends".to_string(),
json!([
{
"name": "Joe",
"email": "joe@example.com",
"gender": "male",
"friends": [{"name": "David", "email": "david@example.com", "gender": "male"}]
},
{
"name": "Sue",
"email": "sue@example.com",
"gender": "female",
"friends": []
}
]),
),
])
);
}
#[test]
fn test_only_include() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new()
.only(vec!["name".to_string()])
.include(vec![SerializationInclude::with_options(
"friends",
SerializationOptions::new().only(vec!["name".to_string()]),
)]),
));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
(
"friends".to_string(),
json!([
{"name": "Joe"},
{"name": "Sue"}
]),
),
])
);
}
#[test]
fn test_except_include() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new()
.except(vec!["gender".to_string()])
.include(vec![SerializationInclude::with_options(
"friends",
SerializationOptions::new().except(vec!["gender".to_string()]),
)]),
));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
(
"friends".to_string(),
json!([
{"name": "Joe", "email": "joe@example.com"},
{"name": "Sue", "email": "sue@example.com"}
]),
),
])
);
}
#[test]
fn test_multiple_includes_with_options() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
SerializationInclude::with_options(
"address",
SerializationOptions::new().only(vec!["street".to_string()]),
),
SerializationInclude::named("friends"),
])));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
("address".to_string(), json!({"street": "123 Lane"})),
(
"friends".to_string(),
json!([
{"name": "Joe", "email": "joe@example.com", "gender": "male"},
{"name": "Sue", "email": "sue@example.com", "gender": "female"}
]),
),
])
);
}
#[test]
fn test_all_includes_with_options() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
SerializationInclude::with_options(
"address",
SerializationOptions::new().only(vec!["street".to_string()]),
),
SerializationInclude::with_options(
"friends",
SerializationOptions::new().only(vec!["name".to_string()]),
),
])));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
("address".to_string(), json!({"street": "123 Lane"})),
(
"friends".to_string(),
json!([
{"name": "Joe"},
{"name": "Sue"}
]),
),
])
);
}
#[test]
fn unknown_includes_are_skipped() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new().include(vec![SerializationInclude::named("missing")]),
));
assert_eq!(
serialized,
SerializationMap::from([
("name".to_string(), json!("David")),
("email".to_string(), json!("david@example.com")),
("gender".to_string(), json!("male")),
])
);
}
#[test]
fn test_multiple_includes_preserve_requested_association_order() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
SerializationInclude::named("friends"),
SerializationInclude::named("address"),
])));
assert_eq!(
serialized.keys().cloned().collect::<Vec<_>>(),
vec![
"name".to_string(),
"email".to_string(),
"gender".to_string(),
"friends".to_string(),
"address".to_string(),
]
);
}
#[test]
fn methods_are_serialized_before_included_associations() {
let user = serialization_user();
let serialized = user.serializable_hash(Some(
SerializationOptions::new()
.methods(vec!["full_name".to_string()])
.include(vec![SerializationInclude::named("address")]),
));
assert_eq!(
serialized.keys().cloned().collect::<Vec<_>>(),
vec![
"name".to_string(),
"email".to_string(),
"gender".to_string(),
"full_name".to_string(),
"address".to_string(),
]
);
}
}