1use std::fmt;
5
6use super::GenError;
7
8pub const MAX_NAME_LEN: usize = 64;
11
12const KEYWORDS: [&str; 52] = [
15 "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop",
16 "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type",
17 "unsafe", "use", "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override",
18 "priv", "typeof", "unsized", "virtual", "yield", "try", "gen",
19];
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum BaseType {
24 I64,
25 I32,
26 F64,
27 Bool,
28 String,
29}
30
31impl BaseType {
32 fn parse(text: &str) -> Option<BaseType> {
33 match text {
34 "i64" => Some(BaseType::I64),
35 "i32" => Some(BaseType::I32),
36 "f64" => Some(BaseType::F64),
37 "bool" => Some(BaseType::Bool),
38 "String" => Some(BaseType::String),
39 _ => None,
40 }
41 }
42
43 pub fn rust(self) -> &'static str {
45 match self {
46 BaseType::I64 => "i64",
47 BaseType::I32 => "i32",
48 BaseType::F64 => "f64",
49 BaseType::Bool => "bool",
50 BaseType::String => "String",
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct FieldType {
59 pub base: BaseType,
60 pub optional: bool,
61}
62
63pub const FIELD_TYPES: &str = "i64, i32, f64, bool, String, Option<T> (T one of those)";
65
66impl FieldType {
67 pub fn parse(text: &str) -> Result<FieldType, String> {
70 let text = text.trim();
71 if let Some(base) = BaseType::parse(text) {
72 return Ok(FieldType { base, optional: false });
73 }
74 let inner = text.strip_prefix("Option<").and_then(|rest| rest.strip_suffix('>')).map(str::trim);
75 match inner.and_then(BaseType::parse) {
76 Some(base) => Ok(FieldType { base, optional: true }),
77 None => Err(format!("unknown type `{text}`: use one of {FIELD_TYPES}")),
78 }
79 }
80
81 pub fn rust(&self) -> String {
83 if self.optional {
84 format!("Option<{}>", self.base.rust())
85 } else {
86 self.base.rust().to_string()
87 }
88 }
89}
90
91impl fmt::Display for FieldType {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.write_str(&self.rust())
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct Field {
100 pub name: String,
102 pub ty: FieldType,
103}
104
105impl Field {
106 pub fn parse(spec: &str) -> Result<Field, GenError> {
110 let Some((name, ty)) = spec.split_once(':') else {
111 return Err(GenError::usage(format!("field `{spec}`: write name:type, e.g. title:String")));
112 };
113 let name = name.trim();
114 check_snake(name, "field")?;
115 if name == "id" {
116 return Err(GenError::usage("field `id`: every model and resource gets an `id: i64` already; leave it out"));
117 }
118 let ty = FieldType::parse(ty).map_err(|e| GenError::usage(format!("field `{name}`: {e}")))?;
119 Ok(Field { name: name.to_string(), ty })
120 }
121
122 pub fn spec(&self) -> String {
124 format!("{}:{}", self.name, self.ty)
125 }
126}
127
128pub fn parse_fields<S: AsRef<str>>(specs: &[S]) -> Result<Vec<Field>, GenError> {
130 let mut fields: Vec<Field> = Vec::new();
131 for spec in specs {
132 let field = Field::parse(spec.as_ref())?;
133 if fields.iter().any(|f| f.name == field.name) {
134 return Err(GenError::usage(format!("field `{}` is given twice", field.name)));
135 }
136 fields.push(field);
137 }
138 Ok(fields)
139}
140
141fn is_identifier(name: &str) -> bool {
143 let mut chars = name.chars();
144 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic()) && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
145}
146
147pub fn check_snake(name: &str, what: &str) -> Result<(), GenError> {
149 if name.is_empty() {
150 return Err(GenError::usage(format!("the {what} name is missing")));
151 }
152 if name.len() > MAX_NAME_LEN {
153 let start: String = name.chars().take(MAX_NAME_LEN).collect();
154 return Err(GenError::usage(format!("{what} `{start}…` is longer than {MAX_NAME_LEN} characters")));
155 }
156 if !is_identifier(name) || name.chars().any(|c| c.is_ascii_uppercase()) || name.contains("__") || name.ends_with('_') {
157 return Err(GenError::usage(format!(
158 "{what} `{name}`: use a snake_case identifier (letters, digits and single `_`, starting with a letter) — it becomes a file name and a Rust identifier"
159 )));
160 }
161 if KEYWORDS.contains(&name) {
162 return Err(GenError::usage(format!("{what} `{name}` is a Rust keyword; pick another name")));
163 }
164 Ok(())
165}
166
167pub fn to_snake(name: &str, what: &str) -> Result<String, GenError> {
170 let name = name.trim();
171 if name.is_empty() || name.len() > MAX_NAME_LEN || !is_identifier(name) {
172 check_snake(name, what)?;
173 }
174 let mut out = String::with_capacity(name.len() + 4);
175 let mut previous_lower = false;
176 for c in name.chars() {
177 if c.is_ascii_uppercase() {
178 if previous_lower {
179 out.push('_');
180 }
181 out.push(c.to_ascii_lowercase());
182 previous_lower = false;
183 } else {
184 out.push(c);
185 previous_lower = c.is_ascii_lowercase() || c.is_ascii_digit();
186 }
187 }
188 check_snake(&out, what)?;
189 Ok(out)
190}
191
192pub fn to_camel(snake: &str) -> String {
194 snake
195 .split('_')
196 .map(|part| {
197 let mut chars = part.chars();
198 match chars.next() {
199 Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
200 None => String::new(),
201 }
202 })
203 .collect()
204}
205
206pub fn pluralize(snake: &str) -> String {
209 let consonant_y = snake.len() > 1
210 && snake.ends_with('y')
211 && !matches!(snake.as_bytes()[snake.len() - 2], b'a' | b'e' | b'i' | b'o' | b'u');
212 if consonant_y {
213 format!("{}ies", &snake[..snake.len() - 1])
214 } else if ["s", "x", "z", "ch", "sh"].iter().any(|end| snake.ends_with(end)) {
215 format!("{snake}es")
216 } else {
217 format!("{snake}s")
218 }
219}
220
221pub fn singularize(snake: &str) -> String {
225 if snake.len() > 3 && snake.ends_with("ies") {
226 return format!("{}y", &snake[..snake.len() - 3]);
227 }
228 for end in ["ses", "xes", "zes", "ches", "shes"] {
229 if snake.len() > end.len() && snake.ends_with(end) {
230 return snake[..snake.len() - 2].to_string();
231 }
232 }
233 if snake.len() > 1 && snake.ends_with('s') && !snake.ends_with("ss") && !snake.ends_with("us") {
234 return snake[..snake.len() - 1].to_string();
235 }
236 snake.to_string()
237}
238
239pub const RESERVED_TYPES: [&str; 21] = [
242 "Self", "Option", "Result", "String", "Vec", "Box", "Json", "Path", "Value", "AppError", "Mutex", "MutexGuard", "BTreeMap",
243 "Serialize", "Deserialize", "StatusCodeReasonPhrase", "AppWithState", "AsyncAppWithState", "Model", "DbPool", "Repository",
244];
245
246pub fn check_type_name(camel: &str, what: &str) -> Result<(), GenError> {
248 if RESERVED_TYPES.contains(&camel) {
249 return Err(GenError::usage(format!(
250 "{what} type `{camel}` would clash with a name the generated code uses ({}); pick another name",
251 RESERVED_TYPES.join(", ")
252 )));
253 }
254 Ok(())
255}