mirror of https://github.com/databricks/cli.git
Add `libs/dyn/jsonsaver`
This package can be used to marshal a `dyn.Value` as JSON and retain the ordering of keys in a mapping. The output does not encode HTML characters as opposed to the default behavior of `json.Marshal`. Otherwise this is no different from using `json.Marshal` with `v.AsAny()`.
This commit is contained in:
parent
5a555de503
commit
4741593579
|
@ -0,0 +1,33 @@
|
||||||
|
package jsonsaver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The encoder type encapsulates a [json.Encoder] and its target buffer.
|
||||||
|
// Escaping of HTML characters in the output is disabled.
|
||||||
|
type encoder struct {
|
||||||
|
*json.Encoder
|
||||||
|
*bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncoder() encoder {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
enc := json.NewEncoder(&buf)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
return encoder{enc, &buf}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalNoEscape(v any) ([]byte, error) {
|
||||||
|
enc := newEncoder()
|
||||||
|
err := enc.Encode(v)
|
||||||
|
return enc.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalIndentNoEscape(v any, prefix, indent string) ([]byte, error) {
|
||||||
|
enc := newEncoder()
|
||||||
|
enc.SetIndent(prefix, indent)
|
||||||
|
err := enc.Encode(v)
|
||||||
|
return enc.Bytes(), err
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package jsonsaver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncoder_marshalNoEscape(t *testing.T) {
|
||||||
|
out, err := marshalNoEscape("1 < 2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Confirm the output.
|
||||||
|
assert.JSONEq(t, `"1 < 2"`, string(out))
|
||||||
|
|
||||||
|
// Confirm that HTML escaping is disabled.
|
||||||
|
assert.False(t, strings.Contains(string(out), "\\u003c"))
|
||||||
|
|
||||||
|
// Confirm that the encoder writes a trailing newline.
|
||||||
|
assert.True(t, strings.HasSuffix(string(out), "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoder_marshalIndentNoEscape(t *testing.T) {
|
||||||
|
out, err := marshalIndentNoEscape([]string{"1 < 2", "2 < 3"}, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Confirm the output.
|
||||||
|
assert.JSONEq(t, `["1 < 2", "2 < 3"]`, string(out))
|
||||||
|
|
||||||
|
// Confirm that HTML escaping is disabled.
|
||||||
|
assert.False(t, strings.Contains(string(out), "\\u003c"))
|
||||||
|
|
||||||
|
// Confirm that the encoder performs indenting and writes a trailing newline.
|
||||||
|
assert.True(t, strings.HasPrefix(string(out), "[\n"))
|
||||||
|
assert.True(t, strings.Contains(string(out), " \"1 < 2\",\n"))
|
||||||
|
assert.True(t, strings.HasSuffix(string(out), "]\n"))
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package jsonsaver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshal is a version of [json.Marshal] for [dyn.Value].
|
||||||
|
//
|
||||||
|
// The output does not escape HTML characters in strings.
|
||||||
|
func Marshal(v dyn.Value) ([]byte, error) {
|
||||||
|
return marshalNoEscape(wrap{v})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalIndent is a version of [json.MarshalIndent] for [dyn.Value].
|
||||||
|
//
|
||||||
|
// The output does not escape HTML characters in strings.
|
||||||
|
func MarshalIndent(v dyn.Value, prefix, indent string) ([]byte, error) {
|
||||||
|
return marshalIndentNoEscape(wrap{v}, prefix, indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper type for [dyn.Value] to expose the [json.Marshaler] interface.
|
||||||
|
type wrap struct {
|
||||||
|
v dyn.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the [json.Marshaler] interface for the [dyn.Value] wrapper type.
|
||||||
|
func (w wrap) MarshalJSON() ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := marshalValue(&buf, w.v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalValue recursively writes JSON for a [dyn.Value] to the buffer.
|
||||||
|
func marshalValue(buf *bytes.Buffer, v dyn.Value) error {
|
||||||
|
switch v.Kind() {
|
||||||
|
case dyn.KindString, dyn.KindBool, dyn.KindInt, dyn.KindFloat, dyn.KindTime, dyn.KindNil:
|
||||||
|
out, err := marshalNoEscape(v.AsAny())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The encoder writes a trailing newline, so we need to remove it
|
||||||
|
// to avoid adding extra newlines when embedding this JSON.
|
||||||
|
out = out[:len(out)-1]
|
||||||
|
buf.Write(out)
|
||||||
|
case dyn.KindMap:
|
||||||
|
buf.WriteByte('{')
|
||||||
|
for i, pair := range v.MustMap().Pairs() {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
// Marshal the key
|
||||||
|
if err := marshalValue(buf, pair.Key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf.WriteByte(':')
|
||||||
|
// Marshal the value
|
||||||
|
if err := marshalValue(buf, pair.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
case dyn.KindSequence:
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, item := range v.MustSequence() {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
if err := marshalValue(buf, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported kind: %d", v.Kind())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package jsonsaver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalString(t *testing.T) {
|
||||||
|
b, err := Marshal(dyn.V("string"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `"string"`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalBool(t *testing.T) {
|
||||||
|
b, err := Marshal(dyn.V(true))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `true`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalInt(t *testing.T) {
|
||||||
|
b, err := Marshal(dyn.V(42))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `42`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalFloat(t *testing.T) {
|
||||||
|
b, err := Marshal(dyn.V(42.1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `42.1`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalTime(t *testing.T) {
|
||||||
|
b, err := Marshal(dyn.V(dyn.MustTime("2021-01-01T00:00:00Z")))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `"2021-01-01T00:00:00Z"`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalMap(t *testing.T) {
|
||||||
|
m := dyn.NewMapping()
|
||||||
|
m.Set(dyn.V("key1"), dyn.V("value1"))
|
||||||
|
m.Set(dyn.V("key2"), dyn.V("value2"))
|
||||||
|
|
||||||
|
b, err := Marshal(dyn.V(m))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `{"key1":"value1","key2":"value2"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalSequence(t *testing.T) {
|
||||||
|
var s []dyn.Value
|
||||||
|
s = append(s, dyn.V("value1"))
|
||||||
|
s = append(s, dyn.V("value2"))
|
||||||
|
|
||||||
|
b, err := Marshal(dyn.V(s))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `["value1","value2"]`, string(b))
|
||||||
|
}
|
Loading…
Reference in New Issue