1use std::collections::HashMap;
26use std::rc::Rc;
27use std::sync::Arc;
28
29use indexmap::IndexMap;
30use hyle::{
31 Blueprint, Column, Field, FieldType, Forma, FormaContext, Manifest, Outcome,
32 PurifyError, Query, Row, Source, Value,
33 display_value, forma_to_query, rows_from_outcome, is_single,
34};
35use hyle::view::derive_columns;
36
37pub struct FieldChange<R = ()> {
44 pub field: Field,
45 pub render: Option<R>,
47}
48
49impl<R> FieldChange<R> {
50 pub fn label(f: &Field, label: impl Into<String>) -> Option<FieldChange<R>> {
53 Some(FieldChange { field: Field { label: label.into(), ..f.clone() }, render: None })
54 }
55}
56
57pub type FieldChangeFn = Rc<dyn Fn(&Field) -> Option<FieldChange>>;
59
60pub type FieldChangeMap = HashMap<String, FieldChangeFn>;
65
66#[derive(Clone, PartialEq)]
70pub enum HyleManifestState {
71 Ready { manifest: Manifest },
72 Error { error: String },
73}
74
75pub struct HyleDataField {
79 pub key: String,
80 pub label: String,
81 pub field: Field,
82 pub raw: Value,
84 pub render: Rc<dyn Fn() -> String>,
86}
87
88impl Clone for HyleDataField {
89 fn clone(&self) -> Self {
90 Self {
91 key: self.key.clone(),
92 label: self.label.clone(),
93 field: self.field.clone(),
94 raw: self.raw.clone(),
95 render: self.render.clone(),
96 }
97 }
98}
99
100impl PartialEq for HyleDataField {
101 fn eq(&self, other: &Self) -> bool {
102 self.key == other.key
103 && self.label == other.label
104 && self.field == other.field
105 && self.raw == other.raw
106 }
107}
108
109#[derive(Clone, PartialEq)]
111pub enum HyleDataState {
112 Loading { manifest: Option<Manifest> },
114 Error {
116 error: String,
117 manifest: Option<Manifest>,
118 },
119 Ready {
121 manifest: Manifest,
122 outcome: Outcome,
123 rows: Vec<Row>,
124 columns: Vec<Column>,
126 row: Option<Row>,
128 fields: Vec<HyleDataField>,
130 },
131}
132
133pub struct HyleFilterField<R = ()> {
145 pub key: String,
146 pub label: String,
147 pub field: Field,
148 pub options: Option<Vec<(String, String)>>,
150 pub display_field_type: Option<FieldType>,
154 pub render: Option<R>,
156}
157
158impl<R: Clone> Clone for HyleFilterField<R> {
159 fn clone(&self) -> Self {
160 Self {
161 key: self.key.clone(),
162 label: self.label.clone(),
163 field: self.field.clone(),
164 options: self.options.clone(),
165 display_field_type: self.display_field_type.clone(),
166 render: self.render.clone(),
167 }
168 }
169}
170
171impl<R> PartialEq for HyleFilterField<R> {
172 fn eq(&self, other: &Self) -> bool {
173 self.key == other.key
174 && self.label == other.label
175 && self.field == other.field
176 && self.options == other.options
177 && self.display_field_type == other.display_field_type
178 }
180}
181
182#[derive(Default)]
191pub struct AdapterFiltersOptions {
192 pub initial_committed: IndexMap<String, String>,
194 pub change: Option<FieldChangeMap>,
196}
197
198pub struct UseFormaOptions {
200 pub context: FormaContext,
202}
203
204impl Default for UseFormaOptions {
205 fn default() -> Self {
206 Self { context: FormaContext::Column }
207 }
208}
209
210#[derive(Default)]
216pub struct AdapterFormOptions {
217 pub initial_committed: IndexMap<String, String>,
219 pub change: Option<FieldChangeMap>,
221}
222
223impl AdapterFormOptions {
224 pub fn with_change(
226 mut self,
227 key: &str,
228 f: impl Fn(&Field) -> Option<FieldChange> + 'static,
229 ) -> Self {
230 self.change
231 .get_or_insert_with(HashMap::new)
232 .insert(key.to_owned(), Rc::new(f));
233 self
234 }
235}
236
237#[derive(Clone, PartialEq, Default, Debug, serde::Serialize, serde::Deserialize)]
242pub struct FormErrors(pub IndexMap<String, String>);
243
244impl FormErrors {
245 pub fn is_empty(&self) -> bool {
246 self.0.is_empty()
247 }
248}
249
250#[doc(hidden)]
260pub fn compute_manifest(blueprint: &Blueprint, query: &Query) -> HyleManifestState {
261 match blueprint.manifest(query.clone()) {
262 Ok(m) => HyleManifestState::Ready { manifest: m },
263 Err(e) => HyleManifestState::Error { error: e.to_string() },
264 }
265}
266
267#[doc(hidden)]
273pub fn compute_data(
274 blueprint: Arc<Blueprint>,
275 manifest: Manifest,
276 source: Source,
277) -> HyleDataState {
278 match blueprint.resolve(&manifest, &source) {
279 Err(e) => HyleDataState::Error {
280 error: e.to_string(),
281 manifest: Some(manifest),
282 },
283 Ok(outcome) => {
284 let rows = outcome.rows.rows();
285 let single = is_single(&manifest, &outcome);
287 let columns = match derive_columns(&blueprint, &manifest) {
288 Ok(c) => c,
289 Err(e) => return HyleDataState::Error { error: e.to_string(), manifest: Some(manifest) },
290 };
291 let row = if single { rows.first().cloned() } else { None };
292 let fields = build_fields(&blueprint, &manifest, &outcome, &columns, row.as_ref());
293 HyleDataState::Ready {
294 manifest,
295 outcome,
296 rows,
297 columns,
298 row,
299 fields,
300 }
301 }
302 }
303}
304
305fn build_fields(
307 blueprint: &Blueprint,
308 manifest: &Manifest,
309 outcome: &Outcome,
310 columns: &[Column],
311 row: Option<&Row>,
312) -> Vec<HyleDataField> {
313 let arc_bp = Arc::new(blueprint.clone());
314 let arc_oc = Arc::new(outcome.clone());
315 let base = manifest.base.clone();
316
317 columns
318 .iter()
319 .map(|col| {
320 let raw = row
321 .and_then(|r| r.get(&col.key))
322 .cloned()
323 .unwrap_or(Value::Null);
324
325 let bp2 = arc_bp.clone();
326 let oc2 = arc_oc.clone();
327 let base2 = base.clone();
328 let key2 = col.key.clone();
329 let raw2 = raw.clone();
330
331 HyleDataField {
332 key: col.key.clone(),
333 label: col.label.clone(),
334 field: col.field.clone(),
335 raw,
336 render: Rc::new(move || display_value(&bp2, &oc2, &base2, &key2, &raw2)),
337 }
338 })
339 .collect()
340}
341
342#[doc(hidden)]
348pub fn build_effective_query(
349 base: &Query,
350 committed: &IndexMap<String, String>,
351 page: usize,
352 per_page: usize,
353 sort_field: Option<&str>,
354 sort_ascending: bool,
355) -> Query {
356 let mut where_ = base.where_.clone();
357 for (k, v) in committed {
358 if !v.is_empty() {
359 where_.insert(k.clone(), Value::String(v.clone()));
360 }
361 }
362
363 let sort = sort_field
364 .map(|f| crate::Sort { field: f.to_owned(), ascending: sort_ascending })
365 .or_else(|| base.sort.clone());
366
367 Query {
368 where_,
369 page: Some(page),
370 per_page: Some(per_page),
371 sort,
372 ..base.clone()
373 }
374}
375
376#[doc(hidden)]
378pub fn run_purify(
379 _blueprint: &Blueprint,
380 _model_name: &str,
381 _form_data: &IndexMap<String, String>,
382) -> Option<Vec<PurifyError>> {
383 None
384}
385
386#[doc(hidden)]
397pub fn build_filter_fields(
398 blueprint: &Blueprint,
399 manifest: &Manifest,
400 outcome: &Outcome,
401) -> Vec<HyleFilterField> {
402 let columns = match derive_columns(blueprint, manifest) {
403 Ok(c) => c,
404 Err(_) => return vec![],
405 };
406
407 columns
408 .into_iter()
409 .map(|col| {
410 let mut options: Option<Vec<(String, String)>> = None;
411 let mut display_field_type: Option<FieldType> = None;
412
413 match &col.field.field_type {
414 FieldType::Reference { reference } => {
415 let pairs = outcome
416 .lookups
417 .get(&reference.entity)
418 .map(|lookup| {
419 lookup
420 .iter()
421 .map(|(id, row)| {
422 let label = row
423 .get(&reference.display_field)
424 .and_then(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
425 .unwrap_or(id.as_str())
426 .to_owned();
427 (id.clone(), label)
428 })
429 .collect::<Vec<_>>()
430 })
431 .unwrap_or_default();
432 options = Some(pairs);
433 display_field_type = blueprint
434 .models
435 .get(&reference.entity)
436 .and_then(|m| m.fields.get(&reference.display_field))
437 .map(|f| f.field_type.clone());
438 }
439 FieldType::Array { item } => {
440 if let FieldType::Reference { reference } = item.as_ref() {
441 let pairs = outcome
442 .lookups
443 .get(&reference.entity)
444 .map(|lookup| {
445 lookup
446 .iter()
447 .map(|(id, row)| {
448 let label = row
449 .get(&reference.display_field)
450 .and_then(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
451 .unwrap_or(id.as_str())
452 .to_owned();
453 (id.clone(), label)
454 })
455 .collect::<Vec<_>>()
456 })
457 .unwrap_or_default();
458 options = Some(pairs);
459 display_field_type = blueprint
460 .models
461 .get(&reference.entity)
462 .and_then(|m| m.fields.get(&reference.display_field))
463 .map(|f| f.field_type.clone());
464 } else {
465 let mut seen = std::collections::HashSet::new();
467 let mut pairs = vec![];
468 for row in rows_from_outcome(outcome) {
469 if let Some(Value::Array(arr)) = row.get(col.key.as_str()) {
470 for item in arr {
471 let s = match item {
472 Value::String(s) => s.clone(),
473 other => other.to_string(),
474 };
475 if seen.insert(s.clone()) {
476 pairs.push((s.clone(), s));
477 }
478 }
479 }
480 }
481 if !pairs.is_empty() {
482 options = Some(pairs);
483 }
484 }
485 }
486 _ => {}
487 };
488
489 HyleFilterField { key: col.key, label: col.label, field: col.field, options, display_field_type, render: None }
490 })
491 .collect()
492}
493
494#[doc(hidden)]
509pub fn apply_change<R>(
510 fields: Vec<HyleFilterField<R>>,
511 change: &HashMap<String, Rc<dyn Fn(&Field) -> Option<FieldChange<R>>>>,
512) -> Vec<HyleFilterField<R>> {
513 fields
514 .into_iter()
515 .filter_map(|mut ff| {
516 if let Some(change_fn) = change.get(&ff.key) {
517 match change_fn(&ff.field) {
518 None => return None,
519 Some(FieldChange { field, render }) => {
520 ff.label = field.label.clone();
521 ff.field = field;
522 ff.render = render;
523 }
524 }
525 }
526 Some(ff)
527 })
528 .collect()
529}
530
531#[doc(hidden)]
537pub fn compute_forma_result(
538 data: &HyleDataState,
539 table_name: &str,
540 id: Option<Value>,
541 context: &FormaContext,
542) -> (Option<Query>, Option<Forma>) {
543 let row = match data {
544 HyleDataState::Ready { row: Some(r), .. } => r,
545 _ => return (None, None),
546 };
547
548 let forma: Forma = match hyle::deser_row(row) {
549 Ok(f) => f,
550 Err(_) => return (None, None),
551 };
552
553 if forma.fields.is_empty() {
554 return (None, Some(forma));
555 }
556
557 let derived = forma_to_query(&forma, table_name, context, id.as_ref());
558 (Some(derived), Some(forma))
559}
560
561#[cfg(test)]
564mod tests {
565 use super::*;
566 use crate::{Field, Model};
567 use hyle::ModelRows;
568 use indexmap::IndexMap;
569 fn user_blueprint() -> Blueprint {
570 Blueprint::new()
571 .model(
572 "user",
573 Model::new()
574 .field("name", Field::string("Name").with_metadata("required", Value::Bool(true)))
575 .field("email", Field::string("Email"))
576 .field("active", Field::boolean("Active")),
577 )
578 }
579
580 fn user_query() -> Query {
581 Query::new("user").select(["name", "email", "active"])
582 }
583
584 fn user_source(rows: Vec<Row>) -> Source {
585 let mut src = Source::new();
586 src.insert("user".to_owned(), hyle::ModelResult::many(rows));
587 src
588 }
589
590 fn alice() -> Row {
591 indexmap::indexmap! {
592 "id".to_owned() => Value::Int(1),
593 "name".to_owned() => Value::String("Alice".to_owned()),
594 "email".to_owned() => Value::String("alice@example.test".to_owned()),
595 "active".to_owned() => Value::Bool(true),
596 }
597 }
598
599 #[test]
602 fn manifest_ok() {
603 let bp = user_blueprint();
604 let state = compute_manifest(&bp, &user_query());
605 assert!(matches!(state, HyleManifestState::Ready { .. }));
606 if let HyleManifestState::Ready { manifest } = state {
607 assert_eq!(manifest.base, "user");
608 assert!(manifest.fields.contains(&"name".to_owned()));
609 }
610 }
611
612 #[test]
613 fn manifest_unknown_model() {
614 let bp = user_blueprint();
615 let state = compute_manifest(&bp, &Query::new("ghost"));
616 assert!(matches!(state, HyleManifestState::Error { .. }));
617 }
618
619 #[test]
622 fn data_ready_with_rows() {
623 let bp = Arc::new(user_blueprint());
624 let manifest = bp.manifest(user_query()).unwrap();
625 let src = user_source(vec![alice()]);
626 let state = compute_data(bp, manifest, src);
627 assert!(matches!(state, HyleDataState::Ready { .. }));
628 if let HyleDataState::Ready { rows, row, .. } = state {
629 assert_eq!(rows.len(), 1);
630 assert!(row.is_none());
631 }
632 }
633
634 #[test]
635 fn data_ready_single_record() {
636 let bp = Arc::new(user_blueprint());
637 let q = Query {
638 model: "user".to_owned(),
639 select: vec!["name".to_owned(), "email".to_owned(), "active".to_owned()],
640 method: Some("one".to_owned()),
641 ..Default::default()
642 };
643 let manifest = bp.manifest(q).unwrap();
644 let state = compute_data(bp, manifest, user_source(vec![alice()]));
645 if let HyleDataState::Ready { row, .. } = state {
646 assert!(row.is_some());
647 } else {
648 panic!("expected Ready");
649 }
650 }
651
652 #[test]
653 fn data_fields_built_for_single_record() {
654 let bp = Arc::new(user_blueprint());
655 let q = Query {
656 model: "user".to_owned(),
657 select: vec!["name".to_owned()],
658 method: Some("one".to_owned()),
659 ..Default::default()
660 };
661 let manifest = bp.manifest(q).unwrap();
662 let state = compute_data(bp, manifest, user_source(vec![alice()]));
663 if let HyleDataState::Ready { fields, .. } = state {
664 assert!(!fields.is_empty());
665 let name_field = fields.iter().find(|f| f.key == "name").unwrap();
666 assert_eq!(name_field.render.as_ref()(), "Alice");
667 } else {
668 panic!("expected Ready");
669 }
670 }
671
672 #[test]
675 fn effective_query_merges_committed() {
676 let mut committed = IndexMap::new();
677 committed.insert("name".to_owned(), "Bob".to_owned());
678 let q = build_effective_query(&user_query(), &committed, 1, 20, None, true);
679 assert_eq!(q.where_.get("name"), Some(&Value::String("Bob".to_owned())));
680 }
681
682 #[test]
683 fn effective_query_skips_empty_values() {
684 let mut committed = IndexMap::new();
685 committed.insert("name".to_owned(), String::new());
686 let q = build_effective_query(&user_query(), &committed, 1, 20, None, true);
687 assert!(!q.where_.contains_key("name"));
688 }
689
690 #[test]
691 fn effective_query_applies_sort() {
692 let q = build_effective_query(&user_query(), &IndexMap::new(), 2, 50, Some("name"), false);
693 assert_eq!(q.page, Some(2));
694 assert_eq!(q.per_page, Some(50));
695 let sort = q.sort.unwrap();
696 assert_eq!(sort.field, "name");
697 assert!(!sort.ascending);
698 }
699
700 fn make_filter_field(key: &str, label: &str) -> HyleFilterField {
703 HyleFilterField {
704 key: key.to_owned(),
705 label: label.to_owned(),
706 field: Field {
707 label: label.to_owned(),
708 field_type: crate::FieldType::Primitive { primitive: crate::Primitive::String },
709 options: Default::default(),
710 },
711 options: None,
712 display_field_type: None,
713 render: None,
714 }
715 }
716
717 #[test]
718 fn apply_change_identity_when_no_map() {
719 let fields = vec![make_filter_field("name", "Name"), make_filter_field("email", "Email")];
720 let result = apply_change(fields, &HashMap::new());
721 assert_eq!(result.len(), 2);
722 }
723
724 #[test]
725 fn apply_change_modifies_label() {
726 let fields = vec![make_filter_field("name", "Name")];
727 let mut change: FieldChangeMap = HashMap::new();
728 change.insert(
729 "name".to_owned(),
730 Rc::new(|f: &Field| Some(FieldChange { field: Field { label: "Full Name".to_owned(), ..f.clone() }, render: None })),
731 );
732 let result = apply_change(fields, &change);
733 assert_eq!(result.len(), 1);
734 assert_eq!(result[0].label, "Full Name");
735 }
736
737 #[test]
738 fn apply_change_none_removes_field() {
739 let fields = vec![make_filter_field("name", "Name"), make_filter_field("email", "Email")];
740 let mut change: FieldChangeMap = HashMap::new();
741 change.insert("name".to_owned(), Rc::new(|_: &Field| None));
742 let result = apply_change(fields, &change);
743 assert_eq!(result.len(), 1);
744 assert_eq!(result[0].key, "email");
745 }
746
747 #[test]
748 fn apply_change_unknown_key_passes_through() {
749 let fields = vec![make_filter_field("name", "Name")];
750 let mut change: FieldChangeMap = HashMap::new();
751 change.insert("ghost".to_owned(), Rc::new(|_: &Field| None));
752 let result = apply_change(fields, &change);
753 assert_eq!(result.len(), 1);
754 assert_eq!(result[0].key, "name");
755 }
756
757 fn make_manifest(base: &str) -> Manifest {
760 Manifest {
761 base: base.to_owned(),
762 id: None,
763 fields: vec![],
764 filter: Default::default(),
765 lookups: vec![],
766 inlines: vec![],
767 page: None,
768 per_page: None,
769 sort: None,
770 method: None,
771 filter_fields: vec![],
772 }
773 }
774
775 fn make_outcome() -> Outcome {
776 Outcome { rows: ModelRows::Many(vec![]), total: 0, lookups: Default::default() }
777 }
778
779 #[test]
780 fn forma_returns_none_while_loading() {
781 let state = HyleDataState::Loading { manifest: None };
782 let (q, f) = compute_forma_result(&state, "user", None, &FormaContext::Column);
783 assert!(q.is_none());
784 assert!(f.is_none());
785 }
786
787 #[test]
788 fn forma_returns_none_query_when_no_fields() {
789 let forma = crate::Forma { fields: vec![], ..Default::default() };
790 let row: Row = serde_json::from_value(serde_json::to_value(&forma).unwrap()).unwrap();
791 let state = HyleDataState::Ready {
792 manifest: make_manifest("forma"),
793 outcome: make_outcome(),
794 rows: vec![row.clone()],
795 columns: vec![],
796 row: Some(row),
797 fields: vec![],
798 };
799 let (q, f) = compute_forma_result(&state, "user", None, &FormaContext::Column);
800 assert!(q.is_none());
801 assert!(f.is_some());
802 }
803
804 #[test]
805 fn forma_derives_query_with_fields() {
806 use crate::{Forma, FormaField, FormaFieldType};
807 let forma = Forma {
808 fields: vec![FormaField {
809 name: "name".to_owned(),
810 label: "Name".to_owned(),
811 field_type: FormaFieldType::Named("string".to_owned()),
812 ..Default::default()
813 }],
814 column: Some(vec!["name".to_owned()]),
815 ..Default::default()
816 };
817 let row: Row = serde_json::from_value(serde_json::to_value(&forma).unwrap()).unwrap();
818 let state = HyleDataState::Ready {
819 manifest: make_manifest("forma"),
820 outcome: make_outcome(),
821 rows: vec![row.clone()],
822 columns: vec![],
823 row: Some(row),
824 fields: vec![],
825 };
826 let (q, f) = compute_forma_result(&state, "user", None, &FormaContext::Column);
827 assert!(f.is_some());
828 let q = q.expect("should have derived query");
829 assert_eq!(q.model, "user");
830 assert!(q.select.contains(&"name".to_owned()));
831 }
832}