1use anyhow::{Context, Result, bail};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FieldType {
11 String,
12 Bool,
13 I32,
14 I64,
15 F64,
16 Uuid,
17 DateTime,
18}
19
20impl FieldType {
21 pub fn parse(raw: &str) -> Result<Self> {
23 let ty = match raw.to_ascii_lowercase().as_str() {
24 "string" | "str" => Self::String,
25 "bool" | "boolean" => Self::Bool,
26 "i32" | "int" => Self::I32,
27 "i64" | "bigint" | "long" => Self::I64,
28 "f64" | "float" | "double" => Self::F64,
29 "uuid" => Self::Uuid,
30 "datetime" | "timestamp" => Self::DateTime,
31 other => bail!(
32 "unknown field type `{other}` (supported: String, Bool, i32, i64, f64, Uuid, DateTime)"
33 ),
34 };
35 Ok(ty)
36 }
37
38 pub fn rust_type(self) -> &'static str {
40 match self {
41 Self::String => "String",
42 Self::Bool => "bool",
43 Self::I32 => "i32",
44 Self::I64 => "i64",
45 Self::F64 => "f64",
46 Self::Uuid => "uuid::Uuid",
47 Self::DateTime => "chrono::DateTime<chrono::Utc>",
48 }
49 }
50
51 pub fn is_copy(self) -> bool {
55 !matches!(self, Self::String)
58 }
59
60 pub fn sql_type(self) -> &'static str {
62 match self {
63 Self::String => "TEXT",
64 Self::Bool => "BOOLEAN",
65 Self::I32 => "INTEGER",
66 Self::I64 => "BIGINT",
67 Self::F64 => "DOUBLE PRECISION",
68 Self::Uuid => "UUID",
69 Self::DateTime => "TIMESTAMPTZ",
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Field {
77 pub name: String,
78 pub ty: FieldType,
79}
80
81impl Field {
82 pub fn parse(token: &str) -> Result<Self> {
84 let (name, ty) = token
85 .split_once(':')
86 .with_context(|| format!("field `{token}` must be in the form name:Type"))?;
87 if name.is_empty() {
88 bail!("field `{token}` has an empty name");
89 }
90 Ok(Self {
91 name: name.to_string(),
92 ty: FieldType::parse(ty)?,
93 })
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct ModelSpec {
100 pub name: String,
101 pub fields: Vec<Field>,
102}
103
104impl ModelSpec {
105 pub fn parse(name: impl Into<String>, tokens: &[String]) -> Result<Self> {
107 let fields = tokens
108 .iter()
109 .map(|t| Field::parse(t))
110 .collect::<Result<Vec<_>>>()?;
111 Ok(Self {
112 name: name.into(),
113 fields,
114 })
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn parses_a_field() {
124 let f = Field::parse("email:String").unwrap();
125 assert_eq!(f.name, "email");
126 assert_eq!(f.ty, FieldType::String);
127 assert_eq!(f.ty.rust_type(), "String");
128 assert_eq!(f.ty.sql_type(), "TEXT");
129 }
130
131 #[test]
132 fn rejects_unknown_type() {
133 assert!(Field::parse("x:Blob").is_err());
134 }
135
136 #[test]
137 fn rejects_malformed_token() {
138 assert!(Field::parse("notype").is_err());
139 }
140
141 #[test]
142 fn parses_a_model() {
143 let m = ModelSpec::parse(
144 "User",
145 &["name:String".to_string(), "active:bool".to_string()],
146 )
147 .unwrap();
148 assert_eq!(m.name, "User");
149 assert_eq!(m.fields.len(), 2);
150 assert_eq!(m.fields[1].ty, FieldType::Bool);
151 }
152}