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