From 768534cbb0d06c61073f5957f623ca357c5b7bbd Mon Sep 17 00:00:00 2001 From: Ville Vainio Date: Thu, 2 Apr 2026 21:30:44 +0300 Subject: [PATCH 1/4] feat: preserve field declaration order in JSON output Objects now emit fields in source declaration order rather than sorted alphabetically. Extended objects (A + B) emit left fields first, then new fields from the right. Object comprehensions preserve insertion order. Implementation: - simpleObject gains a fieldOrder []string field - restrictedObject gains retainedOrder []string - uncachedObjectFieldsOrder traverses the hierarchy for ordered keys - manifestJSON returns jsonOrderedObject instead of map[string]interface{} - serializeJSON and manifestAndSerializeMulti handle jsonOrderedObject - Native function args flattened via flattenJSONForNative Fix typo extVat -> extVar in TestExtReset (was masked by old alpha order). --- builtins.go | 6 +- interpreter.go | 65 +++- jsonnet_test.go | 2 +- testdata/builtinManifestJsonEx.golden | 2 +- testdata/builtinObjectRemoveKey_hidden.golden | 6 +- testdata/builtin_manifestYamlDoc.golden | 12 +- testdata/comparisons.golden | 132 +++---- testdata/dollar_end.golden | 4 +- testdata/dollar_end2.golden | 4 +- testdata/escaped_fields.golden | 4 +- testdata/multi.golden/foo.json | 4 +- testdata/multi_no_newline.golden/foo.json | 4 +- testdata/obj_local_right_level2.golden | 4 +- testdata/obj_local_right_level3.golden | 4 +- testdata/object_comp_super.golden | 10 +- testdata/object_local_self_super.golden | 6 +- testdata/object_super_within.golden | 4 +- testdata/object_various_field_types.golden | 6 +- testdata/stdlib_smoke_test.golden | 342 +++++++++--------- testdata/super_index_desugar.golden | 4 +- testdata/supersugar4.golden | 4 +- thunks.go | 2 +- value.go | 76 +++- 23 files changed, 398 insertions(+), 309 deletions(-) diff --git a/builtins.go b/builtins.go index 9cf0f8be2..cebabe31a 100644 --- a/builtins.go +++ b/builtins.go @@ -1531,6 +1531,7 @@ func builtinUglyObjectFlatMerge(i *interpreter, x value) (value, error) { return nil, err } newFields := make(simpleObjectFieldMap) + var newOrder []string for _, elem := range objarr.elements { obj, err := i.evaluateObject(elem) if err != nil { @@ -1545,7 +1546,8 @@ func builtinUglyObjectFlatMerge(i *interpreter, x value) (value, error) { } // there is only one field, really - for fieldName, fieldVal := range simpleObj.fields { + for _, fieldName := range simpleObj.fieldOrder { + fieldVal := simpleObj.fields[fieldName] if _, alreadyExists := newFields[fieldName]; alreadyExists { return nil, i.Error(duplicateFieldNameErrMsg(fieldName)) } @@ -1557,12 +1559,14 @@ func builtinUglyObjectFlatMerge(i *interpreter, x value) (value, error) { bindings: simpleObj.upValues, }, } + newOrder = append(newOrder, fieldName) } } return makeValueSimpleObject( nil, newFields, + newOrder, []unboundField{}, // No asserts allowed nil, ), nil diff --git a/interpreter.go b/interpreter.go index 26df98a2e..b218ada66 100644 --- a/interpreter.go +++ b/interpreter.go @@ -418,6 +418,7 @@ func (i *interpreter) rawevaluate(a ast.Node, tc tailCallStatus) (value, error) case *ast.DesugaredObject: // Evaluate all the field names. Check for null, dups, etc. fields := make(simpleObjectFieldMap, len(node.Fields)) + var fieldOrder []string for _, field := range node.Fields { fieldNameValue, err := i.evaluate(field.Name, nonTailCall) if err != nil { @@ -442,6 +443,7 @@ func (i *interpreter) rawevaluate(a ast.Node, tc tailCallStatus) (value, error) f = &plusSuperUnboundField{f} } fields[fieldName] = simpleObjectField{f, field.Hide} + fieldOrder = append(fieldOrder, fieldName) } var asserts []unboundField for _, assert := range node.Asserts { @@ -452,7 +454,7 @@ func (i *interpreter) rawevaluate(a ast.Node, tc tailCallStatus) (value, error) locals = append(locals, objectLocal{name: local.Variable, node: local.Body}) } upValues := i.stack.capture(node.FreeVariables()) - return makeValueSimpleObject(upValues, fields, asserts, locals), nil + return makeValueSimpleObject(upValues, fields, fieldOrder, asserts, locals), nil case *ast.Error: msgVal, err := i.evaluate(node.Expr, nonTailCall) @@ -680,6 +682,12 @@ func unparseNumber(v float64) string { return fmt.Sprintf("%.17g", v) } +// jsonOrderedObject is an intermediate representation of a JSON object that preserves field order. +type jsonOrderedObject struct { + keys []string + fields map[string]interface{} +} + // manifestJSON converts to standard JSON representation as in "encoding/json" package func (i *interpreter) manifestJSON(v value) (interface{}, error) { // TODO(sbarzowski) Add nice stack traces indicating the part of the code which @@ -737,8 +745,7 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) { return result, nil case *valueObject: - fieldNames := objectFields(v, withoutHidden) - sort.Strings(fieldNames) + fieldNames := objectFieldsOrdered(v, withoutHidden) msg := ast.MakeLocationRangeMessage("Checking object assertions") i.stack.setCurrentTrace(traceElement{ @@ -751,7 +758,10 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) { } i.stack.clearCurrentTrace() - result := make(map[string]interface{}, len(fieldNames)) + result := jsonOrderedObject{ + keys: fieldNames, + fields: make(map[string]interface{}, len(fieldNames)), + } for _, fieldName := range fieldNames { msg := ast.MakeLocationRangeMessage(fmt.Sprintf("Field %#v", fieldName)) @@ -769,7 +779,7 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) { i.stack.clearCurrentTrace() return nil, err } - result[fieldName] = field + result.fields[fieldName] = field i.stack.clearCurrentTrace() } @@ -829,14 +839,8 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff case float64: buf.WriteString(unparseNumber(v)) - case map[string]interface{}: - fieldNames := make([]string, 0, len(v)) - for name := range v { - fieldNames = append(fieldNames, name) - } - sort.Strings(fieldNames) - - if len(fieldNames) == 0 { + case jsonOrderedObject: + if len(v.keys) == 0 { buf.WriteString("{ }") } else { var prefix string @@ -848,8 +852,8 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff prefix = "{" indent2 = indent } - for _, fieldName := range fieldNames { - fieldVal := v[fieldName] + for _, fieldName := range v.keys { + fieldVal := v.fields[fieldName] buf.WriteString(prefix) buf.WriteString(indent2) @@ -909,8 +913,9 @@ func (i *interpreter) manifestAndSerializeMulti(v value, stringOutputMode bool, return r, err } switch json := json.(type) { - case map[string]interface{}: - for filename, fileJSON := range json { + case jsonOrderedObject: + for _, filename := range json.keys { + fileJSON := json.fields[filename] var buf bytes.Buffer if stringOutputMode { switch val := fileJSON.(type) { @@ -961,6 +966,27 @@ func (i *interpreter) manifestAndSerializeYAMLStream(v value) (r []string, err e return } +// flattenJSONForNative converts jsonOrderedObject to map[string]interface{} recursively, +// for passing to native functions which expect standard Go JSON types. +func flattenJSONForNative(v interface{}) interface{} { + switch v := v.(type) { + case jsonOrderedObject: + m := make(map[string]interface{}, len(v.keys)) + for k, val := range v.fields { + m[k] = flattenJSONForNative(val) + } + return m + case []interface{}: + result := make([]interface{}, len(v)) + for idx, elem := range v { + result[idx] = flattenJSONForNative(elem) + } + return result + default: + return v + } +} + func jsonToValue(i *interpreter, v interface{}) (value, error) { switch v := v.(type) { case nil: @@ -1269,10 +1295,13 @@ func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[string]*cache func buildObject(hide ast.ObjectFieldHide, fields map[string]value) *valueObject { fieldMap := simpleObjectFieldMap{} + fieldOrder := make([]string, 0, len(fields)) for name, v := range fields { fieldMap[name] = simpleObjectField{&readyValue{v}, hide} + fieldOrder = append(fieldOrder, name) } - return makeValueSimpleObject(bindingFrame{}, fieldMap, nil, nil) + sort.Strings(fieldOrder) + return makeValueSimpleObject(bindingFrame{}, fieldMap, fieldOrder, nil, nil) } func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook) (*interpreter, error) { diff --git a/jsonnet_test.go b/jsonnet_test.go index 4a864376f..7c6820890 100644 --- a/jsonnet_test.go +++ b/jsonnet_test.go @@ -242,7 +242,7 @@ func TestExtReset(t *testing.T) { t.Fatalf("unexpected error %v", err) } vm.ExtReset() - _, err = vm.EvaluateAnonymousSnippet("test.jsonnet", `{ str: std.extVat('fooString'), code: std.extVar('fooCode') }`) + _, err = vm.EvaluateAnonymousSnippet("test.jsonnet", `{ str: std.extVar('fooString'), code: std.extVar('fooCode') }`) if err == nil { t.Fatalf("expected error, got nil") } diff --git a/testdata/builtinManifestJsonEx.golden b/testdata/builtinManifestJsonEx.golden index bfb0ea730..f04749c10 100644 --- a/testdata/builtinManifestJsonEx.golden +++ b/testdata/builtinManifestJsonEx.golden @@ -2,7 +2,7 @@ "array": "[\n \"bar\",\n \"bar\",\n 1,\n 1.42,\n -1,\n false,\n true,\n {\n \"cereal\": [\n \"<>& fizbuzz\"\n ],\n \"treats\": [\n {\n \"name\": \"chocolate\"\n }\n ]\n }\n]", "bool": "true", "null": "null", - "number": "42", "object": "{\n \"bam\": true,\n \"bar\": \"bar\",\n \"baz\": 1,\n \"bazel\": 1.42,\n \"bim\": false,\n \"blamo\": {\n \"cereal\": [\n \"<>& fizbuzz\"\n ],\n \"treats\": [\n {\n \"name\": \"chocolate\"\n }\n ]\n },\n \"boom\": -1,\n \"foo\": \"bar\"\n}", + "number": "42", "string": "\"foo\"" } diff --git a/testdata/builtinObjectRemoveKey_hidden.golden b/testdata/builtinObjectRemoveKey_hidden.golden index c329fbee3..a3e2e1beb 100644 --- a/testdata/builtinObjectRemoveKey_hidden.golden +++ b/testdata/builtinObjectRemoveKey_hidden.golden @@ -1,12 +1,12 @@ { + "fields": [ + "baz" + ], "all_fields": [ "bar", "baz" ], "bar": 2, - "fields": [ - "baz" - ], "object": { "baz": 3 } diff --git a/testdata/builtin_manifestYamlDoc.golden b/testdata/builtin_manifestYamlDoc.golden index 03ca87d1c..76fdf1c7c 100644 --- a/testdata/builtin_manifestYamlDoc.golden +++ b/testdata/builtin_manifestYamlDoc.golden @@ -1,14 +1,14 @@ { "object": "\"abc\": \"def\"\n\"bam\": true\n\"bar\": \"baz\"\n\"baz\": 1\n\"bazel\": 1.42\n\"bim\": false\n\"blamo\":\n \"cereal\":\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n \"treats\":\n - \"name\": \"chocolate\"\n\"boom\": -1\n\"foo\": \"baz\"", "object2": "\"\\\"\": 4\n\"array\":\n- \"s\"\n- 1\n-\n - 2\n - 3\n- \"a\":\n - \"0\"\n - \"z\"\n \"r\": 6\n\"arraySection\":\n- \"q\": 1\n- \"w\": 2\n\"bool\": true\n\"emptyArray\": []\n\"emptyArraySection\":\n- {}\n\"emptySection\": {}\n\"escaped\\\"Section\":\n \"z\": \"q\"\n\"key\": \"value\"\n\"notBool\": false\n\"number\": 7\n\"section\":\n \"a\": 1\n \"array\":\n - \"c\": 3\n - \"d\": 4\n \"e$caped\":\n \"q\": \"t\"\n \"nested\":\n \"b\": 2\n \"nestedArray\":\n - \"k\": \"v\"\n \"nested\":\n \"e\": 5\n\"simple\":\n \"t\": 5", - "object2_indent": "\"\\\"\": 4\n\"array\":\n - \"s\"\n - 1\n -\n - 2\n - 3\n - \"a\":\n - \"0\"\n - \"z\"\n \"r\": 6\n\"arraySection\":\n - \"q\": 1\n - \"w\": 2\n\"bool\": true\n\"emptyArray\": []\n\"emptyArraySection\":\n - {}\n\"emptySection\": {}\n\"escaped\\\"Section\":\n \"z\": \"q\"\n\"key\": \"value\"\n\"notBool\": false\n\"number\": 7\n\"section\":\n \"a\": 1\n \"array\":\n - \"c\": 3\n - \"d\": 4\n \"e$caped\":\n \"q\": \"t\"\n \"nested\":\n \"b\": 2\n \"nestedArray\":\n - \"k\": \"v\"\n \"nested\":\n \"e\": 5\n\"simple\":\n \"t\": 5", - "object2_indent_unquoted": "\"\\\"\": 4\narray:\n - \"s\"\n - 1\n -\n - 2\n - 3\n - a:\n - \"0\"\n - \"z\"\n r: 6\narraySection:\n - q: 1\n - w: 2\nbool: true\nemptyArray: []\nemptyArraySection:\n - {}\nemptySection: {}\n\"escaped\\\"Section\":\n z: \"q\"\nkey: \"value\"\nnotBool: false\nnumber: 7\nsection:\n a: 1\n array:\n - c: 3\n - d: 4\n \"e$caped\":\n q: \"t\"\n nested:\n b: 2\n nestedArray:\n - k: \"v\"\n nested:\n e: 5\nsimple:\n t: 5", - "object2_unquoted": "\"\\\"\": 4\narray:\n- \"s\"\n- 1\n-\n - 2\n - 3\n- a:\n - \"0\"\n - \"z\"\n r: 6\narraySection:\n- q: 1\n- w: 2\nbool: true\nemptyArray: []\nemptyArraySection:\n- {}\nemptySection: {}\n\"escaped\\\"Section\":\n z: \"q\"\nkey: \"value\"\nnotBool: false\nnumber: 7\nsection:\n a: 1\n array:\n - c: 3\n - d: 4\n \"e$caped\":\n q: \"t\"\n nested:\n b: 2\n nestedArray:\n - k: \"v\"\n nested:\n e: 5\nsimple:\n t: 5", "object3": "\"-0B1010_0111_0100_1010_1110\": \"string\\n with some\\n newlines\\n \"\n\"0X_0a_74_ae\": \"BARE_KEY\"\n\"1-234-567-8901\": null\n\"192.168.0.1\":\n- \"a\": 2\n \"b\": \"str\"\n- \"c\": []\n\"__-0B1010_0111_0100_1010_1110\": \"a new line\\n \"\n\"__-0X_0a_74_ae\": \"BARE_KEY\"\n\"b\":\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\n\"jsonnet.org/k8s-label-like\": \"0600\"\n\"just-letters-dashes\": \"+1101_1111\"\n\"just_letters_underscores\": 142321\n\"x\": \"BARE_KEY\"", + "object_indent": "\"abc\": \"def\"\n\"bam\": true\n\"bar\": \"baz\"\n\"baz\": 1\n\"bazel\": 1.42\n\"bim\": false\n\"blamo\":\n \"cereal\":\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n \"treats\":\n - \"name\": \"chocolate\"\n\"boom\": -1\n\"foo\": \"baz\"", + "object2_indent": "\"\\\"\": 4\n\"array\":\n - \"s\"\n - 1\n -\n - 2\n - 3\n - \"a\":\n - \"0\"\n - \"z\"\n \"r\": 6\n\"arraySection\":\n - \"q\": 1\n - \"w\": 2\n\"bool\": true\n\"emptyArray\": []\n\"emptyArraySection\":\n - {}\n\"emptySection\": {}\n\"escaped\\\"Section\":\n \"z\": \"q\"\n\"key\": \"value\"\n\"notBool\": false\n\"number\": 7\n\"section\":\n \"a\": 1\n \"array\":\n - \"c\": 3\n - \"d\": 4\n \"e$caped\":\n \"q\": \"t\"\n \"nested\":\n \"b\": 2\n \"nestedArray\":\n - \"k\": \"v\"\n \"nested\":\n \"e\": 5\n\"simple\":\n \"t\": 5", "object3_indent": "\"-0B1010_0111_0100_1010_1110\": \"string\\n with some\\n newlines\\n \"\n\"0X_0a_74_ae\": \"BARE_KEY\"\n\"1-234-567-8901\": null\n\"192.168.0.1\":\n - \"a\": 2\n \"b\": \"str\"\n - \"c\": []\n\"__-0B1010_0111_0100_1010_1110\": \"a new line\\n \"\n\"__-0X_0a_74_ae\": \"BARE_KEY\"\n\"b\":\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\n\"jsonnet.org/k8s-label-like\": \"0600\"\n\"just-letters-dashes\": \"+1101_1111\"\n\"just_letters_underscores\": 142321\n\"x\": \"BARE_KEY\"", - "object3_indent_unquoted": "-0B1010_0111_0100_1010_1110: \"string\\n with some\\n newlines\\n \"\n0X_0a_74_ae: \"BARE_KEY\"\n1-234-567-8901: null\n192.168.0.1:\n - a: 2\n b: \"str\"\n - c: []\n__-0B1010_0111_0100_1010_1110: \"a new line\\n \"\n__-0X_0a_74_ae: \"BARE_KEY\"\nb:\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\njsonnet.org/k8s-label-like: \"0600\"\njust-letters-dashes: \"+1101_1111\"\njust_letters_underscores: 142321\nx: \"BARE_KEY\"", + "object_unquoted": "abc: \"def\"\nbam: true\nbar: \"baz\"\nbaz: 1\nbazel: 1.42\nbim: false\nblamo:\n cereal:\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n treats:\n - name: \"chocolate\"\nboom: -1\nfoo: \"baz\"", + "object2_unquoted": "\"\\\"\": 4\narray:\n- \"s\"\n- 1\n-\n - 2\n - 3\n- a:\n - \"0\"\n - \"z\"\n r: 6\narraySection:\n- q: 1\n- w: 2\nbool: true\nemptyArray: []\nemptyArraySection:\n- {}\nemptySection: {}\n\"escaped\\\"Section\":\n z: \"q\"\nkey: \"value\"\nnotBool: false\nnumber: 7\nsection:\n a: 1\n array:\n - c: 3\n - d: 4\n \"e$caped\":\n q: \"t\"\n nested:\n b: 2\n nestedArray:\n - k: \"v\"\n nested:\n e: 5\nsimple:\n t: 5", "object3_unquoted": "-0B1010_0111_0100_1010_1110: \"string\\n with some\\n newlines\\n \"\n0X_0a_74_ae: \"BARE_KEY\"\n1-234-567-8901: null\n192.168.0.1:\n- a: 2\n b: \"str\"\n- c: []\n__-0B1010_0111_0100_1010_1110: \"a new line\\n \"\n__-0X_0a_74_ae: \"BARE_KEY\"\nb:\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\njsonnet.org/k8s-label-like: \"0600\"\njust-letters-dashes: \"+1101_1111\"\njust_letters_underscores: 142321\nx: \"BARE_KEY\"", - "object_indent": "\"abc\": \"def\"\n\"bam\": true\n\"bar\": \"baz\"\n\"baz\": 1\n\"bazel\": 1.42\n\"bim\": false\n\"blamo\":\n \"cereal\":\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n \"treats\":\n - \"name\": \"chocolate\"\n\"boom\": -1\n\"foo\": \"baz\"", "object_indent_unquoted": "abc: \"def\"\nbam: true\nbar: \"baz\"\nbaz: 1\nbazel: 1.42\nbim: false\nblamo:\n cereal:\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n treats:\n - name: \"chocolate\"\nboom: -1\nfoo: \"baz\"", - "object_unquoted": "abc: \"def\"\nbam: true\nbar: \"baz\"\nbaz: 1\nbazel: 1.42\nbim: false\nblamo:\n cereal:\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n treats:\n - name: \"chocolate\"\nboom: -1\nfoo: \"baz\"" + "object2_indent_unquoted": "\"\\\"\": 4\narray:\n - \"s\"\n - 1\n -\n - 2\n - 3\n - a:\n - \"0\"\n - \"z\"\n r: 6\narraySection:\n - q: 1\n - w: 2\nbool: true\nemptyArray: []\nemptyArraySection:\n - {}\nemptySection: {}\n\"escaped\\\"Section\":\n z: \"q\"\nkey: \"value\"\nnotBool: false\nnumber: 7\nsection:\n a: 1\n array:\n - c: 3\n - d: 4\n \"e$caped\":\n q: \"t\"\n nested:\n b: 2\n nestedArray:\n - k: \"v\"\n nested:\n e: 5\nsimple:\n t: 5", + "object3_indent_unquoted": "-0B1010_0111_0100_1010_1110: \"string\\n with some\\n newlines\\n \"\n0X_0a_74_ae: \"BARE_KEY\"\n1-234-567-8901: null\n192.168.0.1:\n - a: 2\n b: \"str\"\n - c: []\n__-0B1010_0111_0100_1010_1110: \"a new line\\n \"\n__-0X_0a_74_ae: \"BARE_KEY\"\nb:\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\njsonnet.org/k8s-label-like: \"0600\"\njust-letters-dashes: \"+1101_1111\"\njust_letters_underscores: 142321\nx: \"BARE_KEY\"" } diff --git a/testdata/comparisons.golden b/testdata/comparisons.golden index af9e88156..877377d63 100644 --- a/testdata/comparisons.golden +++ b/testdata/comparisons.golden @@ -1,103 +1,99 @@ [ { + "a": 1, + "b": 2, "<": true, - "<=": true, ">": false, - ">=": false, - "a": 1, - "b": 2 + "<=": true, + ">=": false }, { + "a": 2, + "b": 1, "<": false, - "<=": false, ">": true, - ">=": true, - "a": 2, - "b": 1 + "<=": false, + ">=": true }, { - "<": true, - "<=": true, - ">": false, - ">=": false, "a": [ 1 ], "b": [ 2 - ] + ], + "<": true, + ">": false, + "<=": true, + ">=": false }, { - "<": false, - "<=": false, - ">": true, - ">=": true, "a": [ 2 ], "b": [ 1 - ] + ], + "<": false, + ">": true, + "<=": false, + ">=": true }, { + "a": [ ], + "b": [ ], "<": false, - "<=": true, ">": false, - ">=": true, - "a": [ ], - "b": [ ] + "<=": true, + ">=": true }, { - "<": true, - "<=": true, - ">": false, - ">=": false, "a": [ ], "b": [ 1 - ] + ], + "<": true, + ">": false, + "<=": true, + ">=": false }, { - "<": false, - "<=": false, - ">": true, - ">=": true, "a": [ 1 ], - "b": [ ] - }, - { + "b": [ ], "<": false, - "<=": false, ">": true, - ">=": true, + "<=": false, + ">=": true + }, + { "a": [ 1, 2 ], "b": [ 1 - ] + ], + "<": false, + ">": true, + "<=": false, + ">=": true }, { - "<": true, - "<=": true, - ">": false, - ">=": false, "a": [ 1, 2 ], "b": [ 2 - ] - }, - { + ], "<": true, - "<=": true, ">": false, - ">=": false, + "<=": true, + ">=": false + }, + { "a": [ [ 1 @@ -107,13 +103,13 @@ [ 2 ] - ] + ], + "<": true, + ">": false, + "<=": true, + ">=": false }, { - "<": false, - "<=": false, - ">": true, - ">=": true, "a": [ [ 2 @@ -123,25 +119,25 @@ [ 1 ] - ] - }, - { + ], "<": false, - "<=": false, ">": true, - ">=": true, + "<=": false, + ">=": true + }, + { "a": [ "foo" ], "b": [ "bar" - ] + ], + "<": false, + ">": true, + "<=": false, + ">=": true }, { - "<": true, - "<=": true, - ">": false, - ">=": false, "a": [ 0, "a" @@ -149,14 +145,18 @@ "b": [ 0, "b" - ] + ], + "<": true, + ">": false, + "<=": true, + ">=": false }, { + "a": "foo", + "b": "bar", "<": false, - "<=": false, ">": true, - ">=": true, - "a": "foo", - "b": "bar" + "<=": false, + ">=": true } ] diff --git a/testdata/dollar_end.golden b/testdata/dollar_end.golden index d2ebc46eb..a263850df 100644 --- a/testdata/dollar_end.golden +++ b/testdata/dollar_end.golden @@ -1,4 +1,4 @@ { - "bar": -42, - "foo": 42 + "foo": 42, + "bar": -42 } diff --git a/testdata/dollar_end2.golden b/testdata/dollar_end2.golden index 860fdf070..510e5b419 100644 --- a/testdata/dollar_end2.golden +++ b/testdata/dollar_end2.golden @@ -1,4 +1,4 @@ { - "bar": true, - "foo": false + "foo": false, + "bar": true } diff --git a/testdata/escaped_fields.golden b/testdata/escaped_fields.golden index bcb639367..f30e15e34 100644 --- a/testdata/escaped_fields.golden +++ b/testdata/escaped_fields.golden @@ -1,5 +1,5 @@ { - "\n\n": null, "\"": null, - "'": null + "'": null, + "\n\n": null } diff --git a/testdata/multi.golden/foo.json b/testdata/multi.golden/foo.json index 7488d76ec..429d9ec7f 100644 --- a/testdata/multi.golden/foo.json +++ b/testdata/multi.golden/foo.json @@ -1,4 +1,4 @@ { - "baq": "27", - "baz": 3 + "baz": 3, + "baq": "27" } diff --git a/testdata/multi_no_newline.golden/foo.json b/testdata/multi_no_newline.golden/foo.json index 5ad59deab..3005327a8 100644 --- a/testdata/multi_no_newline.golden/foo.json +++ b/testdata/multi_no_newline.golden/foo.json @@ -1,4 +1,4 @@ { - "baq": "27", - "baz": 3 + "baz": 3, + "baq": "27" } \ No newline at end of file diff --git a/testdata/obj_local_right_level2.golden b/testdata/obj_local_right_level2.golden index 350fead2d..79f823f2c 100644 --- a/testdata/obj_local_right_level2.golden +++ b/testdata/obj_local_right_level2.golden @@ -1,4 +1,4 @@ { - "answer": "right", - "bar": "right" + "bar": "right", + "answer": "right" } diff --git a/testdata/obj_local_right_level3.golden b/testdata/obj_local_right_level3.golden index 9e012040b..ba4d054f3 100644 --- a/testdata/obj_local_right_level3.golden +++ b/testdata/obj_local_right_level3.golden @@ -1,4 +1,4 @@ { - "answer": "right", - "bar": "wrong2" + "bar": "wrong2", + "answer": "right" } diff --git a/testdata/object_comp_super.golden b/testdata/object_comp_super.golden index 82b3604d7..b168d88a5 100644 --- a/testdata/object_comp_super.golden +++ b/testdata/object_comp_super.golden @@ -1,8 +1,4 @@ { - "a": "42a", - "b": "42b", - "c": "42c", - "q": 42, "x": [ 1, "x" @@ -14,5 +10,9 @@ "z": [ 1, "z" - ] + ], + "a": "42a", + "b": "42b", + "c": "42c", + "q": 42 } diff --git a/testdata/object_local_self_super.golden b/testdata/object_local_self_super.golden index 62f8836d5..20b4a6f6d 100644 --- a/testdata/object_local_self_super.golden +++ b/testdata/object_local_self_super.golden @@ -1,6 +1,6 @@ { - "bar": 42, - "baz": "xxx", + "z": 42, "foo": 42, - "z": 42 + "bar": 42, + "baz": "xxx" } diff --git a/testdata/object_super_within.golden b/testdata/object_super_within.golden index 54788472a..7e8d3bc99 100644 --- a/testdata/object_super_within.golden +++ b/testdata/object_super_within.golden @@ -1,6 +1,6 @@ { "a": 2, - "b": 1, + "d": 42, "c": 1, - "d": 42 + "b": 1 } diff --git a/testdata/object_various_field_types.golden b/testdata/object_various_field_types.golden index caf46b812..f86d020f5 100644 --- a/testdata/object_various_field_types.golden +++ b/testdata/object_various_field_types.golden @@ -1,9 +1,9 @@ { - "expr, as complex as you want": true, "identifier": true, - "str_block \\n\n": true, "str_double ' \n": true, "str_single \" \n": true, + "str_block \\n\n": true, "str_verbatim_double \" '' \\n": true, - "str_verbatim_single \"\" ' \\n": true + "str_verbatim_single \"\" ' \\n": true, + "expr, as complex as you want": true } diff --git a/testdata/stdlib_smoke_test.golden b/testdata/stdlib_smoke_test.golden index 700d7bfe5..16199401a 100644 --- a/testdata/stdlib_smoke_test.golden +++ b/testdata/stdlib_smoke_test.golden @@ -1,57 +1,142 @@ { + "thisFile": "testdata/stdlib_smoke_test", + "type": "object", + "length": 0, + "objectHas": false, + "objectFields": [ ], + "objectValues": [ ], + "objectKeysValues": [ ], + "objectHasAll": false, + "objectFieldsAll": [ ], + "objectValuesAll": [ ], + "objectKeysValuesAll": [ ], + "prune": { + "y": [ + "42" + ] + }, + "mapWithKey": { + "a": 42 + }, + "get": [ + 17, + 42, + 18, + 42 + ], + "isArray": true, + "isBoolean": true, + "isFunction": true, + "isNumber": true, + "isObject": true, + "isString": true, "abs": 42, - "acos": true, - "asciiLower": "blah", - "asciiUpper": "BLAH", + "sign": 1, + "max": 3, + "min": 2, + "pow": 8, + "exp": 148.4131591025766, + "log": true, + "exponent": 3, + "mantissa": 0.625, + "floor": 5, + "ceil": 5, + "sqrt": true, + "sin": true, + "cos": true, + "tan": true, "asin": true, - "assertEqual": true, + "acos": true, "atan": true, - "base64": [ - "YmxhaA==", - "YmxhaA==" + "assertEqual": true, + "toString": "42", + "codepoint": 65, + "char": "A", + "substr": "s", + "findSubstr": [ + 0, + 5 ], - "base64Decode": "blah\n", - "base64DecodeBytes": [ - 98, - 108, - 97, - 104, - 10 + "startsWith": true, + "endsWith": true, + "stripChars": "bbbb", + "lstripChars": "bbbbcccc", + "rstripChars": "aaabbbb", + "split": [ + "a", + "b", + "c" ], - "ceil": 5, - "char": "A", - "codepoint": 65, - "cos": true, - "count": 1, - "decodeUTF8": "AAA", + "splitLimit": [ + "a", + "b,c" + ], + "splitLimitR": [ + "a,b", + "c" + ], + "strReplace": "bba", + "asciiUpper": "BLAH", + "asciiLower": "blah", + "stringChars": [ + "b", + "l", + "a", + "h" + ], + "format": "test blah 42", + "escapeStringBash": "'test '\"'\"'test'\"'\"'test'", + "escapeStringDollars": "test 'test'test", + "escapeStringJson": "\"test 'test'test\"", + "escapeStringPython": "\"test 'test'test\"", + "parseInt": 42, + "parseOctal": 83, + "parseHex": 3735928559, + "parseJson": { + "a": "b" + }, "encodeUTF8": [ 98, 108, 97, 104 ], - "endsWith": true, - "escapeStringBash": "'test '\"'\"'test'\"'\"'test'", - "escapeStringDollars": "test 'test'test", - "escapeStringJson": "\"test 'test'test\"", - "escapeStringPython": "\"test 'test'test\"", - "exp": 148.4131591025766, - "exponent": 3, - "filter": [ + "decodeUTF8": "AAA", + "manifestIni": "a = 1\nb = 2\n[s1]\nx = 1\ny = 2\n", + "manifestPython": "{\"a\": {\"b\": \"c\"}}", + "manifestPythonVars": "a = {\"b\": \"c\"}\n", + "manifestTomlEx": "\n\n[a]\n b = \"c\"", + "manifestJsonEx": "{\n \"a\": {\n \"b\": \"c\"\n }\n}", + "manifestJsonMinified": "{\"a\":{\"b\":\"c\"}}", + "manifestYamlDoc": "\"a\":\n \"b\": \"c\"", + "manifestYamlStream": "---\n42\n---\n\"a\":\n \"b\": \"c\"\n...\n", + "manifestXmlJsonml": "", + "makeArray": [ + 0, + 1, 2, + 3, 4 ], - "filterMap": [ - 4, - 8 - ], + "count": 1, "find": [ 2, 4 ], - "findSubstr": [ - 0, - 5 + "member": true, + "map": [ + -1, + -2, + -3 + ], + "mapWithIndex": [ + 3, + 3, + 3 + ], + "filterMap": [ + 4, + 8 ], "flatMap": [ 2, @@ -61,18 +146,10 @@ 6, 9 ], - "flattenArrays": [ - 1, + "filter": [ 2, - 3, - 4, - 5, - [ - 6, - 7 - ] + 4 ], - "floor": 5, "foldl": [ 0, 1, @@ -85,93 +162,33 @@ 3, 4 ], - "format": "test blah 42", - "get": [ - 17, - 42, - 18, - 42 - ], - "isArray": true, - "isBoolean": true, - "isFunction": true, - "isNumber": true, - "isObject": true, - "isString": true, - "join": "a,b,c", - "length": 0, - "lines": "a\nb\nc\n", - "log": true, - "lstripChars": "bbbbcccc", - "makeArray": [ - 0, + "repeat": "foofoofoo", + "slice": "o", + "range": [ 1, 2, 3, - 4 - ], - "manifestIni": "a = 1\nb = 2\n[s1]\nx = 1\ny = 2\n", - "manifestJsonEx": "{\n \"a\": {\n \"b\": \"c\"\n }\n}", - "manifestJsonMinified": "{\"a\":{\"b\":\"c\"}}", - "manifestPython": "{\"a\": {\"b\": \"c\"}}", - "manifestPythonVars": "a = {\"b\": \"c\"}\n", - "manifestTomlEx": "\n\n[a]\n b = \"c\"", - "manifestXmlJsonml": "", - "manifestYamlDoc": "\"a\":\n \"b\": \"c\"", - "manifestYamlStream": "---\n42\n---\n\"a\":\n \"b\": \"c\"\n...\n", - "mantissa": 0.625, - "map": [ - -1, - -2, - -3 - ], - "mapWithIndex": [ - 3, - 3, - 3 + 4, + 5 ], - "mapWithKey": { - "a": 42 - }, - "max": 3, - "md5": "1bc29b36f623ba82aaf6724fd3b16718", - "member": true, - "mergePatch": { }, - "min": 2, - "objectFields": [ ], - "objectFieldsAll": [ ], - "objectHas": false, - "objectHasAll": false, - "objectKeysValues": [ ], - "objectKeysValuesAll": [ ], - "objectValues": [ ], - "objectValuesAll": [ ], - "parseHex": 3735928559, - "parseInt": 42, - "parseJson": { - "a": "b" - }, - "parseOctal": 83, - "pow": 8, - "prune": { - "y": [ - "42" - ] - }, - "range": [ + "join": "a,b,c", + "lines": "a\nb\nc\n", + "flattenArrays": [ 1, 2, 3, 4, - 5 + 5, + [ + 6, + 7 + ] ], - "repeat": "foofoofoo", "reverse": [ "a", "b" ], - "rstripChars": "aaabbbb", - "set": [ + "sort": [ [ 1, 2, @@ -183,14 +200,28 @@ 1 ] ], - "setDiff": [ + "uniq": [ [ 1, - 2 + 2, + 3 ], + [ + "a", + "B", + "a" + ] + ], + "set": [ [ 1, + 2, 3 + ], + [ + 3, + 2, + 1 ] ], "setInter": [ @@ -201,10 +232,6 @@ 2 ] ], - "setMember": [ - false, - true - ], "setUnion": [ [ 1, @@ -221,59 +248,32 @@ 5 ] ], - "sign": 1, - "sin": true, - "slice": "o", - "sort": [ + "setDiff": [ [ 1, - 2, - 3 + 2 ], [ - 3, - 2, - 1 + 1, + 3 ] ], - "split": [ - "a", - "b", - "c" - ], - "splitLimit": [ - "a", - "b,c" + "setMember": [ + false, + true ], - "splitLimitR": [ - "a,b", - "c" + "base64": [ + "YmxhaA==", + "YmxhaA==" ], - "sqrt": true, - "startsWith": true, - "strReplace": "bba", - "stringChars": [ - "b", - "l", - "a", - "h" + "base64DecodeBytes": [ + 98, + 108, + 97, + 104, + 10 ], - "stripChars": "bbbb", - "substr": "s", - "tan": true, - "thisFile": "testdata/stdlib_smoke_test", - "toString": "42", - "type": "object", - "uniq": [ - [ - 1, - 2, - 3 - ], - [ - "a", - "B", - "a" - ] - ] + "base64Decode": "blah\n", + "md5": "1bc29b36f623ba82aaf6724fd3b16718", + "mergePatch": { } } diff --git a/testdata/super_index_desugar.golden b/testdata/super_index_desugar.golden index 2e44b03e9..345fd8614 100644 --- a/testdata/super_index_desugar.golden +++ b/testdata/super_index_desugar.golden @@ -1,5 +1,5 @@ { "a": 1, - "c": "a", - "s": 1 + "s": 1, + "c": "a" } diff --git a/testdata/supersugar4.golden b/testdata/supersugar4.golden index 54dc1b92a..a609d3c2f 100644 --- a/testdata/supersugar4.golden +++ b/testdata/supersugar4.golden @@ -1,4 +1,4 @@ { - "x": true, - "y": 43 + "y": 43, + "x": true } diff --git a/thunks.go b/thunks.go index e98c487cb..f513a4fff 100644 --- a/thunks.go +++ b/thunks.go @@ -273,7 +273,7 @@ func (native *NativeFunction) evalCall(arguments callArguments, i *interpreter) if err != nil { return nil, err } - nativeArgs = append(nativeArgs, json) + nativeArgs = append(nativeArgs, flattenJSONForNative(json)) } call := func() (resultJSON interface{}, err error) { defer func() { diff --git a/value.go b/value.go index d5a657166..9383e9aaf 100644 --- a/value.go +++ b/value.go @@ -539,10 +539,11 @@ type objectLocal struct { // Let a = {x: 42} and b = {y: self.x}. Evaluating b.y is an error, // but (a+b).y evaluates to 42. type simpleObject struct { - upValues bindingFrame - fields simpleObjectFieldMap - asserts []unboundField - locals []objectLocal + upValues bindingFrame + fields simpleObjectFieldMap + fieldOrder []string // tracks definition order of fields + asserts []unboundField + locals []objectLocal } func checkAssertionsHelper(i *interpreter, obj *valueObject, curr uncachedObject, superDepth int) error { @@ -592,14 +593,15 @@ func (*simpleObject) inheritanceSize() int { return 1 } -func makeValueSimpleObject(b bindingFrame, fields simpleObjectFieldMap, asserts []unboundField, locals []objectLocal) *valueObject { +func makeValueSimpleObject(b bindingFrame, fields simpleObjectFieldMap, fieldOrder []string, asserts []unboundField, locals []objectLocal) *valueObject { return &valueObject{ cache: make(map[objectCacheKey]value), uncached: &simpleObject{ - upValues: b, - fields: fields, - asserts: asserts, - locals: locals, + upValues: b, + fields: fields, + fieldOrder: fieldOrder, + asserts: asserts, + locals: locals, }, } } @@ -659,6 +661,7 @@ func makeValueExtendedObject(left, right *valueObject) *valueObject { type restrictedObject struct { obj uncachedObject retainedFields fieldHideMap + retainedOrder []string // tracks field order after restriction } func (o *restrictedObject) inheritanceSize() int { @@ -666,11 +669,20 @@ func (o *restrictedObject) inheritanceSize() int { } func makeValueRestrictedObject(obj *valueObject) *valueObject { + retained := objectFieldsVisibility(obj) + fullOrder := uncachedObjectFieldsOrder(obj.uncached) + retainedOrder := make([]string, 0, len(fullOrder)) + for _, f := range fullOrder { + if _, ok := retained[f]; ok { + retainedOrder = append(retainedOrder, f) + } + } return &valueObject{ cache: make(map[objectCacheKey]value), uncached: &restrictedObject{ obj: obj.uncached, - retainedFields: objectFieldsVisibility(obj), + retainedFields: retained, + retainedOrder: retainedOrder, }, } } @@ -769,6 +781,50 @@ func objectHasField(sb selfBinding, fieldName string) bool { type fieldHideMap map[string]ast.ObjectFieldHide +// uncachedObjectFieldsOrder returns field names in definition order, without filtering by visibility. +// For extended objects, left fields come first, then new fields from the right. +func uncachedObjectFieldsOrder(obj uncachedObject) []string { + switch obj := obj.(type) { + case *extendedObject: + leftOrder := uncachedObjectFieldsOrder(obj.left) + rightOrder := uncachedObjectFieldsOrder(obj.right) + result := make([]string, 0, len(leftOrder)+len(rightOrder)) + seen := make(map[string]bool, len(leftOrder)) + for _, f := range leftOrder { + result = append(result, f) + seen[f] = true + } + for _, f := range rightOrder { + if !seen[f] { + result = append(result, f) + } + } + return result + case *restrictedObject: + return obj.retainedOrder + case *simpleObject: + return obj.fieldOrder + } + return nil +} + +// objectFieldsOrdered returns visible field names in definition order. +func objectFieldsOrdered(obj *valueObject, h hidden) []string { + visibility := objectFieldsVisibility(obj) + allKeys := uncachedObjectFieldsOrder(obj.uncached) + result := make([]string, 0, len(allKeys)) + for _, key := range allKeys { + hide, exists := visibility[key] + if !exists { + continue + } + if h == withHidden || hide != ast.ObjectFieldHidden { + result = append(result, key) + } + } + return result +} + func uncachedObjectFieldsVisibility(obj uncachedObject) fieldHideMap { r := make(fieldHideMap) switch obj := obj.(type) { From 4272316ad33c3ef8b8d3c99848d50500153ca582 Mon Sep 17 00:00:00 2001 From: Ville Vainio Date: Thu, 2 Apr 2026 21:46:39 +0300 Subject: [PATCH 2/4] feat: make declaration-order field output opt-in via --preserve-field-order Default behavior reverts to alphabetical sorting (backward compatible with upstream). Pass --preserve-field-order on the CLI or set vm.PreserveFieldOrder = true in the API to get declaration order. The field order tracking in the object model is retained regardless, so switching between modes is cheap (just sort the already-collected keys at serialize time). --- cmd/jsonnet/cmd.go | 2 + interpreter.go | 39 +- testdata/builtinManifestJsonEx.golden | 2 +- testdata/builtinObjectRemoveKey_hidden.golden | 6 +- testdata/builtin_manifestYamlDoc.golden | 12 +- testdata/comparisons.golden | 132 +++---- testdata/dollar_end.golden | 4 +- testdata/dollar_end2.golden | 4 +- testdata/escaped_fields.golden | 4 +- testdata/multi.golden/foo.json | 4 +- testdata/multi_no_newline.golden/foo.json | 4 +- testdata/obj_local_right_level2.golden | 4 +- testdata/obj_local_right_level3.golden | 4 +- testdata/object_comp_super.golden | 10 +- testdata/object_local_self_super.golden | 6 +- testdata/object_super_within.golden | 4 +- testdata/object_various_field_types.golden | 6 +- testdata/stdlib_smoke_test.golden | 342 +++++++++--------- testdata/super_index_desugar.golden | 4 +- testdata/supersugar4.golden | 4 +- vm.go | 7 +- 21 files changed, 309 insertions(+), 295 deletions(-) diff --git a/cmd/jsonnet/cmd.go b/cmd/jsonnet/cmd.go index ada9adf6e..30429821a 100644 --- a/cmd/jsonnet/cmd.go +++ b/cmd/jsonnet/cmd.go @@ -258,6 +258,8 @@ func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArg config.evalStream = true } else if arg == "-S" || arg == "--string" { vm.StringOutput = true + } else if arg == "--preserve-field-order" { + vm.PreserveFieldOrder = true } else if arg == "--no-trailing-newline" { vm.OutputNewline = false } else if len(arg) > 1 && arg[0] == '-' { diff --git a/interpreter.go b/interpreter.go index b218ada66..13bbeccbb 100644 --- a/interpreter.go +++ b/interpreter.go @@ -269,6 +269,9 @@ type interpreter struct { // Native functions nativeFuncs map[string]*NativeFunction + // When true, object fields are output in declaration order rather than sorted + preserveFieldOrder bool + // A part of std object common to all files baseStd *valueObject @@ -794,7 +797,7 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) { } } -func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buffer) { +func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buffer, preserveFieldOrder bool) { switch v := v.(type) { case nil: buf.WriteString("null") @@ -815,7 +818,7 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff for _, elem := range v { buf.WriteString(prefix) buf.WriteString(indent2) - serializeJSON(elem, multiline, indent2, buf) + serializeJSON(elem, multiline, indent2, buf, preserveFieldOrder) if multiline { prefix = ",\n" } else { @@ -840,7 +843,14 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff buf.WriteString(unparseNumber(v)) case jsonOrderedObject: - if len(v.keys) == 0 { + keys := v.keys + if !preserveFieldOrder { + sorted := make([]string, len(keys)) + copy(sorted, keys) + sort.Strings(sorted) + keys = sorted + } + if len(keys) == 0 { buf.WriteString("{ }") } else { var prefix string @@ -852,7 +862,7 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff prefix = "{" indent2 = indent } - for _, fieldName := range v.keys { + for _, fieldName := range keys { fieldVal := v.fields[fieldName] buf.WriteString(prefix) @@ -861,7 +871,7 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff buf.WriteString(unparseString(fieldName)) buf.WriteString(": ") - serializeJSON(fieldVal, multiline, indent2, buf) + serializeJSON(fieldVal, multiline, indent2, buf, preserveFieldOrder) if multiline { prefix = ",\n" @@ -891,7 +901,7 @@ func (i *interpreter) manifestAndSerializeJSON( if err != nil { return err } - serializeJSON(manifested, multiline, indent, buf) + serializeJSON(manifested, multiline, indent, buf, i.preserveFieldOrder) return nil } @@ -927,7 +937,7 @@ func (i *interpreter) manifestAndSerializeMulti(v value, stringOutputMode bool, return r, makeRuntimeError(msg, i.getCurrentStackTrace()) } } else { - serializeJSON(fileJSON, true, "", &buf) + serializeJSON(fileJSON, true, "", &buf, i.preserveFieldOrder) } if outputNewline { buf.WriteString("\n") @@ -953,7 +963,7 @@ func (i *interpreter) manifestAndSerializeYAMLStream(v value) (r []string, err e case []interface{}: for _, doc := range json { var buf bytes.Buffer - serializeJSON(doc, true, "", &buf) + serializeJSON(doc, true, "", &buf, i.preserveFieldOrder) buf.WriteString("\n") r = append(r, buf.String()) } @@ -1304,13 +1314,14 @@ func buildObject(hide ast.ObjectFieldHide, fields map[string]value) *valueObject return makeValueSimpleObject(bindingFrame{}, fieldMap, fieldOrder, nil, nil) } -func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook) (*interpreter, error) { +func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*NativeFunction, maxStack int, ic *importCache, traceOut io.Writer, evalHook EvalHook, preserveFieldOrder bool) (*interpreter, error) { i := interpreter{ - stack: makeCallStack(maxStack), - importCache: ic, - traceOut: traceOut, - nativeFuncs: nativeFuncs, - evalHook: evalHook, + stack: makeCallStack(maxStack), + importCache: ic, + traceOut: traceOut, + nativeFuncs: nativeFuncs, + evalHook: evalHook, + preserveFieldOrder: preserveFieldOrder, } stdObj, err := buildStdObject(&i) diff --git a/testdata/builtinManifestJsonEx.golden b/testdata/builtinManifestJsonEx.golden index f04749c10..bfb0ea730 100644 --- a/testdata/builtinManifestJsonEx.golden +++ b/testdata/builtinManifestJsonEx.golden @@ -2,7 +2,7 @@ "array": "[\n \"bar\",\n \"bar\",\n 1,\n 1.42,\n -1,\n false,\n true,\n {\n \"cereal\": [\n \"<>& fizbuzz\"\n ],\n \"treats\": [\n {\n \"name\": \"chocolate\"\n }\n ]\n }\n]", "bool": "true", "null": "null", - "object": "{\n \"bam\": true,\n \"bar\": \"bar\",\n \"baz\": 1,\n \"bazel\": 1.42,\n \"bim\": false,\n \"blamo\": {\n \"cereal\": [\n \"<>& fizbuzz\"\n ],\n \"treats\": [\n {\n \"name\": \"chocolate\"\n }\n ]\n },\n \"boom\": -1,\n \"foo\": \"bar\"\n}", "number": "42", + "object": "{\n \"bam\": true,\n \"bar\": \"bar\",\n \"baz\": 1,\n \"bazel\": 1.42,\n \"bim\": false,\n \"blamo\": {\n \"cereal\": [\n \"<>& fizbuzz\"\n ],\n \"treats\": [\n {\n \"name\": \"chocolate\"\n }\n ]\n },\n \"boom\": -1,\n \"foo\": \"bar\"\n}", "string": "\"foo\"" } diff --git a/testdata/builtinObjectRemoveKey_hidden.golden b/testdata/builtinObjectRemoveKey_hidden.golden index a3e2e1beb..c329fbee3 100644 --- a/testdata/builtinObjectRemoveKey_hidden.golden +++ b/testdata/builtinObjectRemoveKey_hidden.golden @@ -1,12 +1,12 @@ { - "fields": [ - "baz" - ], "all_fields": [ "bar", "baz" ], "bar": 2, + "fields": [ + "baz" + ], "object": { "baz": 3 } diff --git a/testdata/builtin_manifestYamlDoc.golden b/testdata/builtin_manifestYamlDoc.golden index 76fdf1c7c..03ca87d1c 100644 --- a/testdata/builtin_manifestYamlDoc.golden +++ b/testdata/builtin_manifestYamlDoc.golden @@ -1,14 +1,14 @@ { "object": "\"abc\": \"def\"\n\"bam\": true\n\"bar\": \"baz\"\n\"baz\": 1\n\"bazel\": 1.42\n\"bim\": false\n\"blamo\":\n \"cereal\":\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n \"treats\":\n - \"name\": \"chocolate\"\n\"boom\": -1\n\"foo\": \"baz\"", "object2": "\"\\\"\": 4\n\"array\":\n- \"s\"\n- 1\n-\n - 2\n - 3\n- \"a\":\n - \"0\"\n - \"z\"\n \"r\": 6\n\"arraySection\":\n- \"q\": 1\n- \"w\": 2\n\"bool\": true\n\"emptyArray\": []\n\"emptyArraySection\":\n- {}\n\"emptySection\": {}\n\"escaped\\\"Section\":\n \"z\": \"q\"\n\"key\": \"value\"\n\"notBool\": false\n\"number\": 7\n\"section\":\n \"a\": 1\n \"array\":\n - \"c\": 3\n - \"d\": 4\n \"e$caped\":\n \"q\": \"t\"\n \"nested\":\n \"b\": 2\n \"nestedArray\":\n - \"k\": \"v\"\n \"nested\":\n \"e\": 5\n\"simple\":\n \"t\": 5", - "object3": "\"-0B1010_0111_0100_1010_1110\": \"string\\n with some\\n newlines\\n \"\n\"0X_0a_74_ae\": \"BARE_KEY\"\n\"1-234-567-8901\": null\n\"192.168.0.1\":\n- \"a\": 2\n \"b\": \"str\"\n- \"c\": []\n\"__-0B1010_0111_0100_1010_1110\": \"a new line\\n \"\n\"__-0X_0a_74_ae\": \"BARE_KEY\"\n\"b\":\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\n\"jsonnet.org/k8s-label-like\": \"0600\"\n\"just-letters-dashes\": \"+1101_1111\"\n\"just_letters_underscores\": 142321\n\"x\": \"BARE_KEY\"", - "object_indent": "\"abc\": \"def\"\n\"bam\": true\n\"bar\": \"baz\"\n\"baz\": 1\n\"bazel\": 1.42\n\"bim\": false\n\"blamo\":\n \"cereal\":\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n \"treats\":\n - \"name\": \"chocolate\"\n\"boom\": -1\n\"foo\": \"baz\"", "object2_indent": "\"\\\"\": 4\n\"array\":\n - \"s\"\n - 1\n -\n - 2\n - 3\n - \"a\":\n - \"0\"\n - \"z\"\n \"r\": 6\n\"arraySection\":\n - \"q\": 1\n - \"w\": 2\n\"bool\": true\n\"emptyArray\": []\n\"emptyArraySection\":\n - {}\n\"emptySection\": {}\n\"escaped\\\"Section\":\n \"z\": \"q\"\n\"key\": \"value\"\n\"notBool\": false\n\"number\": 7\n\"section\":\n \"a\": 1\n \"array\":\n - \"c\": 3\n - \"d\": 4\n \"e$caped\":\n \"q\": \"t\"\n \"nested\":\n \"b\": 2\n \"nestedArray\":\n - \"k\": \"v\"\n \"nested\":\n \"e\": 5\n\"simple\":\n \"t\": 5", - "object3_indent": "\"-0B1010_0111_0100_1010_1110\": \"string\\n with some\\n newlines\\n \"\n\"0X_0a_74_ae\": \"BARE_KEY\"\n\"1-234-567-8901\": null\n\"192.168.0.1\":\n - \"a\": 2\n \"b\": \"str\"\n - \"c\": []\n\"__-0B1010_0111_0100_1010_1110\": \"a new line\\n \"\n\"__-0X_0a_74_ae\": \"BARE_KEY\"\n\"b\":\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\n\"jsonnet.org/k8s-label-like\": \"0600\"\n\"just-letters-dashes\": \"+1101_1111\"\n\"just_letters_underscores\": 142321\n\"x\": \"BARE_KEY\"", - "object_unquoted": "abc: \"def\"\nbam: true\nbar: \"baz\"\nbaz: 1\nbazel: 1.42\nbim: false\nblamo:\n cereal:\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n treats:\n - name: \"chocolate\"\nboom: -1\nfoo: \"baz\"", + "object2_indent_unquoted": "\"\\\"\": 4\narray:\n - \"s\"\n - 1\n -\n - 2\n - 3\n - a:\n - \"0\"\n - \"z\"\n r: 6\narraySection:\n - q: 1\n - w: 2\nbool: true\nemptyArray: []\nemptyArraySection:\n - {}\nemptySection: {}\n\"escaped\\\"Section\":\n z: \"q\"\nkey: \"value\"\nnotBool: false\nnumber: 7\nsection:\n a: 1\n array:\n - c: 3\n - d: 4\n \"e$caped\":\n q: \"t\"\n nested:\n b: 2\n nestedArray:\n - k: \"v\"\n nested:\n e: 5\nsimple:\n t: 5", "object2_unquoted": "\"\\\"\": 4\narray:\n- \"s\"\n- 1\n-\n - 2\n - 3\n- a:\n - \"0\"\n - \"z\"\n r: 6\narraySection:\n- q: 1\n- w: 2\nbool: true\nemptyArray: []\nemptyArraySection:\n- {}\nemptySection: {}\n\"escaped\\\"Section\":\n z: \"q\"\nkey: \"value\"\nnotBool: false\nnumber: 7\nsection:\n a: 1\n array:\n - c: 3\n - d: 4\n \"e$caped\":\n q: \"t\"\n nested:\n b: 2\n nestedArray:\n - k: \"v\"\n nested:\n e: 5\nsimple:\n t: 5", + "object3": "\"-0B1010_0111_0100_1010_1110\": \"string\\n with some\\n newlines\\n \"\n\"0X_0a_74_ae\": \"BARE_KEY\"\n\"1-234-567-8901\": null\n\"192.168.0.1\":\n- \"a\": 2\n \"b\": \"str\"\n- \"c\": []\n\"__-0B1010_0111_0100_1010_1110\": \"a new line\\n \"\n\"__-0X_0a_74_ae\": \"BARE_KEY\"\n\"b\":\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\n\"jsonnet.org/k8s-label-like\": \"0600\"\n\"just-letters-dashes\": \"+1101_1111\"\n\"just_letters_underscores\": 142321\n\"x\": \"BARE_KEY\"", + "object3_indent": "\"-0B1010_0111_0100_1010_1110\": \"string\\n with some\\n newlines\\n \"\n\"0X_0a_74_ae\": \"BARE_KEY\"\n\"1-234-567-8901\": null\n\"192.168.0.1\":\n - \"a\": 2\n \"b\": \"str\"\n - \"c\": []\n\"__-0B1010_0111_0100_1010_1110\": \"a new line\\n \"\n\"__-0X_0a_74_ae\": \"BARE_KEY\"\n\"b\":\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\n\"jsonnet.org/k8s-label-like\": \"0600\"\n\"just-letters-dashes\": \"+1101_1111\"\n\"just_letters_underscores\": 142321\n\"x\": \"BARE_KEY\"", + "object3_indent_unquoted": "-0B1010_0111_0100_1010_1110: \"string\\n with some\\n newlines\\n \"\n0X_0a_74_ae: \"BARE_KEY\"\n1-234-567-8901: null\n192.168.0.1:\n - a: 2\n b: \"str\"\n - c: []\n__-0B1010_0111_0100_1010_1110: \"a new line\\n \"\n__-0X_0a_74_ae: \"BARE_KEY\"\nb:\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\njsonnet.org/k8s-label-like: \"0600\"\njust-letters-dashes: \"+1101_1111\"\njust_letters_underscores: 142321\nx: \"BARE_KEY\"", "object3_unquoted": "-0B1010_0111_0100_1010_1110: \"string\\n with some\\n newlines\\n \"\n0X_0a_74_ae: \"BARE_KEY\"\n1-234-567-8901: null\n192.168.0.1:\n- a: 2\n b: \"str\"\n- c: []\n__-0B1010_0111_0100_1010_1110: \"a new line\\n \"\n__-0X_0a_74_ae: \"BARE_KEY\"\nb:\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\njsonnet.org/k8s-label-like: \"0600\"\njust-letters-dashes: \"+1101_1111\"\njust_letters_underscores: 142321\nx: \"BARE_KEY\"", + "object_indent": "\"abc\": \"def\"\n\"bam\": true\n\"bar\": \"baz\"\n\"baz\": 1\n\"bazel\": 1.42\n\"bim\": false\n\"blamo\":\n \"cereal\":\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n \"treats\":\n - \"name\": \"chocolate\"\n\"boom\": -1\n\"foo\": \"baz\"", "object_indent_unquoted": "abc: \"def\"\nbam: true\nbar: \"baz\"\nbaz: 1\nbazel: 1.42\nbim: false\nblamo:\n cereal:\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n treats:\n - name: \"chocolate\"\nboom: -1\nfoo: \"baz\"", - "object2_indent_unquoted": "\"\\\"\": 4\narray:\n - \"s\"\n - 1\n -\n - 2\n - 3\n - a:\n - \"0\"\n - \"z\"\n r: 6\narraySection:\n - q: 1\n - w: 2\nbool: true\nemptyArray: []\nemptyArraySection:\n - {}\nemptySection: {}\n\"escaped\\\"Section\":\n z: \"q\"\nkey: \"value\"\nnotBool: false\nnumber: 7\nsection:\n a: 1\n array:\n - c: 3\n - d: 4\n \"e$caped\":\n q: \"t\"\n nested:\n b: 2\n nestedArray:\n - k: \"v\"\n nested:\n e: 5\nsimple:\n t: 5", - "object3_indent_unquoted": "-0B1010_0111_0100_1010_1110: \"string\\n with some\\n newlines\\n \"\n0X_0a_74_ae: \"BARE_KEY\"\n1-234-567-8901: null\n192.168.0.1:\n - a: 2\n b: \"str\"\n - c: []\n__-0B1010_0111_0100_1010_1110: \"a new line\\n \"\n__-0X_0a_74_ae: \"BARE_KEY\"\nb:\n \"N\": \"boolean false\"\n \"NO\": \"boolean false\"\n \"NULL\": \"null word capital\"\n \"Null\": \"null word\"\n \"OFF\": \"boolean false\"\n \"On\": \"boolean true\"\n \"True\": \"boolean true\"\n \"Yes\": \"boolean true\"\n \"n\": \"boolean false\"\n \"null\": \"null word\"\n \"off\": \"boolean false\"\n \"on\": \"boolean true\"\n \"true\": \"boolean true\"\n \"y\": \"boolean true\"\n \"yes\": \"boolean true\"\njsonnet.org/k8s-label-like: \"0600\"\njust-letters-dashes: \"+1101_1111\"\njust_letters_underscores: 142321\nx: \"BARE_KEY\"" + "object_unquoted": "abc: \"def\"\nbam: true\nbar: \"baz\"\nbaz: 1\nbazel: 1.42\nbim: false\nblamo:\n cereal:\n - \"<>& fizbuzz\"\n -\n - \"a\"\n -\n - \"b\"\n treats:\n - name: \"chocolate\"\nboom: -1\nfoo: \"baz\"" } diff --git a/testdata/comparisons.golden b/testdata/comparisons.golden index 877377d63..af9e88156 100644 --- a/testdata/comparisons.golden +++ b/testdata/comparisons.golden @@ -1,99 +1,103 @@ [ { - "a": 1, - "b": 2, "<": true, - ">": false, "<=": true, - ">=": false + ">": false, + ">=": false, + "a": 1, + "b": 2 }, { - "a": 2, - "b": 1, "<": false, - ">": true, "<=": false, - ">=": true + ">": true, + ">=": true, + "a": 2, + "b": 1 }, { + "<": true, + "<=": true, + ">": false, + ">=": false, "a": [ 1 ], "b": [ 2 - ], - "<": true, - ">": false, - "<=": true, - ">=": false + ] }, { + "<": false, + "<=": false, + ">": true, + ">=": true, "a": [ 2 ], "b": [ 1 - ], - "<": false, - ">": true, - "<=": false, - ">=": true + ] }, { - "a": [ ], - "b": [ ], "<": false, - ">": false, "<=": true, - ">=": true + ">": false, + ">=": true, + "a": [ ], + "b": [ ] }, { + "<": true, + "<=": true, + ">": false, + ">=": false, "a": [ ], "b": [ 1 - ], - "<": true, - ">": false, - "<=": true, - ">=": false + ] }, { + "<": false, + "<=": false, + ">": true, + ">=": true, "a": [ 1 ], - "b": [ ], - "<": false, - ">": true, - "<=": false, - ">=": true + "b": [ ] }, { + "<": false, + "<=": false, + ">": true, + ">=": true, "a": [ 1, 2 ], "b": [ 1 - ], - "<": false, - ">": true, - "<=": false, - ">=": true + ] }, { + "<": true, + "<=": true, + ">": false, + ">=": false, "a": [ 1, 2 ], "b": [ 2 - ], - "<": true, - ">": false, - "<=": true, - ">=": false + ] }, { + "<": true, + "<=": true, + ">": false, + ">=": false, "a": [ [ 1 @@ -103,13 +107,13 @@ [ 2 ] - ], - "<": true, - ">": false, - "<=": true, - ">=": false + ] }, { + "<": false, + "<=": false, + ">": true, + ">=": true, "a": [ [ 2 @@ -119,25 +123,25 @@ [ 1 ] - ], - "<": false, - ">": true, - "<=": false, - ">=": true + ] }, { + "<": false, + "<=": false, + ">": true, + ">=": true, "a": [ "foo" ], "b": [ "bar" - ], - "<": false, - ">": true, - "<=": false, - ">=": true + ] }, { + "<": true, + "<=": true, + ">": false, + ">=": false, "a": [ 0, "a" @@ -145,18 +149,14 @@ "b": [ 0, "b" - ], - "<": true, - ">": false, - "<=": true, - ">=": false + ] }, { - "a": "foo", - "b": "bar", "<": false, - ">": true, "<=": false, - ">=": true + ">": true, + ">=": true, + "a": "foo", + "b": "bar" } ] diff --git a/testdata/dollar_end.golden b/testdata/dollar_end.golden index a263850df..d2ebc46eb 100644 --- a/testdata/dollar_end.golden +++ b/testdata/dollar_end.golden @@ -1,4 +1,4 @@ { - "foo": 42, - "bar": -42 + "bar": -42, + "foo": 42 } diff --git a/testdata/dollar_end2.golden b/testdata/dollar_end2.golden index 510e5b419..860fdf070 100644 --- a/testdata/dollar_end2.golden +++ b/testdata/dollar_end2.golden @@ -1,4 +1,4 @@ { - "foo": false, - "bar": true + "bar": true, + "foo": false } diff --git a/testdata/escaped_fields.golden b/testdata/escaped_fields.golden index f30e15e34..bcb639367 100644 --- a/testdata/escaped_fields.golden +++ b/testdata/escaped_fields.golden @@ -1,5 +1,5 @@ { + "\n\n": null, "\"": null, - "'": null, - "\n\n": null + "'": null } diff --git a/testdata/multi.golden/foo.json b/testdata/multi.golden/foo.json index 429d9ec7f..7488d76ec 100644 --- a/testdata/multi.golden/foo.json +++ b/testdata/multi.golden/foo.json @@ -1,4 +1,4 @@ { - "baz": 3, - "baq": "27" + "baq": "27", + "baz": 3 } diff --git a/testdata/multi_no_newline.golden/foo.json b/testdata/multi_no_newline.golden/foo.json index 3005327a8..5ad59deab 100644 --- a/testdata/multi_no_newline.golden/foo.json +++ b/testdata/multi_no_newline.golden/foo.json @@ -1,4 +1,4 @@ { - "baz": 3, - "baq": "27" + "baq": "27", + "baz": 3 } \ No newline at end of file diff --git a/testdata/obj_local_right_level2.golden b/testdata/obj_local_right_level2.golden index 79f823f2c..350fead2d 100644 --- a/testdata/obj_local_right_level2.golden +++ b/testdata/obj_local_right_level2.golden @@ -1,4 +1,4 @@ { - "bar": "right", - "answer": "right" + "answer": "right", + "bar": "right" } diff --git a/testdata/obj_local_right_level3.golden b/testdata/obj_local_right_level3.golden index ba4d054f3..9e012040b 100644 --- a/testdata/obj_local_right_level3.golden +++ b/testdata/obj_local_right_level3.golden @@ -1,4 +1,4 @@ { - "bar": "wrong2", - "answer": "right" + "answer": "right", + "bar": "wrong2" } diff --git a/testdata/object_comp_super.golden b/testdata/object_comp_super.golden index b168d88a5..82b3604d7 100644 --- a/testdata/object_comp_super.golden +++ b/testdata/object_comp_super.golden @@ -1,4 +1,8 @@ { + "a": "42a", + "b": "42b", + "c": "42c", + "q": 42, "x": [ 1, "x" @@ -10,9 +14,5 @@ "z": [ 1, "z" - ], - "a": "42a", - "b": "42b", - "c": "42c", - "q": 42 + ] } diff --git a/testdata/object_local_self_super.golden b/testdata/object_local_self_super.golden index 20b4a6f6d..62f8836d5 100644 --- a/testdata/object_local_self_super.golden +++ b/testdata/object_local_self_super.golden @@ -1,6 +1,6 @@ { - "z": 42, - "foo": 42, "bar": 42, - "baz": "xxx" + "baz": "xxx", + "foo": 42, + "z": 42 } diff --git a/testdata/object_super_within.golden b/testdata/object_super_within.golden index 7e8d3bc99..54788472a 100644 --- a/testdata/object_super_within.golden +++ b/testdata/object_super_within.golden @@ -1,6 +1,6 @@ { "a": 2, - "d": 42, + "b": 1, "c": 1, - "b": 1 + "d": 42 } diff --git a/testdata/object_various_field_types.golden b/testdata/object_various_field_types.golden index f86d020f5..caf46b812 100644 --- a/testdata/object_various_field_types.golden +++ b/testdata/object_various_field_types.golden @@ -1,9 +1,9 @@ { + "expr, as complex as you want": true, "identifier": true, + "str_block \\n\n": true, "str_double ' \n": true, "str_single \" \n": true, - "str_block \\n\n": true, "str_verbatim_double \" '' \\n": true, - "str_verbatim_single \"\" ' \\n": true, - "expr, as complex as you want": true + "str_verbatim_single \"\" ' \\n": true } diff --git a/testdata/stdlib_smoke_test.golden b/testdata/stdlib_smoke_test.golden index 16199401a..700d7bfe5 100644 --- a/testdata/stdlib_smoke_test.golden +++ b/testdata/stdlib_smoke_test.golden @@ -1,142 +1,57 @@ { - "thisFile": "testdata/stdlib_smoke_test", - "type": "object", - "length": 0, - "objectHas": false, - "objectFields": [ ], - "objectValues": [ ], - "objectKeysValues": [ ], - "objectHasAll": false, - "objectFieldsAll": [ ], - "objectValuesAll": [ ], - "objectKeysValuesAll": [ ], - "prune": { - "y": [ - "42" - ] - }, - "mapWithKey": { - "a": 42 - }, - "get": [ - 17, - 42, - 18, - 42 - ], - "isArray": true, - "isBoolean": true, - "isFunction": true, - "isNumber": true, - "isObject": true, - "isString": true, "abs": 42, - "sign": 1, - "max": 3, - "min": 2, - "pow": 8, - "exp": 148.4131591025766, - "log": true, - "exponent": 3, - "mantissa": 0.625, - "floor": 5, - "ceil": 5, - "sqrt": true, - "sin": true, - "cos": true, - "tan": true, - "asin": true, "acos": true, - "atan": true, + "asciiLower": "blah", + "asciiUpper": "BLAH", + "asin": true, "assertEqual": true, - "toString": "42", - "codepoint": 65, - "char": "A", - "substr": "s", - "findSubstr": [ - 0, - 5 - ], - "startsWith": true, - "endsWith": true, - "stripChars": "bbbb", - "lstripChars": "bbbbcccc", - "rstripChars": "aaabbbb", - "split": [ - "a", - "b", - "c" - ], - "splitLimit": [ - "a", - "b,c" - ], - "splitLimitR": [ - "a,b", - "c" + "atan": true, + "base64": [ + "YmxhaA==", + "YmxhaA==" ], - "strReplace": "bba", - "asciiUpper": "BLAH", - "asciiLower": "blah", - "stringChars": [ - "b", - "l", - "a", - "h" + "base64Decode": "blah\n", + "base64DecodeBytes": [ + 98, + 108, + 97, + 104, + 10 ], - "format": "test blah 42", - "escapeStringBash": "'test '\"'\"'test'\"'\"'test'", - "escapeStringDollars": "test 'test'test", - "escapeStringJson": "\"test 'test'test\"", - "escapeStringPython": "\"test 'test'test\"", - "parseInt": 42, - "parseOctal": 83, - "parseHex": 3735928559, - "parseJson": { - "a": "b" - }, + "ceil": 5, + "char": "A", + "codepoint": 65, + "cos": true, + "count": 1, + "decodeUTF8": "AAA", "encodeUTF8": [ 98, 108, 97, 104 ], - "decodeUTF8": "AAA", - "manifestIni": "a = 1\nb = 2\n[s1]\nx = 1\ny = 2\n", - "manifestPython": "{\"a\": {\"b\": \"c\"}}", - "manifestPythonVars": "a = {\"b\": \"c\"}\n", - "manifestTomlEx": "\n\n[a]\n b = \"c\"", - "manifestJsonEx": "{\n \"a\": {\n \"b\": \"c\"\n }\n}", - "manifestJsonMinified": "{\"a\":{\"b\":\"c\"}}", - "manifestYamlDoc": "\"a\":\n \"b\": \"c\"", - "manifestYamlStream": "---\n42\n---\n\"a\":\n \"b\": \"c\"\n...\n", - "manifestXmlJsonml": "", - "makeArray": [ - 0, - 1, + "endsWith": true, + "escapeStringBash": "'test '\"'\"'test'\"'\"'test'", + "escapeStringDollars": "test 'test'test", + "escapeStringJson": "\"test 'test'test\"", + "escapeStringPython": "\"test 'test'test\"", + "exp": 148.4131591025766, + "exponent": 3, + "filter": [ 2, - 3, 4 ], - "count": 1, + "filterMap": [ + 4, + 8 + ], "find": [ 2, 4 ], - "member": true, - "map": [ - -1, - -2, - -3 - ], - "mapWithIndex": [ - 3, - 3, - 3 - ], - "filterMap": [ - 4, - 8 + "findSubstr": [ + 0, + 5 ], "flatMap": [ 2, @@ -146,10 +61,18 @@ 6, 9 ], - "filter": [ + "flattenArrays": [ + 1, 2, - 4 + 3, + 4, + 5, + [ + 6, + 7 + ] ], + "floor": 5, "foldl": [ 0, 1, @@ -162,33 +85,93 @@ 3, 4 ], - "repeat": "foofoofoo", - "slice": "o", - "range": [ + "format": "test blah 42", + "get": [ + 17, + 42, + 18, + 42 + ], + "isArray": true, + "isBoolean": true, + "isFunction": true, + "isNumber": true, + "isObject": true, + "isString": true, + "join": "a,b,c", + "length": 0, + "lines": "a\nb\nc\n", + "log": true, + "lstripChars": "bbbbcccc", + "makeArray": [ + 0, 1, 2, 3, - 4, - 5 + 4 ], - "join": "a,b,c", - "lines": "a\nb\nc\n", - "flattenArrays": [ + "manifestIni": "a = 1\nb = 2\n[s1]\nx = 1\ny = 2\n", + "manifestJsonEx": "{\n \"a\": {\n \"b\": \"c\"\n }\n}", + "manifestJsonMinified": "{\"a\":{\"b\":\"c\"}}", + "manifestPython": "{\"a\": {\"b\": \"c\"}}", + "manifestPythonVars": "a = {\"b\": \"c\"}\n", + "manifestTomlEx": "\n\n[a]\n b = \"c\"", + "manifestXmlJsonml": "", + "manifestYamlDoc": "\"a\":\n \"b\": \"c\"", + "manifestYamlStream": "---\n42\n---\n\"a\":\n \"b\": \"c\"\n...\n", + "mantissa": 0.625, + "map": [ + -1, + -2, + -3 + ], + "mapWithIndex": [ + 3, + 3, + 3 + ], + "mapWithKey": { + "a": 42 + }, + "max": 3, + "md5": "1bc29b36f623ba82aaf6724fd3b16718", + "member": true, + "mergePatch": { }, + "min": 2, + "objectFields": [ ], + "objectFieldsAll": [ ], + "objectHas": false, + "objectHasAll": false, + "objectKeysValues": [ ], + "objectKeysValuesAll": [ ], + "objectValues": [ ], + "objectValuesAll": [ ], + "parseHex": 3735928559, + "parseInt": 42, + "parseJson": { + "a": "b" + }, + "parseOctal": 83, + "pow": 8, + "prune": { + "y": [ + "42" + ] + }, + "range": [ 1, 2, 3, 4, - 5, - [ - 6, - 7 - ] + 5 ], + "repeat": "foofoofoo", "reverse": [ "a", "b" ], - "sort": [ + "rstripChars": "aaabbbb", + "set": [ [ 1, 2, @@ -200,28 +183,14 @@ 1 ] ], - "uniq": [ + "setDiff": [ [ 1, - 2, - 3 + 2 ], - [ - "a", - "B", - "a" - ] - ], - "set": [ [ 1, - 2, 3 - ], - [ - 3, - 2, - 1 ] ], "setInter": [ @@ -232,6 +201,10 @@ 2 ] ], + "setMember": [ + false, + true + ], "setUnion": [ [ 1, @@ -248,32 +221,59 @@ 5 ] ], - "setDiff": [ + "sign": 1, + "sin": true, + "slice": "o", + "sort": [ [ 1, - 2 + 2, + 3 ], [ - 1, - 3 + 3, + 2, + 1 ] ], - "setMember": [ - false, - true + "split": [ + "a", + "b", + "c" ], - "base64": [ - "YmxhaA==", - "YmxhaA==" + "splitLimit": [ + "a", + "b,c" ], - "base64DecodeBytes": [ - 98, - 108, - 97, - 104, - 10 + "splitLimitR": [ + "a,b", + "c" ], - "base64Decode": "blah\n", - "md5": "1bc29b36f623ba82aaf6724fd3b16718", - "mergePatch": { } + "sqrt": true, + "startsWith": true, + "strReplace": "bba", + "stringChars": [ + "b", + "l", + "a", + "h" + ], + "stripChars": "bbbb", + "substr": "s", + "tan": true, + "thisFile": "testdata/stdlib_smoke_test", + "toString": "42", + "type": "object", + "uniq": [ + [ + 1, + 2, + 3 + ], + [ + "a", + "B", + "a" + ] + ] } diff --git a/testdata/super_index_desugar.golden b/testdata/super_index_desugar.golden index 345fd8614..2e44b03e9 100644 --- a/testdata/super_index_desugar.golden +++ b/testdata/super_index_desugar.golden @@ -1,5 +1,5 @@ { "a": 1, - "s": 1, - "c": "a" + "c": "a", + "s": 1 } diff --git a/testdata/supersugar4.golden b/testdata/supersugar4.golden index a609d3c2f..54dc1b92a 100644 --- a/testdata/supersugar4.golden +++ b/testdata/supersugar4.golden @@ -1,4 +1,4 @@ { - "y": 43, - "x": true + "x": true, + "y": 43 } diff --git a/vm.go b/vm.go index 4a07b0682..db5d3b2b9 100644 --- a/vm.go +++ b/vm.go @@ -43,8 +43,9 @@ type VM struct { //nolint:govet nativeFuncs map[string]*NativeFunction importer Importer ErrorFormatter ErrorFormatter - StringOutput bool // expect to evaluate to a string, and output that string directly - OutputNewline bool // add a trailing newline (default true) + StringOutput bool // expect to evaluate to a string, and output that string directly + OutputNewline bool // add a trailing newline (default true) + PreserveFieldOrder bool // preserve field declaration order instead of sorting alphabetically importCache *importCache traceOut io.Writer EvalHook EvalHook @@ -181,7 +182,7 @@ const ( const version = "v0.22.0" func (vm *VM) buildConfiguredInterpreter() (*interpreter, error) { - return buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook) + return buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.PreserveFieldOrder) } // Evaluate evaluates a Jsonnet program given by an Abstract Syntax Tree From 719f63f4596ab4eaa48084dda7bf383572664d58 Mon Sep 17 00:00:00 2001 From: Ville Vainio Date: Thu, 2 Apr 2026 21:49:09 +0300 Subject: [PATCH 3/4] test: add tests for --preserve-field-order flag --- jsonnet_test.go | 25 +++++++++++++++++++ main_test.go | 21 +++++++++------- testdata/field_order.golden | 23 +++++++++++++++++ testdata/field_order.jsonnet | 10 ++++++++ testdata/field_order.linter.golden | 0 .../field_order_preserve_field_order.golden | 23 +++++++++++++++++ 6 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 testdata/field_order.golden create mode 100644 testdata/field_order.jsonnet create mode 100644 testdata/field_order.linter.golden create mode 100644 testdata/field_order_preserve_field_order.golden diff --git a/jsonnet_test.go b/jsonnet_test.go index 7c6820890..c07d748b7 100644 --- a/jsonnet_test.go +++ b/jsonnet_test.go @@ -251,6 +251,31 @@ func TestExtReset(t *testing.T) { } } +func TestPreserveFieldOrder(t *testing.T) { + input := `{ z: 1, a: 2, m: 3 }` + + vm := MakeVM() + sorted, err := vm.EvaluateAnonymousSnippet("test.jsonnet", input) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if strings.Contains(sorted, `"z": 1`) && strings.Index(sorted, `"a"`) < strings.Index(sorted, `"z"`) { + // "a" comes before "z" — correct alphabetical order + } else if strings.Index(sorted, `"z"`) < strings.Index(sorted, `"a"`) { + t.Errorf("expected alphabetical order by default, got: %s", sorted) + } + + vm2 := MakeVM() + vm2.PreserveFieldOrder = true + ordered, err := vm2.EvaluateAnonymousSnippet("test.jsonnet", input) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if strings.Index(ordered, `"z"`) > strings.Index(ordered, `"a"`) { + t.Errorf("expected declaration order with PreserveFieldOrder=true, got: %s", ordered) + } +} + func TestTLAReset(t *testing.T) { vm := MakeVM() vm.TLAVar("fooString", "bar") diff --git a/main_test.go b/main_test.go index b14f07268..a97ad96e7 100644 --- a/main_test.go +++ b/main_test.go @@ -104,13 +104,14 @@ var nativePanic = &NativeFunction{ } type jsonnetInput struct { - name string - input []byte - eKind evalKind - stringOutputMode bool - noNewline bool // not nice to have a negative flag, but it gives the more relevant default - extVars map[string]string - extCode map[string]string + name string + input []byte + eKind evalKind + stringOutputMode bool + noNewline bool // not nice to have a negative flag, but it gives the more relevant default + preserveFieldOrder bool + extVars map[string]string + extCode map[string]string } type jsonnetResult struct { @@ -136,6 +137,7 @@ func runInternalJsonnet(i jsonnetInput) jsonnetResult { vm.StringOutput = i.stringOutputMode vm.OutputNewline = !i.noNewline + vm.PreserveFieldOrder = i.preserveFieldOrder for name, value := range i.extVars { vm.ExtVar(name, value) } @@ -337,8 +339,9 @@ func runTest(t *testing.T, test *mainTest) { name: test.name, input: input, eKind: eKind, - stringOutputMode: strings.HasSuffix(test.golden, "_string_output.golden"), - noNewline: strings.Contains(test.golden, "_no_newline"), + stringOutputMode: strings.HasSuffix(test.golden, "_string_output.golden"), + noNewline: strings.Contains(test.golden, "_no_newline"), + preserveFieldOrder: strings.Contains(test.golden, "_preserve_field_order"), extVars: test.meta.extVars, extCode: test.meta.extCode, }) diff --git a/testdata/field_order.golden b/testdata/field_order.golden new file mode 100644 index 000000000..b74521eff --- /dev/null +++ b/testdata/field_order.golden @@ -0,0 +1,23 @@ +{ + "extended": { + "a": 2, + "b": 4, + "m": 3, + "z": 1 + }, + "nested": { + "a": { + "c": 40, + "q": 30 + }, + "z": { + "b": 20, + "y": 10 + } + }, + "plain": { + "a": 2, + "m": 3, + "z": 1 + } +} diff --git a/testdata/field_order.jsonnet b/testdata/field_order.jsonnet new file mode 100644 index 000000000..71cdd2627 --- /dev/null +++ b/testdata/field_order.jsonnet @@ -0,0 +1,10 @@ +// Fields are declared in z, a, m order; output should reflect that +// when --preserve-field-order is set, or be sorted alphabetically otherwise. +local obj = { z: 1, a: 2, m: 3 }; +local nested = { z: { y: 10, b: 20 }, a: { q: 30, c: 40 } }; +local extended = { z: 1, a: 2 } + { m: 3, b: 4 }; +{ + plain: obj, + nested: nested, + extended: extended, +} diff --git a/testdata/field_order.linter.golden b/testdata/field_order.linter.golden new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/field_order_preserve_field_order.golden b/testdata/field_order_preserve_field_order.golden new file mode 100644 index 000000000..5d370fd3b --- /dev/null +++ b/testdata/field_order_preserve_field_order.golden @@ -0,0 +1,23 @@ +{ + "plain": { + "z": 1, + "a": 2, + "m": 3 + }, + "nested": { + "z": { + "y": 10, + "b": 20 + }, + "a": { + "q": 30, + "c": 40 + } + }, + "extended": { + "z": 1, + "a": 2, + "m": 3, + "b": 4 + } +} From a0d14d88963e5e5192abe972eacaf44773fde205 Mon Sep 17 00:00:00 2001 From: Ville Vainio Date: Fri, 3 Apr 2026 17:27:48 +0300 Subject: [PATCH 4/4] chore: bump version to v0.22.1-fork.1 for fork releases --- vm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm.go b/vm.go index db5d3b2b9..539ed3f3c 100644 --- a/vm.go +++ b/vm.go @@ -179,7 +179,7 @@ const ( ) // version is the current gojsonnet's version -const version = "v0.22.0" +const version = "v0.22.1-fork.1" func (vm *VM) buildConfiguredInterpreter() (*interpreter, error) { return buildInterpreter(vm.ext, vm.nativeFuncs, vm.MaxStack, vm.importCache, vm.traceOut, vm.EvalHook, vm.PreserveFieldOrder)