use std::collections::HashMap;
use serde_json::Value;
use crate::{AttributeError, Attributes, Conversion, Errors, ModelNaming, Serialization};
pub trait Model:
Attributes + ModelNaming + Conversion + Serialization + Sized + Send + Sync
{
fn new() -> Self;
fn initialize(attrs: HashMap<String, Value>) -> Result<Self, AttributeError> {
let mut model = Self::new();
model.assign_attributes(attrs)?;
Ok(model)
}
fn is_valid(&self) -> bool {
true
}
fn is_invalid(&self) -> bool {
!self.is_valid()
}
fn errors(&self) -> &Errors;
fn errors_mut(&mut self) -> &mut Errors;
fn validate(&mut self) -> bool {
self.errors_mut().clear();
true
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use serde_json::{Value, json};
use super::Model;
use crate::{
AttributeError, Attributes, Conversion, ErrorType, Errors, ModelNaming, Serialization,
};
#[derive(Debug, Default, Clone)]
struct TestUser {
id: Option<u64>,
name: String,
errors: Errors,
}
impl Attributes for TestUser {
fn attribute_names() -> &'static [&'static str] {
&["id", "name"]
}
fn read_attribute(&self, name: &str) -> Option<Value> {
match name {
"id" => Some(self.id.map_or(Value::Null, Value::from)),
"name" => Some(Value::String(self.name.clone())),
_ => None,
}
}
fn write_attribute(&mut self, name: &str, value: Value) -> Result<(), AttributeError> {
match (name, value) {
("id", Value::Null) => {
self.id = None;
Ok(())
}
("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 = Some(id);
Ok(())
}
("name", Value::String(name)) => {
self.name = name;
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(),
}),
(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(), self.id.map_or(Value::Null, Value::from));
attributes.insert("name".to_string(), Value::String(self.name.clone()));
attributes
}
}
impl ModelNaming for TestUser {}
impl Conversion for TestUser {}
impl Model for TestUser {
fn new() -> Self {
Self {
id: None,
name: String::new(),
errors: Errors::new(),
}
}
fn errors(&self) -> &Errors {
&self.errors
}
fn errors_mut(&mut self) -> &mut Errors {
&mut self.errors
}
fn is_valid(&self) -> bool {
self.errors.is_empty()
}
fn validate(&mut self) -> bool {
self.errors.clear();
if self.name.trim().is_empty() {
self.errors.add("name", ErrorType::Blank, "can't be blank");
}
self.errors.is_empty()
}
}
#[test]
fn initialize_assigns_attributes_to_new_model() {
let model = TestUser::initialize(HashMap::from([
("id".to_string(), json!(7)),
("name".to_string(), json!("Alice")),
]))
.expect("test data should initialize");
assert_eq!(model.id, Some(7));
assert_eq!(model.name, "Alice");
}
#[test]
fn validity_defaults_follow_current_errors() {
let mut model = TestUser::new();
assert!(model.is_valid());
model
.errors_mut()
.add("name", ErrorType::Blank, "can't be blank");
assert!(model.is_invalid());
}
#[test]
fn validate_populates_errors() {
let mut model = TestUser::new();
assert!(!model.validate());
assert!(model.is_invalid());
model.name = "Alice".to_string();
assert!(model.validate());
assert!(model.is_valid());
}
#[test]
fn model_trait_composes_conversion_and_serialization_defaults() {
let model = TestUser::initialize(HashMap::from([
("id".to_string(), json!(12)),
("name".to_string(), json!("Alice")),
]))
.expect("test data should initialize");
assert_eq!(model.to_param(), Some("12".to_string()));
assert_eq!(model.to_partial_path(), "test_users/test_user");
assert_eq!(
serde_json::from_str::<Value>(&model.to_json(None)).expect("model JSON should parse"),
json!({"id": 12, "name": "Alice"})
);
}
#[derive(Debug, Default, Clone)]
struct MinimalModel {
errors: Errors,
}
impl Attributes for MinimalModel {
fn attribute_names() -> &'static [&'static str] {
&[]
}
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::new()
}
}
impl ModelNaming for MinimalModel {}
impl Conversion for MinimalModel {}
impl Model for MinimalModel {
fn new() -> Self {
Self {
errors: Errors::new(),
}
}
fn errors(&self) -> &Errors {
&self.errors
}
fn errors_mut(&mut self) -> &mut Errors {
&mut self.errors
}
}
#[test]
fn initialize_propagates_attribute_errors() {
let result = TestUser::initialize(HashMap::from([(
"email".to_string(),
json!("alice@example.com"),
)]));
assert!(matches!(
result,
Err(AttributeError::UnknownAttribute(attribute)) if attribute == "email"
));
}
#[test]
fn default_is_valid_returns_true_without_validation_override() {
let mut model = MinimalModel::new();
model
.errors_mut()
.add("name", ErrorType::Blank, "can't be blank");
assert!(model.is_valid());
assert!(!model.is_invalid());
}
#[test]
fn default_validate_clears_existing_errors_and_returns_true() {
let mut model = MinimalModel::new();
model
.errors_mut()
.add("name", ErrorType::Blank, "can't be blank");
assert!(model.validate());
assert!(model.errors().is_empty());
}
#[test]
fn custom_model_can_override_validity_checks() {
let mut model = TestUser::new();
assert!(model.is_valid());
model.errors.add("name", ErrorType::Blank, "can't be blank");
assert!(model.is_invalid());
}
}