1#![forbid(unsafe_code)]
65
66use serde_json::Value;
67use std::collections::HashMap;
68use sz_rust_orm_facade::{Model, ModelExt, RelationLoader};
69
70pub trait BaseModel: Model + ModelExt + RelationLoader + Send + Sync + 'static {
93 fn append() -> Vec<&'static str> {
103 Vec::new()
104 }
105
106 fn get_appended_value(&self, _field: &str) -> Option<Value> {
119 None
120 }
121
122 fn to_json_with_append(&self) -> Value {
145 let mut json = self.to_json();
146 if let Value::Object(ref mut map) = json {
147 for field in Self::append() {
148 let value = self.get_appended_value(field).unwrap_or(Value::Null);
150 map.insert(field.to_string(), value);
151 }
152 }
153 json
154 }
155}
156
157#[derive(Debug, Clone, PartialEq)]
182pub enum MutatorResult {
183 Value(Value),
185 Skip,
188}
189
190pub trait Accessor {
211 fn data_map(&self) -> &HashMap<String, Value>;
213
214 fn data_map_mut(&mut self) -> &mut HashMap<String, Value>;
216
217 fn accessor_cache(&self) -> &HashMap<String, Value>;
219
220 fn accessor_cache_mut(&mut self) -> &mut HashMap<String, Value>;
222
223 fn real_field_name(&self, name: &str) -> String {
228 name.to_string()
229 }
230
231 fn accessor_for(&self, field: &str, value: Option<&Value>) -> Value {
246 let _ = field;
247 value.cloned().unwrap_or(Value::Null)
248 }
249
250 fn get_attr(&mut self, name: &str) -> Value {
259 let field = self.real_field_name(name);
260
261 if let Some(cached) = self.accessor_cache().get(&field) {
263 return cached.clone();
264 }
265
266 let value = self.data_map().get(&field);
268
269 let result = self.accessor_for(&field, value);
271
272 self.accessor_cache_mut().insert(field, result.clone());
274
275 result
276 }
277
278 fn get_data(&self, name: &str) -> Option<&Value> {
280 let field = self.real_field_name(name);
281 self.data_map().get(&field)
282 }
283
284 fn has_attr(&mut self, name: &str) -> bool {
288 !self.get_attr(name).is_null()
289 }
290}
291
292pub trait Mutator: Accessor {
315 fn mutator_for(
329 &mut self,
330 field: &str,
331 value: &Value,
332 merged_data: &HashMap<String, Value>,
333 ) -> Option<MutatorResult>;
334
335 fn set_attr(&mut self, name: &str, value: Value, data: Option<&HashMap<String, Value>>) {
344 let field = self.real_field_name(name);
345
346 let merged_data = if let Some(d) = data {
348 let mut m = self.data_map().clone();
349 m.extend(d.clone());
350 m
351 } else {
352 self.data_map().clone()
353 };
354
355 let result = self.mutator_for(&field, &value, &merged_data);
357
358 match result {
360 Some(MutatorResult::Skip) => {
362 self.accessor_cache_mut().remove(&field);
363 }
364 Some(MutatorResult::Value(v)) => {
366 self.data_map_mut().insert(field.clone(), v);
367 self.accessor_cache_mut().remove(&field);
368 }
369 None => {
371 self.data_map_mut().insert(field.clone(), value);
372 self.accessor_cache_mut().remove(&field);
373 }
374 }
375 }
376
377 fn set_attrs(&mut self, data: &HashMap<String, Value>) {
381 let fields: Vec<(String, Value)> =
382 data.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
383 for (field, value) in fields {
384 self.set_attr(&field, value, Some(data));
385 }
386 }
387}
388
389#[derive(Debug, Clone, Default)]
412pub struct AppendState {
413 dynamic: Option<Vec<String>>,
415}
416
417impl AppendState {
418 pub fn new() -> Self {
420 Self::default()
421 }
422
423 pub fn replace(&mut self, fields: Vec<String>) {
425 self.dynamic = Some(fields);
426 }
427
428 pub fn merge(&mut self, fields: Vec<String>) {
430 match &mut self.dynamic {
431 Some(existing) => {
432 for field in fields {
433 if !existing.contains(&field) {
434 existing.push(field);
435 }
436 }
437 }
438 None => {
439 self.dynamic = Some(fields);
440 }
441 }
442 }
443
444 pub fn dynamic_fields(&self) -> Option<&Vec<String>> {
446 self.dynamic.as_ref()
447 }
448}
449
450pub trait Appendable: BaseModel + Accessor {
473 fn append_state(&self) -> &AppendState;
475
476 fn append_state_mut(&mut self) -> &mut AppendState;
478
479 fn append_dyn(&mut self, fields: Vec<String>) -> &mut Self {
484 self.append_state_mut().replace(fields);
485 self
486 }
487
488 fn append_merge(&mut self, fields: Vec<String>) -> &mut Self {
493 if self.append_state().dynamic_fields().is_none() {
494 let mut combined: Vec<String> = Self::append().iter().map(|s| s.to_string()).collect();
496 for field in fields {
497 if !combined.contains(&field) {
498 combined.push(field);
499 }
500 }
501 self.append_state_mut().replace(combined);
502 } else {
503 self.append_state_mut().merge(fields);
505 }
506 self
507 }
508
509 fn effective_append(&self) -> Vec<String> {
514 match self.append_state().dynamic_fields() {
515 Some(dyn_fields) => dyn_fields.clone(),
516 None => Self::append().iter().map(|s| s.to_string()).collect(),
517 }
518 }
519
520 fn to_json_with_append_cached(&mut self) -> Value {
528 let mut json = self.to_json();
529 if let Value::Object(ref mut map) = json {
530 let fields = self.effective_append();
531 for field in fields {
532 let value = self.get_attr(&field);
534 map.insert(field, value);
535 }
536 }
537 json
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use serde_json::json;
545 use std::collections::HashMap;
546 use sz_rust_orm_facade::Value as OrmValue;
547 use sz_rust_orm_facade::{Model, ModelExt, RelationLoader, TimestampFields};
548
549 struct UserWithoutAppend {
554 user_id: i64,
555 username: String,
556 password: String,
557 }
558
559 impl Model for UserWithoutAppend {
560 type PrimaryKey = i64;
561
562 fn table_name() -> &'static str {
563 "sz_user"
564 }
565
566 fn pk_name() -> &'static str {
567 "user_id"
568 }
569
570 fn pk(&self) -> Self::PrimaryKey {
571 self.user_id
572 }
573
574 fn set_pk(&mut self, pk: Self::PrimaryKey) {
575 self.user_id = pk;
576 }
577
578 fn timestamp_fields() -> Option<TimestampFields> {
579 None
580 }
581
582 fn soft_delete_field() -> Option<&'static str> {
583 None
584 }
585 }
586
587 impl ModelExt for UserWithoutAppend {
588 fn columns() -> Vec<&'static str> {
589 vec!["user_id", "username", "password"]
590 }
591
592 fn fillable() -> Vec<&'static str> {
593 vec!["username", "password"]
594 }
595
596 fn guarded() -> Vec<&'static str> {
597 vec!["user_id"]
598 }
599
600 fn hidden() -> Vec<&'static str> {
601 vec!["password"]
602 }
603
604 fn get_column_value(&self, column: &str) -> Option<OrmValue> {
605 match column {
606 "user_id" => Some(OrmValue::I64(self.user_id)),
607 "username" => Some(OrmValue::String(self.username.clone())),
608 "password" => Some(OrmValue::String(self.password.clone())),
609 _ => None,
610 }
611 }
612
613 fn from_value(&mut self, _map: HashMap<String, OrmValue>) {
614 }
616 }
617
618 impl RelationLoader for UserWithoutAppend {
619 fn get_relation(&self, _name: &str) -> Option<&OrmValue> {
620 None
621 }
622
623 fn set_relation_data(&mut self, _name: &str, _data: OrmValue) {}
624
625 fn get_relation_fk_value(&self, _fk_name: &str) -> String {
626 String::new()
627 }
628 }
629
630 impl BaseModel for UserWithoutAppend {}
631
632 struct CustomerWithAppend {
637 customer_id: i64,
638 status: i32,
639 name: String,
640 }
641
642 impl Model for CustomerWithAppend {
643 type PrimaryKey = i64;
644
645 fn table_name() -> &'static str {
646 "szoa_customer"
647 }
648
649 fn pk_name() -> &'static str {
650 "customer_id"
651 }
652
653 fn pk(&self) -> Self::PrimaryKey {
654 self.customer_id
655 }
656
657 fn set_pk(&mut self, pk: Self::PrimaryKey) {
658 self.customer_id = pk;
659 }
660 }
661
662 impl ModelExt for CustomerWithAppend {
663 fn columns() -> Vec<&'static str> {
664 vec!["customer_id", "status", "name"]
665 }
666
667 fn fillable() -> Vec<&'static str> {
668 vec!["status", "name"]
669 }
670
671 fn guarded() -> Vec<&'static str> {
672 vec!["customer_id"]
673 }
674
675 fn get_column_value(&self, column: &str) -> Option<OrmValue> {
676 match column {
677 "customer_id" => Some(OrmValue::I64(self.customer_id)),
678 "status" => Some(OrmValue::I32(self.status)),
679 "name" => Some(OrmValue::String(self.name.clone())),
680 _ => None,
681 }
682 }
683
684 fn from_value(&mut self, _map: HashMap<String, OrmValue>) {}
685 }
686
687 impl RelationLoader for CustomerWithAppend {
688 fn get_relation(&self, _name: &str) -> Option<&OrmValue> {
689 None
690 }
691
692 fn set_relation_data(&mut self, _name: &str, _data: OrmValue) {}
693
694 fn get_relation_fk_value(&self, _fk_name: &str) -> String {
695 String::new()
696 }
697 }
698
699 impl BaseModel for CustomerWithAppend {
700 fn append() -> Vec<&'static str> {
701 vec!["status_text"]
702 }
703
704 fn get_appended_value(&self, field: &str) -> Option<Value> {
705 match field {
706 "status_text" => Some(json!(match self.status {
707 0 => "禁用",
708 1 => "启用",
709 _ => "未知",
710 })),
711 _ => None,
712 }
713 }
714 }
715
716 #[test]
721 fn test_base_model_table_name() {
722 assert_eq!(UserWithoutAppend::table_name(), "sz_user");
724 assert_eq!(CustomerWithAppend::table_name(), "szoa_customer");
725 }
726
727 #[test]
728 fn test_base_model_pk_name() {
729 assert_eq!(UserWithoutAppend::pk_name(), "user_id");
731 assert_eq!(CustomerWithAppend::pk_name(), "customer_id");
732 }
733
734 #[test]
735 fn test_base_model_fillable() {
736 assert_eq!(UserWithoutAppend::fillable(), vec!["username", "password"]);
738 assert_eq!(CustomerWithAppend::fillable(), vec!["status", "name"]);
739 }
740
741 #[test]
742 fn test_base_model_guarded() {
743 assert_eq!(UserWithoutAppend::guarded(), vec!["user_id"]);
745 assert_eq!(CustomerWithAppend::guarded(), vec!["customer_id"]);
746 }
747
748 #[test]
749 fn test_base_model_hidden() {
750 assert_eq!(UserWithoutAppend::hidden(), vec!["password"]);
752 assert_eq!(CustomerWithAppend::hidden(), Vec::<&str>::new());
753 }
754
755 #[test]
756 fn test_base_model_pk_value() {
757 let user = UserWithoutAppend {
758 user_id: 42,
759 username: "alice".to_string(),
760 password: "secret".to_string(),
761 };
762 assert_eq!(user.pk(), 42);
763 }
764
765 #[test]
770 fn test_base_model_append_default_empty() {
771 assert_eq!(UserWithoutAppend::append(), Vec::<&str>::new());
773 }
774
775 #[test]
776 fn test_base_model_append_with_status_text() {
777 assert_eq!(CustomerWithAppend::append(), vec!["status_text"]);
779 }
780
781 #[test]
782 fn test_base_model_get_appended_value_default_none() {
783 let user = UserWithoutAppend {
784 user_id: 1,
785 username: "alice".to_string(),
786 password: "secret".to_string(),
787 };
788 assert_eq!(user.get_appended_value("any_field"), None);
790 }
791
792 #[test]
793 fn test_base_model_get_appended_value_status_text() {
794 let customer = CustomerWithAppend {
796 customer_id: 1,
797 status: 0,
798 name: "Alice Corp".to_string(),
799 };
800 assert_eq!(
801 customer.get_appended_value("status_text"),
802 Some(json!("禁用"))
803 );
804
805 let customer = CustomerWithAppend {
806 customer_id: 1,
807 status: 1,
808 name: "Alice Corp".to_string(),
809 };
810 assert_eq!(
811 customer.get_appended_value("status_text"),
812 Some(json!("启用"))
813 );
814
815 let customer = CustomerWithAppend {
816 customer_id: 1,
817 status: 99,
818 name: "Alice Corp".to_string(),
819 };
820 assert_eq!(
821 customer.get_appended_value("status_text"),
822 Some(json!("未知"))
823 );
824
825 assert_eq!(customer.get_appended_value("unknown_field"), None);
827 }
828
829 #[test]
834 fn test_base_model_to_json_without_append() {
835 let user = UserWithoutAppend {
837 user_id: 1,
838 username: "alice".to_string(),
839 password: "secret".to_string(),
840 };
841 let json = user.to_json_with_append();
842
843 assert_eq!(json["user_id"], 1);
845 assert_eq!(json["username"], "alice");
846 assert!(json.get("password").is_none(), "password 应被 hidden 隐藏");
847 assert!(json.get("status_text").is_none(), "无 append 字段");
848 }
849
850 #[test]
851 fn test_base_model_to_json_with_append() {
852 let customer = CustomerWithAppend {
854 customer_id: 1,
855 status: 1,
856 name: "Alice Corp".to_string(),
857 };
858 let json = customer.to_json_with_append();
859
860 assert_eq!(json["customer_id"], 1);
862 assert_eq!(json["status"], 1);
863 assert_eq!(json["name"], "Alice Corp");
864
865 assert_eq!(json["status_text"], "启用");
867 }
868
869 #[test]
870 fn test_base_model_to_json_append_field_order() {
871 let customer = CustomerWithAppend {
873 customer_id: 1,
874 status: 0,
875 name: "Test".to_string(),
876 };
877 let json = customer.to_json_with_append();
878
879 if let Value::Object(map) = json {
880 let keys: Vec<&String> = map.keys().collect();
881 let customer_id_pos = keys.iter().position(|k| *k == "customer_id").unwrap();
883 let status_pos = keys.iter().position(|k| *k == "status").unwrap();
884 let name_pos = keys.iter().position(|k| *k == "name").unwrap();
885 let status_text_pos = keys.iter().position(|k| *k == "status_text").unwrap();
886
887 assert!(
888 customer_id_pos < status_text_pos,
889 "customer_id 应在 status_text 之前"
890 );
891 assert!(status_pos < status_text_pos, "status 应在 status_text 之前");
892 assert!(name_pos < status_text_pos, "name 应在 status_text 之前");
893 } else {
894 panic!("to_json_with_append 应返回 JSON Object");
895 }
896 }
897
898 #[test]
903 fn test_php_consistency_model_name_aligns_php_name_property() {
904 assert_eq!(CustomerWithAppend::table_name(), "szoa_customer");
907 }
908
909 #[test]
910 fn test_php_consistency_model_pk_aligns_php_pk_property() {
911 assert_eq!(CustomerWithAppend::pk_name(), "customer_id");
914 }
915
916 #[test]
917 fn test_php_consistency_model_append_aligns_php_append_property() {
918 assert_eq!(CustomerWithAppend::append(), vec!["status_text"]);
921 }
922
923 #[test]
924 fn test_php_consistency_model_hidden_aligns_php_hidden_property() {
925 let user = UserWithoutAppend {
929 user_id: 1,
930 username: "alice".to_string(),
931 password: "secret".to_string(),
932 };
933 let json = user.to_json_with_append();
934 assert!(
935 json.get("password").is_none(),
936 "password 应被 hidden 隐藏(对齐 PHP $hidden)"
937 );
938 }
939
940 #[test]
941 fn test_php_consistency_get_xxx_attr_aligns_php_accessor() {
942 let test_cases = vec![(0i32, "禁用"), (1, "启用"), (99, "未知")];
945
946 for (status, expected) in test_cases {
947 let customer = CustomerWithAppend {
948 customer_id: 1,
949 status,
950 name: "Test".to_string(),
951 };
952 assert_eq!(
953 customer.get_appended_value("status_text"),
954 Some(json!(expected)),
955 "status={} 应返回 '{}'",
956 status,
957 expected
958 );
959 }
960 }
961
962 #[test]
963 fn test_php_consistency_serialization_includes_append_fields() {
964 let customer = CustomerWithAppend {
967 customer_id: 1,
968 status: 1,
969 name: "Alice Corp".to_string(),
970 };
971 let json = customer.to_json_with_append();
972
973 assert_eq!(json["customer_id"], 1);
975 assert_eq!(json["status"], 1);
976 assert_eq!(json["name"], "Alice Corp");
977
978 assert_eq!(json["status_text"], "启用");
980 }
981
982 struct AccessorTestModel {
988 data: HashMap<String, Value>,
989 get_cache: HashMap<String, Value>,
990 }
991
992 impl AccessorTestModel {
993 fn new() -> Self {
994 Self {
995 data: HashMap::new(),
996 get_cache: HashMap::new(),
997 }
998 }
999
1000 fn with_data(mut self, key: &str, value: Value) -> Self {
1001 self.data.insert(key.to_string(), value);
1002 self
1003 }
1004 }
1005
1006 impl Accessor for AccessorTestModel {
1007 fn data_map(&self) -> &HashMap<String, Value> {
1008 &self.data
1009 }
1010
1011 fn data_map_mut(&mut self) -> &mut HashMap<String, Value> {
1012 &mut self.data
1013 }
1014
1015 fn accessor_cache(&self) -> &HashMap<String, Value> {
1016 &self.get_cache
1017 }
1018
1019 fn accessor_cache_mut(&mut self) -> &mut HashMap<String, Value> {
1020 &mut self.get_cache
1021 }
1022
1023 fn accessor_for(&self, field: &str, value: Option<&Value>) -> Value {
1029 match field {
1030 "status_text" => {
1031 let status = self
1032 .data
1033 .get("status")
1034 .and_then(|v| v.as_i64())
1035 .unwrap_or(-1);
1036 json!(match status {
1037 0 => "禁用",
1038 1 => "启用",
1039 _ => "未知",
1040 })
1041 }
1042 "rentarea_ids" => {
1043 let raw = value.and_then(|v| v.as_str()).unwrap_or("");
1045 if raw.is_empty() {
1046 json!([])
1047 } else {
1048 let arr: Vec<Value> = raw
1049 .split(',')
1050 .filter(|s| !s.is_empty())
1051 .filter_map(|s| s.parse::<i64>().ok())
1052 .map(Value::from)
1053 .collect();
1054 json!(arr)
1055 }
1056 }
1057 "contract_price" => {
1058 let price = value
1061 .and_then(|v| {
1062 if v.is_null() {
1063 Some(0.0)
1064 } else if let Some(f) = v.as_f64() {
1065 Some(f)
1066 } else if let Some(s) = v.as_str() {
1067 s.parse::<f64>().ok()
1068 } else {
1069 None
1070 }
1071 })
1072 .unwrap_or(0.0);
1073 json!(price)
1074 }
1075 _ => value.cloned().unwrap_or(Value::Null),
1076 }
1077 }
1078 }
1079
1080 impl Mutator for AccessorTestModel {
1081 fn mutator_for(
1084 &mut self,
1085 field: &str,
1086 value: &Value,
1087 merged_data: &HashMap<String, Value>,
1088 ) -> Option<MutatorResult> {
1089 match field {
1090 "rentarea_ids" => {
1091 let arr: Vec<String> = match value {
1092 Value::Array(items) => items
1093 .iter()
1094 .filter_map(|v| {
1095 let s = match v {
1096 Value::String(s) => s.trim().to_string(),
1097 _ => v.to_string(),
1098 };
1099 if s.is_empty() {
1100 None
1101 } else {
1102 Some(s)
1103 }
1104 })
1105 .collect(),
1106 _ => return Some(MutatorResult::Value(Value::String(String::new()))),
1107 };
1108 Some(MutatorResult::Value(Value::String(arr.join(","))))
1109 }
1110 "field_b" => {
1111 let field_a = merged_data
1113 .get("field_a")
1114 .and_then(|v| v.as_i64())
1115 .unwrap_or(0);
1116 let val_b = value.as_i64().unwrap_or(0);
1117 Some(MutatorResult::Value(json!(format!(
1118 "{}_{}",
1119 field_a, val_b
1120 ))))
1121 }
1122 "special_attr" => {
1123 self.data.insert("field_a".to_string(), json!("A"));
1125 self.data.insert("field_b".to_string(), json!("B"));
1126 Some(MutatorResult::Skip)
1127 }
1128 _ => None,
1129 }
1130 }
1131 }
1132
1133 #[test]
1134 fn test_accessor_basic_status_text() {
1135 let mut model = AccessorTestModel::new().with_data("status", json!(0));
1137 assert_eq!(model.get_attr("status_text"), json!("禁用"));
1138
1139 let mut model = AccessorTestModel::new().with_data("status", json!(1));
1140 assert_eq!(model.get_attr("status_text"), json!("启用"));
1141
1142 let mut model = AccessorTestModel::new().with_data("status", json!(99));
1143 assert_eq!(model.get_attr("status_text"), json!("未知"));
1144 }
1145
1146 #[test]
1147 fn test_accessor_real_field_value() {
1148 let mut model = AccessorTestModel::new().with_data("rentarea_ids", json!("1,2,3"));
1150 assert_eq!(model.get_attr("rentarea_ids"), json!([1, 2, 3]));
1151
1152 let mut model = AccessorTestModel::new().with_data("rentarea_ids", json!(""));
1153 assert_eq!(model.get_attr("rentarea_ids"), json!([]));
1154 }
1155
1156 #[test]
1157 fn test_accessor_float_coercion() {
1158 let mut model = AccessorTestModel::new().with_data("contract_price", Value::Null);
1160 assert_eq!(model.get_attr("contract_price"), json!(0.0));
1161
1162 let mut model = AccessorTestModel::new().with_data("contract_price", json!("100.5"));
1163 assert_eq!(model.get_attr("contract_price"), json!(100.5));
1164 }
1165
1166 #[test]
1167 fn test_accessor_cache_hit() {
1168 let mut model = AccessorTestModel::new().with_data("status", json!(1));
1170 let v1 = model.get_attr("status_text");
1171 model.data.insert("status".to_string(), json!(0));
1173 let v2 = model.get_attr("status_text");
1174 assert_eq!(v1, v2, "缓存命中,访问器不重新执行");
1175 assert_eq!(v1, json!("启用"));
1176 }
1177
1178 #[test]
1179 fn test_accessor_cache_invalidation_on_set_same_field() {
1180 let mut model = AccessorTestModel::new().with_data("status", json!(1));
1182 let v1 = model.get_attr("status_text");
1183 assert_eq!(v1, json!("启用"));
1184
1185 model.set_attr("status", json!(0), None);
1188
1189 let v2 = model.get_attr("status_text");
1191 assert_eq!(v2, json!("启用"), "status_text 缓存未失效(PHP bug 复刻)");
1192 }
1193
1194 #[test]
1195 fn test_mutator_basic_array_to_string() {
1196 let mut model = AccessorTestModel::new();
1198 model.set_attr("rentarea_ids", json!([1, 2, 3]), None);
1199 assert_eq!(model.data.get("rentarea_ids"), Some(&json!("1,2,3")));
1200
1201 model.set_attr("rentarea_ids", json!([]), None);
1203 assert_eq!(model.data.get("rentarea_ids"), Some(&json!("")));
1204 }
1205
1206 #[test]
1207 fn test_mutator_skip_php_bug_replication() {
1208 let mut model = AccessorTestModel::new();
1211 model.set_attr("special_attr", json!("X"), None);
1212
1213 assert!(
1215 !model.data.contains_key("special_attr"),
1216 "special_attr 应被跳过(PHP bug 复刻)"
1217 );
1218 assert_eq!(model.data.get("field_a"), Some(&json!("A")));
1220 assert_eq!(model.data.get("field_b"), Some(&json!("B")));
1221 }
1222
1223 #[test]
1224 fn test_mutator_merged_data() {
1225 let mut model = AccessorTestModel::new();
1228 model.data.insert("field_a".to_string(), json!(1));
1229
1230 let mut batch = HashMap::new();
1231 batch.insert("field_a".to_string(), json!(100));
1232 batch.insert("field_b".to_string(), json!(2));
1233
1234 model.set_attrs(&batch);
1235
1236 assert_eq!(model.data.get("field_b"), Some(&json!("100_2")));
1238 }
1239
1240 #[test]
1241 fn test_set_attrs_batch() {
1242 let mut model = AccessorTestModel::new();
1244 let mut batch = HashMap::new();
1245 batch.insert("status".to_string(), json!(1));
1246 batch.insert("name".to_string(), json!("Alice"));
1247
1248 model.set_attrs(&batch);
1249
1250 assert_eq!(model.data.get("status"), Some(&json!(1)));
1251 assert_eq!(model.data.get("name"), Some(&json!("Alice")));
1252 }
1253
1254 #[test]
1255 fn test_has_attr_triggers_accessor() {
1256 let mut model = AccessorTestModel::new().with_data("status", json!(1));
1258 assert!(model.has_attr("status_text"), "status_text 应存在");
1259
1260 assert!(!model.has_attr("nonexistent"), "不存在的字段应返回 false");
1262 }
1263
1264 #[test]
1265 fn test_get_data_returns_raw_value() {
1266 let model = AccessorTestModel::new().with_data("rentarea_ids", json!("1,2,3"));
1268 assert_eq!(model.get_data("rentarea_ids"), Some(&json!("1,2,3")));
1270 }
1271
1272 #[test]
1273 fn test_accessor_for_unknown_field_returns_null() {
1274 let mut model = AccessorTestModel::new();
1276 let v = model.get_attr("nonexistent_field");
1277 assert!(v.is_null(), "未知字段应返回 Null");
1278 }
1279
1280 #[test]
1281 fn test_real_field_name_default_identity() {
1282 let model = AccessorTestModel::new();
1284 assert_eq!(model.real_field_name("status_text"), "status_text");
1285 assert_eq!(model.real_field_name("user_id"), "user_id");
1286 }
1287
1288 #[test]
1293 fn test_php_consistency_accessor_cache_asymmetric_invalidation() {
1294 let mut model = AccessorTestModel::new().with_data("status", json!(1));
1297
1298 let v1 = model.get_attr("status_text");
1300 assert_eq!(v1, json!("启用"));
1301
1302 model.set_attr("status", json!(0), None);
1304
1305 let v2 = model.get_attr("status_text");
1307 assert_eq!(
1308 v2,
1309 json!("启用"),
1310 "PHP bug 复刻:status_text 缓存未失效,仍返回旧值"
1311 );
1312 }
1313
1314 #[test]
1315 fn test_php_consistency_mutator_skip_with_data_modification() {
1316 let mut model = AccessorTestModel::new();
1319 model.set_attr("special_attr", json!("X"), None);
1320
1321 assert!(
1324 !model.data.contains_key("special_attr"),
1325 "special_attr 应被跳过"
1326 );
1327 assert_eq!(model.data.get("field_a"), Some(&json!("A")));
1329 assert_eq!(model.data.get("field_b"), Some(&json!("B")));
1330 }
1331
1332 #[test]
1333 fn test_php_consistency_mutator_receives_merged_data() {
1334 let mut model = AccessorTestModel::new();
1337 model.data.insert("field_a".to_string(), json!(1));
1338
1339 let mut batch = HashMap::new();
1341 batch.insert("field_a".to_string(), json!(100));
1342 batch.insert("field_b".to_string(), json!(2));
1343 model.set_attrs(&batch);
1344
1345 assert_eq!(
1348 model.data.get("field_b"),
1349 Some(&json!("100_2")),
1350 "修改器应使用 merged_data 中的 field_a=100"
1351 );
1352 }
1353
1354 #[test]
1355 fn test_php_consistency_append_field_without_accessor_returns_null() {
1356 let mut model = AccessorTestModel::new();
1359 let v = model.get_attr("nonexistent_append_field");
1360 assert!(v.is_null(), "PHP 行为复刻:$append 字段无访问器应返回 null");
1361 }
1362
1363 #[test]
1364 fn test_php_consistency_isset_triggers_accessor() {
1365 let mut model = AccessorTestModel::new().with_data("status", json!(1));
1368
1369 assert!(model.has_attr("status_text"));
1371
1372 let v = model.get_attr("status_text");
1374 assert_eq!(v, json!("启用"));
1375 }
1376
1377 #[test]
1378 fn test_php_consistency_accessor_overrides_raw_value() {
1379 let mut model = AccessorTestModel::new().with_data("contract_price", Value::Null);
1383
1384 assert_eq!(model.get_data("contract_price"), Some(&Value::Null));
1386
1387 assert_eq!(model.get_attr("contract_price"), json!(0.0));
1389 }
1390
1391 #[test]
1392 fn test_php_consistency_set_attrs_preserves_batch_context() {
1393 let mut model = AccessorTestModel::new();
1396
1397 let mut batch = HashMap::new();
1398 batch.insert("field_a".to_string(), json!(50));
1399 batch.insert("field_b".to_string(), json!(99));
1400
1401 model.set_attrs(&batch);
1402
1403 assert_eq!(model.data.get("field_b"), Some(&json!("50_99")));
1405 assert_eq!(model.data.get("field_a"), Some(&json!(50)));
1407 }
1408
1409 struct AppendableTestModel {
1418 data: HashMap<String, Value>,
1419 get_cache: HashMap<String, Value>,
1420 append_state: AppendState,
1421 }
1422
1423 impl AppendableTestModel {
1424 fn new() -> Self {
1425 Self {
1426 data: HashMap::new(),
1427 get_cache: HashMap::new(),
1428 append_state: AppendState::new(),
1429 }
1430 }
1431
1432 fn with_data(mut self, key: &str, value: Value) -> Self {
1433 self.data.insert(key.to_string(), value);
1434 self
1435 }
1436 }
1437
1438 impl Model for AppendableTestModel {
1439 type PrimaryKey = i64;
1440
1441 fn table_name() -> &'static str {
1442 "test_appendable"
1443 }
1444
1445 fn pk_name() -> &'static str {
1446 "id"
1447 }
1448
1449 fn pk(&self) -> Self::PrimaryKey {
1450 self.data.get("id").and_then(|v| v.as_i64()).unwrap_or(0)
1451 }
1452
1453 fn set_pk(&mut self, pk: Self::PrimaryKey) {
1454 self.data.insert("id".to_string(), json!(pk));
1455 }
1456 }
1457
1458 impl ModelExt for AppendableTestModel {
1459 fn columns() -> Vec<&'static str> {
1460 vec![
1461 "id",
1462 "status",
1463 "name",
1464 "password",
1465 "add_time",
1466 "sales_initial",
1467 "sales_actual",
1468 ]
1469 }
1470
1471 fn fillable() -> Vec<&'static str> {
1472 vec![
1473 "status",
1474 "name",
1475 "password",
1476 "add_time",
1477 "sales_initial",
1478 "sales_actual",
1479 ]
1480 }
1481
1482 fn guarded() -> Vec<&'static str> {
1483 vec!["id"]
1484 }
1485
1486 fn hidden() -> Vec<&'static str> {
1487 vec!["password"]
1489 }
1490
1491 fn get_column_value(&self, column: &str) -> Option<OrmValue> {
1492 match column {
1493 "id" => self
1494 .data
1495 .get("id")
1496 .and_then(|v| v.as_i64())
1497 .map(OrmValue::I64),
1498 "status" => self
1499 .data
1500 .get("status")
1501 .and_then(|v| v.as_i64())
1502 .map(|i| OrmValue::I32(i as i32)),
1503 "name" => self
1504 .data
1505 .get("name")
1506 .and_then(|v| v.as_str())
1507 .map(|s| OrmValue::String(s.to_string())),
1508 "password" => self
1509 .data
1510 .get("password")
1511 .and_then(|v| v.as_str())
1512 .map(|s| OrmValue::String(s.to_string())),
1513 "add_time" => self
1514 .data
1515 .get("add_time")
1516 .and_then(|v| v.as_i64())
1517 .map(OrmValue::I64),
1518 "sales_initial" => self
1519 .data
1520 .get("sales_initial")
1521 .and_then(|v| v.as_i64())
1522 .map(OrmValue::I64),
1523 "sales_actual" => self
1524 .data
1525 .get("sales_actual")
1526 .and_then(|v| v.as_i64())
1527 .map(OrmValue::I64),
1528 _ => None,
1529 }
1530 }
1531
1532 fn from_value(&mut self, _map: HashMap<String, OrmValue>) {}
1533 }
1534
1535 impl RelationLoader for AppendableTestModel {
1536 fn get_relation(&self, _name: &str) -> Option<&OrmValue> {
1537 None
1538 }
1539 fn set_relation_data(&mut self, _name: &str, _data: OrmValue) {}
1540 fn get_relation_fk_value(&self, _fk_name: &str) -> String {
1541 String::new()
1542 }
1543 }
1544
1545 impl BaseModel for AppendableTestModel {
1546 fn append() -> Vec<&'static str> {
1547 vec!["status_text", "no_accessor_field"]
1549 }
1550 }
1553
1554 impl Accessor for AppendableTestModel {
1555 fn data_map(&self) -> &HashMap<String, Value> {
1556 &self.data
1557 }
1558
1559 fn data_map_mut(&mut self) -> &mut HashMap<String, Value> {
1560 &mut self.data
1561 }
1562
1563 fn accessor_cache(&self) -> &HashMap<String, Value> {
1564 &self.get_cache
1565 }
1566
1567 fn accessor_cache_mut(&mut self) -> &mut HashMap<String, Value> {
1568 &mut self.get_cache
1569 }
1570
1571 fn accessor_for(&self, field: &str, _value: Option<&Value>) -> Value {
1576 match field {
1577 "status_text" => {
1578 let status = self
1579 .data
1580 .get("status")
1581 .and_then(|v| v.as_i64())
1582 .unwrap_or(-1);
1583 json!(match status {
1584 0 => "禁用",
1585 1 => "启用",
1586 _ => "未知",
1587 })
1588 }
1589 "stat_day" => {
1590 let timestamp = self
1592 .data
1593 .get("add_time")
1594 .and_then(|v| v.as_i64())
1595 .unwrap_or(0);
1596 json!(format!("day_{}", timestamp / 86400))
1597 }
1598 "product_sales" => {
1599 let initial = self
1601 .data
1602 .get("sales_initial")
1603 .and_then(|v| v.as_i64())
1604 .unwrap_or(0);
1605 let actual = self
1606 .data
1607 .get("sales_actual")
1608 .and_then(|v| v.as_i64())
1609 .unwrap_or(0);
1610 json!(initial + actual)
1611 }
1612 _ => Value::Null,
1613 }
1614 }
1615 }
1616
1617 impl Appendable for AppendableTestModel {
1618 fn append_state(&self) -> &AppendState {
1619 &self.append_state
1620 }
1621
1622 fn append_state_mut(&mut self) -> &mut AppendState {
1623 &mut self.append_state
1624 }
1625 }
1626
1627 #[test]
1630 fn test_base_model_to_json_with_append_outputs_null_for_no_accessor() {
1631 let model = AppendableTestModel::new()
1634 .with_data("id", json!(1))
1635 .with_data("status", json!(1));
1636 let json = model.to_json_with_append();
1637 assert_eq!(
1640 json["status_text"],
1641 Value::Null,
1642 "无访问器 append 字段应输出 null"
1643 );
1644 assert_eq!(
1645 json["no_accessor_field"],
1646 Value::Null,
1647 "无访问器 append 字段应输出 null"
1648 );
1649 }
1650
1651 #[test]
1652 fn test_appendable_to_json_with_append_cached_uses_accessor() {
1653 let mut model = AppendableTestModel::new()
1655 .with_data("id", json!(1))
1656 .with_data("status", json!(1));
1657 let json = model.to_json_with_append_cached();
1658 assert_eq!(json["status_text"], "启用");
1660 assert_eq!(json["no_accessor_field"], Value::Null);
1662 }
1663
1664 #[test]
1665 fn test_appendable_caches_accessor_result() {
1666 let mut model = AppendableTestModel::new()
1669 .with_data("id", json!(1))
1670 .with_data("status", json!(1));
1671 let json1 = model.to_json_with_append_cached();
1672 assert_eq!(json1["status_text"], "启用");
1673
1674 model.data.insert("status".to_string(), json!(0));
1676 let json2 = model.to_json_with_append_cached();
1677 assert_eq!(
1678 json2["status_text"], "启用",
1679 "缓存命中,访问器不重新执行(PHP bug 复刻)"
1680 );
1681 }
1682
1683 #[test]
1684 fn test_append_field_bypasses_hidden_filter() {
1685 let mut model = AppendableTestModel::new()
1688 .with_data("id", json!(1))
1689 .with_data("status", json!(1))
1690 .with_data("password", json!("secret"));
1691 let json = model.to_json_with_append_cached();
1692 assert!(json.get("password").is_none(), "password 应被 hidden 过滤");
1694 assert_eq!(json["status_text"], "启用");
1696 }
1697
1698 #[test]
1699 fn test_append_dyn_overrides_static_append() {
1700 let mut model = AppendableTestModel::new()
1703 .with_data("id", json!(1))
1704 .with_data("status", json!(1));
1705 model.append_dyn(vec!["dynamic_field".to_string()]);
1708 let json = model.to_json_with_append_cached();
1709 assert!(
1711 json.get("status_text").is_none(),
1712 "status_text 应被动态 append 覆盖"
1713 );
1714 assert!(
1716 json.get("no_accessor_field").is_none(),
1717 "no_accessor_field 应被动态 append 覆盖"
1718 );
1719 assert_eq!(json["dynamic_field"], Value::Null);
1721 }
1722
1723 #[test]
1724 fn test_append_merge_combines_with_static() {
1725 let mut model = AppendableTestModel::new()
1728 .with_data("id", json!(1))
1729 .with_data("status", json!(1));
1730 model.append_merge(vec!["extra_field".to_string()]);
1731 let json = model.to_json_with_append_cached();
1732 assert_eq!(json["status_text"], "启用");
1734 assert_eq!(json["no_accessor_field"], Value::Null);
1735 assert_eq!(json["extra_field"], Value::Null);
1737 }
1738
1739 #[test]
1740 fn test_append_dyn_returns_self_for_chaining() {
1741 let mut model = AppendableTestModel::new()
1744 .with_data("id", json!(1))
1745 .with_data("status", json!(1));
1746 model
1748 .append_merge(vec!["field1".to_string()])
1749 .append_merge(vec!["field2".to_string()]);
1750 let json = model.to_json_with_append_cached();
1751 assert!(json.get("field1").is_some(), "链式 append_merge 应生效");
1752 assert!(json.get("field2").is_some(), "链式 append_merge 应生效");
1753 assert_eq!(json["status_text"], "启用");
1755 }
1756
1757 #[test]
1758 fn test_effective_append_priority() {
1759 let model = AppendableTestModel::new();
1761 assert_eq!(
1763 model.effective_append(),
1764 vec!["status_text".to_string(), "no_accessor_field".to_string()]
1765 );
1766
1767 let mut model = model;
1768 model.append_dyn(vec!["override".to_string()]);
1769 assert_eq!(model.effective_append(), vec!["override".to_string()]);
1770 }
1771
1772 #[test]
1773 fn test_append_state_replace_and_merge() {
1774 let mut state = AppendState::new();
1776 assert!(state.dynamic_fields().is_none(), "初始状态无动态字段");
1777
1778 state.replace(vec!["a".to_string(), "b".to_string()]);
1779 assert_eq!(
1780 state.dynamic_fields().unwrap(),
1781 &vec!["a".to_string(), "b".to_string()]
1782 );
1783
1784 state.merge(vec!["b".to_string(), "c".to_string()]);
1786 assert_eq!(
1787 state.dynamic_fields().unwrap(),
1788 &vec!["a".to_string(), "b".to_string(), "c".to_string()]
1789 );
1790 }
1791
1792 #[test]
1795 fn test_php_consistency_status_text_pattern() {
1796 let test_cases = vec![(0i64, "禁用"), (1, "启用"), (99, "未知")];
1799 for (status, expected) in test_cases {
1800 let mut model = AppendableTestModel::new()
1801 .with_data("id", json!(1))
1802 .with_data("status", json!(status));
1803 let json = model.to_json_with_append_cached();
1804 assert_eq!(
1805 json["status_text"], expected,
1806 "status={} 应返回 '{}'",
1807 status, expected
1808 );
1809 }
1810 }
1811
1812 #[test]
1813 fn test_php_consistency_stat_day_pattern() {
1814 let mut model = AppendableTestModel::new()
1819 .with_data("id", json!(1))
1820 .with_data("add_time", json!(1690000000));
1821 model.append_merge(vec!["stat_day".to_string()]);
1823 let json = model.to_json_with_append_cached();
1824 assert_eq!(json["stat_day"], "day_19560");
1826 }
1827
1828 #[test]
1829 fn test_php_consistency_product_sales_pattern() {
1830 let mut model = AppendableTestModel::new()
1833 .with_data("id", json!(1))
1834 .with_data("sales_initial", json!(100))
1835 .with_data("sales_actual", json!(50));
1836 model.append_merge(vec!["product_sales".to_string()]);
1838 let json = model.to_json_with_append_cached();
1839 assert_eq!(json["product_sales"], 150);
1840 }
1841
1842 #[test]
1843 fn test_php_consistency_append_always_outputs_even_without_accessor() {
1844 let mut model = AppendableTestModel::new()
1848 .with_data("id", json!(1))
1849 .with_data("status", json!(1));
1850 let json = model.to_json_with_append_cached();
1852 assert_eq!(
1854 json["no_accessor_field"],
1855 Value::Null,
1856 "PHP 行为复刻:append 字段无访问器应输出 null"
1857 );
1858 }
1859
1860 #[test]
1861 fn test_php_consistency_append_overrides_hidden() {
1862 let mut model = AppendableTestModel::new()
1866 .with_data("id", json!(1))
1867 .with_data("status", json!(1))
1868 .with_data("password", json!("secret"));
1869 let json = model.to_json_with_append_cached();
1872 assert!(json.get("password").is_none(), "password 应被 hidden 过滤");
1873 assert_eq!(json["status_text"], "启用", "append 字段应绕过 hidden");
1874 }
1875
1876 #[test]
1877 fn test_php_consistency_dynamic_append_overrides_static() {
1878 let mut model = AppendableTestModel::new()
1881 .with_data("id", json!(1))
1882 .with_data("status", json!(1));
1883 model.append_dyn(vec!["stat_day".to_string()]);
1885 let json = model.to_json_with_append_cached();
1886 assert!(
1888 json.get("status_text").is_none(),
1889 "动态 append 应覆盖静态,status_text 不应输出"
1890 );
1891 assert!(json.get("stat_day").is_some(), "动态 append 字段应输出");
1893 }
1894
1895 #[test]
1896 fn test_php_consistency_append_method_returns_this_for_chaining() {
1897 let mut model = AppendableTestModel::new()
1900 .with_data("id", json!(1))
1901 .with_data("status", json!(1));
1902 model
1904 .append_merge(vec!["stat_day".to_string()])
1905 .append_merge(vec!["product_sales".to_string()]);
1906 let json = model.to_json_with_append_cached();
1907 assert_eq!(json["status_text"], "启用");
1909 assert!(json.get("stat_day").is_some());
1910 assert!(json.get("product_sales").is_some());
1911 }
1912}