diff --git a/crates/spk-schema/crates/liquid/src/error_test.rs b/crates/spk-schema/crates/liquid/src/error_test.rs index 9be829c9ba..7f02eb296f 100644 --- a/crates/spk-schema/crates/liquid/src/error_test.rs +++ b/crates/spk-schema/crates/liquid/src/error_test.rs @@ -12,7 +12,7 @@ fn test_error_position_extraction() { crate::render_template(TPL, &json!({})).expect_err("expected template render to fail"); let expected = r#" 1 | {% default = data | replace ''%} - | ^ unexpected "="; expected Identifier + | ^ unexpected "="; expected Variable "#; let message = err.to_string(); assert_eq!(message, expected); diff --git a/crates/spk-schema/crates/liquid/src/tag_default.rs b/crates/spk-schema/crates/liquid/src/tag_default.rs index 84cca9fced..f4cb3f350d 100644 --- a/crates/spk-schema/crates/liquid/src/tag_default.rs +++ b/crates/spk-schema/crates/liquid/src/tag_default.rs @@ -2,9 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/imageworks/spk +use liquid::model::map::Entry; +use liquid::{Object, ValueView}; use liquid_core::error::ResultLiquidExt; use liquid_core::parser::FilterChain; -use liquid_core::{Language, ParseTag, Renderable, Result, Runtime, TagReflection, TagTokenIter}; +use liquid_core::runtime::Variable; +use liquid_core::{ + Language, + ParseTag, + Renderable, + Result, + Runtime, + TagReflection, + TagTokenIter, + Value, +}; #[cfg(test)] #[path = "./tag_default_test.rs"] @@ -36,11 +48,9 @@ impl ParseTag for DefaultTag { options: &Language, ) -> Result> { let dst = arguments - .expect_next("Identifier expected.")? - .expect_identifier() - .into_result()? - .to_string() - .into(); + .expect_next("Variable expected.")? + .expect_variable() + .into_result()?; arguments .expect_next("Assignment operator \"=\" expected.")? @@ -65,7 +75,7 @@ impl ParseTag for DefaultTag { #[derive(Debug)] struct Default { - dst: liquid_core::model::KString, + dst: Variable, src: FilterChain, } @@ -83,10 +93,49 @@ impl Renderable for Default { .trace_with(|| self.trace().into())? .into_owned(); - let name = self.dst.as_str().into(); - if runtime.try_get(&[name]).is_none() { - runtime.set_global(self.dst.clone(), value); + let variable = self.dst.evaluate(runtime)?; + let mut path = variable.into_iter().collect::>().into_iter(); + let root = path.next().expect("at least one entry in path"); + + let mut current_pos = liquid::model::Path::with_index(root.clone()); + let type_err = |pos: &liquid::model::Path| { + liquid::Error::with_msg("Cannot set default") + .trace("Stepping into non-object") + .context("position", pos.to_string()) + .context("target", self.dst.to_string()) + }; + + let Some(last) = path.next_back() else { + if runtime.get(&[root.clone()]).is_err() { + runtime.set_global(root.to_kstr().into(), value); + } + return Ok(()); + }; + let mut data = runtime + .get(&[root.to_owned().into()]) + .map(|v| v.into_owned()) + .unwrap_or_else(|_| Value::Object(Object::new())); + let mut data_ref = &mut data; + for step in path { + data_ref = data_ref + .as_object_mut() + .ok_or_else(|| type_err(¤t_pos))? + .entry(step.to_kstr()) + .or_insert_with(|| Value::Object(Object::new())); + current_pos.push(step.to_owned()); } + match data_ref + .as_object_mut() + .ok_or_else(|| type_err(¤t_pos))? + .entry(last.to_kstr()) + { + Entry::Occupied(_) => {} + Entry::Vacant(v) => { + v.insert(value); + runtime.set_global(root.to_kstr().into(), data); + } + } + Ok(()) } } diff --git a/crates/spk-schema/crates/liquid/src/tag_default_test.rs b/crates/spk-schema/crates/liquid/src/tag_default_test.rs index 3d6eedfc7b..d2b910c732 100644 --- a/crates/spk-schema/crates/liquid/src/tag_default_test.rs +++ b/crates/spk-schema/crates/liquid/src/tag_default_test.rs @@ -55,3 +55,57 @@ sources: crate::render_template(TPL, &options).expect("template should not fail to render"); assert_eq!(rendered, EXPECTED); } + +#[rstest] +fn test_template_rendering_default_nested() { + // ensure that setting a nested default value + // works as expected + + let options = json!({ + "nested": { + "existing": "existing" + } + }); + static TPL: &str = r#" +{% default nested.existing = "ignored" %} +{% default nested.other = "something" %} +pkg: {{ nested.other }}-{{ nested.existing }} +"#; + static EXPECTED: &str = r#" + + +pkg: something-existing +"#; + let rendered = match crate::render_template(TPL, &options) { + Ok(r) => r, + Err(err) => { + println!("{err}"); + panic!("template should not fail to render"); + } + }; + assert_eq!(rendered, EXPECTED); +} + +#[rstest] +fn test_template_rendering_default_nested_not_object() { + // ensure that setting a nested default value + // works as expected + + let options = json!({ + "integer": 64, + }); + static TPL: &str = r#" +{% default integer.nested = "invalid" %} +"#; + let err = crate::render_template(TPL, &options) + .expect_err("Should fail when setting default under non-object"); + let expected = r#"liquid: Cannot set default +from: Stepping into non-object + with: + position=integer + target=integer["nested"] + +"#; + let message = err.to_string(); + assert_eq!(message, expected); +} diff --git a/docs/use/spec.md b/docs/use/spec.md index dd85d789c6..a8f56897c6 100644 --- a/docs/use/spec.md +++ b/docs/use/spec.md @@ -456,8 +456,14 @@ In addition to the default tags and filters within the liquid language, spk prov The `default` tag can be used to more easily declare the default value for a variable. The following two statements are equivalent: ```liquid -{% assign version = version | default: "2.3.4" %} -{% default version = "2.3.4" %} +{% assign var = var | default: "2.3.4" %} +{% default var = "2.3.4" %} +``` + +Additionally, this tag can be used to set defaults in nested structures. Often, for options that may be provided at the command line. + +```liquid +{% default opt.version = "2.3.4" %} ``` ##### Filters @@ -494,7 +500,7 @@ The `parse_version` filter breaks down an spk version into its components, eithe The `replace_re` filter works like the built-in `replace` filter, except that it matches using a perl-style regular expression and allows group replacement in the output. These regular expressions do not support look-arounds or back-references. For example: ```liquid -{% default version = "2.3.4" %} +{% assign version = opt.version | default: "2.3.4" %} {% assign major_minor = version | replace_re: "(\d+)\.(\d+).*", "$1.$2" %} {{ major_minor }} # 2.3 ``` diff --git a/packages/gnu/gcc/gcc48.spk.yaml b/packages/gnu/gcc/gcc48.spk.yaml index 9423cd5da0..21fcd07645 100644 --- a/packages/gnu/gcc/gcc48.spk.yaml +++ b/packages/gnu/gcc/gcc48.spk.yaml @@ -1,4 +1,4 @@ -# {{ assign version = opt.version | default: "4.8.5" }} +# {% assign version = opt.version | default: "4.8.5" %} pkg: gcc/{{ version }} api: v0/package