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