grounddb_codegen/
type_utils.rs1use grounddb::schema::{FieldDefinition, FieldType, ItemType, RefTarget};
2use heck::{ToPascalCase, ToSnakeCase};
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5
6pub fn collection_struct_name(collection_name: &str) -> String {
9 singularize(collection_name).to_pascal_case()
10}
11
12pub fn collection_method_name(collection_name: &str) -> String {
15 collection_name.to_snake_case()
16}
17
18pub fn enum_type_name(collection_name: &str, field_name: &str) -> String {
21 let singular = singularize(collection_name);
22 format!(
23 "{}{}",
24 singular.to_pascal_case(),
25 field_name.to_pascal_case()
26 )
27}
28
29pub fn ref_enum_name(field_name: &str) -> String {
32 format!("{}Ref", field_name.to_pascal_case())
33}
34
35pub fn view_row_name(view_name: &str) -> String {
38 format!("{}Row", view_name.to_pascal_case())
39}
40
41pub fn view_params_name(view_name: &str) -> String {
44 format!("{}Params", view_name.to_pascal_case())
45}
46
47pub fn partial_struct_name(struct_name: &str) -> String {
50 format!("{}Partial", struct_name)
51}
52
53pub fn field_to_rust_type(
57 field: &FieldDefinition,
58 collection_name: &str,
59 field_name: &str,
60 known_types: &[String],
61) -> TokenStream {
62 let base_type = field_base_type(field, collection_name, field_name, known_types);
63
64 if !field.required && field.default.is_none() {
66 match &field.field_type {
68 FieldType::List => base_type,
69 FieldType::Object => {
70 quote! { Option<#base_type> }
71 }
72 _ => {
73 quote! { Option<#base_type> }
74 }
75 }
76 } else {
77 base_type
78 }
79}
80
81pub fn field_base_type(
83 field: &FieldDefinition,
84 collection_name: &str,
85 field_name: &str,
86 known_types: &[String],
87) -> TokenStream {
88 if field.enum_values.is_some() {
90 let name = enum_type_name(collection_name, field_name);
91 let ident = format_ident!("{}", name);
92 return quote! { #ident };
93 }
94
95 match &field.field_type {
96 FieldType::String => quote! { String },
97 FieldType::Number => quote! { f64 },
98 FieldType::Boolean => quote! { bool },
99 FieldType::Date => quote! { chrono::NaiveDate },
100 FieldType::Datetime => quote! { chrono::DateTime<chrono::Utc> },
101 FieldType::Object => quote! { serde_json::Value },
102 FieldType::List => {
103 let item_type = list_item_type(field, collection_name, field_name, known_types);
104 quote! { Vec<#item_type> }
105 }
106 FieldType::Ref => ref_rust_type(field, field_name),
107 FieldType::Custom(type_name) => {
108 if known_types.contains(type_name) {
109 let ident = format_ident!("{}", type_name.to_pascal_case());
110 quote! { #ident }
111 } else {
112 quote! { serde_json::Value }
114 }
115 }
116 }
117}
118
119fn list_item_type(
121 field: &FieldDefinition,
122 _collection_name: &str,
123 _field_name: &str,
124 known_types: &[String],
125) -> TokenStream {
126 match &field.items {
127 Some(ItemType::Simple(s)) => match s.as_str() {
128 "string" => quote! { String },
129 "number" => quote! { f64 },
130 "boolean" => quote! { bool },
131 "date" => quote! { chrono::NaiveDate },
132 "datetime" => quote! { chrono::DateTime<chrono::Utc> },
133 "object" => quote! { serde_json::Value },
134 other => {
135 if known_types.contains(&other.to_string()) {
136 let ident = format_ident!("{}", other.to_pascal_case());
137 quote! { #ident }
138 } else {
139 quote! { serde_json::Value }
140 }
141 }
142 },
143 Some(ItemType::Complex(inner)) => {
144 match &inner.field_type {
146 FieldType::Ref => {
147 quote! { String }
149 }
150 _ => quote! { serde_json::Value },
151 }
152 }
153 None => quote! { serde_json::Value },
154 }
155}
156
157fn ref_rust_type(field: &FieldDefinition, field_name: &str) -> TokenStream {
159 match &field.target {
160 Some(RefTarget::Single(_)) => quote! { String },
161 Some(RefTarget::Multiple(_)) => {
162 let name = ref_enum_name(field_name);
163 let ident = format_ident!("{}", name);
164 quote! { #ident }
165 }
166 None => quote! { String },
167 }
168}
169
170pub fn singularize(word: &str) -> String {
172 let w = word.to_lowercase();
173 if w.ends_with("ies") {
174 format!("{}y", &w[..w.len() - 3])
175 } else if w.ends_with("ses") || w.ends_with("xes") || w.ends_with("zes") {
176 w[..w.len() - 2].to_string()
177 } else if w.ends_with("ves") {
178 format!("{}f", &w[..w.len() - 3])
179 } else if w.ends_with('s') && !w.ends_with("ss") {
180 w[..w.len() - 1].to_string()
181 } else {
182 w
183 }
184}
185
186pub fn safe_field_ident(name: &str) -> proc_macro2::Ident {
188 match name {
189 "type" | "struct" | "enum" | "fn" | "let" | "mut" | "ref" | "self" | "super" | "crate"
190 | "mod" | "use" | "pub" | "impl" | "trait" | "for" | "loop" | "while" | "if" | "else"
191 | "match" | "return" | "break" | "continue" | "as" | "in" | "where" | "async"
192 | "await" | "dyn" | "move" | "static" | "const" | "unsafe" | "extern" | "true"
193 | "false" | "abstract" | "become" | "box" | "do" | "final" | "macro" | "override"
194 | "priv" | "typeof" | "unsized" | "virtual" | "yield" | "try" => {
195 format_ident!("r#{}", name)
196 }
197 _ => format_ident!("{}", name.to_snake_case()),
198 }
199}
200
201pub fn enum_variant_ident(value: &str) -> proc_macro2::Ident {
203 format_ident!("{}", value.to_pascal_case())
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_singularize() {
212 assert_eq!(singularize("users"), "user");
213 assert_eq!(singularize("posts"), "post");
214 assert_eq!(singularize("comments"), "comment");
215 assert_eq!(singularize("events"), "event");
216 assert_eq!(singularize("categories"), "category");
217 assert_eq!(singularize("addresses"), "address");
218 }
219
220 #[test]
221 fn test_collection_struct_name() {
222 assert_eq!(collection_struct_name("users"), "User");
223 assert_eq!(collection_struct_name("posts"), "Post");
224 assert_eq!(collection_struct_name("comments"), "Comment");
225 assert_eq!(collection_struct_name("events"), "Event");
226 }
227
228 #[test]
229 fn test_enum_type_name() {
230 assert_eq!(enum_type_name("users", "role"), "UserRole");
231 assert_eq!(enum_type_name("posts", "status"), "PostStatus");
232 assert_eq!(enum_type_name("events", "severity"), "EventSeverity");
233 }
234
235 #[test]
236 fn test_ref_enum_name() {
237 assert_eq!(ref_enum_name("parent"), "ParentRef");
238 }
239
240 #[test]
241 fn test_view_names() {
242 assert_eq!(view_row_name("post_feed"), "PostFeedRow");
243 assert_eq!(view_params_name("post_comments"), "PostCommentsParams");
244 }
245
246 #[test]
247 fn test_safe_field_ident() {
248 let ident = safe_field_ident("type");
249 assert_eq!(ident.to_string(), "r#type");
250
251 let ident = safe_field_ident("name");
252 assert_eq!(ident.to_string(), "name");
253 }
254}