use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use indexmap::IndexMap;
use serde_json::Value as JsonValue;
use crate::{
Blueprint, Column, Field, FieldType, Forma, FormaContext, Manifest, Outcome,
PurifyError, Query, Row, Source, Value,
display_value, forma_to_query, purify_row_sync,
};
use crate::view::derive_columns;
use crate::raw::rows_from_outcome;
pub struct FieldChange<R = ()> {
pub field: Field,
pub render: Option<R>,
}
impl<R> FieldChange<R> {
pub fn label(f: &Field, label: impl Into<String>) -> Option<FieldChange<R>> {
Some(FieldChange { field: Field { label: label.into(), ..f.clone() }, render: None })
}
}
pub type FieldChangeFn = Rc<dyn Fn(&Field) -> Option<FieldChange>>;
pub type FieldChangeMap = HashMap<String, FieldChangeFn>;
#[derive(Clone, PartialEq)]
pub enum HyleManifestState {
Ready { manifest: Manifest },
Error { error: String },
}
pub struct HyleDataField {
pub key: String,
pub label: String,
pub field: Field,
pub raw: Value,
pub render: Rc<dyn Fn() -> String>,
}
impl Clone for HyleDataField {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
label: self.label.clone(),
field: self.field.clone(),
raw: self.raw.clone(),
render: self.render.clone(),
}
}
}
impl PartialEq for HyleDataField {
fn eq(&self, other: &Self) -> bool {
self.key == other.key
&& self.label == other.label
&& self.field == other.field
&& self.raw == other.raw
}
}
#[derive(Clone, PartialEq)]
pub enum HyleDataState {
Loading { manifest: Option<Manifest> },
Error {
error: String,
manifest: Option<Manifest>,
},
Ready {
manifest: Manifest,
outcome: Outcome,
rows: Vec<Row>,
columns: Vec<Column>,
row: Option<Row>,
fields: Vec<HyleDataField>,
},
}
pub struct HyleFilterField<R = ()> {
pub key: String,
pub label: String,
pub field: Field,
pub options: Option<Vec<(String, String)>>,
pub display_field_type: Option<FieldType>,
pub render: Option<R>,
}
impl<R: Clone> Clone for HyleFilterField<R> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
label: self.label.clone(),
field: self.field.clone(),
options: self.options.clone(),
display_field_type: self.display_field_type.clone(),
render: self.render.clone(),
}
}
}
impl<R> PartialEq for HyleFilterField<R> {
fn eq(&self, other: &Self) -> bool {
self.key == other.key
&& self.label == other.label
&& self.field == other.field
&& self.options == other.options
&& self.display_field_type == other.display_field_type
}
}
#[derive(Default)]
pub struct AdapterFiltersOptions {
pub initial_committed: IndexMap<String, String>,
pub change: Option<FieldChangeMap>,
}
pub struct UseFormaOptions {
pub context: FormaContext,
}
impl Default for UseFormaOptions {
fn default() -> Self {
Self { context: FormaContext::Column }
}
}
#[derive(Default)]
pub struct AdapterFormOptions {
pub initial_committed: IndexMap<String, String>,
pub change: Option<FieldChangeMap>,
}
impl AdapterFormOptions {
pub fn with_change(
mut self,
key: &str,
f: impl Fn(&Field) -> Option<FieldChange> + 'static,
) -> Self {
self.change
.get_or_insert_with(HashMap::new)
.insert(key.to_owned(), Rc::new(f));
self
}
}
#[derive(Clone, PartialEq, Default, Debug, serde::Serialize, serde::Deserialize)]
pub struct FormErrors(pub IndexMap<String, String>);
impl FormErrors {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[doc(hidden)]
pub fn compute_manifest(blueprint: &Blueprint, query: &Query) -> HyleManifestState {
match blueprint.manifest(query.clone()) {
Ok(m) => HyleManifestState::Ready { manifest: m },
Err(e) => HyleManifestState::Error { error: e.to_string() },
}
}
#[doc(hidden)]
pub fn compute_data(
blueprint: Arc<Blueprint>,
manifest: Manifest,
source: Source,
) -> HyleDataState {
match blueprint.resolve_and_view(&manifest, &source) {
Err(e) => HyleDataState::Error {
error: e.to_string(),
manifest: Some(manifest),
},
Ok(view) => {
let row = if view.is_single { view.rows.first().cloned() } else { None };
let fields = build_fields(&blueprint, &manifest, &view.outcome, &view.columns, row.as_ref());
HyleDataState::Ready {
manifest,
outcome: view.outcome,
rows: view.rows,
columns: view.columns,
row,
fields,
}
}
}
}
fn build_fields(
blueprint: &Blueprint,
manifest: &Manifest,
outcome: &Outcome,
columns: &[Column],
row: Option<&Row>,
) -> Vec<HyleDataField> {
let arc_bp = Arc::new(blueprint.clone());
let arc_oc = Arc::new(outcome.clone());
let base = manifest.base.clone();
columns
.iter()
.map(|col| {
let raw = row
.and_then(|r| r.get(&col.key))
.cloned()
.unwrap_or(Value::Null);
let bp2 = arc_bp.clone();
let oc2 = arc_oc.clone();
let base2 = base.clone();
let key2 = col.key.clone();
let raw2 = raw.clone();
HyleDataField {
key: col.key.clone(),
label: col.label.clone(),
field: col.field.clone(),
raw,
render: Rc::new(move || display_value(&bp2, &oc2, &base2, &key2, &raw2)),
}
})
.collect()
}
#[doc(hidden)]
pub fn build_effective_query(
base: &Query,
committed: &IndexMap<String, String>,
page: usize,
per_page: usize,
sort_field: Option<&str>,
sort_ascending: bool,
) -> Query {
let mut where_ = base.where_.clone();
for (k, v) in committed {
if !v.is_empty() {
where_.insert(k.clone(), JsonValue::String(v.clone()));
}
}
let sort = sort_field
.map(|f| crate::Sort { field: f.to_owned(), ascending: sort_ascending })
.or_else(|| base.sort.clone());
Query {
where_,
page: Some(page),
per_page: Some(per_page),
sort,
..base.clone()
}
}
#[doc(hidden)]
pub fn run_purify(
blueprint: &Blueprint,
model_name: &str,
form_data: &IndexMap<String, String>,
) -> Option<Vec<PurifyError>> {
let row: Row = form_data
.iter()
.map(|(k, v)| (k.clone(), JsonValue::String(v.clone())))
.collect();
purify_row_sync(blueprint, model_name, &row).err()
}
#[doc(hidden)]
pub fn build_filter_fields(
blueprint: &Blueprint,
manifest: &Manifest,
outcome: &Outcome,
) -> Vec<HyleFilterField> {
let columns = match derive_columns(blueprint, manifest) {
Ok(c) => c,
Err(_) => return vec![],
};
columns
.into_iter()
.map(|col| {
let mut options: Option<Vec<(String, String)>> = None;
let mut display_field_type: Option<FieldType> = None;
match &col.field.field_type {
FieldType::Reference { reference } => {
let pairs = outcome
.lookups
.get(&reference.entity)
.map(|lookup| {
lookup
.iter()
.map(|(id, row)| {
let label = row
.get(&reference.display_field)
.and_then(|v| v.as_str())
.unwrap_or(id.as_str())
.to_owned();
(id.clone(), label)
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
options = Some(pairs);
display_field_type = blueprint
.models
.get(&reference.entity)
.and_then(|m| m.fields.get(&reference.display_field))
.map(|f| f.field_type.clone());
}
FieldType::Array { item } => {
if let FieldType::Reference { reference } = item.as_ref() {
let pairs = outcome
.lookups
.get(&reference.entity)
.map(|lookup| {
lookup
.iter()
.map(|(id, row)| {
let label = row
.get(&reference.display_field)
.and_then(|v| v.as_str())
.unwrap_or(id.as_str())
.to_owned();
(id.clone(), label)
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
options = Some(pairs);
display_field_type = blueprint
.models
.get(&reference.entity)
.and_then(|m| m.fields.get(&reference.display_field))
.map(|f| f.field_type.clone());
} else {
let mut seen = std::collections::HashSet::new();
let mut pairs = vec![];
for row in rows_from_outcome(outcome) {
if let Some(Value::Array(arr)) = row.get(col.key.as_str()) {
for item in arr {
let s = match item {
Value::String(s) => s.clone(),
other => other.to_string(),
};
if seen.insert(s.clone()) {
pairs.push((s.clone(), s));
}
}
}
}
if !pairs.is_empty() {
options = Some(pairs);
}
}
}
_ => {}
};
HyleFilterField { key: col.key, label: col.label, field: col.field, options, display_field_type, render: None }
})
.collect()
}
#[doc(hidden)]
pub fn apply_change<R>(
fields: Vec<HyleFilterField<R>>,
change: &HashMap<String, Rc<dyn Fn(&Field) -> Option<FieldChange<R>>>>,
) -> Vec<HyleFilterField<R>> {
fields
.into_iter()
.filter_map(|mut ff| {
if let Some(change_fn) = change.get(&ff.key) {
match change_fn(&ff.field) {
None => return None,
Some(FieldChange { field, render }) => {
ff.label = field.label.clone();
ff.field = field;
ff.render = render;
}
}
}
Some(ff)
})
.collect()
}
#[doc(hidden)]
pub fn compute_forma_result(
data: &HyleDataState,
table_name: &str,
id: Option<JsonValue>,
context: &FormaContext,
) -> (Option<Query>, Option<Forma>) {
let row = match data {
HyleDataState::Ready { row: Some(r), .. } => r,
_ => return (None, None),
};
let forma: Forma = match serde_json::from_value(JsonValue::Object(
row.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
)) {
Ok(f) => f,
Err(_) => return (None, None),
};
if forma.fields.is_empty() {
return (None, Some(forma));
}
let derived = forma_to_query(&forma, table_name, context, id.as_ref());
(Some(derived), Some(forma))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Field, Model};
use crate::raw::ModelRows;
use indexmap::IndexMap;
use serde_json::json;
fn user_blueprint() -> Blueprint {
Blueprint::new()
.model(
"user",
Model::new()
.field("name", Field::string("Name").with_metadata("required", json!(true)))
.field("email", Field::string("Email"))
.field("active", Field::boolean("Active")),
)
}
fn user_query() -> Query {
Query::new("user").select(["name", "email", "active"])
}
fn user_source(rows: Vec<Row>) -> Source {
let mut src = Source::new();
src.insert("user".to_owned(), crate::ModelResult::many(rows));
src
}
fn alice() -> Row {
indexmap::indexmap! {
"id".to_owned() => json!(1),
"name".to_owned() => json!("Alice"),
"email".to_owned() => json!("alice@example.test"),
"active".to_owned() => json!(true),
}
}
#[test]
fn manifest_ok() {
let bp = user_blueprint();
let state = compute_manifest(&bp, &user_query());
assert!(matches!(state, HyleManifestState::Ready { .. }));
if let HyleManifestState::Ready { manifest } = state {
assert_eq!(manifest.base, "user");
assert!(manifest.fields.contains(&"name".to_owned()));
}
}
#[test]
fn manifest_unknown_model() {
let bp = user_blueprint();
let state = compute_manifest(&bp, &Query::new("ghost"));
assert!(matches!(state, HyleManifestState::Error { .. }));
}
#[test]
fn data_ready_with_rows() {
let bp = Arc::new(user_blueprint());
let manifest = bp.manifest(user_query()).unwrap();
let src = user_source(vec![alice()]);
let state = compute_data(bp, manifest, src);
assert!(matches!(state, HyleDataState::Ready { .. }));
if let HyleDataState::Ready { rows, row, .. } = state {
assert_eq!(rows.len(), 1);
assert!(row.is_none());
}
}
#[test]
fn data_ready_single_record() {
let bp = Arc::new(user_blueprint());
let q = Query {
model: "user".to_owned(),
select: vec!["name".to_owned(), "email".to_owned(), "active".to_owned()],
method: Some("one".to_owned()),
..Default::default()
};
let manifest = bp.manifest(q).unwrap();
let state = compute_data(bp, manifest, user_source(vec![alice()]));
if let HyleDataState::Ready { row, .. } = state {
assert!(row.is_some());
} else {
panic!("expected Ready");
}
}
#[test]
fn data_fields_built_for_single_record() {
let bp = Arc::new(user_blueprint());
let q = Query {
model: "user".to_owned(),
select: vec!["name".to_owned()],
method: Some("one".to_owned()),
..Default::default()
};
let manifest = bp.manifest(q).unwrap();
let state = compute_data(bp, manifest, user_source(vec![alice()]));
if let HyleDataState::Ready { fields, .. } = state {
assert!(!fields.is_empty());
let name_field = fields.iter().find(|f| f.key == "name").unwrap();
assert_eq!(name_field.render.as_ref()(), "Alice");
} else {
panic!("expected Ready");
}
}
#[test]
fn effective_query_merges_committed() {
let mut committed = IndexMap::new();
committed.insert("name".to_owned(), "Bob".to_owned());
let q = build_effective_query(&user_query(), &committed, 1, 20, None, true);
assert_eq!(q.where_.get("name"), Some(&json!("Bob")));
}
#[test]
fn effective_query_skips_empty_values() {
let mut committed = IndexMap::new();
committed.insert("name".to_owned(), String::new());
let q = build_effective_query(&user_query(), &committed, 1, 20, None, true);
assert!(!q.where_.contains_key("name"));
}
#[test]
fn effective_query_applies_sort() {
let q = build_effective_query(&user_query(), &IndexMap::new(), 2, 50, Some("name"), false);
assert_eq!(q.page, Some(2));
assert_eq!(q.per_page, Some(50));
let sort = q.sort.unwrap();
assert_eq!(sort.field, "name");
assert!(!sort.ascending);
}
#[test]
fn purify_passes_valid_row() {
let bp = user_blueprint();
let mut form = IndexMap::new();
form.insert("name".to_owned(), "Alice".to_owned());
assert!(run_purify(&bp, "user", &form).is_none());
}
#[test]
fn purify_catches_required_violation() {
let bp = user_blueprint();
let errors = run_purify(&bp, "user", &IndexMap::new());
assert!(errors.is_some());
assert!(errors.unwrap().iter().any(|e| e.field == "name" && e.rule == "required"));
}
fn make_filter_field(key: &str, label: &str) -> HyleFilterField {
HyleFilterField {
key: key.to_owned(),
label: label.to_owned(),
field: Field {
label: label.to_owned(),
field_type: crate::FieldType::Primitive { primitive: crate::Primitive::String },
options: Default::default(),
},
options: None,
display_field_type: None,
render: None,
}
}
#[test]
fn apply_change_identity_when_no_map() {
let fields = vec![make_filter_field("name", "Name"), make_filter_field("email", "Email")];
let result = apply_change(fields, &HashMap::new());
assert_eq!(result.len(), 2);
}
#[test]
fn apply_change_modifies_label() {
let fields = vec![make_filter_field("name", "Name")];
let mut change: FieldChangeMap = HashMap::new();
change.insert(
"name".to_owned(),
Rc::new(|f: &Field| Some(FieldChange { field: Field { label: "Full Name".to_owned(), ..f.clone() }, render: None })),
);
let result = apply_change(fields, &change);
assert_eq!(result.len(), 1);
assert_eq!(result[0].label, "Full Name");
}
#[test]
fn apply_change_none_removes_field() {
let fields = vec![make_filter_field("name", "Name"), make_filter_field("email", "Email")];
let mut change: FieldChangeMap = HashMap::new();
change.insert("name".to_owned(), Rc::new(|_: &Field| None));
let result = apply_change(fields, &change);
assert_eq!(result.len(), 1);
assert_eq!(result[0].key, "email");
}
#[test]
fn apply_change_unknown_key_passes_through() {
let fields = vec![make_filter_field("name", "Name")];
let mut change: FieldChangeMap = HashMap::new();
change.insert("ghost".to_owned(), Rc::new(|_: &Field| None));
let result = apply_change(fields, &change);
assert_eq!(result.len(), 1);
assert_eq!(result[0].key, "name");
}
fn make_manifest(base: &str) -> Manifest {
Manifest {
base: base.to_owned(),
id: None,
fields: vec![],
filter: Default::default(),
lookups: vec![],
inlines: vec![],
page: None,
per_page: None,
sort: None,
method: None,
filter_fields: vec![],
}
}
fn make_outcome() -> Outcome {
Outcome { rows: ModelRows::Many(vec![]), total: 0, lookups: Default::default() }
}
#[test]
fn forma_returns_none_while_loading() {
let state = HyleDataState::Loading { manifest: None };
let (q, f) = compute_forma_result(&state, "user", None, &FormaContext::Column);
assert!(q.is_none());
assert!(f.is_none());
}
#[test]
fn forma_returns_none_query_when_no_fields() {
let forma = crate::Forma { fields: vec![], ..Default::default() };
let row: Row = serde_json::from_value(serde_json::to_value(&forma).unwrap()).unwrap();
let state = HyleDataState::Ready {
manifest: make_manifest("forma"),
outcome: make_outcome(),
rows: vec![row.clone()],
columns: vec![],
row: Some(row),
fields: vec![],
};
let (q, f) = compute_forma_result(&state, "user", None, &FormaContext::Column);
assert!(q.is_none());
assert!(f.is_some());
}
#[test]
fn forma_derives_query_with_fields() {
use crate::{Forma, FormaField, FormaFieldType};
let forma = Forma {
fields: vec![FormaField {
name: "name".to_owned(),
label: "Name".to_owned(),
field_type: FormaFieldType::Named("string".to_owned()),
..Default::default()
}],
column: Some(vec!["name".to_owned()]),
..Default::default()
};
let row: Row = serde_json::from_value(serde_json::to_value(&forma).unwrap()).unwrap();
let state = HyleDataState::Ready {
manifest: make_manifest("forma"),
outcome: make_outcome(),
rows: vec![row.clone()],
columns: vec![],
row: Some(row),
fields: vec![],
};
let (q, f) = compute_forma_result(&state, "user", None, &FormaContext::Column);
assert!(f.is_some());
let q = q.expect("should have derived query");
assert_eq!(q.model, "user");
assert!(q.select.contains(&"name".to_owned()));
}
}