1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4
5use crate::field::{Field, ShapeField};
6use crate::query::Query;
7
8#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum FormaFieldType {
13 Named(String),
15 Array {
16 array: Box<FormaFieldType>,
17 },
18 Shape {
19 shape: Vec<FormaField>,
20 },
21}
22
23impl Default for FormaFieldType {
24 fn default() -> Self {
25 FormaFieldType::Named("string".to_owned())
26 }
27}
28
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct FormaField {
32 pub name: String,
33 pub label: String,
34 #[serde(default)]
35 pub field_type: FormaFieldType,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub detail_type: Option<FormaFieldType>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub form_type: Option<FormaFieldType>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub column_type: Option<FormaFieldType>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub fixed_value: Option<JsonValue>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub rule: Option<String>,
46}
47
48#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct Forma {
51 #[serde(default)]
52 pub fields: Vec<FormaField>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub detail: Option<Vec<String>>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub form: Option<Vec<String>>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub column: Option<Vec<String>>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub filters: Option<Vec<Vec<String>>>,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub enum FormaContext {
67 Detail,
68 Form,
69 Column,
70}
71
72impl Default for FormaContext {
73 fn default() -> Self {
74 FormaContext::Column
75 }
76}
77
78pub fn forma_to_query(
83 forma: &Forma,
84 table_name: &str,
85 context: &FormaContext,
86 id: Option<&JsonValue>,
87) -> Query {
88 let context_names: Option<&Vec<String>> = match context {
90 FormaContext::Detail => forma.detail.as_ref(),
91 FormaContext::Form => forma.form.as_ref(),
92 FormaContext::Column => forma.column.as_ref(),
93 };
94
95 let active_fields: Vec<&FormaField> = if let Some(names) = context_names {
96 names
97 .iter()
98 .filter_map(|n| forma.fields.iter().find(|f| &f.name == n))
99 .collect()
100 } else {
101 forma.fields.iter().collect()
102 };
103
104 let select: Vec<String> = active_fields.iter().map(|f| f.name.clone()).collect();
105
106 let mut where_ = IndexMap::new();
107 if let Some(id_val) = id {
108 where_.insert("id".to_owned(), id_val.clone());
109 }
110
111 Query {
112 model: table_name.to_owned(),
113 select,
114 where_,
115 filters: forma.filters.clone().unwrap_or_default(),
116 page: None,
117 per_page: None,
118 sort: None,
119 method: id.map(|_| "one".to_owned()),
120 }
121}
122
123#[allow(dead_code)]
125pub(crate) fn forma_field_to_field(sf: &FormaField, context: &FormaContext) -> Field {
126 let ftype = match context {
128 FormaContext::Detail => sf.detail_type.as_ref().unwrap_or(&sf.field_type),
129 FormaContext::Form => sf.form_type.as_ref().unwrap_or(&sf.field_type),
130 FormaContext::Column => sf.column_type.as_ref().unwrap_or(&sf.field_type),
131 };
132
133 let mut field = forma_field_type_to_field(&sf.label, ftype);
134
135 if let Some(fixed) = &sf.fixed_value {
136 field.options.fixed_value = Some(fixed.clone());
137 }
138 if let Some(rule) = &sf.rule {
139 field.options.rule = Some(rule.clone());
140 }
141
142 field
143}
144
145#[allow(dead_code)]
146fn forma_field_type_to_field(label: &str, ftype: &FormaFieldType) -> Field {
147 match ftype {
148 FormaFieldType::Named(name) => match name.as_str() {
149 "string" => Field::string(label),
150 "number" => Field::number(label),
151 "boolean" => Field::boolean(label),
152 "file" => Field::file(label),
153 entity => Field::reference(label, entity),
154 },
155 FormaFieldType::Array { array } => {
156 let item_field = forma_field_type_to_field(label, array);
157 Field::array(label, item_field.field_type)
158 }
159 FormaFieldType::Shape { shape } => {
160 let mut shape_fields = IndexMap::new();
161 for sf in shape {
162 let f = forma_field_type_to_field(&sf.label, &sf.field_type);
163 shape_fields.insert(
164 sf.name.clone(),
165 ShapeField::new(&sf.label, f.field_type),
166 );
167 }
168 Field::shape(label, shape_fields)
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use serde_json::json;
177
178 #[test]
179 fn forma_to_query_column_context() {
180 let forma = Forma {
181 fields: vec![
182 FormaField {
183 name: "name".to_owned(),
184 label: "Name".to_owned(),
185 field_type: FormaFieldType::Named("string".to_owned()),
186 ..Default::default()
187 },
188 FormaField {
189 name: "role".to_owned(),
190 label: "Role".to_owned(),
191 field_type: FormaFieldType::Named("role".to_owned()),
192 ..Default::default()
193 },
194 ],
195 column: Some(vec!["name".to_owned()]),
196 ..Default::default()
197 };
198
199 let query = forma_to_query(&forma, "user", &FormaContext::Column, None);
200 assert_eq!(query.model, "user");
201 assert_eq!(query.select, vec!["name"]);
202 assert!(query.where_.is_empty());
203 assert!(query.method.is_none());
204 }
205
206 #[test]
207 fn forma_to_query_with_id() {
208 let forma = Forma {
209 fields: vec![FormaField {
210 name: "name".to_owned(),
211 label: "Name".to_owned(),
212 field_type: FormaFieldType::Named("string".to_owned()),
213 ..Default::default()
214 }],
215 ..Default::default()
216 };
217
218 let query = forma_to_query(&forma, "user", &FormaContext::Form, Some(&json!(42)));
219 assert_eq!(query.where_.get("id"), Some(&json!(42)));
220 assert_eq!(query.method, Some("one".to_owned()));
221 }
222
223 #[test]
224 fn forma_field_to_field_maps_primitive_and_reference_kinds() {
225 use crate::field::FieldType;
226 let ctx = FormaContext::Column;
227 let cases: &[(&str, bool)] = &[
228 ("string", false),
229 ("number", false),
230 ("boolean", false),
231 ("file", false),
232 ("role", true),
233 ];
234 for (kind, is_ref) in cases {
235 let sf = FormaField {
236 name: "f".into(),
237 label: "F".into(),
238 field_type: FormaFieldType::Named(kind.to_string()),
239 ..Default::default()
240 };
241 let field = forma_field_to_field(&sf, &ctx);
242 match &field.field_type {
243 FieldType::Reference { reference } => {
244 assert!(is_ref, "expected primitive for kind={kind}");
245 assert_eq!(reference.entity, *kind);
246 }
247 FieldType::Primitive { .. } => {
248 assert!(!is_ref, "expected reference for kind={kind}");
249 }
250 other => panic!("unexpected field type for kind={kind}: {other:?}"),
251 }
252 }
253 }
254}
255
256impl Default for FormaField {
257 fn default() -> Self {
258 Self {
259 name: String::new(),
260 label: String::new(),
261 field_type: FormaFieldType::default(),
262 detail_type: None,
263 form_type: None,
264 column_type: None,
265 fixed_value: None,
266 rule: None,
267 }
268 }
269}