Skip to content
Merged
19 changes: 17 additions & 2 deletions libs/dyn/convert/to_typed.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,10 @@ func toTypedBool(dst reflect.Value, src dyn.Value) error {
case dyn.KindString:
// See https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/decode.go#L684-L693.
switch src.MustString() {
case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON":
case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON", "true":
dst.SetBool(true)
return nil
case "n", "N", "no", "No", "NO", "off", "Off", "OFF":
case "n", "N", "no", "No", "NO", "off", "Off", "OFF", "false":
Comment thread
andrewnester marked this conversation as resolved.
dst.SetBool(false)
return nil
}
Expand All @@ -246,6 +246,17 @@ func toTypedInt(dst reflect.Value, src dyn.Value) error {
case dyn.KindInt:
dst.SetInt(src.MustInt())
return nil
case dyn.KindFloat:
v := src.MustFloat()
if canConvertToInt(v) {
dst.SetInt(int64(src.MustFloat()))
Comment thread
andrewnester marked this conversation as resolved.
return nil
}

return TypeError{
value: src,
msg: fmt.Sprintf("expected an int, found a %s", src.Kind()),
}
Comment thread
andrewnester marked this conversation as resolved.
case dyn.KindString:
if i64, err := strconv.ParseInt(src.MustString(), 10, 64); err == nil {
dst.SetInt(i64)
Expand All @@ -264,6 +275,10 @@ func toTypedInt(dst reflect.Value, src dyn.Value) error {
}
}

func canConvertToInt(v float64) bool {
return v == float64(int(v))
Comment thread
andrewnester marked this conversation as resolved.
Outdated
}

Comment thread
andrewnester marked this conversation as resolved.
Outdated
func toTypedFloat(dst reflect.Value, src dyn.Value) error {
switch src.Kind() {
case dyn.KindFloat:
Expand Down
90 changes: 90 additions & 0 deletions libs/dyn/jsonloader/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package jsonloader

import (
"bytes"
"encoding/json"
"fmt"
"io"

"github.com/databricks/cli/libs/dyn"
)

func LoadJSON(data []byte) (dyn.Value, error) {
offsets := BuildLineOffsets(data)
reader := bytes.NewReader(data)
decoder := json.NewDecoder(reader)

// Start decoding from the top-level value
value, err := decodeValue(decoder, offsets)
if err != nil {
if err == io.EOF {
err = fmt.Errorf("unexpected end of JSON input")
}
return dyn.InvalidValue, err
}
return value, nil
}

func decodeValue(decoder *json.Decoder, offsets []LineOffset) (dyn.Value, error) {
// Read the next JSON token
token, err := decoder.Token()
if err != nil {
return dyn.InvalidValue, err
}

// Get the current byte offset
offset := decoder.InputOffset()
location := GetPosition(offset, offsets)

switch tok := token.(type) {
case json.Delim:
if tok == '{' {
Comment thread
andrewnester marked this conversation as resolved.
// Decode JSON object
obj := make(map[string]dyn.Value)
for decoder.More() {
// Decode the key
keyToken, err := decoder.Token()
if err != nil {
return dyn.InvalidValue, err
}
key, ok := keyToken.(string)
if !ok {
return dyn.InvalidValue, fmt.Errorf("expected string for object key")
}

// Decode the value recursively
val, err := decodeValue(decoder, offsets)
if err != nil {
return dyn.InvalidValue, err
}

obj[key] = val
}
// Consume the closing '}'
if _, err := decoder.Token(); err != nil {
return dyn.InvalidValue, err
}
return dyn.NewValue(obj, []dyn.Location{location}), nil
} else if tok == '[' {
Comment thread
andrewnester marked this conversation as resolved.
// Decode JSON array
var arr []dyn.Value
for decoder.More() {
val, err := decodeValue(decoder, offsets)
if err != nil {
return dyn.InvalidValue, err
}
arr = append(arr, val)
}
// Consume the closing ']'
if _, err := decoder.Token(); err != nil {
return dyn.InvalidValue, err
}
return dyn.NewValue(arr, []dyn.Location{location}), nil
}
default:
// Primitive types: string, number, bool, or null
return dyn.NewValue(tok, []dyn.Location{location}), nil
}

return dyn.InvalidValue, fmt.Errorf("unexpected token: %v", token)
}
53 changes: 53 additions & 0 deletions libs/dyn/jsonloader/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package jsonloader

import (
"testing"

"github.com/databricks/cli/libs/dyn/convert"
"github.com/databricks/databricks-sdk-go/service/jobs"
"github.com/stretchr/testify/require"
)

const jsonData = `
{
"job_id": 123,
"new_settings": {
"name": "xxx",
"email_notifications": {
"on_start": [],
"on_success": [],
"on_failure": []
},
"webhook_notifications": {
"on_start": [],
"on_failure": []
},
"notification_settings": {
"no_alert_for_skipped_runs": true,
"no_alert_for_canceled_runs": true
},
"timeout_seconds": 0,
"max_concurrent_runs": 1,
"tasks": [
{
"task_key": "xxx",
"email_notifications": {},
"notification_settings": {},
"timeout_seconds": 0,
"max_retries": 0,
"min_retry_interval_millis": 0,
"retry_on_timeout": "true"
}
]
}
}
`

func TestJsonLoader(t *testing.T) {
v, err := LoadJSON([]byte(jsonData))
require.NoError(t, err)

var r jobs.ResetJob
err = convert.ToTyped(&r, v)
require.NoError(t, err)
}
Comment thread
andrewnester marked this conversation as resolved.
44 changes: 44 additions & 0 deletions libs/dyn/jsonloader/locations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package jsonloader

import (
"sort"

"github.com/databricks/cli/libs/dyn"
)

type LineOffset struct {
Line int
Start int64
}

// buildLineOffsets scans the input data and records the starting byte offset of each line.
func BuildLineOffsets(data []byte) []LineOffset {
Comment thread
andrewnester marked this conversation as resolved.
Outdated
offsets := []LineOffset{{Line: 1, Start: 0}}
line := 1
for i, b := range data {
if b == '\n' {
line++
offsets = append(offsets, LineOffset{Line: line, Start: int64(i + 1)})
}
}
return offsets
}

// GetPosition maps a byte offset to its corresponding line and column numbers.
func GetPosition(offset int64, offsets []LineOffset) dyn.Location {
// Binary search to find the line
idx := sort.Search(len(offsets), func(i int) bool {
return offsets[i].Start > offset
}) - 1

if idx < 0 {
idx = 0
}

lineOffset := offsets[idx]
return dyn.Location{
File: "(inline)",
Comment thread
andrewnester marked this conversation as resolved.
Outdated
Line: lineOffset.Line,
Column: int(offset-lineOffset.Start) + 1,
}
}
33 changes: 32 additions & 1 deletion libs/flags/json_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"encoding/json"
"fmt"
"os"

"github.com/databricks/cli/libs/dyn/convert"
"github.com/databricks/cli/libs/dyn/jsonloader"
"github.com/databricks/databricks-sdk-go/marshal"
)

type JsonFlag struct {
Expand Down Expand Up @@ -33,7 +37,34 @@ func (j *JsonFlag) Unmarshal(v any) error {
if j.raw == nil {
return nil
}
return json.Unmarshal(j.raw, v)

dv, err := jsonloader.LoadJSON(j.raw)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we do the same for the YAML flag?

I believe that's the only reason we still depend on github.com/ghodss/yaml. This package uses the json tags in the types as opposed to yaml tags, which the upstream YAML package uses. Since we use the json tags as well in the dyn package it should be possible to replace it.

Not blocking for this PR, of course.

if err != nil {
return err
}

// First normalize the input data.
// It will convert all the values to the correct types.
// For example string lterals for booleans and integers will be converted to the correct types.
Comment thread
andrewnester marked this conversation as resolved.
Outdated
nv, diags := convert.Normalize(v, dv)
if len(diags) > 0 {
summary := ""
for _, diag := range diags {
summary += fmt.Sprintf("- %s\n", diag.Summary)
}
return fmt.Errorf("json input error:\n%v", summary)
Comment thread
andrewnester marked this conversation as resolved.
Outdated
}
Comment thread
andrewnester marked this conversation as resolved.

// Then marshal the normalized data to the output.
// It will serialize all set data with the correct types.
data, err := json.Marshal(nv.AsAny())
if err != nil {
return err
}

// Finally unmarshal the normalized data to the output.
// It will fill in the ForceSendFields field if the struct contains it.
return marshal.Unmarshal(data, v)
}

func (j *JsonFlag) Type() string {
Expand Down
Loading