1use super::names::to_snake;
4use doido_core::anyhow::{anyhow, bail};
5use doido_core::Result;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ColumnType {
9 String,
10 Text,
11 Integer,
12 BigInteger,
13 Float,
14 Double,
15 Decimal,
16 Boolean,
17 Timestamp,
18 Date,
19 Json,
20 Uuid,
21 Binary,
22 References,
23}
24
25impl ColumnType {
26 fn parse(token: &str) -> Result<Self> {
27 Ok(match token.to_lowercase().as_str() {
28 "string" => Self::String,
29 "text" => Self::Text,
30 "integer" | "int" => Self::Integer,
31 "bigint" | "biginteger" | "big_integer" | "long" => Self::BigInteger,
32 "float" => Self::Float,
33 "double" => Self::Double,
34 "decimal" | "numeric" => Self::Decimal,
35 "boolean" | "bool" => Self::Boolean,
36 "timestamp" | "datetime" => Self::Timestamp,
37 "date" => Self::Date,
38 "json" | "jsonb" => Self::Json,
39 "uuid" => Self::Uuid,
40 "binary" | "blob" | "bytes" => Self::Binary,
41 "references" | "reference" | "belongs_to" => Self::References,
42 other => bail!("unknown column type `{other}`"),
43 })
44 }
45
46 fn builder_method(self) -> &'static str {
47 match self {
48 Self::String => "string",
49 Self::Text => "text",
50 Self::Integer => "integer",
51 Self::BigInteger => "big_integer",
52 Self::Float => "float",
53 Self::Double => "double",
54 Self::Decimal => "decimal",
55 Self::Boolean => "boolean",
56 Self::Timestamp => "timestamp",
57 Self::Date => "date",
58 Self::Json => "json",
59 Self::Uuid => "uuid",
60 Self::Binary => "binary",
61 Self::References => "references",
62 }
63 }
64
65 fn rust_type(self) -> &'static str {
66 match self {
67 Self::String | Self::Text => "String",
68 Self::Integer => "i32",
69 Self::BigInteger | Self::References => "i64",
70 Self::Float => "f32",
71 Self::Double => "f64",
72 Self::Decimal => "Decimal",
73 Self::Boolean => "bool",
74 Self::Timestamp => "DateTime",
75 Self::Date => "Date",
76 Self::Json => "Json",
77 Self::Uuid => "Uuid",
78 Self::Binary => "Vec<u8>",
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
84pub struct Field {
85 raw_name: String,
86 ty: ColumnType,
87 not_null: bool,
88 unique: bool,
89 index: bool,
90}
91
92impl Field {
93 pub fn parse(spec: &str) -> Result<Self> {
94 let mut parts = spec.split(':');
95 let name = parts
96 .next()
97 .filter(|s| !s.is_empty())
98 .ok_or_else(|| anyhow!("empty field spec"))?;
99
100 let ty = match parts.next() {
101 Some(t) if !t.is_empty() => ColumnType::parse(t)?,
102 _ => ColumnType::String,
103 };
104
105 let mut field = Field {
106 raw_name: to_snake(name),
107 ty,
108 not_null: false,
109 unique: false,
110 index: false,
111 };
112
113 for modifier in parts {
114 match modifier.to_lowercase().as_str() {
115 "" => {}
116 "not_null" | "notnull" | "required" => field.not_null = true,
117 "unique" | "uniq" => field.unique = true,
118 "index" => field.index = true,
119 other => bail!("unknown modifier `{other}` in field `{spec}`"),
120 }
121 }
122
123 Ok(field)
124 }
125
126 pub fn parse_all(specs: &[&str]) -> Result<Vec<Field>> {
127 specs.iter().map(|s| Field::parse(s)).collect()
128 }
129
130 pub fn column_name(&self) -> String {
131 match self.ty {
132 ColumnType::References => format!("{}_id", self.raw_name),
133 _ => self.raw_name.clone(),
134 }
135 }
136
137 pub fn is_required(&self) -> bool {
138 self.not_null || self.ty == ColumnType::References
139 }
140
141 pub fn wants_index(&self) -> bool {
142 self.index
143 }
144
145 pub fn is_user_reference(&self) -> bool {
146 self.ty == ColumnType::References && self.raw_name == "user"
147 }
148
149 fn rust_type(&self) -> String {
150 let ty = self.ty.rust_type();
151 if self.is_required() {
152 ty.to_string()
153 } else {
154 format!("Option<{ty}>")
155 }
156 }
157
158 pub fn params_struct_field(&self) -> String {
159 format!("pub {}: {},", self.column_name(), self.rust_type())
160 }
161
162 pub fn active_model_set(&self) -> String {
163 let col = self.column_name();
164 format!("{col}: Set(form.{col}),")
165 }
166
167 pub fn active_model_assign(&self) -> String {
168 let col = self.column_name();
169 format!("record.{col} = Set(form.{col});")
170 }
171
172 pub fn html_input_type(&self) -> &'static str {
173 match self.ty {
174 ColumnType::Text => "textarea",
175 ColumnType::Boolean => "checkbox",
176 ColumnType::Integer
177 | ColumnType::BigInteger
178 | ColumnType::Float
179 | ColumnType::Double
180 | ColumnType::Decimal
181 | ColumnType::References => "number",
182 ColumnType::Date => "date",
183 ColumnType::Timestamp => "datetime-local",
184 _ => "text",
185 }
186 }
187
188 pub fn migration_line(&self) -> String {
189 let arg = &self.raw_name;
190 let mut line = format!("t.{}(\"{arg}\")", self.ty.builder_method());
191 if self.not_null && self.ty != ColumnType::References {
192 line.push_str(".not_null()");
193 }
194 if self.unique {
195 line.push_str(".unique_key()");
196 }
197 line.push(';');
198 line
199 }
200
201 pub fn model_field(&self) -> String {
202 format!("pub {}: {},", self.column_name(), self.rust_type())
203 }
204}