From f94b055d0648ca1393b53f184cb009ff31dd1f98 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 22 Sep 2023 11:04:27 -0700 Subject: [PATCH 01/10] guard against trailing non-widget fields in EditFoo.svelte template --- src/scaffold/entry_type.rs | 10 ++++++++++ src/scaffold/entry_type/definitions.rs | 1 + ..._case entry_type.name}}.svelte{{\302\241if}}.hbs" | 12 ++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/scaffold/entry_type.rs b/src/scaffold/entry_type.rs index 4fc148bf7..78d58b769 100644 --- a/src/scaffold/entry_type.rs +++ b/src/scaffold/entry_type.rs @@ -101,6 +101,15 @@ pub fn scaffold_entry_type( } }; + let widget_fields = fields + .iter() + .map(|f| f.clone()) + .filter(|f| match f.widget { + Some(_) => true, + None => false + }) + .collect(); + let reference_entry_hash = match maybe_reference_entry_hash { Some(r) => r.clone(), None => { @@ -147,6 +156,7 @@ pub fn scaffold_entry_type( let entry_def = EntryDefinition { name: name.clone(), fields, + widget_fields, reference_entry_hash, }; diff --git a/src/scaffold/entry_type/definitions.rs b/src/scaffold/entry_type/definitions.rs index 04cf9507e..d262545de 100644 --- a/src/scaffold/entry_type/definitions.rs +++ b/src/scaffold/entry_type/definitions.rs @@ -298,6 +298,7 @@ impl Referenceable { pub struct EntryDefinition { pub name: String, pub fields: Vec, + pub widget_fields: Vec, pub reference_entry_hash: bool, } diff --git "a/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" "b/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" index 5e8ec4dea..45917c7ff 100644 --- "a/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" +++ "b/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" @@ -39,7 +39,7 @@ let {{camel_case field_name}}: Array<{{> (concat field_type.type "/type")}} | un let errorSnackbar: Snackbar; -$: {{#each entry_type.fields}}{{#if widget}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/if}}{{/each}}; +$: {{#each entry_type.widget_fields}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/each}}; $: is{{pascal_case entry_type.name}}Valid = true{{#each entry_type.fields}}{{#if widget}}{{#if (eq cardinality "single")}} && {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate=(camel_case field_name) }}{{/if}}{{#if (eq cardinality "vector")}} && {{camel_case field_name}}.every(e => {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate="e" }}){{/if}}{{/if}}{{/each}}; onMount(() => { @@ -55,7 +55,7 @@ onMount(() => { async function update{{pascal_case entry_type.name}}() { - const {{camel_case entry_type.name}}: {{pascal_case entry_type.name}} = { + const {{camel_case entry_type.name}}: {{pascal_case entry_type.name}} = { {{#each entry_type.fields}} {{#if widget}} {{#if (eq cardinality "single") }} @@ -86,7 +86,7 @@ async function update{{pascal_case entry_type.name}}() { updated_{{snake_case entry_type.name}}: {{camel_case entry_type.name}} } }); - + dispatch('{{kebab_case entry_type.name}}-updated', { actionHash: updateRecord.signed_action.hashed.hash }); } catch (e) { errorSnackbar.labelText = `Error updating the {{lower_case entry_type.name}}: ${e.data.data}`; @@ -99,7 +99,7 @@ async function update{{pascal_case entry_type.name}}() {
Edit {{pascal_case entry_type.name}} - + {{#each entry_type.fields}} {{#if widget}}
@@ -108,7 +108,7 @@ async function update{{pascal_case entry_type.name}}() { {{else}} {{> Vec/edit/render field_name=field_name field_type=field_type widget=widget }} {{/if}} - +
{{/if}} @@ -121,7 +121,7 @@ async function update{{pascal_case entry_type.name}}() { on:click={() => dispatch('edit-canceled')} style="flex: 1; margin-right: 16px" > - Date: Thu, 28 Sep 2023 13:31:40 -0700 Subject: [PATCH 02/10] filter helper for handlebars --- src/templates.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/templates.rs b/src/templates.rs index 644097606..7539f131c 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -4,10 +4,10 @@ use dialoguer::theme::ColorfulTheme; use dialoguer::Select; use handlebars::{ handlebars_helper, Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, - RenderError, Renderable, StringOutput, + RenderError, Renderable, ScopedJson, StringOutput, }; use regex::Regex; -use serde_json::Value; +use serde_json::{json, Value}; use std::collections::{BTreeMap, HashSet}; use std::ffi::OsString; use std::path::PathBuf; @@ -60,6 +60,7 @@ pub fn register_helpers<'a>(h: Handlebars<'a>) -> Handlebars<'a> { let h = register_pluralize_helpers(h); let h = register_merge_scope(h); let h = register_uniq_lines(h); + let h = register_filter(h); h } @@ -275,6 +276,83 @@ pub fn register_case_helpers<'a>(mut h: Handlebars<'a>) -> Handlebars<'a> { h } +#[derive(Clone, Copy)] +pub struct FilterHelper; + +/// A Handlebars helper to filter an iterable JSON value. +/// It receives the value to be filtered and a string containing the condition predicate, +/// then uses Handlebars' truthy logic to filter the items in the value. +/// It also supports the `#if` helper's `includeZero` optional parameter. +impl HelperDef for FilterHelper { + fn call_inner<'reg: 'rc, 'rc>( + &self, + h: &Helper<'reg, 'rc>, + r: &'reg Handlebars<'reg>, + _ctx: &'rc Context, + _rc: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + let value_param = h + .param(0) + .ok_or(RenderError::new("Filter helper: Param not found for index 0; must be value to be filtered"))?; + + let value = value_param.value(); + + let condition_param = h + .param(1) + .ok_or(RenderError::new("Filter helper: Param not found for index 1; must be string containing filter condition predicate"))?; + + let condition = condition_param + .value() + .as_str() + .ok_or(RenderError::new("Filter helper: filter condition predicate must be a string"))?; + + let include_zero = h + .hash_get("includeZero") + .and_then(|v| v.value().as_bool()) + .unwrap_or(false); + + let items: Vec = match value { + Value::Array(items) => Ok( + items + .iter() + .map(|item| item.clone()) + .collect() + ), + // FIXME: This doesn't preserve object keys. + // That's probably unexpected for consumers. + Value::Object(items) => Ok( + items + .values() + .map(|item| item.clone()) + .collect() + ), + _ => Err(RenderError::new("Filter helper: value to be filtered must be an array or object")) + }?; + + // This template allows us to evaluate the condition according to Handlebars' + // available helper functions and existing truthiness logic. + let template = format!("{}{}{}{}", "{{#if ", match include_zero { true => " includeZero=true", _ => "" }, condition, "}}true{{else}}false{{/if}}"); + // Try the template with one of the items to see if the template parses. + r.render_template(&*template, &items.first().clone())?; + + let filtered_items: Vec<&Value> = items + .iter() + .filter(|item| match r.render_template(&*template, item) { + Ok(s) => s.as_str() == "true", + // FIXME: this swallows error messages that don't involve parsing the template. + _ => false + }) + .collect(); + return Ok(ScopedJson::Derived(json!(filtered_items))); + } +} + +fn register_filter<'a>(mut h: Handlebars<'a>) -> Handlebars<'a> { + h.register_helper("filter", Box::new(FilterHelper)); + + h +} + pub fn register_all_partials_in_dir<'a>( mut h: Handlebars<'a>, file_tree: &FileTree, From 9a7286dd21afbe64264c2796beb7cccf21c26e6f Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 28 Sep 2023 13:37:47 -0700 Subject: [PATCH 03/10] guard against trailing non-widget fields in EditFoo.svelte template, take 2 --- src/scaffold/entry_type.rs | 10 ---------- src/scaffold/entry_type/definitions.rs | 1 - ...al_case entry_type.name}}.svelte{{\302\241if}}.hbs" | 2 +- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/scaffold/entry_type.rs b/src/scaffold/entry_type.rs index 78d58b769..4fc148bf7 100644 --- a/src/scaffold/entry_type.rs +++ b/src/scaffold/entry_type.rs @@ -101,15 +101,6 @@ pub fn scaffold_entry_type( } }; - let widget_fields = fields - .iter() - .map(|f| f.clone()) - .filter(|f| match f.widget { - Some(_) => true, - None => false - }) - .collect(); - let reference_entry_hash = match maybe_reference_entry_hash { Some(r) => r.clone(), None => { @@ -156,7 +147,6 @@ pub fn scaffold_entry_type( let entry_def = EntryDefinition { name: name.clone(), fields, - widget_fields, reference_entry_hash, }; diff --git a/src/scaffold/entry_type/definitions.rs b/src/scaffold/entry_type/definitions.rs index d262545de..04cf9507e 100644 --- a/src/scaffold/entry_type/definitions.rs +++ b/src/scaffold/entry_type/definitions.rs @@ -298,7 +298,6 @@ impl Referenceable { pub struct EntryDefinition { pub name: String, pub fields: Vec, - pub widget_fields: Vec, pub reference_entry_hash: bool, } diff --git "a/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" "b/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" index 45917c7ff..288a5c795 100644 --- "a/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" +++ "b/templates/svelte/entry-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if crud.update}}Edit{{pascal_case entry_type.name}}.svelte{{\302\241if}}.hbs" @@ -39,7 +39,7 @@ let {{camel_case field_name}}: Array<{{> (concat field_type.type "/type")}} | un let errorSnackbar: Snackbar; -$: {{#each entry_type.widget_fields}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/each}}; +$: {{#each (filter entry_type.fields "widget")}}{{camel_case field_name}}{{#unless @last}}, {{/unless}}{{/each}}; $: is{{pascal_case entry_type.name}}Valid = true{{#each entry_type.fields}}{{#if widget}}{{#if (eq cardinality "single")}} && {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate=(camel_case field_name) }}{{/if}}{{#if (eq cardinality "vector")}} && {{camel_case field_name}}.every(e => {{> (concat field_type.type "/" widget "/is-valid") variable_to_validate="e" }}){{/if}}{{/if}}{{/each}}; onMount(() => { From 6b9f85e9d1957b8fbf2530126e8c3621c5acd074 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 19 Jan 2024 11:12:12 -0800 Subject: [PATCH 04/10] Apply suggestions from code review Co-authored-by: Collins Muriuki --- src/templates.rs | 53 ++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/templates.rs b/src/templates.rs index 7539f131c..a92d2ad22 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -291,17 +291,17 @@ impl HelperDef for FilterHelper { _ctx: &'rc Context, _rc: &mut RenderContext<'reg, 'rc>, ) -> Result, RenderError> { - let value_param = h - .param(0) - .ok_or(RenderError::new("Filter helper: Param not found for index 0; must be value to be filtered"))?; - - let value = value_param.value(); - - let condition_param = h - .param(1) - .ok_or(RenderError::new("Filter helper: Param not found for index 1; must be string containing filter condition predicate"))?; + let mut params = h.params().iter(); + let value = params + .next() + .ok_or(RenderError::new( + "Filter helper: Param not found for index 0; must be value to be filtered", + ))? + .value(); - let condition = condition_param + let condition = params + .next() + .ok_or(RenderError::new("Filter helper: Param not found for index 1; must be string containing filter condition predicate"))? .value() .as_str() .ok_or(RenderError::new("Filter helper: filter condition predicate must be a string"))?; @@ -331,18 +331,27 @@ impl HelperDef for FilterHelper { // This template allows us to evaluate the condition according to Handlebars' // available helper functions and existing truthiness logic. - let template = format!("{}{}{}{}", "{{#if ", match include_zero { true => " includeZero=true", _ => "" }, condition, "}}true{{else}}false{{/if}}"); - // Try the template with one of the items to see if the template parses. - r.render_template(&*template, &items.first().clone())?; - - let filtered_items: Vec<&Value> = items - .iter() - .filter(|item| match r.render_template(&*template, item) { - Ok(s) => s.as_str() == "true", - // FIXME: this swallows error messages that don't involve parsing the template. - _ => false - }) - .collect(); + let template = format!( + "{}{}{}{}", + "{{#if ", + include_zero.then_some("includeZero=true").unwrap_or(""), + condition, + "}}true{{else}}false{{/if}}" + ); + + let mut filtered_items = vec![]; + for item in items.iter() { + match r.render_template(&template, item) { + Ok(s) => { + if s.as_str() == "true" { + filtered_items.push(item); + } + }, + Err(e) => { + return Err(e); + } + } + } return Ok(ScopedJson::Derived(json!(filtered_items))); } } From 43bf5177c29bb0c0bf8950b16a3b36cb23d9ecb4 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 19 Jan 2024 15:19:17 -0800 Subject: [PATCH 05/10] support JSON objects in filter HBS helper --- src/templates.rs | 63 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/src/templates.rs b/src/templates.rs index a92d2ad22..0a54875d2 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -7,7 +7,7 @@ use handlebars::{ RenderError, Renderable, ScopedJson, StringOutput, }; use regex::Regex; -use serde_json::{json, Value}; +use serde_json::{json, Value, Map}; use std::collections::{BTreeMap, HashSet}; use std::ffi::OsString; use std::path::PathBuf; @@ -279,6 +279,11 @@ pub fn register_case_helpers<'a>(mut h: Handlebars<'a>) -> Handlebars<'a> { #[derive(Clone, Copy)] pub struct FilterHelper; +pub enum FilterableValues { + Array(Vec), + Object(Map), // imported from serde_json +} + /// A Handlebars helper to filter an iterable JSON value. /// It receives the value to be filtered and a string containing the condition predicate, /// then uses Handlebars' truthy logic to filter the items in the value. @@ -311,20 +316,16 @@ impl HelperDef for FilterHelper { .and_then(|v| v.value().as_bool()) .unwrap_or(false); - let items: Vec = match value { + let items: FilterableValues = match value { Value::Array(items) => Ok( - items + FilterableValues::Array(items .iter() - .map(|item| item.clone()) + .cloned() .collect() + ) ), - // FIXME: This doesn't preserve object keys. - // That's probably unexpected for consumers. Value::Object(items) => Ok( - items - .values() - .map(|item| item.clone()) - .collect() + FilterableValues::Object(items.clone()) ), _ => Err(RenderError::new("Filter helper: value to be filtered must be an array or object")) }?; @@ -339,20 +340,42 @@ impl HelperDef for FilterHelper { "}}true{{else}}false{{/if}}" ); - let mut filtered_items = vec![]; - for item in items.iter() { - match r.render_template(&template, item) { - Ok(s) => { - if s.as_str() == "true" { - filtered_items.push(item); + match items { + FilterableValues::Array(items) => { + let mut filtered_array = vec![]; + for item in items.iter() { + match r.render_template(&template, item) { + Ok(s) => { + if s.as_str() == "true" { + filtered_array.push(item); + } + }, + Err(e) => { + return Err(e); + } + } + } + Ok(ScopedJson::Derived(json!(filtered_array))) + }, + FilterableValues::Object(object) => { + let mut filtered_object = Map::new(); + for key in object.keys() { + if let Some(v) = object.get(key) { + match r.render_template(&template, v) { + Ok(s) => { + if s.as_str() == "true" { + filtered_object.insert(key.into(), v.clone()); + } + }, + Err(e) => { + return Err(e); + } + } } - }, - Err(e) => { - return Err(e); } + Ok(ScopedJson::Derived(json!(filtered_object))) } } - return Ok(ScopedJson::Derived(json!(filtered_items))); } } From ad49724c5c9ab333a9816b75a9889767a0055734 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 23 Jan 2024 08:10:39 -0800 Subject: [PATCH 06/10] accept small bit of feedback Co-authored-by: Collins Muriuki --- src/templates/helpers/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/helpers/filter.rs b/src/templates/helpers/filter.rs index a4359298e..8c0927045 100644 --- a/src/templates/helpers/filter.rs +++ b/src/templates/helpers/filter.rs @@ -9,7 +9,7 @@ pub struct FilterHelper; pub enum FilterableValues { Array(Vec), - Object(Map), // imported from serde_json + Object(Map), } /// A Handlebars helper to filter an iterable JSON value. From 88e792b4a8a59d5f7d40433bca92d7c0a298a2b1 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 25 Jan 2024 14:36:20 -0800 Subject: [PATCH 07/10] simplify HBS filter helper logic for arrays/objects --- src/templates/helpers/filter.rs | 34 ++++++++------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/templates/helpers/filter.rs b/src/templates/helpers/filter.rs index a4359298e..80fbaa39a 100644 --- a/src/templates/helpers/filter.rs +++ b/src/templates/helpers/filter.rs @@ -7,11 +7,6 @@ use serde_json::{json, Value, Map}; #[derive(Clone, Copy)] pub struct FilterHelper; -pub enum FilterableValues { - Array(Vec), - Object(Map), // imported from serde_json -} - /// A Handlebars helper to filter an iterable JSON value. /// It receives the value to be filtered and a string containing the condition predicate, /// then uses Handlebars' truthy logic to filter the items in the value. @@ -44,20 +39,6 @@ impl HelperDef for FilterHelper { .and_then(|v| v.value().as_bool()) .unwrap_or(false); - let items: FilterableValues = match value { - Value::Array(items) => Ok( - FilterableValues::Array(items - .iter() - .cloned() - .collect() - ) - ), - Value::Object(items) => Ok( - FilterableValues::Object(items.clone()) - ), - _ => Err(RenderError::new("Filter helper: value to be filtered must be an array or object")) - }?; - // This template allows us to evaluate the condition according to Handlebars' // available helper functions and existing truthiness logic. let template = format!( @@ -68,11 +49,11 @@ impl HelperDef for FilterHelper { "}}true{{else}}false{{/if}}" ); - match items { - FilterableValues::Array(items) => { + match value { + Value::Array(items) => { let mut filtered_array = vec![]; for item in items.iter() { - match r.render_template(&template, item) { + match r.render_template(&template, &item) { Ok(s) => { if s.as_str() == "true" { filtered_array.push(item); @@ -85,11 +66,11 @@ impl HelperDef for FilterHelper { } Ok(ScopedJson::Derived(json!(filtered_array))) }, - FilterableValues::Object(object) => { + Value::Object(object) => { let mut filtered_object = Map::new(); - for key in object.keys() { + for key in object.clone().keys() { if let Some(v) = object.get(key) { - match r.render_template(&template, v) { + match r.render_template(&template, &v) { Ok(s) => { if s.as_str() == "true" { filtered_object.insert(key.into(), v.clone()); @@ -102,7 +83,8 @@ impl HelperDef for FilterHelper { } } Ok(ScopedJson::Derived(json!(filtered_object))) - } + }, + _ => Err(RenderError::new("Filter helper: value to be filtered must be an array or object")) } } } From 666b5fa023164985d2e36199e6ff56cab7c81548 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 25 Jan 2024 14:36:43 -0800 Subject: [PATCH 08/10] fix includeZero bug in filter HBS helper --- src/templates/helpers/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/helpers/filter.rs b/src/templates/helpers/filter.rs index 80fbaa39a..fb1d2d56d 100644 --- a/src/templates/helpers/filter.rs +++ b/src/templates/helpers/filter.rs @@ -44,8 +44,8 @@ impl HelperDef for FilterHelper { let template = format!( "{}{}{}{}", "{{#if ", - include_zero.then_some("includeZero=true").unwrap_or(""), condition, + include_zero.then_some(" includeZero=true").unwrap_or(""), "}}true{{else}}false{{/if}}" ); From 60c1470f9e386d7f108acafa8a811ef951d515ed Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 25 Jan 2024 14:37:12 -0800 Subject: [PATCH 09/10] update comment to be more correct/informative --- src/templates/helpers/filter.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/templates/helpers/filter.rs b/src/templates/helpers/filter.rs index fb1d2d56d..7db92937a 100644 --- a/src/templates/helpers/filter.rs +++ b/src/templates/helpers/filter.rs @@ -39,8 +39,9 @@ impl HelperDef for FilterHelper { .and_then(|v| v.value().as_bool()) .unwrap_or(false); - // This template allows us to evaluate the condition according to Handlebars' - // available helper functions and existing truthiness logic. + // This template allows us to evaluate the condition according to + // Handlebars' available context/property logic, helper functions, and + // truthiness logic. let template = format!( "{}{}{}{}", "{{#if ", From 01d1f205fb9a48fd3d131dccfd832f2cadcda309 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 25 Jan 2024 14:37:31 -0800 Subject: [PATCH 10/10] add a few simple tests for filter HBS helper --- src/templates/helpers/filter.rs | 60 +++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/templates/helpers/filter.rs b/src/templates/helpers/filter.rs index 7db92937a..aa854c1ac 100644 --- a/src/templates/helpers/filter.rs +++ b/src/templates/helpers/filter.rs @@ -95,3 +95,63 @@ pub fn register_filter<'a>(mut h: Handlebars<'a>) -> Handlebars<'a> { h } + +#[cfg(test)] +mod tests { + use handlebars::Handlebars; + use serde_json::json; + use crate::templates::helpers::register_filter; + + fn setup_handlebars<'a>() -> Handlebars<'a> { + let hbs = Handlebars::new(); + let hbs = register_filter(hbs); + hbs + } + + #[test] + fn respects_include_zero() { + let hbs = setup_handlebars(); + let value = json!([0, 1, 0, 2, 0, 3, 0, 4, 0, 5]); + // The predicate filters out zeroes. + let template = "{{#each (filter this \"this\")}}{{this}}{{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "12345", "`filter` helper did not filter out falsy zero"), + Err(e) => panic!("{}", e) + } + // This predicate, however, does not. + let template = "{{#each (filter this \"this\" includeZero=true)}}{{this}}{{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "0102030405", "`filter` helper did not treat zero as truthy"), + Err(e) => panic!("{}", e) + } + } + + #[test] + fn can_filter_object_by_value() { + let hbs = setup_handlebars(); + let value = json!({"name": "Alice", "age": 24, "wild": false, "species": "iguana"}); + // The predicate filters out the 'wild' property. + let template = "{{#each (filter this \"this\")}}{{@key}}: {{this}}, {{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "name: Alice, age: 24, species: iguana, ", "`filter` helper did not filter object key/value pairs by value"), + Err(e) => panic!("{}", e) + } + } + + + #[test] + fn can_filter_complex_value() { + let hbs = setup_handlebars(); + let value = json!([ + {"name": "Alice", "age": 24, "wild": true, "species": "iguana"}, + {"name": "Bob", "age": 3, "wild": false, "species": "hamster"}, + {"name": "Carol", "age": 1, "wild": true, "species": "octopus"} + ]); + // The predicate filters out domestic animals. + let template = "{{#each (filter this \"wild\")}}{{name}} the {{species}} is {{age}}. {{/each}}"; + match hbs.render_template(&template, &value) { + Ok(s) => assert_eq!(s, "Alice the iguana is 24. Carol the octopus is 1. ", "`filter` helper did not operate on a list full of complex values"), + Err(e) => panic!("{}", e) + } + } +} \ No newline at end of file