use zenith_core::{PropertyValue, RecipeDef};
use crate::json_types::{RecipeInspectJson, RecipeParamInspectJson};
pub fn build_recipe_entries(recipes: &[RecipeDef]) -> Vec<RecipeInspectJson> {
recipes.iter().map(recipe_to_json).collect()
}
fn recipe_to_json(r: &RecipeDef) -> RecipeInspectJson {
RecipeInspectJson {
id: r.id.clone(),
kind: r.kind.clone(),
seed: r.seed,
generator: r.generator.clone(),
bounds: r.bounds.clone(),
detached: r.detached,
params: r
.params
.iter()
.map(|p| RecipeParamInspectJson {
name: p.name.clone(),
value: property_value_str(&p.value),
})
.collect(),
palette: r.palette.clone(),
expanded: r.expanded.clone(),
}
}
pub fn render_recipes_human(recipes: &[RecipeDef]) -> String {
if recipes.is_empty() {
return String::new();
}
let mut out = String::new();
for r in recipes {
out.push_str(&format!("recipe {} kind={}\n", r.id, r.kind));
if let Some(seed) = r.seed {
out.push_str(&format!(" seed={}\n", seed));
}
if let Some(ref g) = r.generator {
out.push_str(&format!(" generator={}\n", g));
}
if let Some(ref bounds) = r.bounds {
out.push_str(&format!(" bounds={}\n", bounds));
}
if let Some(detached) = r.detached {
out.push_str(&format!(" detached={}\n", detached));
}
for p in &r.params {
out.push_str(&format!(
" param {} {}\n",
p.name,
property_value_str(&p.value)
));
}
if !r.palette.is_empty() {
if r.palette.len() < 5 {
out.push_str(&format!(" palette [{}]\n", r.palette.join(", ")));
} else {
out.push_str(&format!(" palette [{} tokens]\n", r.palette.len()));
}
}
if !r.expanded.is_empty() {
if r.expanded.len() < 5 {
out.push_str(&format!(" expanded [{}]\n", r.expanded.join(", ")));
} else {
out.push_str(&format!(" expanded [{} nodes]\n", r.expanded.len()));
}
}
}
out.trim_end().to_owned()
}
pub fn property_value_str(pv: &PropertyValue) -> String {
match pv {
PropertyValue::TokenRef(id) => id.clone(),
PropertyValue::Literal(s) => s.clone(),
PropertyValue::Dimension(d) => d.to_kdl_string(),
PropertyValue::DataRef(path) => path.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use zenith_core::{KdlAdapter, KdlSource};
const DOC_WITH_RECIPES: &str = r##"zenith version=1 {
project id="proj.r" name="Recipe Inspect Test"
tokens format="zenith-token-v1" {
color id="color.sky" value="#87CEEB"
color id="color.dusk" value="#FFB347"
}
styles {}
recipes {
recipe id="recipe.aurora" kind="aurora" seed=42 generator="aurora@1" bounds="page.1" detached=#false {
param name="density" value=(px)16
param name="label" value="hello"
palette token="color.sky"
palette token="color.dusk"
expanded node="node.a"
expanded node="node.b"
}
recipe id="recipe.scatter" kind="scatter" {
param name="count" value=(px)8
}
}
document id="doc.r" title="Recipe Inspect Test" {
page id="page.1" w=(px)800 h=(px)600 {
rect id="node.a" x=(px)0 y=(px)0 w=(px)100 h=(px)100
rect id="node.b" x=(px)110 y=(px)0 w=(px)100 h=(px)100
}
}
}
"##;
const DOC_NO_RECIPES: &str = r##"zenith version=1 {
project id="proj.nr" name="No Recipes"
tokens format="zenith-token-v1" {}
styles {}
document id="doc.nr" title="No Recipes" {
page id="page.nr" w=(px)400 h=(px)300 {
rect id="rect.nr" x=(px)0 y=(px)0 w=(px)50 h=(px)50
}
}
}
"##;
#[test]
fn build_entries_preserves_source_order() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let entries = build_recipe_entries(&doc.recipes);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].id, "recipe.aurora");
assert_eq!(entries[1].id, "recipe.scatter");
}
#[test]
fn build_entries_scalars_present() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let entries = build_recipe_entries(&doc.recipes);
let aurora = &entries[0];
assert_eq!(aurora.kind, "aurora");
assert_eq!(aurora.seed, Some(42));
assert_eq!(aurora.generator.as_deref(), Some("aurora@1"));
assert_eq!(aurora.bounds.as_deref(), Some("page.1"));
assert_eq!(aurora.detached, Some(false));
}
#[test]
fn build_entries_scalars_absent_for_minimal_recipe() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let entries = build_recipe_entries(&doc.recipes);
let scatter = &entries[1];
assert_eq!(scatter.seed, None);
assert_eq!(scatter.generator, None);
assert_eq!(scatter.bounds, None);
assert_eq!(scatter.detached, None);
}
#[test]
fn build_entries_params_and_palette_and_expanded() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let entries = build_recipe_entries(&doc.recipes);
let aurora = &entries[0];
assert_eq!(aurora.params.len(), 2);
assert_eq!(aurora.params[0].name, "density");
assert_eq!(aurora.params[0].value, "(px)16");
assert_eq!(aurora.params[1].name, "label");
assert_eq!(aurora.params[1].value, "hello");
assert_eq!(aurora.palette, vec!["color.sky", "color.dusk"]);
assert_eq!(aurora.expanded, vec!["node.a", "node.b"]);
}
#[test]
fn build_entries_empty_when_no_recipes() {
let doc = KdlAdapter.parse(DOC_NO_RECIPES.as_bytes()).unwrap();
let entries = build_recipe_entries(&doc.recipes);
assert!(entries.is_empty());
}
#[test]
fn human_output_contains_recipe_ids_and_kinds() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let out = render_recipes_human(&doc.recipes);
assert!(
out.contains("recipe.aurora"),
"must contain first recipe id"
);
assert!(
out.contains("kind=aurora"),
"must contain first recipe kind"
);
assert!(
out.contains("recipe.scatter"),
"must contain second recipe id"
);
assert!(
out.contains("kind=scatter"),
"must contain second recipe kind"
);
}
#[test]
fn human_output_contains_scalars() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let out = render_recipes_human(&doc.recipes);
assert!(out.contains("seed=42"), "must contain seed");
assert!(out.contains("generator=aurora@1"), "must contain generator");
assert!(out.contains("bounds=page.1"), "must contain bounds");
assert!(out.contains("detached=false"), "must contain detached");
}
#[test]
fn human_output_contains_params() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let out = render_recipes_human(&doc.recipes);
assert!(out.contains("param density"), "must contain density param");
assert!(out.contains("(px)16"), "must contain dimension value");
assert!(out.contains("param label"), "must contain label param");
assert!(out.contains("hello"), "must contain literal value");
}
#[test]
fn human_output_contains_palette_and_expanded() {
let doc = KdlAdapter.parse(DOC_WITH_RECIPES.as_bytes()).unwrap();
let out = render_recipes_human(&doc.recipes);
assert!(out.contains("color.sky"), "must contain palette token");
assert!(out.contains("color.dusk"), "must contain palette token");
assert!(out.contains("node.a"), "must contain expanded node");
assert!(out.contains("node.b"), "must contain expanded node");
}
#[test]
fn human_output_empty_when_no_recipes() {
let doc = KdlAdapter.parse(DOC_NO_RECIPES.as_bytes()).unwrap();
let out = render_recipes_human(&doc.recipes);
assert!(
out.is_empty(),
"must return empty string for doc with no recipes"
);
}
#[test]
fn property_value_str_token_ref() {
let pv = PropertyValue::TokenRef("color.primary".to_owned());
assert_eq!(property_value_str(&pv), "color.primary");
}
#[test]
fn property_value_str_literal() {
let pv = PropertyValue::Literal("hello".to_owned());
assert_eq!(property_value_str(&pv), "hello");
}
#[test]
fn property_value_str_dimension_px() {
use zenith_core::{Dimension, Unit};
let pv = PropertyValue::Dimension(Dimension {
value: 16.0,
unit: Unit::Px,
});
assert_eq!(property_value_str(&pv), "(px)16");
}
#[test]
fn property_value_str_dimension_pt_fractional() {
use zenith_core::{Dimension, Unit};
let pv = PropertyValue::Dimension(Dimension {
value: 13.5,
unit: Unit::Pt,
});
assert_eq!(property_value_str(&pv), "(pt)13.5");
}
}