Skip to content

Commit c2dac9a

Browse files
committed
Add initial integration of liquid template language
Signed-off-by: Ryan Bottriell <[email protected]>
1 parent b16157d commit c2dac9a

22 files changed

+998
-72
lines changed

Cargo.lock

Lines changed: 229 additions & 64 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ members = [
1212
"crates/spk-exec",
1313
"crates/spk-launcher",
1414
"crates/spk-schema",
15+
"crates/spk-schema/crates/*",
1516
"crates/spk-solve",
1617
"crates/spk-solve/crates/*",
1718
"crates/spk-storage",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
authors = ["Ryan Bottriell <[email protected]>"]
3+
edition = "2021"
4+
name = "spk-cmd-make-recipe"
5+
version = "0.36.0"
6+
7+
[dependencies]
8+
anyhow = "1.0"
9+
async-trait = "0.1"
10+
clap = { version = "3.2", features = ["derive", "env"] }
11+
spk-cli-common = { path = '../common' }
12+
spk-schema = { path = '../../spk-schema' }
13+
spk-schema-liquid = { path = '../../spk-schema/crates/liquid' }
14+
tracing = "0.1.35"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Sony Pictures Imageworks, et al.
2+
// SPDX-License-Identifier: Apache-2.0
3+
// https://github.com/imageworks/spk
4+
5+
use std::sync::Arc;
6+
7+
use anyhow::{Context, Result};
8+
use clap::Args;
9+
use spk_cli_common::{flags, CommandArgs, Run};
10+
use spk_schema::foundation::format::FormatOptionMap;
11+
use spk_schema::foundation::spec_ops::Named;
12+
use spk_schema::{SpecTemplate, Template, TemplateExt};
13+
14+
/// Render a package spec template into a recipe
15+
///
16+
/// This is done automatically when building packages, but can
17+
/// be a useful debugging tool when writing package spec files.
18+
#[derive(Args)]
19+
#[clap(visible_aliases = &["mkrecipe", "mkr"])]
20+
pub struct MakeRecipe {
21+
#[clap(flatten)]
22+
pub options: flags::Options,
23+
24+
#[clap(short, long, global = true, parse(from_occurrences))]
25+
pub verbose: u32,
26+
27+
/// The package spec file to render
28+
#[clap(name = "SPEC_FILE")]
29+
pub package: Option<String>,
30+
}
31+
32+
impl CommandArgs for MakeRecipe {
33+
// The important positional arg for a make-recipe is the package
34+
fn get_positional_args(&self) -> Vec<String> {
35+
match self.package.clone() {
36+
Some(p) => vec![p],
37+
None => vec![],
38+
}
39+
}
40+
}
41+
42+
#[async_trait::async_trait]
43+
impl Run for MakeRecipe {
44+
async fn run(&mut self) -> Result<i32> {
45+
let options = self.options.get_options()?;
46+
47+
let template = match flags::find_package_template(&self.package)? {
48+
flags::FindPackageTemplateResult::NotFound(name) => {
49+
Arc::new(SpecTemplate::from_file(name.as_ref())?)
50+
}
51+
res => {
52+
let (_, template) = res.must_be_found();
53+
template
54+
}
55+
};
56+
57+
tracing::info!("rendering template for {}", template.name());
58+
tracing::info!("using options {}", options.format_option_map());
59+
let rendered = spk_schema_liquid::render_template(template.source(), &options)
60+
.context("Failed to render template")?;
61+
print!("{rendered}");
62+
63+
match template.render(&options) {
64+
Err(err) => {
65+
tracing::error!("This template did not render into a valid spec {err}");
66+
Ok(1)
67+
}
68+
Ok(_) => {
69+
tracing::info!("Successfully rendered a valid spec");
70+
Ok(0)
71+
}
72+
}
73+
}
74+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright (c) Sony Pictures Imageworks, et al.
2+
// SPDX-License-Identifier: Apache-2.0
3+
// https://github.com/imageworks/spk
4+
5+
#![deny(unsafe_op_in_unsafe_fn)]
6+
7+
pub mod cmd_make_recipe;

crates/spk-schema/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ spfs = { version = '0.34.6', path = "../spfs" }
2525
spk-schema-foundation = { path = "./crates/foundation" }
2626
spk-schema-ident = { path = "./crates/ident" }
2727
spk-schema-validators = { path = "./crates/validators" }
28+
spk-schema-liquid = { path = "./crates/liquid" }
2829
sys-info = "0.9.0"
2930
tempfile = "3.3"
3031
thiserror = "1.0"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
authors = ["Ryan Bottriell <[email protected]>"]
3+
edition = "2021"
4+
name = "spk-schema-liquid"
5+
version = "0.36.0"
6+
7+
[features]
8+
migration-to-components = ["spk-schema-foundation/migration-to-components"]
9+
10+
[dependencies]
11+
liquid = "0.26.0"
12+
liquid-core = "0.26.0"
13+
serde = "1.0"
14+
serde_json = "1.0"
15+
spk-schema-foundation = { path = "../foundation" }
16+
tracing = "0.1.35"
17+
18+
[dev-dependencies]
19+
rstest = "0.15.0"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Sony Pictures Imageworks, et al.
2+
// SPDX-License-Identifier: Apache-2.0
3+
// https://github.com/imageworks/spk
4+
5+
use std::str::FromStr;
6+
7+
use liquid::model::Scalar;
8+
use liquid::ValueView;
9+
use liquid_core::{
10+
Display_filter,
11+
Expression,
12+
Filter,
13+
FilterParameters,
14+
FilterReflection,
15+
FromFilterParameters,
16+
ParseFilter,
17+
Result,
18+
Runtime,
19+
Value,
20+
};
21+
use spk_schema_foundation::version::Version;
22+
use spk_schema_foundation::version_range::{Ranged, VersionFilter};
23+
24+
#[cfg(test)]
25+
#[path = "./filter_compare_version_test.rs"]
26+
mod filter_compare_version_test;
27+
28+
#[derive(Debug, FilterParameters)]
29+
struct CompareVersionArgs {
30+
#[parameter(description = "The comparison operation to perform", arg_type = "str")]
31+
operator: Expression,
32+
#[parameter(
33+
description = "The version to compare with, if not part of the operator string",
34+
arg_type = "str"
35+
)]
36+
rhs: Option<Expression>,
37+
}
38+
39+
#[derive(Clone, ParseFilter, FilterReflection)]
40+
#[filter(
41+
name = "compare_version",
42+
description = "Compares one version to another using spk ordering semantics",
43+
parameters(CompareVersionArgs),
44+
parsed(CompareVersionFilter)
45+
)]
46+
pub struct CompareVersion;
47+
48+
#[derive(Debug, FromFilterParameters, Display_filter)]
49+
#[name = "compare_version"]
50+
struct CompareVersionFilter {
51+
#[parameters]
52+
args: CompareVersionArgs,
53+
}
54+
55+
impl Filter for CompareVersionFilter {
56+
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
57+
let args = self.args.evaluate(runtime)?;
58+
let input = input.as_scalar().ok_or_else(|| {
59+
liquid::Error::with_msg("Expected a scalar value for filter 'compare_version'")
60+
})?;
61+
let lhs = Version::from_str(input.into_string().as_str())
62+
.map_err(|err| liquid::Error::with_msg(err.to_string()))?;
63+
let range_str = match &args.rhs {
64+
None => args.operator.to_kstr().to_string(),
65+
Some(rhs) => format!("{}{}", args.operator, rhs),
66+
};
67+
let range = VersionFilter::from_str(range_str.as_str())
68+
.map_err(|err| liquid::Error::with_msg(err.to_string()))?;
69+
70+
let result = range.is_applicable(&lhs).is_ok();
71+
Ok(Value::Scalar(Scalar::new(result).to_owned()))
72+
}
73+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Sony Pictures Imageworks, et al.
2+
// SPDX-License-Identifier: Apache-2.0
3+
// https://github.com/imageworks/spk
4+
5+
use rstest::rstest;
6+
use serde_json::json;
7+
8+
#[rstest]
9+
fn test_template_rendering_version_range() {
10+
// the compare_version helper should be useful in if blocks
11+
// in order to render section based on version ranges
12+
13+
let options = json!({});
14+
static TPL: &str = r#"
15+
{% default version = "1.2.3" %}
16+
{% assign use_new_download = version | compare_version: ">=1.0" %}
17+
pkg: package/{{ version }}
18+
sources:
19+
{%- if use_new_download %}
20+
- git: https://downloads.testing/package/v{{ version }}
21+
{%- else %}
22+
- git: https://olddownloads.testing/package/v{{ version }}
23+
{%- endif %}
24+
"#;
25+
static EXPECTED: &str = r#"
26+
27+
28+
pkg: package/1.2.3
29+
sources:
30+
- git: https://downloads.testing/package/v1.2.3
31+
"#;
32+
let rendered =
33+
crate::render_template(TPL, &options).expect("template should not fail to render");
34+
assert_eq!(rendered, EXPECTED);
35+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Sony Pictures Imageworks, et al.
2+
// SPDX-License-Identifier: Apache-2.0
3+
// https://github.com/imageworks/spk
4+
5+
use std::ops::Deref;
6+
use std::str::FromStr;
7+
8+
use liquid::ValueView;
9+
use liquid_core::runtime::StackFrame;
10+
use liquid_core::{
11+
Display_filter,
12+
Expression,
13+
Filter,
14+
FilterParameters,
15+
FilterReflection,
16+
FromFilterParameters,
17+
ParseFilter,
18+
Result,
19+
Runtime,
20+
Value,
21+
};
22+
use spk_schema_foundation::version::Version;
23+
24+
#[cfg(test)]
25+
#[path = "./filter_parse_version_test.rs"]
26+
mod filter_parse_version_test;
27+
28+
#[derive(Debug, FilterParameters)]
29+
struct ParseVersionArgs {
30+
#[parameter(description = "An optional sub-component to access", arg_type = "str")]
31+
path: Option<Expression>,
32+
}
33+
34+
#[derive(Clone, ParseFilter, FilterReflection)]
35+
#[filter(
36+
name = "parse_version",
37+
description = "Parses an spk version, outputting one or all components",
38+
parameters(ParseVersionArgs),
39+
parsed(ParseVersionFilter)
40+
)]
41+
pub struct ParseVersion;
42+
43+
#[derive(Debug, FromFilterParameters, Display_filter)]
44+
#[name = "parse_version"]
45+
struct ParseVersionFilter {
46+
#[parameters]
47+
args: ParseVersionArgs,
48+
}
49+
50+
impl Filter for ParseVersionFilter {
51+
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
52+
let input = input.as_scalar().ok_or_else(|| {
53+
liquid::Error::with_msg("Expected a scalar value for filter 'parse_version'")
54+
})?;
55+
let version = Version::from_str(input.into_string().as_str())
56+
.map_err(|err| liquid::Error::with_msg(err.to_string()))?;
57+
58+
let data = liquid::object!({
59+
"major": version.major(),
60+
"minor": version.minor(),
61+
"patch": version.patch(),
62+
"base": version.base(),
63+
"parts": version.parts.parts,
64+
"plus_epsilon": version.parts.plus_epsilon,
65+
"pre": version.pre.deref(),
66+
"post": version.post.deref(),
67+
});
68+
69+
if let Some(path) = &self.args.path {
70+
if matches!(path, Expression::Literal(..)) {
71+
return Err(liquid::Error::with_msg(
72+
"parse_version expected a path to evaluate, but found a literal value",
73+
));
74+
}
75+
let rt = StackFrame::new(runtime, data);
76+
let value = path.evaluate(&rt)?;
77+
Ok(value.into_owned())
78+
} else {
79+
Ok(data.into())
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)