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)]
17#[non_exhaustive]
18pub struct RelationshipField {
19 pub name: String,
21 pub target_type: String,
23 pub target_local_name: String,
25 pub is_list: bool,
27 pub relationship: Relationship,
29 pub description: Option<String>,
31 pub arguments: Vec<(String, String, bool)>,
38}
39
40impl RelationshipField {
41 pub fn from_relationship(rel: &Relationship) -> Self {
43 let base_name = rel.foreign_table().name.clone();
44 Self::from_relationship_named(rel, &base_name, None)
45 }
46
47 pub fn from_relationship_named(
53 rel: &Relationship,
54 target_base_name: &str,
55 given_name: Option<&str>,
56 ) -> Self {
57 Self::from_relationship_typed(
58 rel,
59 target_base_name,
60 target_base_name,
61 target_base_name,
62 given_name,
63 )
64 }
65
66 pub fn from_relationship_typed(
69 rel: &Relationship,
70 target_base_name: &str,
71 target_type: &str,
72 target_local_name: &str,
73 given_name: Option<&str>,
74 ) -> Self {
75 let is_list = !rel.is_to_one();
76
77 let name = match given_name {
83 Some(given) => given.to_string(),
87 None => match rel {
88 Relationship::Computed { function, .. } => function.name.clone(),
89 _ if is_list => pluralize(target_base_name),
90 _ => singularize(target_base_name),
91 },
92 };
93
94 let description = Some(format!(
95 "Related {} via {}",
96 if is_list { "records" } else { "record" },
97 get_constraint_name(rel)
98 ));
99
100 Self {
101 name,
102 target_type: target_type.to_string(),
103 target_local_name: target_local_name.to_string(),
104 is_list,
105 relationship: rel.clone(),
106 description,
107 arguments: Vec::new(),
108 }
109 }
110
111 pub fn type_string(&self) -> String {
113 if self.is_list {
114 format!("[{}!]!", self.target_type)
115 } else {
116 self.target_type.clone()
117 }
118 }
119
120 pub fn join_columns(&self) -> Vec<(String, String)> {
122 self.relationship.join_columns()
123 }
124}
125
126fn pluralize(s: &str) -> String {
128 if s.ends_with('s') && !s.ends_with("ss") {
130 return s.to_string();
131 }
132
133 if s.ends_with('x') || s.ends_with("ch") || s.ends_with("sh") || s.ends_with("ss") {
134 format!("{}es", s)
135 } else if s.ends_with('y') && !s.ends_with("ey") && !s.ends_with("ay") && !s.ends_with("oy") {
136 format!("{}ies", &s[..s.len() - 1])
137 } else {
138 format!("{}s", s)
139 }
140}
141
142fn singularize(s: &str) -> String {
144 if let Some(stem) = s.strip_suffix("ies") {
145 format!("{}y", stem)
146 } else if s.ends_with("ses") || s.ends_with("xes") || s.ends_with("ches") || s.ends_with("shes")
147 {
148 s.strip_suffix("es").unwrap_or(s).to_string()
149 } else if s.ends_with('s') && !s.ends_with("ss") {
150 s.strip_suffix('s').unwrap_or(s).to_string()
151 } else {
152 s.to_string()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use postrust_core::api_request::QualifiedIdentifier;
160 use postrust_core::schema_cache::Cardinality;
161 use pretty_assertions::assert_eq;
162
163 fn create_m2o_relationship() -> Relationship {
164 Relationship::ForeignKey {
166 table: QualifiedIdentifier::new("public", "orders"),
167 foreign_table: QualifiedIdentifier::new("public", "users"),
168 is_self: false,
169 cardinality: Cardinality::M2O {
170 constraint: "orders_user_id_fkey".into(),
171 columns: vec![("user_id".into(), "id".into())],
172 },
173 table_is_view: false,
174 foreign_table_is_view: false,
175 constraint_name: "orders_user_id_fkey".into(),
176 }
177 }
178
179 fn create_o2m_relationship() -> Relationship {
180 Relationship::ForeignKey {
182 table: QualifiedIdentifier::new("public", "users"),
183 foreign_table: QualifiedIdentifier::new("public", "orders"),
184 is_self: false,
185 cardinality: Cardinality::O2M {
186 constraint: "orders_user_id_fkey".into(),
187 columns: vec![("id".into(), "user_id".into())],
188 },
189 table_is_view: false,
190 foreign_table_is_view: false,
191 constraint_name: "orders_user_id_fkey".into(),
192 }
193 }
194
195 fn create_o2o_relationship() -> Relationship {
196 Relationship::ForeignKey {
198 table: QualifiedIdentifier::new("public", "users"),
199 foreign_table: QualifiedIdentifier::new("public", "user_profiles"),
200 is_self: false,
201 cardinality: Cardinality::O2O {
202 constraint: "user_profiles_user_id_fkey".into(),
203 columns: vec![("id".into(), "user_id".into())],
204 is_parent: true,
205 },
206 table_is_view: false,
207 foreign_table_is_view: false,
208 constraint_name: "user_profiles_user_id_fkey".into(),
209 }
210 }
211
212 #[test]
213 fn test_pluralize() {
214 assert_eq!(pluralize("user"), "users");
215 assert_eq!(pluralize("order"), "orders");
216 assert_eq!(pluralize("category"), "categories");
217 assert_eq!(pluralize("box"), "boxes");
218 assert_eq!(pluralize("match"), "matches");
219 assert_eq!(pluralize("dish"), "dishes");
220 assert_eq!(pluralize("key"), "keys"); assert_eq!(pluralize("day"), "days"); }
223
224 #[test]
225 fn test_singularize() {
226 assert_eq!(singularize("users"), "user");
227 assert_eq!(singularize("orders"), "order");
228 assert_eq!(singularize("categories"), "category");
229 assert_eq!(singularize("boxes"), "box");
230 assert_eq!(singularize("matches"), "match");
231 assert_eq!(singularize("class"), "class"); }
233
234 #[test]
235 fn test_m2o_relationship_field() {
236 let rel = create_m2o_relationship();
237 let field = RelationshipField::from_relationship(&rel);
238
239 assert_eq!(field.name, "user"); assert_eq!(field.target_type, "users");
241 assert!(!field.is_list); }
243
244 #[test]
245 fn test_o2m_relationship_field() {
246 let rel = create_o2m_relationship();
247 let field = RelationshipField::from_relationship(&rel);
248
249 assert_eq!(field.name, "orders"); assert_eq!(field.target_type, "orders");
251 assert!(field.is_list); }
253
254 #[test]
255 fn test_o2o_relationship_field() {
256 let rel = create_o2o_relationship();
257 let field = RelationshipField::from_relationship(&rel);
258
259 assert_eq!(field.name, "user_profile"); assert_eq!(field.target_type, "user_profiles");
261 assert!(!field.is_list); }
263
264 #[test]
265 fn test_relationship_type_string_list() {
266 let rel = create_o2m_relationship();
267 let field = RelationshipField::from_relationship(&rel);
268
269 assert_eq!(field.type_string(), "[orders!]!");
270 }
271
272 #[test]
273 fn test_relationship_type_string_single() {
274 let rel = create_m2o_relationship();
275 let field = RelationshipField::from_relationship(&rel);
276
277 assert_eq!(field.type_string(), "users");
278 }
279
280 #[test]
281 fn test_relationship_join_columns() {
282 let rel = create_m2o_relationship();
283 let field = RelationshipField::from_relationship(&rel);
284
285 let columns = field.join_columns();
286 assert_eq!(columns.len(), 1);
287 assert_eq!(columns[0], ("user_id".into(), "id".into()));
288 }
289
290 #[test]
291 fn test_relationship_description() {
292 let rel = create_m2o_relationship();
293 let field = RelationshipField::from_relationship(&rel);
294
295 assert!(field.description.is_some());
296 assert!(field
297 .description
298 .as_ref()
299 .unwrap()
300 .contains("orders_user_id_fkey"));
301 }
302
303 #[test]
304 fn test_m2m_relationship_field() {
305 let rel = Relationship::ForeignKey {
307 table: QualifiedIdentifier::new("public", "users"),
308 foreign_table: QualifiedIdentifier::new("public", "tags"),
309 is_self: false,
310 cardinality: Cardinality::M2M(postrust_core::schema_cache::Junction {
311 table: QualifiedIdentifier::new("public", "user_tags"),
312 constraint1: "user_tags_user_id_fkey".into(),
313 constraint2: "user_tags_tag_id_fkey".into(),
314 source_columns: vec![("id".into(), "user_id".into())],
315 target_columns: vec![("tag_id".into(), "id".into())],
316 }),
317 table_is_view: false,
318 foreign_table_is_view: false,
319 constraint_name: "user_tags_user_id_fkey".into(),
320 };
321
322 let field = RelationshipField::from_relationship(&rel);
323
324 assert_eq!(field.name, "tags"); assert_eq!(field.target_type, "tags");
326 assert!(field.is_list);
327 }
328}