1use crate::schema::object::to_pascal_case;
4use postrust_core::schema_cache::Relationship;
5
6fn get_constraint_name(rel: &Relationship) -> &str {
8 match rel {
9 Relationship::ForeignKey { constraint_name, .. } => constraint_name,
10 Relationship::Computed { function, .. } => &function.name,
11 }
12}
13
14#[derive(Debug, Clone)]
16pub struct RelationshipField {
17 pub name: String,
19 pub target_type: String,
21 pub is_list: bool,
23 pub relationship: Relationship,
25 pub description: Option<String>,
27}
28
29impl RelationshipField {
30 pub fn from_relationship(rel: &Relationship) -> Self {
32 let foreign_table = rel.foreign_table();
33 let is_list = !rel.is_to_one();
34
35 let name = if is_list {
37 pluralize(&foreign_table.name)
39 } else {
40 singularize(&foreign_table.name)
42 };
43
44 let target_type = to_pascal_case(&foreign_table.name);
45
46 let description = Some(format!(
47 "Related {} via {}",
48 if is_list { "records" } else { "record" },
49 get_constraint_name(rel)
50 ));
51
52 Self {
53 name,
54 target_type,
55 is_list,
56 relationship: rel.clone(),
57 description,
58 }
59 }
60
61 pub fn type_string(&self) -> String {
63 if self.is_list {
64 format!("[{}!]!", self.target_type)
65 } else {
66 self.target_type.clone()
67 }
68 }
69
70 pub fn join_columns(&self) -> Vec<(String, String)> {
72 self.relationship.join_columns()
73 }
74}
75
76fn pluralize(s: &str) -> String {
78 if s.ends_with('s') && !s.ends_with("ss") {
80 return s.to_string();
81 }
82
83 if s.ends_with('x') || s.ends_with("ch") || s.ends_with("sh") || s.ends_with("ss") {
84 format!("{}es", s)
85 } else if s.ends_with('y') && !s.ends_with("ey") && !s.ends_with("ay") && !s.ends_with("oy") {
86 format!("{}ies", &s[..s.len() - 1])
87 } else {
88 format!("{}s", s)
89 }
90}
91
92fn singularize(s: &str) -> String {
94 if s.ends_with("ies") {
95 format!("{}y", &s[..s.len() - 3])
96 } else if s.ends_with("es") && (s.ends_with("ses") || s.ends_with("xes") || s.ends_with("ches") || s.ends_with("shes")) {
97 s[..s.len() - 2].to_string()
98 } else if s.ends_with('s') && !s.ends_with("ss") {
99 s[..s.len() - 1].to_string()
100 } else {
101 s.to_string()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use postrust_core::api_request::QualifiedIdentifier;
109 use postrust_core::schema_cache::Cardinality;
110 use pretty_assertions::assert_eq;
111
112 fn create_m2o_relationship() -> Relationship {
113 Relationship::ForeignKey {
115 table: QualifiedIdentifier::new("public", "orders"),
116 foreign_table: QualifiedIdentifier::new("public", "users"),
117 is_self: false,
118 cardinality: Cardinality::M2O {
119 constraint: "orders_user_id_fkey".into(),
120 columns: vec![("user_id".into(), "id".into())],
121 },
122 table_is_view: false,
123 foreign_table_is_view: false,
124 constraint_name: "orders_user_id_fkey".into(),
125 }
126 }
127
128 fn create_o2m_relationship() -> Relationship {
129 Relationship::ForeignKey {
131 table: QualifiedIdentifier::new("public", "users"),
132 foreign_table: QualifiedIdentifier::new("public", "orders"),
133 is_self: false,
134 cardinality: Cardinality::O2M {
135 constraint: "orders_user_id_fkey".into(),
136 columns: vec![("id".into(), "user_id".into())],
137 },
138 table_is_view: false,
139 foreign_table_is_view: false,
140 constraint_name: "orders_user_id_fkey".into(),
141 }
142 }
143
144 fn create_o2o_relationship() -> Relationship {
145 Relationship::ForeignKey {
147 table: QualifiedIdentifier::new("public", "users"),
148 foreign_table: QualifiedIdentifier::new("public", "user_profiles"),
149 is_self: false,
150 cardinality: Cardinality::O2O {
151 constraint: "user_profiles_user_id_fkey".into(),
152 columns: vec![("id".into(), "user_id".into())],
153 is_parent: true,
154 },
155 table_is_view: false,
156 foreign_table_is_view: false,
157 constraint_name: "user_profiles_user_id_fkey".into(),
158 }
159 }
160
161 #[test]
162 fn test_pluralize() {
163 assert_eq!(pluralize("user"), "users");
164 assert_eq!(pluralize("order"), "orders");
165 assert_eq!(pluralize("category"), "categories");
166 assert_eq!(pluralize("box"), "boxes");
167 assert_eq!(pluralize("match"), "matches");
168 assert_eq!(pluralize("dish"), "dishes");
169 assert_eq!(pluralize("key"), "keys"); assert_eq!(pluralize("day"), "days"); }
172
173 #[test]
174 fn test_singularize() {
175 assert_eq!(singularize("users"), "user");
176 assert_eq!(singularize("orders"), "order");
177 assert_eq!(singularize("categories"), "category");
178 assert_eq!(singularize("boxes"), "box");
179 assert_eq!(singularize("matches"), "match");
180 assert_eq!(singularize("class"), "class"); }
182
183 #[test]
184 fn test_m2o_relationship_field() {
185 let rel = create_m2o_relationship();
186 let field = RelationshipField::from_relationship(&rel);
187
188 assert_eq!(field.name, "user"); assert_eq!(field.target_type, "Users");
190 assert!(!field.is_list); }
192
193 #[test]
194 fn test_o2m_relationship_field() {
195 let rel = create_o2m_relationship();
196 let field = RelationshipField::from_relationship(&rel);
197
198 assert_eq!(field.name, "orders"); assert_eq!(field.target_type, "Orders");
200 assert!(field.is_list); }
202
203 #[test]
204 fn test_o2o_relationship_field() {
205 let rel = create_o2o_relationship();
206 let field = RelationshipField::from_relationship(&rel);
207
208 assert_eq!(field.name, "user_profile"); assert_eq!(field.target_type, "UserProfiles");
210 assert!(!field.is_list); }
212
213 #[test]
214 fn test_relationship_type_string_list() {
215 let rel = create_o2m_relationship();
216 let field = RelationshipField::from_relationship(&rel);
217
218 assert_eq!(field.type_string(), "[Orders!]!");
219 }
220
221 #[test]
222 fn test_relationship_type_string_single() {
223 let rel = create_m2o_relationship();
224 let field = RelationshipField::from_relationship(&rel);
225
226 assert_eq!(field.type_string(), "Users");
227 }
228
229 #[test]
230 fn test_relationship_join_columns() {
231 let rel = create_m2o_relationship();
232 let field = RelationshipField::from_relationship(&rel);
233
234 let columns = field.join_columns();
235 assert_eq!(columns.len(), 1);
236 assert_eq!(columns[0], ("user_id".into(), "id".into()));
237 }
238
239 #[test]
240 fn test_relationship_description() {
241 let rel = create_m2o_relationship();
242 let field = RelationshipField::from_relationship(&rel);
243
244 assert!(field.description.is_some());
245 assert!(field.description.as_ref().unwrap().contains("orders_user_id_fkey"));
246 }
247
248 #[test]
249 fn test_m2m_relationship_field() {
250 let rel = Relationship::ForeignKey {
252 table: QualifiedIdentifier::new("public", "users"),
253 foreign_table: QualifiedIdentifier::new("public", "tags"),
254 is_self: false,
255 cardinality: Cardinality::M2M(postrust_core::schema_cache::Junction {
256 table: QualifiedIdentifier::new("public", "user_tags"),
257 constraint1: "user_tags_user_id_fkey".into(),
258 constraint2: "user_tags_tag_id_fkey".into(),
259 source_columns: vec![("id".into(), "user_id".into())],
260 target_columns: vec![("tag_id".into(), "id".into())],
261 }),
262 table_is_view: false,
263 foreign_table_is_view: false,
264 constraint_name: "user_tags_user_id_fkey".into(),
265 };
266
267 let field = RelationshipField::from_relationship(&rel);
268
269 assert_eq!(field.name, "tags"); assert_eq!(field.target_type, "Tags");
271 assert!(field.is_list);
272 }
273}