1use postrust_core::schema_cache::Relationship;
4
5fn get_constraint_name(rel: &Relationship) -> &str {
7 match rel {
8 Relationship::ForeignKey {
9 constraint_name, ..
10 } => constraint_name,
11 Relationship::Computed { function, .. } => &function.name,
12 }
13}
14
15#[derive(Debug, Clone)]
17pub struct RelationshipField {
18 pub name: String,
20 pub target_type: String,
22 pub is_list: bool,
24 pub relationship: Relationship,
26 pub description: Option<String>,
28 pub arguments: Vec<(String, String, bool)>,
35}
36
37impl RelationshipField {
38 pub fn from_relationship(rel: &Relationship) -> Self {
40 let base_name = rel.foreign_table().name.clone();
41 Self::from_relationship_named(rel, &base_name, None)
42 }
43
44 pub fn from_relationship_named(
50 rel: &Relationship,
51 target_base_name: &str,
52 given_name: Option<&str>,
53 ) -> Self {
54 let is_list = !rel.is_to_one();
55
56 let name = match given_name {
62 Some(given) => given.to_string(),
66 None => match rel {
67 Relationship::Computed { function, .. } => function.name.clone(),
68 _ if is_list => pluralize(target_base_name),
69 _ => singularize(target_base_name),
70 },
71 };
72
73 let target_type = target_base_name.to_string();
74
75 let description = Some(format!(
76 "Related {} via {}",
77 if is_list { "records" } else { "record" },
78 get_constraint_name(rel)
79 ));
80
81 Self {
82 name,
83 target_type,
84 is_list,
85 relationship: rel.clone(),
86 description,
87 arguments: Vec::new(),
88 }
89 }
90
91 pub fn type_string(&self) -> String {
93 if self.is_list {
94 format!("[{}!]!", self.target_type)
95 } else {
96 self.target_type.clone()
97 }
98 }
99
100 pub fn join_columns(&self) -> Vec<(String, String)> {
102 self.relationship.join_columns()
103 }
104}
105
106fn pluralize(s: &str) -> String {
108 if s.ends_with('s') && !s.ends_with("ss") {
110 return s.to_string();
111 }
112
113 if s.ends_with('x') || s.ends_with("ch") || s.ends_with("sh") || s.ends_with("ss") {
114 format!("{}es", s)
115 } else if s.ends_with('y') && !s.ends_with("ey") && !s.ends_with("ay") && !s.ends_with("oy") {
116 format!("{}ies", &s[..s.len() - 1])
117 } else {
118 format!("{}s", s)
119 }
120}
121
122fn singularize(s: &str) -> String {
124 if let Some(stem) = s.strip_suffix("ies") {
125 format!("{}y", stem)
126 } else if s.ends_with("ses") || s.ends_with("xes") || s.ends_with("ches") || s.ends_with("shes")
127 {
128 s.strip_suffix("es").unwrap_or(s).to_string()
129 } else if s.ends_with('s') && !s.ends_with("ss") {
130 s.strip_suffix('s').unwrap_or(s).to_string()
131 } else {
132 s.to_string()
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use postrust_core::api_request::QualifiedIdentifier;
140 use postrust_core::schema_cache::Cardinality;
141 use pretty_assertions::assert_eq;
142
143 fn create_m2o_relationship() -> Relationship {
144 Relationship::ForeignKey {
146 table: QualifiedIdentifier::new("public", "orders"),
147 foreign_table: QualifiedIdentifier::new("public", "users"),
148 is_self: false,
149 cardinality: Cardinality::M2O {
150 constraint: "orders_user_id_fkey".into(),
151 columns: vec![("user_id".into(), "id".into())],
152 },
153 table_is_view: false,
154 foreign_table_is_view: false,
155 constraint_name: "orders_user_id_fkey".into(),
156 }
157 }
158
159 fn create_o2m_relationship() -> Relationship {
160 Relationship::ForeignKey {
162 table: QualifiedIdentifier::new("public", "users"),
163 foreign_table: QualifiedIdentifier::new("public", "orders"),
164 is_self: false,
165 cardinality: Cardinality::O2M {
166 constraint: "orders_user_id_fkey".into(),
167 columns: vec![("id".into(), "user_id".into())],
168 },
169 table_is_view: false,
170 foreign_table_is_view: false,
171 constraint_name: "orders_user_id_fkey".into(),
172 }
173 }
174
175 fn create_o2o_relationship() -> Relationship {
176 Relationship::ForeignKey {
178 table: QualifiedIdentifier::new("public", "users"),
179 foreign_table: QualifiedIdentifier::new("public", "user_profiles"),
180 is_self: false,
181 cardinality: Cardinality::O2O {
182 constraint: "user_profiles_user_id_fkey".into(),
183 columns: vec![("id".into(), "user_id".into())],
184 is_parent: true,
185 },
186 table_is_view: false,
187 foreign_table_is_view: false,
188 constraint_name: "user_profiles_user_id_fkey".into(),
189 }
190 }
191
192 #[test]
193 fn test_pluralize() {
194 assert_eq!(pluralize("user"), "users");
195 assert_eq!(pluralize("order"), "orders");
196 assert_eq!(pluralize("category"), "categories");
197 assert_eq!(pluralize("box"), "boxes");
198 assert_eq!(pluralize("match"), "matches");
199 assert_eq!(pluralize("dish"), "dishes");
200 assert_eq!(pluralize("key"), "keys"); assert_eq!(pluralize("day"), "days"); }
203
204 #[test]
205 fn test_singularize() {
206 assert_eq!(singularize("users"), "user");
207 assert_eq!(singularize("orders"), "order");
208 assert_eq!(singularize("categories"), "category");
209 assert_eq!(singularize("boxes"), "box");
210 assert_eq!(singularize("matches"), "match");
211 assert_eq!(singularize("class"), "class"); }
213
214 #[test]
215 fn test_m2o_relationship_field() {
216 let rel = create_m2o_relationship();
217 let field = RelationshipField::from_relationship(&rel);
218
219 assert_eq!(field.name, "user"); assert_eq!(field.target_type, "users");
221 assert!(!field.is_list); }
223
224 #[test]
225 fn test_o2m_relationship_field() {
226 let rel = create_o2m_relationship();
227 let field = RelationshipField::from_relationship(&rel);
228
229 assert_eq!(field.name, "orders"); assert_eq!(field.target_type, "orders");
231 assert!(field.is_list); }
233
234 #[test]
235 fn test_o2o_relationship_field() {
236 let rel = create_o2o_relationship();
237 let field = RelationshipField::from_relationship(&rel);
238
239 assert_eq!(field.name, "user_profile"); assert_eq!(field.target_type, "user_profiles");
241 assert!(!field.is_list); }
243
244 #[test]
245 fn test_relationship_type_string_list() {
246 let rel = create_o2m_relationship();
247 let field = RelationshipField::from_relationship(&rel);
248
249 assert_eq!(field.type_string(), "[orders!]!");
250 }
251
252 #[test]
253 fn test_relationship_type_string_single() {
254 let rel = create_m2o_relationship();
255 let field = RelationshipField::from_relationship(&rel);
256
257 assert_eq!(field.type_string(), "users");
258 }
259
260 #[test]
261 fn test_relationship_join_columns() {
262 let rel = create_m2o_relationship();
263 let field = RelationshipField::from_relationship(&rel);
264
265 let columns = field.join_columns();
266 assert_eq!(columns.len(), 1);
267 assert_eq!(columns[0], ("user_id".into(), "id".into()));
268 }
269
270 #[test]
271 fn test_relationship_description() {
272 let rel = create_m2o_relationship();
273 let field = RelationshipField::from_relationship(&rel);
274
275 assert!(field.description.is_some());
276 assert!(field
277 .description
278 .as_ref()
279 .unwrap()
280 .contains("orders_user_id_fkey"));
281 }
282
283 #[test]
284 fn test_m2m_relationship_field() {
285 let rel = Relationship::ForeignKey {
287 table: QualifiedIdentifier::new("public", "users"),
288 foreign_table: QualifiedIdentifier::new("public", "tags"),
289 is_self: false,
290 cardinality: Cardinality::M2M(postrust_core::schema_cache::Junction {
291 table: QualifiedIdentifier::new("public", "user_tags"),
292 constraint1: "user_tags_user_id_fkey".into(),
293 constraint2: "user_tags_tag_id_fkey".into(),
294 source_columns: vec![("id".into(), "user_id".into())],
295 target_columns: vec![("tag_id".into(), "id".into())],
296 }),
297 table_is_view: false,
298 foreign_table_is_view: false,
299 constraint_name: "user_tags_user_id_fkey".into(),
300 };
301
302 let field = RelationshipField::from_relationship(&rel);
303
304 assert_eq!(field.name, "tags"); assert_eq!(field.target_type, "tags");
306 assert!(field.is_list);
307 }
308}