1use std::any::type_name;
2
3use serde_json::Value;
4
5use crate::{Attributes, ModelNaming};
6
7pub trait Conversion: Attributes + ModelNaming {
9 fn to_key(&self) -> Option<Vec<Value>> {
14 self.read_attribute("id")
15 .filter(|value| !matches!(value, Value::Null))
16 .map(|value| match value {
17 Value::Array(values) => values,
18 value => vec![value],
19 })
20 }
21
22 fn param_delimiter() -> &'static str {
24 "-"
25 }
26
27 fn to_param(&self) -> Option<String> {
29 self.to_key().and_then(|values| {
30 let parts = values
31 .into_iter()
32 .map(value_to_param_component)
33 .collect::<Option<Vec<_>>>()?;
34
35 if parts.is_empty() {
36 None
37 } else {
38 Some(parts.join(Self::param_delimiter()))
39 }
40 })
41 }
42
43 fn to_partial_path(&self) -> String {
45 let model_name = Self::model_name();
46 format!("{}/{}", model_name.route_key, model_name.element)
47 }
48
49 fn model_name_for_conversion() -> &'static str {
51 let type_name = type_name::<Self>();
52 type_name.rsplit("::").next().unwrap_or(type_name)
53 }
54}
55
56fn value_to_param_component(value: Value) -> Option<String> {
57 match value {
58 Value::Null => None,
59 Value::String(string) if string.is_empty() => None,
60 Value::String(string) => Some(string),
61 Value::Number(number) => Some(number.to_string()),
62 Value::Bool(boolean) => Some(boolean.to_string()),
63 other => Some(other.to_string()),
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use serde_json::{Value, json};
70
71 use super::Conversion;
72 use crate::{AttributeError, Attributes, ModelNaming};
73
74 #[derive(Debug, Clone, Default)]
75 struct TestUser {
76 id: Option<u64>,
77 name: String,
78 }
79
80 impl Attributes for TestUser {
81 fn attribute_names() -> &'static [&'static str] {
82 &["id", "name"]
83 }
84
85 fn read_attribute(&self, name: &str) -> Option<Value> {
86 match name {
87 "id" => Some(self.id.map_or(Value::Null, Value::from)),
88 "name" => Some(Value::String(self.name.clone())),
89 _ => None,
90 }
91 }
92
93 fn write_attribute(&mut self, name: &str, value: Value) -> Result<(), AttributeError> {
94 match (name, value) {
95 ("id", Value::Null) => {
96 self.id = None;
97 Ok(())
98 }
99 ("id", Value::Number(number)) => {
100 let id = number
101 .as_u64()
102 .ok_or_else(|| AttributeError::TypeMismatch {
103 attribute: "id".to_string(),
104 expected: "u64".to_string(),
105 actual: "number".to_string(),
106 })?;
107 self.id = Some(id);
108 Ok(())
109 }
110 ("name", Value::String(name)) => {
111 self.name = name;
112 Ok(())
113 }
114 ("id", other) => Err(AttributeError::TypeMismatch {
115 attribute: "id".to_string(),
116 expected: "u64".to_string(),
117 actual: other.to_string(),
118 }),
119 ("name", other) => Err(AttributeError::TypeMismatch {
120 attribute: "name".to_string(),
121 expected: "string".to_string(),
122 actual: other.to_string(),
123 }),
124 (unknown, _) => Err(AttributeError::UnknownAttribute(unknown.to_string())),
125 }
126 }
127
128 fn assign_attributes(
129 &mut self,
130 attrs: std::collections::HashMap<String, Value>,
131 ) -> Result<(), AttributeError> {
132 for (name, value) in attrs {
133 self.write_attribute(&name, value)?;
134 }
135 Ok(())
136 }
137
138 fn attributes(&self) -> std::collections::HashMap<String, Value> {
139 let mut attributes = std::collections::HashMap::new();
140 attributes.insert("id".to_string(), self.id.map_or(Value::Null, Value::from));
141 attributes.insert("name".to_string(), Value::String(self.name.clone()));
142 attributes
143 }
144 }
145
146 impl ModelNaming for TestUser {}
147 impl Conversion for TestUser {}
148
149 #[test]
150 fn returns_key_from_id_attribute() {
151 let user = TestUser {
152 id: Some(7),
153 name: "Alice".to_string(),
154 };
155
156 assert_eq!(user.to_key(), Some(vec![json!(7)]));
157 }
158
159 #[test]
160 fn returns_none_when_id_is_missing() {
161 let user = TestUser::default();
162
163 assert_eq!(user.to_key(), None);
164 assert_eq!(user.to_param(), None);
165 }
166
167 #[test]
168 fn builds_param_and_partial_path() {
169 let user = TestUser {
170 id: Some(42),
171 name: "Alice".to_string(),
172 };
173
174 assert_eq!(user.to_param(), Some("42".to_string()));
175 assert_eq!(user.to_partial_path(), "test_users/test_user");
176 assert_eq!(TestUser::model_name_for_conversion(), "TestUser");
177 }
178 #[derive(Debug, Clone, Default)]
179 struct CompositeKeyRecord;
180
181 impl Attributes for CompositeKeyRecord {
182 fn attribute_names() -> &'static [&'static str] {
183 &[]
184 }
185
186 fn read_attribute(&self, _name: &str) -> Option<Value> {
187 None
188 }
189
190 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
191 Err(AttributeError::UnknownAttribute(name.to_string()))
192 }
193
194 fn attributes(&self) -> std::collections::HashMap<String, Value> {
195 std::collections::HashMap::new()
196 }
197 }
198
199 impl ModelNaming for CompositeKeyRecord {
200 fn model_name() -> crate::ModelName {
201 crate::ModelName::new("Person")
202 }
203 }
204
205 impl Conversion for CompositeKeyRecord {
206 fn to_key(&self) -> Option<Vec<Value>> {
207 Some(vec![json!("north"), json!(7)])
208 }
209 }
210
211 #[test]
212 fn to_param_supports_string_keys() {
213 struct StringKey;
214
215 impl Attributes for StringKey {
216 fn attribute_names() -> &'static [&'static str] {
217 &["id"]
218 }
219
220 fn read_attribute(&self, _name: &str) -> Option<Value> {
221 Some(Value::String("abc".to_string()))
222 }
223
224 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
225 Err(AttributeError::UnknownAttribute(name.to_string()))
226 }
227
228 fn attributes(&self) -> std::collections::HashMap<String, Value> {
229 std::collections::HashMap::from([(
230 "id".to_string(),
231 Value::String("abc".to_string()),
232 )])
233 }
234 }
235
236 impl ModelNaming for StringKey {}
237 impl Conversion for StringKey {}
238
239 assert_eq!(StringKey.to_param(), Some("abc".to_string()));
240 }
241
242 #[test]
243 fn to_param_joins_composite_keys_with_dashes() {
244 let record = CompositeKeyRecord;
245 assert_eq!(record.to_param(), Some("north-7".to_string()));
246 }
247
248 #[test]
249 fn to_param_returns_none_for_empty_string_key_component() {
250 struct EmptyKey;
251
252 impl Attributes for EmptyKey {
253 fn attribute_names() -> &'static [&'static str] {
254 &["id"]
255 }
256
257 fn read_attribute(&self, _name: &str) -> Option<Value> {
258 Some(Value::String(String::new()))
259 }
260
261 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
262 Err(AttributeError::UnknownAttribute(name.to_string()))
263 }
264
265 fn attributes(&self) -> std::collections::HashMap<String, Value> {
266 std::collections::HashMap::from([("id".to_string(), Value::String(String::new()))])
267 }
268 }
269
270 impl ModelNaming for EmptyKey {}
271 impl Conversion for EmptyKey {}
272
273 assert_eq!(EmptyKey.to_param(), None);
274 }
275
276 #[test]
277 fn to_partial_path_uses_irregular_route_keys() {
278 let record = CompositeKeyRecord;
279 assert_eq!(record.to_partial_path(), "people/person");
280 }
281
282 #[derive(Debug, Clone, Default)]
283 struct StringIdRecord;
284
285 impl Attributes for StringIdRecord {
286 fn attribute_names() -> &'static [&'static str] {
287 &["id"]
288 }
289
290 fn read_attribute(&self, _name: &str) -> Option<Value> {
291 Some(Value::String("abc".to_string()))
292 }
293
294 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
295 Err(AttributeError::UnknownAttribute(name.to_string()))
296 }
297
298 fn attributes(&self) -> std::collections::HashMap<String, Value> {
299 std::collections::HashMap::from([("id".to_string(), Value::String("abc".to_string()))])
300 }
301 }
302
303 impl ModelNaming for StringIdRecord {}
304 impl Conversion for StringIdRecord {}
305
306 #[derive(Debug, Clone, Default)]
307 struct BoolIdRecord;
308
309 impl Attributes for BoolIdRecord {
310 fn attribute_names() -> &'static [&'static str] {
311 &["id"]
312 }
313
314 fn read_attribute(&self, _name: &str) -> Option<Value> {
315 Some(Value::Bool(false))
316 }
317
318 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
319 Err(AttributeError::UnknownAttribute(name.to_string()))
320 }
321
322 fn attributes(&self) -> std::collections::HashMap<String, Value> {
323 std::collections::HashMap::from([("id".to_string(), Value::Bool(false))])
324 }
325 }
326
327 impl ModelNaming for BoolIdRecord {}
328 impl Conversion for BoolIdRecord {}
329
330 #[derive(Debug, Clone, Default)]
331 struct ZeroIdRecord;
332
333 impl Attributes for ZeroIdRecord {
334 fn attribute_names() -> &'static [&'static str] {
335 &["id"]
336 }
337
338 fn read_attribute(&self, _name: &str) -> Option<Value> {
339 Some(json!(0))
340 }
341
342 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
343 Err(AttributeError::UnknownAttribute(name.to_string()))
344 }
345
346 fn attributes(&self) -> std::collections::HashMap<String, Value> {
347 std::collections::HashMap::from([("id".to_string(), json!(0))])
348 }
349 }
350
351 impl ModelNaming for ZeroIdRecord {}
352 impl Conversion for ZeroIdRecord {}
353
354 #[derive(Debug, Clone, Default)]
355 struct ObjectKeyRecord;
356
357 impl Attributes for ObjectKeyRecord {
358 fn attribute_names() -> &'static [&'static str] {
359 &[]
360 }
361
362 fn read_attribute(&self, _name: &str) -> Option<Value> {
363 None
364 }
365
366 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
367 Err(AttributeError::UnknownAttribute(name.to_string()))
368 }
369
370 fn attributes(&self) -> std::collections::HashMap<String, Value> {
371 std::collections::HashMap::new()
372 }
373 }
374
375 impl ModelNaming for ObjectKeyRecord {}
376
377 impl Conversion for ObjectKeyRecord {
378 fn to_key(&self) -> Option<Vec<Value>> {
379 Some(vec![json!({ "region": "north" })])
380 }
381 }
382
383 #[derive(Debug, Clone, Default)]
384 struct ArrayKeyRecord;
385
386 impl Attributes for ArrayKeyRecord {
387 fn attribute_names() -> &'static [&'static str] {
388 &[]
389 }
390
391 fn read_attribute(&self, _name: &str) -> Option<Value> {
392 None
393 }
394
395 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
396 Err(AttributeError::UnknownAttribute(name.to_string()))
397 }
398
399 fn attributes(&self) -> std::collections::HashMap<String, Value> {
400 std::collections::HashMap::new()
401 }
402 }
403
404 impl ModelNaming for ArrayKeyRecord {}
405
406 impl Conversion for ArrayKeyRecord {
407 fn to_key(&self) -> Option<Vec<Value>> {
408 Some(vec![json!(["north", 7])])
409 }
410 }
411
412 #[derive(Debug, Clone, Default)]
413 struct NullCompositeKeyRecord;
414
415 impl Attributes for NullCompositeKeyRecord {
416 fn attribute_names() -> &'static [&'static str] {
417 &[]
418 }
419
420 fn read_attribute(&self, _name: &str) -> Option<Value> {
421 None
422 }
423
424 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
425 Err(AttributeError::UnknownAttribute(name.to_string()))
426 }
427
428 fn attributes(&self) -> std::collections::HashMap<String, Value> {
429 std::collections::HashMap::new()
430 }
431 }
432
433 impl ModelNaming for NullCompositeKeyRecord {}
434
435 impl Conversion for NullCompositeKeyRecord {
436 fn to_key(&self) -> Option<Vec<Value>> {
437 Some(vec![json!("north"), Value::Null])
438 }
439 }
440
441 #[derive(Debug, Clone, Default)]
442 struct EmptyCompositeKeyRecord;
443
444 impl Attributes for EmptyCompositeKeyRecord {
445 fn attribute_names() -> &'static [&'static str] {
446 &[]
447 }
448
449 fn read_attribute(&self, _name: &str) -> Option<Value> {
450 None
451 }
452
453 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
454 Err(AttributeError::UnknownAttribute(name.to_string()))
455 }
456
457 fn attributes(&self) -> std::collections::HashMap<String, Value> {
458 std::collections::HashMap::new()
459 }
460 }
461
462 impl ModelNaming for EmptyCompositeKeyRecord {}
463
464 impl Conversion for EmptyCompositeKeyRecord {
465 fn to_key(&self) -> Option<Vec<Value>> {
466 Some(Vec::new())
467 }
468 }
469
470 #[derive(Debug, Clone, Default)]
471 struct CustomPathRecord;
472
473 impl Attributes for CustomPathRecord {
474 fn attribute_names() -> &'static [&'static str] {
475 &[]
476 }
477
478 fn read_attribute(&self, _name: &str) -> Option<Value> {
479 None
480 }
481
482 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
483 Err(AttributeError::UnknownAttribute(name.to_string()))
484 }
485
486 fn attributes(&self) -> std::collections::HashMap<String, Value> {
487 std::collections::HashMap::new()
488 }
489 }
490
491 impl ModelNaming for CustomPathRecord {
492 fn model_name() -> crate::ModelName {
493 crate::ModelName::new("Admin::BillingAddress")
494 }
495 }
496
497 impl Conversion for CustomPathRecord {}
498
499 mod nested_types {
500 use super::*;
501
502 #[derive(Debug, Clone, Default)]
503 pub(super) struct AuditLog;
504
505 impl Attributes for AuditLog {
506 fn attribute_names() -> &'static [&'static str] {
507 &[]
508 }
509
510 fn read_attribute(&self, _name: &str) -> Option<Value> {
511 None
512 }
513
514 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
515 Err(AttributeError::UnknownAttribute(name.to_string()))
516 }
517
518 fn attributes(&self) -> std::collections::HashMap<String, Value> {
519 std::collections::HashMap::new()
520 }
521 }
522
523 impl ModelNaming for AuditLog {}
524 impl Conversion for AuditLog {}
525 }
526
527 #[derive(Debug, Clone, Default)]
528 struct InvoiceLine;
529
530 impl Attributes for InvoiceLine {
531 fn attribute_names() -> &'static [&'static str] {
532 &[]
533 }
534
535 fn read_attribute(&self, _name: &str) -> Option<Value> {
536 None
537 }
538
539 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
540 Err(AttributeError::UnknownAttribute(name.to_string()))
541 }
542
543 fn attributes(&self) -> std::collections::HashMap<String, Value> {
544 std::collections::HashMap::new()
545 }
546 }
547
548 impl ModelNaming for InvoiceLine {}
549 impl Conversion for InvoiceLine {}
550
551 #[test]
552 fn to_key_supports_string_ids_from_attributes() {
553 assert_eq!(StringIdRecord.to_key(), Some(vec![json!("abc")]));
554 }
555
556 #[test]
557 fn to_key_preserves_false_boolean_ids() {
558 assert_eq!(BoolIdRecord.to_key(), Some(vec![json!(false)]));
559 }
560
561 #[test]
562 fn to_param_supports_false_boolean_ids() {
563 assert_eq!(BoolIdRecord.to_param(), Some("false".to_string()));
564 }
565
566 #[test]
567 fn to_param_supports_zero_numeric_ids() {
568 assert_eq!(ZeroIdRecord.to_param(), Some("0".to_string()));
569 }
570
571 #[test]
572 fn to_param_uses_json_for_object_key_components() {
573 assert_eq!(
574 ObjectKeyRecord.to_param(),
575 Some("{\"region\":\"north\"}".to_string())
576 );
577 }
578
579 #[test]
580 fn to_param_uses_json_for_array_key_components() {
581 assert_eq!(ArrayKeyRecord.to_param(), Some("[\"north\",7]".to_string()));
582 }
583
584 #[test]
585 fn to_param_returns_none_when_a_composite_key_contains_null() {
586 assert_eq!(NullCompositeKeyRecord.to_param(), None);
587 }
588
589 #[test]
590 fn to_param_returns_none_when_to_key_is_empty() {
591 assert_eq!(EmptyCompositeKeyRecord.to_param(), None);
592 }
593
594 #[test]
595 fn to_partial_path_uses_the_model_name_route_key_and_element() {
596 assert_eq!(
597 CustomPathRecord.to_partial_path(),
598 "admin_billing_addresses/billing_address"
599 );
600 }
601
602 #[test]
603 fn model_name_for_conversion_uses_the_last_type_path_segment() {
604 assert_eq!(
605 nested_types::AuditLog::model_name_for_conversion(),
606 "AuditLog"
607 );
608 }
609
610 #[test]
611 fn to_partial_path_uses_default_model_naming_for_compound_types() {
612 assert_eq!(InvoiceLine.to_partial_path(), "invoice_lines/invoice_line");
613 }
614
615 #[test]
616 fn string_ids_round_trip_through_to_param() {
617 assert_eq!(StringIdRecord.to_param(), Some("abc".to_string()));
618 }
619
620 #[derive(Debug, Clone, Default)]
621 struct WhitespaceIdRecord;
622
623 impl Attributes for WhitespaceIdRecord {
624 fn attribute_names() -> &'static [&'static str] {
625 &["id"]
626 }
627
628 fn read_attribute(&self, _name: &str) -> Option<Value> {
629 Some(Value::String(" abc ".to_string()))
630 }
631
632 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
633 Err(AttributeError::UnknownAttribute(name.to_string()))
634 }
635
636 fn attributes(&self) -> std::collections::HashMap<String, Value> {
637 std::collections::HashMap::from([(
638 "id".to_string(),
639 Value::String(" abc ".to_string()),
640 )])
641 }
642 }
643
644 impl ModelNaming for WhitespaceIdRecord {}
645 impl Conversion for WhitespaceIdRecord {}
646
647 #[derive(Debug, Clone, Default)]
648 struct MixedCompositeKeyRecord;
649
650 impl Attributes for MixedCompositeKeyRecord {
651 fn attribute_names() -> &'static [&'static str] {
652 &[]
653 }
654
655 fn read_attribute(&self, _name: &str) -> Option<Value> {
656 None
657 }
658
659 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
660 Err(AttributeError::UnknownAttribute(name.to_string()))
661 }
662
663 fn attributes(&self) -> std::collections::HashMap<String, Value> {
664 std::collections::HashMap::new()
665 }
666 }
667
668 impl ModelNaming for MixedCompositeKeyRecord {
669 fn model_name() -> crate::ModelName {
670 crate::ModelName::new("WarehouseEntry")
671 }
672 }
673
674 impl Conversion for MixedCompositeKeyRecord {
675 fn to_key(&self) -> Option<Vec<Value>> {
676 Some(vec![json!("north"), json!(false), json!(0)])
677 }
678 }
679
680 #[derive(Debug, Clone, Default)]
681 struct AlternateCustomPathRecord;
682
683 impl Attributes for AlternateCustomPathRecord {
684 fn attribute_names() -> &'static [&'static str] {
685 &[]
686 }
687
688 fn read_attribute(&self, _name: &str) -> Option<Value> {
689 None
690 }
691
692 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
693 Err(AttributeError::UnknownAttribute(name.to_string()))
694 }
695
696 fn attributes(&self) -> std::collections::HashMap<String, Value> {
697 std::collections::HashMap::new()
698 }
699 }
700
701 impl ModelNaming for AlternateCustomPathRecord {
702 fn model_name() -> crate::ModelName {
703 let mut model_name = crate::ModelName::new("AttackHelicopter");
704 model_name.element = "ah-64".to_string();
705 model_name
706 }
707 }
708
709 impl Conversion for AlternateCustomPathRecord {}
710
711 mod deeper_nested_types {
712 use super::*;
713
714 pub(crate) mod reports {
715 use super::*;
716
717 #[derive(Debug, Clone, Default)]
718 pub(crate) struct DailySummary;
719
720 impl Attributes for DailySummary {
721 fn attribute_names() -> &'static [&'static str] {
722 &[]
723 }
724
725 fn read_attribute(&self, _name: &str) -> Option<Value> {
726 None
727 }
728
729 fn write_attribute(
730 &mut self,
731 name: &str,
732 _value: Value,
733 ) -> Result<(), AttributeError> {
734 Err(AttributeError::UnknownAttribute(name.to_string()))
735 }
736
737 fn attributes(&self) -> std::collections::HashMap<String, Value> {
738 std::collections::HashMap::new()
739 }
740 }
741
742 impl ModelNaming for DailySummary {}
743 impl Conversion for DailySummary {}
744 }
745 }
746
747 #[test]
748 fn to_param_preserves_surrounding_whitespace_in_string_ids() {
749 assert_eq!(WhitespaceIdRecord.to_key(), Some(vec![json!(" abc ")]));
750 assert_eq!(WhitespaceIdRecord.to_param(), Some(" abc ".to_string()));
751 }
752
753 #[test]
754 fn to_param_joins_mixed_composite_key_components() {
755 assert_eq!(
756 MixedCompositeKeyRecord.to_param(),
757 Some("north-false-0".to_string())
758 );
759 }
760
761 #[test]
762 fn to_partial_path_is_class_specific_for_custom_model_names() {
763 assert_eq!(
764 CustomPathRecord.to_partial_path(),
765 "admin_billing_addresses/billing_address"
766 );
767 assert_eq!(
768 AlternateCustomPathRecord.to_partial_path(),
769 "attack_helicopters/ah-64"
770 );
771 }
772
773 #[test]
774 fn model_name_for_conversion_uses_last_segment_for_deeply_nested_types() {
775 assert_eq!(
776 deeper_nested_types::reports::DailySummary::model_name_for_conversion(),
777 "DailySummary"
778 );
779 }
780
781 fn rails_contact(id: Option<u64>) -> TestUser {
782 TestUser {
783 id,
784 name: String::new(),
785 }
786 }
787
788 #[test]
789 fn test_rails_to_model_default_implementation_returns_self() {
790 let contact = rails_contact(Some(7));
791 assert_eq!(contact.to_model().to_param(), Some("7".to_string()));
792 }
793
794 #[test]
795 fn test_rails_to_key_default_implementation_returns_nil_for_new_records() {
796 assert_eq!(rails_contact(None).to_key(), None);
797 }
798
799 #[test]
800 fn test_rails_to_key_default_implementation_returns_the_id_in_an_array_for_persisted_records() {
801 assert_eq!(rails_contact(Some(1)).to_key(), Some(vec![json!(1)]));
802 }
803
804 #[test]
805 fn test_rails_to_key_does_not_double_wrap_composite_ids() {
806 #[derive(Debug, Clone, Default)]
807 struct ArrayIdRecord;
808
809 impl Attributes for ArrayIdRecord {
810 fn attribute_names() -> &'static [&'static str] {
811 &["id"]
812 }
813
814 fn read_attribute(&self, name: &str) -> Option<Value> {
815 (name == "id").then(|| json!(["north", 7]))
816 }
817
818 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
819 Err(AttributeError::UnknownAttribute(name.to_string()))
820 }
821
822 fn attributes(&self) -> std::collections::HashMap<String, Value> {
823 std::collections::HashMap::from([("id".to_string(), json!(["north", 7]))])
824 }
825 }
826
827 impl ModelNaming for ArrayIdRecord {}
828 impl Conversion for ArrayIdRecord {}
829
830 let record = ArrayIdRecord;
831 assert_eq!(record.to_key(), Some(vec![json!("north"), json!(7)]));
832 assert_eq!(record.to_param(), Some("north-7".to_string()));
833 }
834
835 #[test]
836 fn test_rails_to_param_default_implementation_returns_nil_for_new_records() {
837 assert_eq!(rails_contact(None).to_param(), None);
838 }
839
840 #[test]
841 fn test_rails_to_param_default_implementation_returns_a_string_of_ids_for_persisted_records() {
842 assert_eq!(rails_contact(Some(1)).to_param(), Some("1".to_string()));
843 }
844
845 #[test]
846 fn test_rails_to_param_returns_the_string_joined_by_dash() {
847 assert_eq!(CompositeKeyRecord.to_param(), Some("north-7".to_string()));
848 }
849
850 #[test]
851 fn test_rails_to_param_returns_nil_if_composite_id_is_incomplete() {
852 assert_eq!(NullCompositeKeyRecord.to_param(), None);
853 }
854
855 #[test]
856 fn test_rails_to_param_returns_nil_if_to_key_is_nil() {
857 #[derive(Debug, Clone, Default)]
858 struct NilKeyRecord;
859
860 impl Attributes for NilKeyRecord {
861 fn attribute_names() -> &'static [&'static str] {
862 &[]
863 }
864
865 fn read_attribute(&self, _name: &str) -> Option<Value> {
866 None
867 }
868
869 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
870 Err(AttributeError::UnknownAttribute(name.to_string()))
871 }
872
873 fn attributes(&self) -> std::collections::HashMap<String, Value> {
874 std::collections::HashMap::new()
875 }
876 }
877
878 impl ModelNaming for NilKeyRecord {}
879
880 impl Conversion for NilKeyRecord {
881 fn to_key(&self) -> Option<Vec<Value>> {
882 None
883 }
884 }
885
886 assert_eq!(NilKeyRecord.to_param(), None);
887 }
888
889 #[test]
890 fn test_rails_to_partial_path_default_implementation_returns_a_relative_path() {
891 assert_eq!(
892 rails_contact(Some(1)).to_partial_path(),
893 "test_users/test_user"
894 );
895 assert_eq!(InvoiceLine.to_partial_path(), "invoice_lines/invoice_line");
896 }
897
898 #[test]
899 fn test_rails_to_partial_path_handles_namespaced_models() {
900 #[derive(Debug, Clone, Default)]
901 struct Comanche;
902
903 impl Attributes for Comanche {
904 fn attribute_names() -> &'static [&'static str] {
905 &[]
906 }
907
908 fn read_attribute(&self, _name: &str) -> Option<Value> {
909 None
910 }
911
912 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
913 Err(AttributeError::UnknownAttribute(name.to_string()))
914 }
915
916 fn attributes(&self) -> std::collections::HashMap<String, Value> {
917 std::collections::HashMap::new()
918 }
919 }
920
921 impl ModelNaming for Comanche {
922 fn model_name() -> crate::ModelName {
923 let mut model_name = crate::ModelName::new("Helicopter::Comanche");
924 model_name.route_key = "helicopter/comanches".to_string();
925 model_name
926 }
927 }
928
929 impl Conversion for Comanche {}
930
931 assert_eq!(Comanche.to_partial_path(), "helicopter/comanches/comanche");
932 }
933
934 #[test]
935 fn test_rails_to_partial_path_handles_non_standard_model_name() {
936 assert_eq!(
937 AlternateCustomPathRecord.to_partial_path(),
938 "attack_helicopters/ah-64"
939 );
940 }
941
942 #[test]
943 fn test_rails_to_param_delimiter_allows_redefining_the_delimiter_used_in_to_param() {
944 #[derive(Debug, Clone, Default)]
945 struct SlashDelimitedRecord;
946
947 impl Attributes for SlashDelimitedRecord {
948 fn attribute_names() -> &'static [&'static str] {
949 &[]
950 }
951
952 fn read_attribute(&self, _name: &str) -> Option<Value> {
953 None
954 }
955
956 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
957 Err(AttributeError::UnknownAttribute(name.to_string()))
958 }
959
960 fn attributes(&self) -> std::collections::HashMap<String, Value> {
961 std::collections::HashMap::new()
962 }
963 }
964
965 impl ModelNaming for SlashDelimitedRecord {}
966 impl Conversion for SlashDelimitedRecord {
967 fn to_key(&self) -> Option<Vec<Value>> {
968 Some(vec![json!("north"), json!(7)])
969 }
970
971 fn param_delimiter() -> &'static str {
972 "/"
973 }
974 }
975
976 assert_eq!(SlashDelimitedRecord.to_param(), Some("north/7".to_string()));
977 }
978
979 #[test]
980 fn test_rails_to_param_delimiter_is_defined_per_class() {
981 #[derive(Debug, Clone, Default)]
982 struct DotDelimitedRecord;
983
984 impl Attributes for DotDelimitedRecord {
985 fn attribute_names() -> &'static [&'static str] {
986 &[]
987 }
988
989 fn read_attribute(&self, _name: &str) -> Option<Value> {
990 None
991 }
992
993 fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
994 Err(AttributeError::UnknownAttribute(name.to_string()))
995 }
996
997 fn attributes(&self) -> std::collections::HashMap<String, Value> {
998 std::collections::HashMap::new()
999 }
1000 }
1001
1002 impl ModelNaming for DotDelimitedRecord {}
1003 impl Conversion for DotDelimitedRecord {
1004 fn to_key(&self) -> Option<Vec<Value>> {
1005 Some(vec![json!("north"), json!(7)])
1006 }
1007
1008 fn param_delimiter() -> &'static str {
1009 "."
1010 }
1011 }
1012
1013 assert_eq!(CompositeKeyRecord.to_param(), Some("north-7".to_string()));
1014 assert_eq!(DotDelimitedRecord.to_param(), Some("north.7".to_string()));
1015 }
1016}