use std::any::type_name;
use serde_json::Value;
use crate::{Attributes, ModelNaming};
pub trait Conversion: Attributes + ModelNaming {
fn to_key(&self) -> Option<Vec<Value>> {
self.read_attribute("id")
.filter(|value| !matches!(value, Value::Null))
.map(|value| match value {
Value::Array(values) => values,
value => vec![value],
})
}
fn param_delimiter() -> &'static str {
"-"
}
fn to_param(&self) -> Option<String> {
self.to_key().and_then(|values| {
let parts = values
.into_iter()
.map(value_to_param_component)
.collect::<Option<Vec<_>>>()?;
if parts.is_empty() {
None
} else {
Some(parts.join(Self::param_delimiter()))
}
})
}
fn to_partial_path(&self) -> String {
let model_name = Self::model_name();
format!("{}/{}", model_name.route_key, model_name.element)
}
fn model_name_for_conversion() -> &'static str {
let type_name = type_name::<Self>();
type_name.rsplit("::").next().unwrap_or(type_name)
}
}
fn value_to_param_component(value: Value) -> Option<String> {
match value {
Value::Null => None,
Value::String(string) if string.is_empty() => None,
Value::String(string) => Some(string),
Value::Number(number) => Some(number.to_string()),
Value::Bool(boolean) => Some(boolean.to_string()),
other => Some(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use super::Conversion;
use crate::{AttributeError, Attributes, ModelNaming};
#[derive(Debug, Clone, Default)]
struct TestUser {
id: Option<u64>,
name: String,
}
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: std::collections::HashMap<String, Value>,
) -> Result<(), AttributeError> {
for (name, value) in attrs {
self.write_attribute(&name, value)?;
}
Ok(())
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
let mut attributes = std::collections::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 {}
#[test]
fn returns_key_from_id_attribute() {
let user = TestUser {
id: Some(7),
name: "Alice".to_string(),
};
assert_eq!(user.to_key(), Some(vec![json!(7)]));
}
#[test]
fn returns_none_when_id_is_missing() {
let user = TestUser::default();
assert_eq!(user.to_key(), None);
assert_eq!(user.to_param(), None);
}
#[test]
fn builds_param_and_partial_path() {
let user = TestUser {
id: Some(42),
name: "Alice".to_string(),
};
assert_eq!(user.to_param(), Some("42".to_string()));
assert_eq!(user.to_partial_path(), "test_users/test_user");
assert_eq!(TestUser::model_name_for_conversion(), "TestUser");
}
#[derive(Debug, Clone, Default)]
struct CompositeKeyRecord;
impl Attributes for CompositeKeyRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for CompositeKeyRecord {
fn model_name() -> crate::ModelName {
crate::ModelName::new("Person")
}
}
impl Conversion for CompositeKeyRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(vec![json!("north"), json!(7)])
}
}
#[test]
fn to_param_supports_string_keys() {
struct StringKey;
impl Attributes for StringKey {
fn attribute_names() -> &'static [&'static str] {
&["id"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
Some(Value::String("abc".to_string()))
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::from([(
"id".to_string(),
Value::String("abc".to_string()),
)])
}
}
impl ModelNaming for StringKey {}
impl Conversion for StringKey {}
assert_eq!(StringKey.to_param(), Some("abc".to_string()));
}
#[test]
fn to_param_joins_composite_keys_with_dashes() {
let record = CompositeKeyRecord;
assert_eq!(record.to_param(), Some("north-7".to_string()));
}
#[test]
fn to_param_returns_none_for_empty_string_key_component() {
struct EmptyKey;
impl Attributes for EmptyKey {
fn attribute_names() -> &'static [&'static str] {
&["id"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
Some(Value::String(String::new()))
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::from([("id".to_string(), Value::String(String::new()))])
}
}
impl ModelNaming for EmptyKey {}
impl Conversion for EmptyKey {}
assert_eq!(EmptyKey.to_param(), None);
}
#[test]
fn to_partial_path_uses_irregular_route_keys() {
let record = CompositeKeyRecord;
assert_eq!(record.to_partial_path(), "people/person");
}
#[derive(Debug, Clone, Default)]
struct StringIdRecord;
impl Attributes for StringIdRecord {
fn attribute_names() -> &'static [&'static str] {
&["id"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
Some(Value::String("abc".to_string()))
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::from([("id".to_string(), Value::String("abc".to_string()))])
}
}
impl ModelNaming for StringIdRecord {}
impl Conversion for StringIdRecord {}
#[derive(Debug, Clone, Default)]
struct BoolIdRecord;
impl Attributes for BoolIdRecord {
fn attribute_names() -> &'static [&'static str] {
&["id"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
Some(Value::Bool(false))
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::from([("id".to_string(), Value::Bool(false))])
}
}
impl ModelNaming for BoolIdRecord {}
impl Conversion for BoolIdRecord {}
#[derive(Debug, Clone, Default)]
struct ZeroIdRecord;
impl Attributes for ZeroIdRecord {
fn attribute_names() -> &'static [&'static str] {
&["id"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
Some(json!(0))
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::from([("id".to_string(), json!(0))])
}
}
impl ModelNaming for ZeroIdRecord {}
impl Conversion for ZeroIdRecord {}
#[derive(Debug, Clone, Default)]
struct ObjectKeyRecord;
impl Attributes for ObjectKeyRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for ObjectKeyRecord {}
impl Conversion for ObjectKeyRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(vec![json!({ "region": "north" })])
}
}
#[derive(Debug, Clone, Default)]
struct ArrayKeyRecord;
impl Attributes for ArrayKeyRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for ArrayKeyRecord {}
impl Conversion for ArrayKeyRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(vec![json!(["north", 7])])
}
}
#[derive(Debug, Clone, Default)]
struct NullCompositeKeyRecord;
impl Attributes for NullCompositeKeyRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for NullCompositeKeyRecord {}
impl Conversion for NullCompositeKeyRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(vec![json!("north"), Value::Null])
}
}
#[derive(Debug, Clone, Default)]
struct EmptyCompositeKeyRecord;
impl Attributes for EmptyCompositeKeyRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for EmptyCompositeKeyRecord {}
impl Conversion for EmptyCompositeKeyRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(Vec::new())
}
}
#[derive(Debug, Clone, Default)]
struct CustomPathRecord;
impl Attributes for CustomPathRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for CustomPathRecord {
fn model_name() -> crate::ModelName {
crate::ModelName::new("Admin::BillingAddress")
}
}
impl Conversion for CustomPathRecord {}
mod nested_types {
use super::*;
#[derive(Debug, Clone, Default)]
pub(super) struct AuditLog;
impl Attributes for AuditLog {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for AuditLog {}
impl Conversion for AuditLog {}
}
#[derive(Debug, Clone, Default)]
struct InvoiceLine;
impl Attributes for InvoiceLine {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for InvoiceLine {}
impl Conversion for InvoiceLine {}
#[test]
fn to_key_supports_string_ids_from_attributes() {
assert_eq!(StringIdRecord.to_key(), Some(vec![json!("abc")]));
}
#[test]
fn to_key_preserves_false_boolean_ids() {
assert_eq!(BoolIdRecord.to_key(), Some(vec![json!(false)]));
}
#[test]
fn to_param_supports_false_boolean_ids() {
assert_eq!(BoolIdRecord.to_param(), Some("false".to_string()));
}
#[test]
fn to_param_supports_zero_numeric_ids() {
assert_eq!(ZeroIdRecord.to_param(), Some("0".to_string()));
}
#[test]
fn to_param_uses_json_for_object_key_components() {
assert_eq!(
ObjectKeyRecord.to_param(),
Some("{\"region\":\"north\"}".to_string())
);
}
#[test]
fn to_param_uses_json_for_array_key_components() {
assert_eq!(ArrayKeyRecord.to_param(), Some("[\"north\",7]".to_string()));
}
#[test]
fn to_param_returns_none_when_a_composite_key_contains_null() {
assert_eq!(NullCompositeKeyRecord.to_param(), None);
}
#[test]
fn to_param_returns_none_when_to_key_is_empty() {
assert_eq!(EmptyCompositeKeyRecord.to_param(), None);
}
#[test]
fn to_partial_path_uses_the_model_name_route_key_and_element() {
assert_eq!(
CustomPathRecord.to_partial_path(),
"admin_billing_addresses/billing_address"
);
}
#[test]
fn model_name_for_conversion_uses_the_last_type_path_segment() {
assert_eq!(
nested_types::AuditLog::model_name_for_conversion(),
"AuditLog"
);
}
#[test]
fn to_partial_path_uses_default_model_naming_for_compound_types() {
assert_eq!(InvoiceLine.to_partial_path(), "invoice_lines/invoice_line");
}
#[test]
fn string_ids_round_trip_through_to_param() {
assert_eq!(StringIdRecord.to_param(), Some("abc".to_string()));
}
#[derive(Debug, Clone, Default)]
struct WhitespaceIdRecord;
impl Attributes for WhitespaceIdRecord {
fn attribute_names() -> &'static [&'static str] {
&["id"]
}
fn read_attribute(&self, _name: &str) -> Option<Value> {
Some(Value::String(" abc ".to_string()))
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::from([(
"id".to_string(),
Value::String(" abc ".to_string()),
)])
}
}
impl ModelNaming for WhitespaceIdRecord {}
impl Conversion for WhitespaceIdRecord {}
#[derive(Debug, Clone, Default)]
struct MixedCompositeKeyRecord;
impl Attributes for MixedCompositeKeyRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for MixedCompositeKeyRecord {
fn model_name() -> crate::ModelName {
crate::ModelName::new("WarehouseEntry")
}
}
impl Conversion for MixedCompositeKeyRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(vec![json!("north"), json!(false), json!(0)])
}
}
#[derive(Debug, Clone, Default)]
struct AlternateCustomPathRecord;
impl Attributes for AlternateCustomPathRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for AlternateCustomPathRecord {
fn model_name() -> crate::ModelName {
let mut model_name = crate::ModelName::new("AttackHelicopter");
model_name.element = "ah-64".to_string();
model_name
}
}
impl Conversion for AlternateCustomPathRecord {}
mod deeper_nested_types {
use super::*;
pub(crate) mod reports {
use super::*;
#[derive(Debug, Clone, Default)]
pub(crate) struct DailySummary;
impl Attributes for DailySummary {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for DailySummary {}
impl Conversion for DailySummary {}
}
}
#[test]
fn to_param_preserves_surrounding_whitespace_in_string_ids() {
assert_eq!(WhitespaceIdRecord.to_key(), Some(vec![json!(" abc ")]));
assert_eq!(WhitespaceIdRecord.to_param(), Some(" abc ".to_string()));
}
#[test]
fn to_param_joins_mixed_composite_key_components() {
assert_eq!(
MixedCompositeKeyRecord.to_param(),
Some("north-false-0".to_string())
);
}
#[test]
fn to_partial_path_is_class_specific_for_custom_model_names() {
assert_eq!(
CustomPathRecord.to_partial_path(),
"admin_billing_addresses/billing_address"
);
assert_eq!(
AlternateCustomPathRecord.to_partial_path(),
"attack_helicopters/ah-64"
);
}
#[test]
fn model_name_for_conversion_uses_last_segment_for_deeply_nested_types() {
assert_eq!(
deeper_nested_types::reports::DailySummary::model_name_for_conversion(),
"DailySummary"
);
}
fn rails_contact(id: Option<u64>) -> TestUser {
TestUser {
id,
name: String::new(),
}
}
#[test]
fn test_rails_to_model_default_implementation_returns_self() {
let contact = rails_contact(Some(7));
assert_eq!(contact.to_model().to_param(), Some("7".to_string()));
}
#[test]
fn test_rails_to_key_default_implementation_returns_nil_for_new_records() {
assert_eq!(rails_contact(None).to_key(), None);
}
#[test]
fn test_rails_to_key_default_implementation_returns_the_id_in_an_array_for_persisted_records() {
assert_eq!(rails_contact(Some(1)).to_key(), Some(vec![json!(1)]));
}
#[test]
fn test_rails_to_key_does_not_double_wrap_composite_ids() {
#[derive(Debug, Clone, Default)]
struct ArrayIdRecord;
impl Attributes for ArrayIdRecord {
fn attribute_names() -> &'static [&'static str] {
&["id"]
}
fn read_attribute(&self, name: &str) -> Option<Value> {
(name == "id").then(|| json!(["north", 7]))
}
fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
Err(AttributeError::UnknownAttribute(name.to_string()))
}
fn attributes(&self) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::from([("id".to_string(), json!(["north", 7]))])
}
}
impl ModelNaming for ArrayIdRecord {}
impl Conversion for ArrayIdRecord {}
let record = ArrayIdRecord;
assert_eq!(record.to_key(), Some(vec![json!("north"), json!(7)]));
assert_eq!(record.to_param(), Some("north-7".to_string()));
}
#[test]
fn test_rails_to_param_default_implementation_returns_nil_for_new_records() {
assert_eq!(rails_contact(None).to_param(), None);
}
#[test]
fn test_rails_to_param_default_implementation_returns_a_string_of_ids_for_persisted_records() {
assert_eq!(rails_contact(Some(1)).to_param(), Some("1".to_string()));
}
#[test]
fn test_rails_to_param_returns_the_string_joined_by_dash() {
assert_eq!(CompositeKeyRecord.to_param(), Some("north-7".to_string()));
}
#[test]
fn test_rails_to_param_returns_nil_if_composite_id_is_incomplete() {
assert_eq!(NullCompositeKeyRecord.to_param(), None);
}
#[test]
fn test_rails_to_param_returns_nil_if_to_key_is_nil() {
#[derive(Debug, Clone, Default)]
struct NilKeyRecord;
impl Attributes for NilKeyRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for NilKeyRecord {}
impl Conversion for NilKeyRecord {
fn to_key(&self) -> Option<Vec<Value>> {
None
}
}
assert_eq!(NilKeyRecord.to_param(), None);
}
#[test]
fn test_rails_to_partial_path_default_implementation_returns_a_relative_path() {
assert_eq!(
rails_contact(Some(1)).to_partial_path(),
"test_users/test_user"
);
assert_eq!(InvoiceLine.to_partial_path(), "invoice_lines/invoice_line");
}
#[test]
fn test_rails_to_partial_path_handles_namespaced_models() {
#[derive(Debug, Clone, Default)]
struct Comanche;
impl Attributes for Comanche {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for Comanche {
fn model_name() -> crate::ModelName {
let mut model_name = crate::ModelName::new("Helicopter::Comanche");
model_name.route_key = "helicopter/comanches".to_string();
model_name
}
}
impl Conversion for Comanche {}
assert_eq!(Comanche.to_partial_path(), "helicopter/comanches/comanche");
}
#[test]
fn test_rails_to_partial_path_handles_non_standard_model_name() {
assert_eq!(
AlternateCustomPathRecord.to_partial_path(),
"attack_helicopters/ah-64"
);
}
#[test]
fn test_rails_to_param_delimiter_allows_redefining_the_delimiter_used_in_to_param() {
#[derive(Debug, Clone, Default)]
struct SlashDelimitedRecord;
impl Attributes for SlashDelimitedRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for SlashDelimitedRecord {}
impl Conversion for SlashDelimitedRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(vec![json!("north"), json!(7)])
}
fn param_delimiter() -> &'static str {
"/"
}
}
assert_eq!(SlashDelimitedRecord.to_param(), Some("north/7".to_string()));
}
#[test]
fn test_rails_to_param_delimiter_is_defined_per_class() {
#[derive(Debug, Clone, Default)]
struct DotDelimitedRecord;
impl Attributes for DotDelimitedRecord {
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) -> std::collections::HashMap<String, Value> {
std::collections::HashMap::new()
}
}
impl ModelNaming for DotDelimitedRecord {}
impl Conversion for DotDelimitedRecord {
fn to_key(&self) -> Option<Vec<Value>> {
Some(vec![json!("north"), json!(7)])
}
fn param_delimiter() -> &'static str {
"."
}
}
assert_eq!(CompositeKeyRecord.to_param(), Some("north-7".to_string()));
assert_eq!(DotDelimitedRecord.to_param(), Some("north.7".to_string()));
}
}