Skip to content

Commit

Permalink
track location correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewnester committed Oct 3, 2024
1 parent 58ab2f2 commit 0aca374
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 105 deletions.
81 changes: 75 additions & 6 deletions libs/dyn/jsonloader/json.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,90 @@
package jsonloader

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

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

func LoadJSON(data []byte) (dyn.Value, error) {
var root map[string]interface{}
err := json.Unmarshal(data, &root)
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
}

loc := dyn.Location{
Line: 1,
Column: 1,
// Get the current byte offset
offset := decoder.InputOffset()
location := GetPosition(offset, offsets)

switch tok := token.(type) {
case json.Delim:
if tok == '{' {
// 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 == '[' {
// 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 newLoader().load(&root, loc)

return dyn.InvalidValue, fmt.Errorf("unexpected token: %v", token)
}
99 changes: 0 additions & 99 deletions libs/dyn/jsonloader/loader.go

This file was deleted.

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 {
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)",
Line: lineOffset.Line,
Column: int(offset-lineOffset.Start) + 1,
}
}
42 changes: 42 additions & 0 deletions libs/flags/json_flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,45 @@ func TestJsonUnmarshalRequestMismatch(t *testing.T) {
require.ErrorContains(t, err, `json input error:
- unknown field: settings`)
}

const wrontTypeJsonData = `
{
"job_id": 123,
"new_settings": {
"name": "new job",
"email_notifications": {
"on_start": [],
"on_success": [],
"on_failure": []
},
"notification_settings": {
"no_alert_for_skipped_runs": true,
"no_alert_for_canceled_runs": true
},
"timeout_seconds": "wrong_type",
"max_concurrent_runs": {},
"tasks": [
{
"task_key": "new task",
"email_notifications": {},
"notification_settings": {},
"timeout_seconds": 0,
"max_retries": 0,
"min_retry_interval_millis": 0,
"retry_on_timeout": "true"
}
]
}
}
`

func TestJsonUnmarshalWrongTypeReportsCorrectLocation(t *testing.T) {
var body JsonFlag

var r jobs.ResetJob
err := body.Set(wrontTypeJsonData)
require.NoError(t, err)

err = body.Unmarshal(&r)
require.ErrorContains(t, err, `(inline):15:40: expected an int, found a string`)
}

0 comments on commit 0aca374

Please sign in to comment.