miller/pkg/mlrval/mlrmap_json.go
John Kerl 5b6a1d4713
JSONL output does not properly handle keys with quotes (#1425)
* mlr --l2j, --j2l

* make dev for previous commit

* fix #1424

* unit-test cases

* iterate
2023-11-11 18:58:49 -05:00

185 lines
5.2 KiB
Go

// ================================================================
// See mlrval_json.go for details. This is the unmarshal/marshal solely for Mlrmap.
// ================================================================
package mlrval
import (
"bytes"
"github.com/johnkerl/miller/pkg/colorizer"
"github.com/johnkerl/miller/pkg/lib"
)
// ----------------------------------------------------------------
func (mlrmap *Mlrmap) MarshalJSON(
jsonFormatting TJSONFormatting,
outputIsStdout bool,
) (string, error) {
var buffer bytes.Buffer
s, err := mlrmap.marshalJSONAux(jsonFormatting, 1, outputIsStdout)
if err != nil {
return "", err
}
buffer.WriteString(s)
// Do not write the final newline here, so the caller can write commas
// in the right place if desired.
// buffer.WriteString("\n")
return buffer.String(), nil
}
// For a map we only write from opening curly brace to closing curly brace. In
// nested-map contexts, mlrmap particular map might be written with a comma
// immediately after its closing curly brace, or a newline, and only the caller
// can know that.
//
// The element nesting depth is how deeply our element should be indented. Our
// closing curly brace is indented one less than that. For example, a
// root-level record '{"a":1,"b":2}' should be formatted as
//
// {
// "a": 1, <-- element nesting depth is 1 for root-level map
// "b": 2 <-- element nesting depth is 1 for root-level map
// } <-- closing curly brace nesting depth is 0 for root-level map
func (mlrmap *Mlrmap) marshalJSONAux(
jsonFormatting TJSONFormatting,
elementNestingDepth int,
outputIsStdout bool,
) (string, error) {
if jsonFormatting == JSON_MULTILINE {
return mlrmap.marshalJSONAuxMultiline(jsonFormatting, elementNestingDepth, outputIsStdout)
} else if jsonFormatting == JSON_SINGLE_LINE {
return mlrmap.marshalJSONAuxSingleLine(jsonFormatting, elementNestingDepth, outputIsStdout)
} else {
lib.InternalCodingErrorIf(true)
return "", nil // not reached
}
}
func (mlrmap *Mlrmap) marshalJSONAuxMultiline(
jsonFormatting TJSONFormatting,
elementNestingDepth int,
outputIsStdout bool,
) (string, error) {
var buffer bytes.Buffer
buffer.WriteString("{")
// Write empty map as '{}'. For anything else, opening curly brace in a
// line of its own, one key-value pair per line, closing curly brace on a
// line of its own.
if mlrmap.Head != nil {
buffer.WriteString("\n")
}
for pe := mlrmap.Head; pe != nil; pe = pe.Next {
// Write the key which is necessarily string-valued in Miller, and in
// JSON for that matter :)
for i := 0; i < elementNestingDepth; i++ {
buffer.WriteString(JSON_INDENT_STRING)
}
encoded := string(millerJSONEncodeString(pe.Key))
colorized := colorizer.MaybeColorizeKey(encoded, outputIsStdout)
buffer.WriteString(colorized)
buffer.WriteString(": ")
// Write the value which is a mlrval
valueString, err := pe.Value.marshalJSONAux(jsonFormatting, elementNestingDepth+1, outputIsStdout)
if err != nil {
return "", err
}
_, err = buffer.WriteString(valueString)
if err != nil {
return "", err
}
if pe.Next != nil {
buffer.WriteString(",")
}
buffer.WriteString("\n")
}
// Write empty map as '{}'.
if mlrmap.Head != nil {
for i := 0; i < elementNestingDepth-1; i++ {
buffer.WriteString(JSON_INDENT_STRING)
}
}
buffer.WriteString("}")
return buffer.String(), nil
}
func (mlrmap *Mlrmap) marshalJSONAuxSingleLine(
jsonFormatting TJSONFormatting,
elementNestingDepth int,
outputIsStdout bool,
) (string, error) {
var buffer bytes.Buffer
buffer.WriteString("{")
for pe := mlrmap.Head; pe != nil; pe = pe.Next {
// Write the key which is necessarily string-valued in Miller, and in
// JSON for that matter :)
encoded := string(millerJSONEncodeString(pe.Key))
buffer.WriteString(colorizer.MaybeColorizeKey(encoded, outputIsStdout))
buffer.WriteString(": ")
// Write the value which is a mlrval
valueString, err := pe.Value.marshalJSONAux(jsonFormatting, elementNestingDepth+1, outputIsStdout)
if err != nil {
return "", err
}
_, err = buffer.WriteString(valueString)
if err != nil {
return "", err
}
if pe.Next != nil {
buffer.WriteString(", ")
}
}
buffer.WriteString("}")
return buffer.String(), nil
}
// ----------------------------------------------------------------
// JSON-stringifies a single field of a record
func (entry *MlrmapEntry) JSONStringifyInPlace(
jsonFormatting TJSONFormatting,
) {
outputBytes, err := entry.Value.MarshalJSON(jsonFormatting, false)
if err != nil {
entry.Value = FromError(err)
} else {
entry.Value = FromString(string(outputBytes))
}
}
// ----------------------------------------------------------------
// JSON-parses a single field of a record
func (entry *MlrmapEntry) JSONParseInPlace() {
input := entry.Value.String()
err := entry.Value.UnmarshalJSON([]byte(input))
if err != nil {
entry.Value = FromError(err)
}
}
func (entry *MlrmapEntry) JSONTryParseInPlace() {
input := entry.Value.String()
pmv, err := TryUnmarshalJSON([]byte(input))
if err == nil {
entry.Value = pmv
}
}
// StringifyValuesRecursively is nominally for the `--jvquoteall` flag.
func (mlrmap *Mlrmap) StringifyValuesRecursively() {
for pe := mlrmap.Head; pe != nil; pe = pe.Next {
pe.Value.StringifyValuesRecursively()
}
}