// Jsonnet renderer.
//
// Renders Pkl `Value`s as [Jsonnet](https://jsonnet.org) text following the
// shape Apple Pkl's `pkl:jsonnet` produces. The module is intentionally
// self-contained — it only depends on the Pkl `Value` enum and the
// shared `find_object_class_tag` / `apply_value_renderer_converters`
// helpers in eval_expr.mbt. The Jsonnet semantics encoded here
// (single-quoted strings, `|||` heredoc for newline-containing text,
// trailing comma after each container element, `null`-field elision
// when `omitNullProperties = true`, special `ExtVar` / `ImportStr`
// constructor projection) live in this file alone, so this is the
// designated extraction candidate if a standalone `mizchi/jsonnet`
// MoonBit package is needed later — the only external surface is the
// `render_value_as_jsonnet*` functions below.
///|
pub fn render_value_as_jsonnet(value : Value) -> String {
render_value_as_jsonnet_with_options(value, " ", true)
}
///|
pub fn render_value_as_jsonnet_with_indent(
value : Value,
indent_text : String,
) -> String {
render_value_as_jsonnet_with_options(value, indent_text, true)
}
///|
pub fn render_value_as_jsonnet_with_options(
value : Value,
indent_text : String,
omit_null_properties : Bool,
) -> String {
let buf = StringBuilder::new()
let ctx : JsonnetCtx = {
indent_text,
omit_null_properties,
compact: indent_text == "",
}
jsonnet_emit_value(value, 0, ctx, buf)
buf.to_string()
}
///|
pub fn render_value_as_jsonnet_document(value : Value) -> String {
render_value_as_jsonnet_document_with_options(value, " ", true)
}
///|
pub fn render_value_as_jsonnet_document_with_indent(
value : Value,
indent_text : String,
) -> String {
render_value_as_jsonnet_document_with_options(value, indent_text, true)
}
///|
pub fn render_value_as_jsonnet_document_with_options(
value : Value,
indent_text : String,
omit_null_properties : Bool,
) -> String {
let rendered = render_value_as_jsonnet_with_options(
value, indent_text, omit_null_properties,
)
if rendered.has_suffix("\n") {
rendered
} else {
rendered + "\n"
}
}
///|
priv struct JsonnetCtx {
indent_text : String
omit_null_properties : Bool
compact : Bool
}
///|
fn jsonnet_emit_value(
value : Value,
indent : Int,
ctx : JsonnetCtx,
buf : StringBuilder,
) -> Unit {
// PKL-153f: directive short-circuit — emit the `text` verbatim
// wherever the value appears, mirroring how other renderers handle
// `new RenderDirective {...}`.
match render_directive_text(value) {
Some(text) => {
buf.write_string(text)
return
}
None => ()
}
match value {
ThunkValue(_) =>
jsonnet_emit_value(force_eval_thunk(value), indent, ctx, buf)
NullValue => buf.write_string("null")
DeferredImportValue(_) => buf.write_string("null")
BoolValue(true) => buf.write_string("true")
BoolValue(false) => buf.write_string("false")
IntValue(n) => buf.write_string("\{n}")
FloatValue(d) => buf.write_string(jsonnet_float_text(d))
StringValue(s) => jsonnet_emit_string(s, indent, ctx, buf)
ObjectValue(members) =>
jsonnet_emit_object_from_members(members, indent, ctx, buf)
MappingValue(entries) | DefaultedMappingValue(_, entries, _) =>
jsonnet_emit_object_from_entries(entries, indent, ctx, buf)
MapValue(entries) =>
jsonnet_emit_object_from_entries(entries, indent, ctx, buf)
ListingValue(elements)
| DefaultedListingValue(_, elements, _)
| ListValue(elements)
| SetValue(elements) => jsonnet_emit_array(elements, indent, ctx, buf)
// The renderer-error dispatch (`jsonnet_renderer_value_error`)
// rejects these before we reach the emit path, so reaching them
// here means a caller bypassed the error gate. Emit `null` as a
// last resort rather than crashing.
DurationValue(_, _)
| DataSizeValue(_, _)
| PairValue(_, _)
| IntSeqValue(_, _, _)
| FunctionValue(_, _, _, _, _)
| RegexValue(_)
| BytesValue(_) => buf.write_string("null")
}
}
///|
/// Detect the `pkl:jsonnet` ExtVar / ImportStr constructor shape so
/// `someExternalVariable = jsonnet.ExtVar("MY_VARIABLE")` renders as
/// `std.extVar('MY_VARIABLE')` and `someImportStr = jsonnet.ImportStr("foo")`
/// renders as `importstr 'foo'`. Both classes are single-string
/// payloads tagged via the universal class marker (see
/// `tag_object_with_class` in eval_module.mbt).
fn jsonnet_special_object_text(members : Array[ValueMember]) -> String? {
// The stub constructors in `analysis.mbt`'s pkl:jsonnet text stamp
// a `_jsonnet_kind = "ExtVar" / "ImportStr"` hidden marker on the
// resulting `new` body; we cannot rely on the class-tag marker
// because Pkl `new { ... }` inside a lambda body doesn't propagate
// the class tag through the typed-return-path today.
let kind = match lookup_member(members, "__jsonnet_kind__") {
Some(StringValue(s)) => Some(s)
_ =>
match find_object_class_tag(members) {
Some("ExtVar") => Some("ExtVar")
Some("ImportStr") => Some("ImportStr")
_ => None
}
}
match kind {
Some("ExtVar") =>
match lookup_visible_member(members, "name") {
Some(StringValue(name)) =>
Some("std.extVar(" + jsonnet_quoted_string(name) + ")")
_ => None
}
Some("ImportStr") =>
match lookup_visible_member(members, "path") {
Some(StringValue(path)) =>
Some("importstr " + jsonnet_quoted_string(path))
_ => None
}
_ => None
}
}
///|
fn jsonnet_emit_object_from_members(
members : Array[ValueMember],
indent : Int,
ctx : JsonnetCtx,
buf : StringBuilder,
) -> Unit {
match jsonnet_special_object_text(members) {
Some(text) => {
buf.write_string(text)
return
}
None => ()
}
let entries : Array[(String, Value)] = []
for field in visible_members(members) {
if field.name == class_tag_member_name() {
continue
}
// `__jsonnet_kind__` is the internal marker the pkl:jsonnet stub
// uses to identify ExtVar / ImportStr constructor results (see
// `jsonnet_special_object_text`); it must never reach the rendered
// text.
if field.name == "__jsonnet_kind__" {
continue
}
if ctx.omit_null_properties && field.value is NullValue {
continue
}
entries.push((field.name, field.value))
}
jsonnet_emit_object_entries_typed(entries, indent, ctx, buf)
}
///|
fn jsonnet_emit_object_from_entries(
entries : Array[ValueEntry],
indent : Int,
ctx : JsonnetCtx,
buf : StringBuilder,
) -> Unit {
let kvs : Array[(String, Value)] = []
for entry in entries {
if ctx.omit_null_properties && entry.value is NullValue {
continue
}
let directive_key = render_directive_text(entry.key)
let key = match directive_key {
Some(text) => text
None =>
match entry.key {
StringValue(s) => s
IntValue(n) => "\{n}"
FloatValue(d) => jsonnet_float_text(d)
BoolValue(b) => if b { "true" } else { "false" }
_ => jsonnet_value_to_string_key(entry.key)
}
}
if directive_key is Some(_) {
// PKL-153f: directive-sourced keys go in verbatim — bypass the
// identifier-validity quoting `jsonnet_object_key` would otherwise
// apply (emoji / non-ASCII keys come out unquoted in Apple Pkl).
kvs.push(("\u{001f}\{key}", entry.value))
} else {
kvs.push((key, entry.value))
}
}
jsonnet_emit_object_entries_typed(kvs, indent, ctx, buf)
}
///|
fn jsonnet_emit_object_entries_typed(
entries : Array[(String, Value)],
indent : Int,
ctx : JsonnetCtx,
buf : StringBuilder,
) -> Unit {
if entries.length() == 0 {
buf.write_string("{}")
return
}
if ctx.compact {
buf.write_string("{ ")
for i = 0; i < entries.length(); i = i + 1 {
if i > 0 {
buf.write_string(", ")
}
let (key, value) = entries[i]
buf.write_string(jsonnet_object_key(key))
buf.write_string(": ")
jsonnet_emit_value(value, indent + 1, ctx, buf)
}
buf.write_string(" }")
return
}
buf.write_string("{\n")
let inner_indent = indent + 1
for entry in entries {
let (key, value) = entry
jsonnet_write_indent(inner_indent, ctx, buf)
buf.write_string(jsonnet_object_key(key))
buf.write_string(": ")
jsonnet_emit_value(value, inner_indent, ctx, buf)
buf.write_string(",\n")
}
jsonnet_write_indent(indent, ctx, buf)
buf.write_char('}')
}
///|
fn jsonnet_emit_array(
elements : Array[Value],
indent : Int,
ctx : JsonnetCtx,
buf : StringBuilder,
) -> Unit {
if elements.length() == 0 {
buf.write_string("[]")
return
}
if ctx.compact {
buf.write_char('[')
for i = 0; i < elements.length(); i = i + 1 {
if i > 0 {
buf.write_string(", ")
}
jsonnet_emit_value(elements[i], indent + 1, ctx, buf)
}
buf.write_char(']')
return
}
buf.write_string("[\n")
let inner_indent = indent + 1
for element in elements {
jsonnet_write_indent(inner_indent, ctx, buf)
jsonnet_emit_value(element, inner_indent, ctx, buf)
buf.write_string(",\n")
}
jsonnet_write_indent(indent, ctx, buf)
buf.write_char(']')
}
///|
fn jsonnet_write_indent(
indent : Int,
ctx : JsonnetCtx,
buf : StringBuilder,
) -> Unit {
for _ in 0.. String {
// PKL-153f: directive-sourced keys carry the `\u{001f}` sentinel set
// in `jsonnet_emit_object_from_entries`; pass them through raw.
if key.length() >= 1 && key[0] == 0x1f {
return String::unsafe_substring(key, start=1, end=key.length())
}
if jsonnet_is_simple_identifier(key) && !jsonnet_is_reserved_keyword(key) {
return key
}
jsonnet_quoted_string(key)
}
///|
/// Jsonnet reserved words. Bare identifiers matching one of these must
/// be quoted as object keys (jsonnetfmt-compatible). The list comes
/// from the Jsonnet language spec — kept narrow to just what shows up
/// in the upstream fixtures (`local`); the rest are included so we
/// don't regress on future fixtures that exercise edge cases.
fn jsonnet_is_reserved_keyword(name : String) -> Bool {
match name {
"assert"
| "else"
| "error"
| "false"
| "for"
| "function"
| "if"
| "import"
| "importstr"
| "in"
| "local"
| "null"
| "self"
| "super"
| "tailstrict"
| "then"
| "true" => true
_ => false
}
}
///|
fn jsonnet_is_simple_identifier(key : String) -> Bool {
if key.length() == 0 {
return false
}
let first = key[0].to_int().unsafe_to_char()
if !((first >= 'a' && first <= 'z') ||
(first >= 'A' && first <= 'Z') ||
first == '_') {
return false
}
for i = 1; i < key.length(); i = i + 1 {
let c = key[i].to_int().unsafe_to_char()
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_') {
return false
}
}
true
}
///|
fn jsonnet_value_to_string_key(value : Value) -> String {
match value {
StringValue(s) => s
IntValue(n) => "\{n}"
FloatValue(d) => jsonnet_float_text(d)
BoolValue(b) => if b { "true" } else { "false" }
NullValue => "null"
_ => "?"
}
}
///|
/// Single-quote strings unless the content contains a `'` and no `"`,
/// in which case switch to double-quote so the renderer doesn't need
/// to escape the apostrophe. Matches `jsonnetfmt`'s quoting choice
/// observed in the upstream fixtures (`"single'quote"` vs `'foo.bar'`).
fn jsonnet_quoted_string(s : String) -> String {
let has_single = string_contains_char(s, '\'')
let has_double = string_contains_char(s, '"')
let use_double = has_single && !has_double
let buf = StringBuilder::new()
if use_double {
buf.write_char('"')
} else {
buf.write_char('\'')
}
for ch in s {
jsonnet_write_escaped_char(ch, use_double, buf)
}
if use_double {
buf.write_char('"')
} else {
buf.write_char('\'')
}
buf.to_string()
}
///|
fn jsonnet_write_escaped_char(
ch : Char,
use_double : Bool,
buf : StringBuilder,
) -> Unit {
let code = ch.to_int()
if code == '\\'.to_int() {
buf.write_string("\\\\")
} else if code == '\n'.to_int() {
buf.write_string("\\n")
} else if code == '\r'.to_int() {
buf.write_string("\\r")
} else if code == '\t'.to_int() {
buf.write_string("\\t")
} else if code == '\u{08}' {
buf.write_string("\\b")
} else if code == '\u{0c}' {
buf.write_string("\\f")
} else if use_double && code == '"'.to_int() {
buf.write_string("\\\"")
} else if !use_double && code == '\''.to_int() {
buf.write_string("\\'")
} else if code < 0x20 {
let hex = jsonnet_hex4(code)
buf.write_string("\\u")
buf.write_string(hex)
} else {
buf.write_char(ch)
}
}
///|
fn jsonnet_hex4(code : Int) -> String {
let digits = "0123456789abcdef"
let buf = StringBuilder::new()
buf.write_char(digits[(code >> 12) & 0xf].to_int().unsafe_to_char())
buf.write_char(digits[(code >> 8) & 0xf].to_int().unsafe_to_char())
buf.write_char(digits[(code >> 4) & 0xf].to_int().unsafe_to_char())
buf.write_char(digits[code & 0xf].to_int().unsafe_to_char())
buf.to_string()
}
///|
/// Newline-containing strings render as the `|||` heredoc block
/// jsonnetfmt prefers. Without this an interpolated multi-line value
/// would emit one giant single-quoted string with `\n` escapes, which
/// reads poorly compared to the canonical Pkl output.
fn jsonnet_emit_string(
s : String,
indent : Int,
ctx : JsonnetCtx,
buf : StringBuilder,
) -> Unit {
if ctx.compact || !string_contains_char(s, '\n') {
buf.write_string(jsonnet_quoted_string(s))
return
}
buf.write_string("|||\n")
let inner_indent = indent + 1
let lines = s.split("\n").collect()
let trailing_newline = s.has_suffix("\n")
let last_idx = if trailing_newline {
lines.length() - 2
} else {
lines.length() - 1
}
for i = 0; i <= last_idx; i = i + 1 {
let line = lines[i].to_owned()
if line == "" {
buf.write_char('\n')
continue
}
jsonnet_write_indent(inner_indent, ctx, buf)
buf.write_string(line)
buf.write_char('\n')
}
jsonnet_write_indent(indent, ctx, buf)
buf.write_string("|||")
}
///|
fn jsonnet_float_text(d : Double) -> String {
if d.is_nan() {
// Jsonnet has no NaN literal — the renderer error gate would
// normally trap this, but if it slips through fall back to the
// jsonnetfmt-style `null` placeholder rather than crashing.
return "null"
}
if d.is_inf() {
return "null"
}
if d == d.floor() && d.abs() < 9.0e15 {
"\{d.to_int64()}"
} else {
"\{d}"
}
}