use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::field::{Field, ShapeField};
use crate::query::Query;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FormaFieldType {
Named(String),
Array {
array: Box<FormaFieldType>,
},
Shape {
shape: Vec<FormaField>,
},
}
impl Default for FormaFieldType {
fn default() -> Self {
FormaFieldType::Named("string".to_owned())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FormaField {
pub name: String,
pub label: String,
#[serde(default)]
pub field_type: FormaFieldType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail_type: Option<FormaFieldType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub form_type: Option<FormaFieldType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column_type: Option<FormaFieldType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fixed_value: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rule: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Forma {
#[serde(default)]
pub fields: Vec<FormaField>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub form: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filters: Option<Vec<Vec<String>>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FormaContext {
Detail,
Form,
Column,
}
impl Default for FormaContext {
fn default() -> Self {
FormaContext::Column
}
}
pub fn forma_to_query(
forma: &Forma,
table_name: &str,
context: &FormaContext,
id: Option<&JsonValue>,
) -> Query {
let context_names: Option<&Vec<String>> = match context {
FormaContext::Detail => forma.detail.as_ref(),
FormaContext::Form => forma.form.as_ref(),
FormaContext::Column => forma.column.as_ref(),
};
let active_fields: Vec<&FormaField> = if let Some(names) = context_names {
names
.iter()
.filter_map(|n| forma.fields.iter().find(|f| &f.name == n))
.collect()
} else {
forma.fields.iter().collect()
};
let select: Vec<String> = active_fields.iter().map(|f| f.name.clone()).collect();
let mut where_ = IndexMap::new();
if let Some(id_val) = id {
where_.insert("id".to_owned(), id_val.clone());
}
Query {
model: table_name.to_owned(),
select,
where_,
filters: forma.filters.clone().unwrap_or_default(),
page: None,
per_page: None,
sort: None,
method: id.map(|_| "one".to_owned()),
}
}
#[allow(dead_code)]
pub(crate) fn forma_field_to_field(sf: &FormaField, context: &FormaContext) -> Field {
let ftype = match context {
FormaContext::Detail => sf.detail_type.as_ref().unwrap_or(&sf.field_type),
FormaContext::Form => sf.form_type.as_ref().unwrap_or(&sf.field_type),
FormaContext::Column => sf.column_type.as_ref().unwrap_or(&sf.field_type),
};
let mut field = forma_field_type_to_field(&sf.label, ftype);
if let Some(fixed) = &sf.fixed_value {
field.options.fixed_value = Some(fixed.clone());
}
if let Some(rule) = &sf.rule {
field.options.rule = Some(rule.clone());
}
field
}
#[allow(dead_code)]
fn forma_field_type_to_field(label: &str, ftype: &FormaFieldType) -> Field {
match ftype {
FormaFieldType::Named(name) => match name.as_str() {
"string" => Field::string(label),
"number" => Field::number(label),
"boolean" => Field::boolean(label),
"file" => Field::file(label),
entity => Field::reference(label, entity),
},
FormaFieldType::Array { array } => {
let item_field = forma_field_type_to_field(label, array);
Field::array(label, item_field.field_type)
}
FormaFieldType::Shape { shape } => {
let mut shape_fields = IndexMap::new();
for sf in shape {
let f = forma_field_type_to_field(&sf.label, &sf.field_type);
shape_fields.insert(
sf.name.clone(),
ShapeField::new(&sf.label, f.field_type),
);
}
Field::shape(label, shape_fields)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn forma_to_query_column_context() {
let forma = Forma {
fields: vec![
FormaField {
name: "name".to_owned(),
label: "Name".to_owned(),
field_type: FormaFieldType::Named("string".to_owned()),
..Default::default()
},
FormaField {
name: "role".to_owned(),
label: "Role".to_owned(),
field_type: FormaFieldType::Named("role".to_owned()),
..Default::default()
},
],
column: Some(vec!["name".to_owned()]),
..Default::default()
};
let query = forma_to_query(&forma, "user", &FormaContext::Column, None);
assert_eq!(query.model, "user");
assert_eq!(query.select, vec!["name"]);
assert!(query.where_.is_empty());
assert!(query.method.is_none());
}
#[test]
fn forma_to_query_with_id() {
let forma = Forma {
fields: vec![FormaField {
name: "name".to_owned(),
label: "Name".to_owned(),
field_type: FormaFieldType::Named("string".to_owned()),
..Default::default()
}],
..Default::default()
};
let query = forma_to_query(&forma, "user", &FormaContext::Form, Some(&json!(42)));
assert_eq!(query.where_.get("id"), Some(&json!(42)));
assert_eq!(query.method, Some("one".to_owned()));
}
#[test]
fn forma_field_to_field_maps_primitive_and_reference_kinds() {
use crate::field::FieldType;
let ctx = FormaContext::Column;
let cases: &[(&str, bool)] = &[
("string", false),
("number", false),
("boolean", false),
("file", false),
("role", true),
];
for (kind, is_ref) in cases {
let sf = FormaField {
name: "f".into(),
label: "F".into(),
field_type: FormaFieldType::Named(kind.to_string()),
..Default::default()
};
let field = forma_field_to_field(&sf, &ctx);
match &field.field_type {
FieldType::Reference { reference } => {
assert!(is_ref, "expected primitive for kind={kind}");
assert_eq!(reference.entity, *kind);
}
FieldType::Primitive { .. } => {
assert!(!is_ref, "expected reference for kind={kind}");
}
other => panic!("unexpected field type for kind={kind}: {other:?}"),
}
}
}
}
impl Default for FormaField {
fn default() -> Self {
Self {
name: String::new(),
label: String::new(),
field_type: FormaFieldType::default(),
detail_type: None,
form_type: None,
column_type: None,
fixed_value: None,
rule: None,
}
}
}