use super::*;
const DYNAMIC: &str = r#"
local d = new Dynamic {
name = "pigeon"
nested { list = new Listing { 1; "two"; new Dynamic { z = true } }; empty {} }
["entry key"] = "v"
missing = null
ratio = 1.5
}
"#;
fn render(expr: &str) -> String {
let json = eval(&format!(
"{DYNAMIC}\nclass Person {{ name: String; age: Int }}\nres = {expr}"
));
json["res"].as_str().unwrap().to_string()
}
#[test]
fn json_renderer_document() {
assert_eq!(
render("new JsonRenderer {}.renderDocument(d)"),
"{\n \"name\": \"pigeon\",\n \"nested\": {\n \"list\": [\n 1,\n \"two\",\n {\n \"z\": true\n }\n ],\n \"empty\": {}\n },\n \"ratio\": 1.5,\n \"entry key\": \"v\"\n}\n"
);
}
#[test]
fn json_renderer_settings() {
assert_eq!(
render(
r#"new JsonRenderer { indent = "" }.renderValue(new Person { name = "a\"b"; age = 1 })"#
),
r#"{"name":"a\"b","age":1}"#
);
assert_eq!(
render(
"new JsonRenderer { omitNullProperties = false }.renderValue(new Dynamic { a = null })"
),
"{\n \"a\": null\n}"
);
}
#[test]
fn json_renderer_refuses_durations() {
let err = eval_fails("res = new JsonRenderer {}.renderValue(1.min)");
assert!(
err.contains("Cannot render value of type `Duration` as JSON."),
"{err}"
);
}
#[test]
fn yaml_renderer_document() {
assert_eq!(
render("new YamlRenderer {}.renderDocument(d)"),
"name: pigeon\nnested:\n list:\n - 1\n - two\n - z: true\n empty: {}\nratio: 1.5\nentry key: v\n"
);
}
#[test]
fn yaml_renderer_quotes_strings_by_mode() {
assert_eq!(
render(
r#"new YamlRenderer {}.renderValue(List("yes", "0123", "1:30", "- a", "a: b", "", " lead", "tab\there", "multi\nline"))"#
),
"- 'yes'\n- '0123'\n- '1:30'\n- '- a'\n- 'a: b'\n- ''\n- ' lead'\n- \"tab\\there\"\n- |-\n multi\n line"
);
assert_eq!(
render(r#"new YamlRenderer { mode = "1.2" }.renderValue(List("yes", "0123", "on"))"#),
"- yes\n- '0123'\n- on"
);
}
#[test]
fn yaml_renderer_indent_width_and_stream() {
assert_eq!(
render(
"new YamlRenderer { indentWidth = 4 }.renderDocument(new Dynamic { a { b = List(1, new Dynamic { c = 2 }) } })"
),
"a:\n b:\n - 1\n - c: 2\n"
);
assert_eq!(
render(
r#"new YamlRenderer { isStream = true }.renderDocument(List(new Dynamic { a = 1 }, "two"))"#
),
"a: 1\n--- two\n"
);
}
#[test]
fn properties_renderer() {
assert_eq!(
render(
r#"new PropertiesRenderer {}.renderDocument(new Dynamic { a = 1; b { c = "x y"; d = null; e = 1.5 }; ["k:v"] = "=!" })"#
),
"a = 1\nb.c = x y\nb.e = 1.5\nk\\:v = \\=\\!\n"
);
assert_eq!(
render(
r#"new PropertiesRenderer { restrictCharset = true }.renderDocument(new Dynamic { a = "é" })"#
),
"a = \\u00E9\n"
);
}
#[test]
fn pcf_renderer() {
assert_eq!(
render("new PcfRenderer {}.renderDocument(d)"),
"name = \"pigeon\"\nnested {\n list {\n 1\n \"two\"\n new {\n z = true\n }\n }\n empty {}\n}\nmissing = null\nratio = 1.5\n[\"entry key\"] = \"v\"\n"
);
assert_eq!(
render(r#"new PcfRenderer { useCustomStringDelimiters = true }.renderValue("a\"b\\c")"#),
r##"#"a"b\c"#"##
);
}
#[test]
fn renderer_converters_by_class_and_path() {
assert_eq!(
render(
r#"new JsonRenderer {
indent = ""
converters {
[String] = (s) -> s + "!"
[Person] = (p) -> p.name
["^b"] = (_) -> 42
["c.*"] = (_) -> "path"
}
}.renderValue(new Dynamic { a = "x"; b = "y"; c { d = 1 }; e = new Person { name = "p"; age = 1 } })"#
),
r#"{"a":"x!","b":42,"c":{"d":"path"},"e":"p"}"#
);
}
#[test]
fn render_directive_is_rendered_verbatim() {
assert_eq!(
render(
r#"new YamlRenderer {}.renderDocument(new Dynamic { a = new RenderDirective { text = " raw" } })"#
),
"a: raw\n"
);
}
#[cfg(feature = "native-io")]
fn module_file(name: &str, src: &str) -> (TestTempDir, std::path::PathBuf) {
let dir = TestTempDir::new(name);
let path = dir.path().join("main.pkl");
std::fs::write(&path, src).unwrap();
(dir, path)
}
#[cfg(feature = "native-io")]
#[test]
fn output_text_uses_module_renderer() {
let (_dir, path) = module_file(
"pklr_render_output_renderer",
"a = 1\nb { c = \"x\" }\noutput { renderer = new YamlRenderer {} }\n",
);
assert_eq!(pklr::eval_to_text(&path).unwrap(), "a: 1\nb:\n c: x\n");
}
#[cfg(feature = "native-io")]
#[test]
fn output_text_defaults_to_pcf() {
let (_dir, path) = module_file("pklr_render_output_pcf", "a = 1\nb { c = \"x\" }\n");
assert_eq!(
pklr::eval_to_text(&path).unwrap(),
"a = 1\nb {\n c = \"x\"\n}\n"
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_text_and_value() {
let (_dir, path) = module_file("pklr_render_output_text", "output { text = \"hi\" }\n");
assert_eq!(pklr::eval_to_text(&path).unwrap(), "hi");
let (_dir, path) = module_file(
"pklr_render_output_value",
"pigeon { name = \"p\" }\noutput { value = pigeon; renderer = new JsonRenderer {} }\n",
);
assert_eq!(
pklr::eval_to_text(&path).unwrap(),
"{\n \"name\": \"p\"\n}\n"
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"name": "p"})
);
}
#[cfg(feature = "native-io")]
#[test]
fn eval_to_json_refuses_what_json_renderer_refuses() {
for (src, message) in [
(
"a = 5.min",
"Cannot render value of type `Duration` as JSON.",
),
(
"a = 3.mb",
"Cannot render value of type `DataSize` as JSON.",
),
(
"a = Regex(\"x\")",
"Cannot render value of type `Regex` as JSON.",
),
("a = NaN", "Cannot render value `NaN` as JSON."),
(
"a = List((x) -> x)",
"Cannot render value of type `Function1` as JSON.",
),
] {
let (_dir, path) = module_file("pklr_render_json_refuses", src);
let err = pklr::eval_to_json(&path).unwrap_err().to_string();
assert!(err.contains(message), "{src}: {err}");
}
}
#[cfg(feature = "native-io")]
#[test]
fn eval_to_json_omits_nulls_only_when_renderer_asks() {
let (_dir, path) = module_file("pklr_render_json_nulls", "a = null\nb = 1\n");
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"a": null, "b": 1})
);
let (_dir, path) = module_file(
"pklr_render_json_omit_nulls",
"a = null\nb = 1\noutput { renderer = new JsonRenderer { omitNullProperties = true } }\n",
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"b": 1})
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_renderer_defaults_keep_nulls_when_only_converters_are_set() {
let (_dir, path) = module_file(
"pklr_render_converter_keeps_nulls",
"a = null\nb = 1\noutput { renderer = new JsonRenderer { converters { [Int] = (n) -> n + 1 } } }\n",
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"a": null, "b": 2})
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_must_be_module_output() {
for (src, message) in [
(
"output: String = \"abc\"",
"to be of type `ModuleOutput`, but got type `String`.",
),
(
"output = null",
"Expected value of type `ModuleOutput`, but got `null`.",
),
] {
let (_dir, path) = module_file("pklr_render_invalid_output", src);
let err = pklr::eval_to_json(&path).unwrap_err().to_string();
assert!(err.contains(message), "{src}: {err}");
}
}
#[cfg(feature = "native-io")]
#[test]
fn output_path_converters_apply_to_json() {
let (_dir, path) = module_file(
"pklr_render_path_converters",
r#"name = "pigeon"
friends = new Listing { "barn owl"; "parrot" }
hobbies { ["surfing"] { skill = "low" } }
address { street = "Norton St."; zip = 12345 }
output {
renderer = new JsonRenderer {
converters {
["friends[*]"] = (it) -> it + "x"
["hobbies[*]"] = (it) -> it.skill
["address.street"] = (it) -> "Other St."
["address.*"] = (it) -> "changed"
}
}
}
"#,
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({
"name": "pigeon",
"friends": ["barn owlx", "parrotx"],
"hobbies": {"surfing": "low"},
"address": {"street": "Other St.", "zip": "changed"},
})
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_is_inherited_through_the_amends_chain() {
let dir = TestTempDir::new("pklr_render_output_chain");
std::fs::write(
dir.path().join("c.pkl"),
"local suffix = \"!\"\nx = \"c\"\noutput {\n renderer = new JsonRenderer {\n converters { [String] = (s) -> s + suffix }\n }\n}\n",
)
.unwrap();
std::fs::write(dir.path().join("b.pkl"), "amends \"c.pkl\"\nx = \"b\"\n").unwrap();
std::fs::write(dir.path().join("a.pkl"), "amends \"b.pkl\"\nx = \"a\"\n").unwrap();
let path = dir.path().join("a.pkl");
assert_eq!(
pklr::eval_to_text(&path).unwrap(),
"{\n \"x\": \"a!\"\n}\n"
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"x": "a!"})
);
}
#[cfg(feature = "native-io")]
#[test]
fn inherited_output_keeps_base_lexicals_but_reads_final_members() {
let dir = TestTempDir::new("pklr_render_base_output_scope");
std::fs::write(dir.path().join("base-lib.pkl"), "label = \"base-import\"\n").unwrap();
std::fs::write(
dir.path().join("child-lib.pkl"),
"label = \"child-import\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("base.pkl"),
r#"import "pkl:base" as pkl
import "base-lib.pkl" as Lib
local prefix = "base-local"
class Marker { label = "base-class" }
function Regex(pattern: String): Regex = pkl.Regex(pattern)
childValue = "base-member"
output {
value = prefix + ":" + Lib.label + ":" + (new Marker {}).label + ":" + childValue
renderer {
converters {
[Regex] = (value) -> value.pattern
[String] = (value) -> value + "!"
}
}
}
"#,
)
.unwrap();
let child = dir.path().join("child.pkl");
std::fs::write(
&child,
r#"amends "base.pkl"
import "child-lib.pkl" as Lib
local prefix = "child-local"
local class Marker { label = "child-class" }
childValue = "child-member"
"#,
)
.unwrap();
assert_eq!(
pklr::eval_to_json(&child).unwrap(),
serde_json::json!("base-local:base-import:base-class:child-member!")
);
}
#[cfg(feature = "native-io")]
#[test]
fn inherited_output_captures_transitive_type_aliases() {
let dir = TestTempDir::new("pklr_render_inherited_output_type_aliases");
std::fs::write(
dir.path().join("base.pkl"),
r#"class Marker { label = "base" }
typealias Rendered = Intermediate
typealias Intermediate = Marker
output {
renderer {
converters {
[Any] = (value) -> if (value is Rendered) "base" else "wrong"
}
}
}
"#,
)
.unwrap();
let child = dir.path().join("child.pkl");
std::fs::write(
&child,
"amends \"base.pkl\"\noutput { value = new Marker {} }\n",
)
.unwrap();
assert_eq!(
pklr::eval_to_json(&child).unwrap(),
serde_json::json!("base")
);
}
#[cfg(feature = "native-io")]
#[test]
fn inherited_output_aliases_keep_their_base_scope() {
let dir = TestTempDir::new("pklr_render_inherited_output_alias_scope");
std::fs::write(
dir.path().join("base.pkl"),
r#"typealias Alias = String
output {
renderer {
converters { [String] = (value) -> value as Alias }
}
}
"#,
)
.unwrap();
let child = dir.path().join("child.pkl");
std::fs::write(
&child,
"amends \"base.pkl\"\nlocal typealias String = Int\noutput { value = \"child\" }\n",
)
.unwrap();
assert_eq!(
pklr::eval_to_json(&child).unwrap(),
serde_json::json!("child")
);
}
#[cfg(feature = "native-io")]
#[test]
fn converter_key_uses_builtin_type_when_a_function_has_its_name() {
let (_dir, path) = module_file(
"pklr_render_converter_builtin_function_name",
r#"import "pkl:base" as base
function Regex(pattern: String): Regex = base.Regex(pattern)
pattern = Regex("x")
output {
renderer {
converters {
[Regex] = (value) -> value.pattern + "!"
}
}
}
"#,
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"pattern": "x!"})
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_super_text_uses_the_inherited_value_and_renderer() {
let dir = TestTempDir::new("pklr_render_inherited_super_text");
std::fs::write(
dir.path().join("base.pkl"),
"output {\n value = new Dynamic { inherited = \"yes\" }\n renderer = new JsonRenderer {}\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("child.pkl"),
"amends \"base.pkl\"\noutput { text = super.text }\n",
)
.unwrap();
assert_eq!(
pklr::eval_to_text(&dir.path().join("child.pkl")).unwrap(),
"{\n \"inherited\": \"yes\"\n}\n"
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_super_text_preserves_explicit_value_kinds() {
for (name, output, expected) in [
(
"pklr_render_super_text_mapping",
r#"value = new Mapping { ["a"] = 1 }
renderer = new JsonRenderer { converters { [Mapping] = (_) -> "mapping" } }"#,
"\"mapping\"\n",
),
(
"pklr_render_super_text_listing",
r#"value = new Listing { 1; 2 }
renderer = new JsonRenderer { converters { [Listing] = (_) -> "listing" } }"#,
"\"listing\"\n",
),
(
"pklr_render_super_text_scalar",
r#"value = 1
renderer = new JsonRenderer { converters { [Int] = (_) -> "integer" } }"#,
"\"integer\"\n",
),
] {
let dir = TestTempDir::new(name);
std::fs::write(
dir.path().join("base.pkl"),
format!("output {{\n {output}\n}}\n"),
)
.unwrap();
std::fs::write(
dir.path().join("child.pkl"),
"amends \"base.pkl\"\noutput { text = super.text }\n",
)
.unwrap();
assert_eq!(
pklr::eval_to_text(&dir.path().join("child.pkl")).unwrap(),
expected,
"{name}"
);
}
}
#[cfg(feature = "native-io")]
#[test]
fn output_super_text_treats_implicit_module_values_as_typed() {
let dir = TestTempDir::new("pklr_render_super_text_implicit_module");
std::fs::write(
dir.path().join("base.pkl"),
"a = \"module\"\noutput { renderer = new JsonRenderer {} }\n",
)
.unwrap();
std::fs::write(
dir.path().join("child.pkl"),
"amends \"base.pkl\"\noutput { text = super.text }\n",
)
.unwrap();
assert_eq!(
pklr::eval_to_text(&dir.path().join("child.pkl")).unwrap(),
"{\n \"a\": \"module\"\n}\n"
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_super_text_falls_back_for_empty_module_output() {
let dir = TestTempDir::new("pklr_render_super_text_empty_output");
std::fs::write(
dir.path().join("base.pkl"),
"a = 1\noutput = new ModuleOutput {}\n",
)
.unwrap();
std::fs::write(
dir.path().join("child.pkl"),
"amends \"base.pkl\"\noutput { text = super.text }\n",
)
.unwrap();
assert_eq!(
pklr::eval_to_text(&dir.path().join("base.pkl")).unwrap(),
"a = 1\n"
);
assert_eq!(
pklr::eval_to_text(&dir.path().join("child.pkl")).unwrap(),
"a = 1\n"
);
}
#[cfg(feature = "native-io")]
#[test]
fn output_super_text_uses_effective_generated_values() {
for (name, base, expected) in [
(
"pklr_render_super_text_conditional_value",
r#"a = "implicit"
output {
when (true) {
value = "generated"
}
renderer = new JsonRenderer {}
}
"#,
"\"generated\"\n",
),
(
"pklr_render_super_text_absent_conditional_value",
r#"a = "implicit"
output {
when (false) {
value = "not generated"
}
renderer = new JsonRenderer {}
}
"#,
"{\n \"a\": \"implicit\"\n}\n",
),
(
"pklr_render_super_text_spread_value",
r#"a = "implicit"
local generated = new Dynamic {
value = "spread"
}
output {
...generated
renderer = new JsonRenderer {}
}
"#,
"\"spread\"\n",
),
(
"pklr_render_super_text_for_spread_value",
r#"a = "implicit"
output {
for (generated in List(new Dynamic { value = "for" })) {
...generated
}
renderer = new JsonRenderer {}
}
"#,
"\"for\"\n",
),
] {
let dir = TestTempDir::new(name);
std::fs::write(dir.path().join("base.pkl"), base).unwrap();
std::fs::write(
dir.path().join("child.pkl"),
"amends \"base.pkl\"\noutput { text = super.text }\n",
)
.unwrap();
for path in [dir.path().join("base.pkl"), dir.path().join("child.pkl")] {
assert_eq!(
pklr::eval_to_text(&path).unwrap(),
expected,
"{name}: {path:?}"
);
}
}
}
#[cfg(feature = "native-io")]
#[test]
fn output_body_sees_default_renderer_and_value() {
let (_dir, path) = module_file(
"pklr_render_output_defaults",
"x = 1\noutput {\n value = new Dynamic { a = 1 }\n text = renderer.renderDocument(value)\n}\n",
);
assert_eq!(pklr::eval_to_text(&path).unwrap(), "a = 1\n");
}
#[cfg(feature = "native-io")]
#[test]
fn output_super_text_is_evaluated_lazily() {
for (name, output) in [
(
"pklr_render_output_unreachable_super_text",
"text = if (false) super.text else \"ok\"",
),
(
"pklr_render_output_uncalled_super_text_lambda",
"local delayed = () -> super.text\n text = \"ok\"",
),
] {
let (_dir, path) = module_file(
name,
&format!(
r#"m = new Mapping {{ [1.s] = "value" }}
output {{
{output}
renderer = new JsonRenderer {{
converters {{ [Duration] = (_) -> "duration-key" }}
}}
}}
"#
),
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"m": {"duration-key": "value"}}),
"{name}"
);
}
let (_dir, path) = module_file(
"pklr_render_output_called_super_text_lambda",
"a = 1\noutput {\n local delayed = () -> super.text\n text = delayed()\n}\n",
);
assert_eq!(pklr::eval_to_text(&path).unwrap(), "a = 1\n");
}
#[cfg(feature = "native-io")]
#[test]
fn eval_to_json_converts_entry_keys() {
let (_dir, path) = module_file(
"pklr_render_json_keys",
r#"m = new Mapping { ["a"] = "b" }
d { x = "y"; ["k"] = "v" }
output {
renderer = new JsonRenderer {
converters { [String] = (s) -> s + "!"; ["d[k!]"] = (_) -> 1 }
}
}
"#,
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"m": {"a!": "b!"}, "d": {"x": "y!", "k!": 1}})
);
}
#[test]
fn eval_to_json_decodes_non_string_mapping_keys() {
let mut evaluator = Evaluator::new();
let value = evaluator
.eval_source(
r#"m = new Mapping { [1] = "value" }"#,
std::path::Path::new("test.pkl"),
)
.unwrap();
assert_eq!(
value.try_to_json().unwrap(),
serde_json::json!({"m": {"1": "value"}})
);
}
#[cfg(feature = "native-io")]
#[test]
fn typed_entry_key_conversion_updates_the_value_path() {
let (_dir, path) = module_file(
"pklr_render_json_typed_keys",
r#"m = new Mapping { [1] = "value" }
output {
renderer = new JsonRenderer {
converters { [Int] = (_) -> "one"; ["m[one]"] = (_) -> "converted" }
}
}
"#,
);
assert_eq!(
pklr::eval_to_json(&path).unwrap(),
serde_json::json!({"m": {"one": "converted"}})
);
assert_eq!(
pklr::eval_to_text(&path).unwrap(),
"{\n \"m\": {\n \"one\": \"converted\"\n }\n}\n"
);
}
#[cfg(feature = "native-io")]
#[test]
fn eval_to_json_rejects_invalid_xml_comments() {
let (_dir, path) = module_file(
"pklr_render_json_xml_comment",
"import \"pkl:xml\"\nx = xml.Comment(\"--\")\n",
);
let err = pklr::eval_to_json(&path).unwrap_err().to_string();
assert!(err.contains("XML comments must not contain `--`"), "{err}");
}
#[test]
fn apply_converters_converts_lists() {
let mut ev = Evaluator::new();
let path = std::path::Path::new("test.pkl");
ev.eval_source(
"output { renderer { converters { [Int] = (i) -> i + 1 } } }\n",
path,
)
.unwrap();
let list = pklr::Value::from(serde_json::json!([1, [2]]));
let converted = ev.apply_converters(list).unwrap();
assert_eq!(converted.to_json(), serde_json::json!([2, [3]]));
}
#[test]
fn apply_converters_does_not_reenter_converter_through_its_container_result() {
let mut ev = Evaluator::new();
let value = ev
.eval_source(
r#"
class D {}
d = new D {}
output {
renderer {
converters {
[D] = (value) -> List(value)
}
}
}
"#,
std::path::Path::new("test.pkl"),
)
.unwrap();
assert_eq!(
ev.apply_converters(value).unwrap().to_json(),
serde_json::json!({"d": [{}]})
);
}
#[test]
fn apply_converters_does_not_reenter_native_scalar_converter_roots() {
let mut ev = Evaluator::new();
let value = ev
.eval_source(
r#"
d = 1.s
size = 1.kb
nan = (0.0 / 0.0).s
pattern = Regex("x")
output {
renderer {
converters {
[Duration] = (value) -> List(value)
[DataSize] = (value) -> List(value)
[Regex] = (value) -> List(value)
}
}
}
"#,
std::path::Path::new("test.pkl"),
)
.unwrap();
assert_eq!(
ev.apply_converters(value).unwrap().to_json(),
serde_json::json!({
"d": ["1.s"],
"size": ["1.kb"],
"nan": ["NaN.s"],
"pattern": [{"_type": "regex", "pattern": "x"}],
})
);
}
#[test]
fn renderer_quirks_match_pkl() {
let json = eval(
r#"
pcfEntryOnly = new PcfRenderer {}.renderDocument(new Dynamic { ["a"] = 1 })
pcfValue = new PcfRenderer {}.renderValue(new Dynamic { a = 1 })
yamlEmpty = new YamlRenderer {}.renderDocument(new Dynamic {})
yamlStream = new YamlRenderer { isStream = true; converters { [Int] = (i) -> i + 1 } }.renderDocument(List(1, new Dynamic { a = 2 }))
anyConverter = new JsonRenderer { indent = ""; converters { [Any] = (_) -> "any"; [Number] = (_) -> 0 } }.renderValue(List(1, "a"))
"#,
);
assert_eq!(json["pcfEntryOnly"], "\n[\"a\"] = 1\n");
assert_eq!(json["pcfValue"], "{\n a = 1\n}");
assert_eq!(json["yamlEmpty"], " {}\n");
assert_eq!(json["yamlStream"], "1\n---\na: 3\n");
assert_eq!(json["anyConverter"], r#"[1,"a"]"#);
}
#[cfg(feature = "native-io")]
#[test]
fn default_output_value_is_the_typed_module() {
let (_dir, path) = module_file(
"pklr_render_output_default_value",
"x = 1\ny { z = 2 }\noutput {\n text = renderer.renderDocument(value) + \"--\\n\" + new YamlRenderer {}.renderDocument(value)\n}\n",
);
assert_eq!(
pklr::eval_to_text(&path).unwrap(),
"x = 1\ny {\n z = 2\n}\n--\nx: 1\n'y':\n z: 2\n"
);
}
#[cfg(feature = "native-io")]
#[test]
fn base_output_uses_base_locals_and_final_properties() {
let dir = TestTempDir::new("pklr_render_output_base_scope");
std::fs::write(
dir.path().join("base.pkl"),
"local suffix = \"!\"\nsuffix2 = \"?\"\nx = \"base\"\noutput {\n renderer = new JsonRenderer {\n converters { [String] = (s) -> s + suffix + suffix2 }\n }\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("main.pkl"),
"amends \"base.pkl\"\nlocal suffix = \"#\"\nsuffix2 = \"%\"\nx = \"child\"\n",
)
.unwrap();
assert_eq!(
pklr::eval_to_json(&dir.path().join("main.pkl")).unwrap(),
serde_json::json!({"suffix2": "%!%", "x": "child!%"})
);
}
fn render_with_imports(expr: &str) -> String {
let json = eval(&format!(
"import \"pkl:jsonnet\"\nimport \"pkl:xml\"\n{DYNAMIC}\nclass Person {{ name: String; age: Int }}\nres = {expr}"
));
json["res"].as_str().unwrap().to_string()
}
#[test]
fn plist_renderer() {
assert_eq!(
render(r#"new PListRenderer {}.renderValue(List(1, 2.5, true, "<&>"))"#),
"<array>\n <integer>1</integer>\n <real>2.5</real>\n <true/>\n <string><&></string>\n</array>"
);
assert_eq!(
render("new PListRenderer {}.renderDocument(new Dynamic { a = 1; b {} })"),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>a</key>\n <integer>1</integer>\n <key>b</key>\n <dict/>\n</dict>\n</plist>\n"
);
}
#[test]
fn jsonnet_renderer() {
assert_eq!(
render_with_imports(
r#"new jsonnet.Renderer { indent = "" }.renderDocument((d) { q = "it's"; `local` = 1 })"#
),
"{ name: 'pigeon', nested: { list: [1, 'two', { z: true }], empty: {} }, ratio: 1.5, q: \"it's\", 'local': 1, 'entry key': 'v' }\n"
);
assert_eq!(
render_with_imports(
r#"new jsonnet.Renderer {}.renderDocument(new Dynamic { a = jsonnet.ExtVar("x"); b = jsonnet.ImportStr("f.txt") })"#
),
"{\n a: std.extVar('x'),\n b: importstr 'f.txt',\n}\n"
);
}
#[test]
fn unrelated_renderer_class_is_not_a_jsonnet_renderer() {
let err = eval_fails("class Renderer {}\nres = new Renderer {}.renderValue(1)");
assert!(err.contains("unknown method 'renderValue'"), "{err}");
}
#[test]
fn xml_renderer() {
assert_eq!(
render_with_imports(
r#"new xml.Renderer { rootElementName = "people"; rootElementAttributes { ["v"] = 1 } }.renderDocument(new Dynamic { p = new Person { name = "a"; age = 2 }; l = new Listing { new Person { name = "b"; age = 3 }; "x"; xml.Comment("c"); xml.CData("<d>") } })"#
),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<people v=\"1\">\n <p>\n <name>a</name>\n <age>2</age>\n </p>\n <l>\n <Person>\n <name>b</name>\n <age>3</age>\n </Person>x\n <!--c--><![CDATA[<d>]]>\n </l>\n</people>\n"
);
let err = eval_fails(
"import \"pkl:xml\"\nres = new xml.Renderer {}.renderDocument(new Dynamic { [\"entry key\"] = 1 })",
);
assert!(
err.contains("Invalid XML 1.0 element name: `entry key`"),
"{err}"
);
}
#[test]
fn xml_constraints_apply_to_all_renderers() {
let err = eval_fails(
"import \"pkl:xml\"\nres = new JsonRenderer {}.renderValue(xml.Comment(\"--\"))",
);
assert!(err.contains("XML comments must not contain `--`"), "{err}");
let err = eval_fails(
"import \"pkl:xml\"\nres = new xml.Renderer { xmlVersion = \"2.0\" }.renderValue(1)",
);
assert!(err.contains("\"1.0\"|\"1.1\""), "{err}");
}
#[test]
fn xml_renderer_preserves_element_content_and_attribute_whitespace() {
assert_eq!(
render_with_imports(
r#"new xml.Renderer {}.renderDocument((xml.Element("parent")) { "before"; (xml.Element("child")) { "nested" }; "after"; ignored = "property" })"#
),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<parent>before\n <child>nested</child>after\n</parent>\n"
);
assert_eq!(
render_with_imports(
r#"new xml.Renderer { rootElementAttributes { value = "a\tb\nc\rd" } }.renderDocument("x")"#
),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root value=\"a	b
c
d\">x</root>\n"
);
assert_eq!(
render_with_imports(
r#"new xml.Renderer { xmlVersion = "1.1"; rootElementAttributes { value = "a\u{85}b\u{2028}c" } }.renderDocument("x")"#
),
"<?xml version=\"1.1\" encoding=\"UTF-8\"?>\n<root value=\"a…b
c\">x</root>\n"
);
assert_eq!(
render_with_imports(
r#"new xml.Renderer {}.renderDocument((xml.Element("parent")) { for (v in List("before", "after")) { v }; ...List("list", "items"); ...new Listing { "listing" }; ...Set("set"); ...new Dynamic { (xml.Element("child")) { "nested" } }; ignored = "property" })"#
),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<parent>beforeafterlistitemslistingset\n <child>nested</child>\n</parent>\n"
);
}
#[test]
fn xml_renderer_rejects_invalid_comment_and_characters() {
let err = eval_fails(
"import \"pkl:xml\"\nres = new xml.Renderer {}.renderValue(xml.Comment(\"trailing-\"))",
);
assert!(
err.contains("must not contain `--` or end with `-`"),
"{err}"
);
let err =
eval_fails("import \"pkl:xml\"\nres = new xml.Renderer {}.renderValue(\"bad\\u{0}\")");
assert!(err.contains("Invalid XML 1.0 character U+0000"), "{err}");
let err = eval_fails("res = new PListRenderer {}.renderValue(\"bad\\u{0}\")");
assert!(err.contains("Invalid XML 1.0 character U+0000"), "{err}");
assert_eq!(
render_with_imports(r#"new xml.Renderer { xmlVersion = "1.1" }.renderValue("a\u{1}b")"#),
"ab"
);
}
#[test]
fn nullable_indent_is_jsonnet_only() {
assert_eq!(
render_with_imports("new jsonnet.Renderer { indent = null }.renderValue(List(1, 2))"),
"[1, 2]"
);
let err = eval_fails("res = new JsonRenderer { indent = null }.renderValue(List(1))");
assert!(err.contains("property 'indent' expected String"), "{err}");
}
#[test]
fn render_directive_keys() {
let json = eval(
r#"local m = new Mapping { ["key"] = "value"; [new RenderDirective { text = "🔑" }] = 42 }
res = new YamlRenderer {}.renderValue(m)
json = new JsonRenderer { indent = "" }.renderValue(m)"#,
);
assert_eq!(json["res"], "key: value\n🔑: 42");
assert_eq!(json["json"], r#"{"key":"value",🔑:42}"#);
}
#[test]
fn json_parser() {
let json = eval(
r##"import "pkl:json"
local parser = new json.Parser {}
a = parser.parse(#"{ "name": "Pigeon", "tags": ["a", 1, 2.5, true, null], "nested": { "x": "é\n" } }"#)
b = parser.parse("123.0")
c = new json.Parser { useMapping = true }.parse(#"{ "k": 1 }"#).length
dynamicAmended = (new json.Parser {}.parse(#"{ "x": 1 }"#)) { y = 2 }
dynamicSuper = (new json.Parser {}.parse(#"{ "x": 1 }"#)) { fromSuper = super.x; fromSuperLength = super.length }
mappingAmended = (new json.Parser { useMapping = true }.parse(#"{ "x": 1 }"#)) { ["y"] = 2 }
"##,
);
assert_eq!(
json["a"],
serde_json::json!({"name": "Pigeon", "tags": ["a", 1, 2.5, true, null], "nested": {"x": "é\n"}})
);
assert_eq!(json["b"], serde_json::json!(123.0));
assert_eq!(json["c"], 1);
assert_eq!(json["dynamicAmended"], serde_json::json!({"x": 1, "y": 2}));
assert_eq!(
json["dynamicSuper"],
serde_json::json!({"x": 1, "fromSuper": 1, "fromSuperLength": 1})
);
assert_eq!(json["mappingAmended"], serde_json::json!({"x": 1, "y": 2}));
}
#[test]
fn json_parser_converters_and_errors() {
let json = eval(
r##"import "pkl:json"
local parser = new json.Parser { converters { [Int] = (it) -> it + 1; [String] = (it) -> it + "x"; ["tags[*]"] = (_) -> "element" } }
a = parser.parse(#"{ "n": 1, "s": "a", "tags": [1, 2] }"#)
durationLike = new JsonRenderer {}.renderValue(new json.Parser {}.parse(#"{ "value": 1, "unit": "s" }"#))
"##,
);
assert_eq!(
json["a"],
serde_json::json!({"n": 2, "s": "ax", "tags": ["element", "element"]})
);
assert_eq!(
json["durationLike"],
"{\n \"value\": 1,\n \"unit\": \"s\"\n}"
);
for doc in ["0123", "0x1A3", "", "[1,]", "{\"a\" 1}"] {
let err = eval_fails(&format!(
"import \"pkl:json\"\nres = new json.Parser {{}}.parse({doc:?})"
));
assert!(err.contains("Error parsing JSON document."), "{doc}: {err}");
}
let err = eval_fails("import \"pkl:json\"\nres = new json.Parser {}.parse(\"\\\"\\\\é\")");
assert!(err.contains("Error parsing JSON document."), "{err}");
for doc in [r#""\u+123""#, r#""\u12é4""#] {
let err = eval_fails(&format!(
"import \"pkl:json\"\nres = new json.Parser {{}}.parse({doc:?})"
));
assert!(err.contains("Expected hexadecimal digit"), "{doc}: {err}");
}
let json = eval(
r#"import "pkl:json"
res = new json.Parser {}.parse("\"\\uD800\\u0041\"")
"#,
);
assert_eq!(json["res"], "�A");
let nested = format!("{}0{}", "[".repeat(1_001), "]".repeat(1_001));
let err = eval_fails(&format!(
"import \"pkl:json\"\nres = new json.Parser {{}}.parse({nested:?})"
));
assert!(err.contains("Maximum nesting depth exceeded"), "{err}");
}