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