Skip to main content

doido_auth/generators/
field.rs

1//! Parsing of `name:type[:modifier...]` field specs for auth scaffolds.
2
3use 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}