Repository: ogen-go/ogen
Pull request #417 (2022-06-22 15:41:18)

feat(ogen): add line and column to parse error


Patch:
diff --git a/parse.go b/parse.go
index 093c2a41..dd313ad0 100644
--- a/parse.go
+++ b/parse.go
@@ -9,22 +9,16 @@ import (
 )
 
 // Parse parses JSON/YAML into OpenAPI Spec.
-func Parse(data []byte) (*Spec, error) {
+func Parse(data []byte) (s *Spec, err error) {
+	s = &Spec{}
 	if !jx.Valid(data) {
-		d, err := yaml.YAMLToJSON(data)
-		if err != nil {
-			return nil, err
+		if err := yaml.Unmarshal(data, s); err != nil {
+			return nil, errors.Wrap(err, "yaml")
+		}
+	} else {
+		if err := json.Unmarshal(data, s); err != nil {
+			return nil, errors.Wrap(wrapLineOffset(data, err), "json")
 		}
-		data = d
-	}
-	if len(data) == 0 {
-		return nil, errors.New("blank data")
-	}
-
-	s := &Spec{}
-	if err := json.Unmarshal(data, s); err != nil {
-		return nil, err
 	}
-
 	return s, nil
 }
diff --git a/parse_error.go b/parse_error.go
new file mode 100644
index 00000000..c568df7f
--- /dev/null
+++ b/parse_error.go
@@ -0,0 +1,27 @@
+package ogen
+
+import (
+	"bytes"
+	"encoding/json"
+
+	"github.com/go-faster/errors"
+)
+
+func wrapLineOffset(data []byte, err error) error {
+	loc, ok := errors.Into[*json.UnmarshalTypeError](err)
+	if !ok {
+		return err
+	}
+
+	if loc.Offset < 0 || int64(len(data)) <= loc.Offset {
+		return err
+	}
+
+	lines := data[:loc.Offset]
+	// Lines count from 1.
+	line := bytes.Count(lines, []byte("\n")) + 1
+	lastNL := int64(bytes.LastIndexByte(lines, '\n'))
+	column := loc.Offset - lastNL
+
+	return errors.Wrapf(err, "line %d:%d", line, column)
+}


Test patch:
diff --git a/parse_error_test.go b/parse_error_test.go
new file mode 100644
index 00000000..2c84a49a
--- /dev/null
+++ b/parse_error_test.go
@@ -0,0 +1,48 @@
+package ogen
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_wrapLineOffset(t *testing.T) {
+	tests := []struct {
+		input        string
+		target       func([]byte) error
+		line, offset int
+	}{
+		{"{\n\t\"a\":1,\n\t\"b\":2\n}", func(input []byte) error {
+			var target struct {
+				A int  `json:"a"`
+				B bool `json:"b"`
+			}
+			return json.Unmarshal(input, &target)
+		}, 3, 7},
+
+		{"[\n0,\ntrue\n]", func(input []byte) error {
+			var target []int
+			return json.Unmarshal(input, &target)
+		}, 3, 5},
+	}
+	for i, tt := range tests {
+		tt := tt
+		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+			input := []byte(tt.input)
+
+			a := require.New(t)
+			err := tt.target(input)
+			a.Error(err)
+
+			err = wrapLineOffset(input, err)
+			a.Error(err)
+
+			msg := err.Error()
+			prefix := fmt.Sprintf("line %d:%d", tt.line, tt.offset)
+			a.Truef(strings.HasPrefix(msg, prefix), "prefix: %s, msg: %s", prefix, msg)
+		})
+	}
+}
diff --git a/parse_test.go b/parse_test.go
new file mode 100644
index 00000000..209743cc
--- /dev/null
+++ b/parse_test.go
@@ -0,0 +1,39 @@
+package ogen_test
+
+import (
+	"io/fs"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen"
+)
+
+func TestParse(t *testing.T) {
+	testDataPath := "_testdata/positive"
+	if err := fs.WalkDir(testdata, testDataPath, func(path string, d fs.DirEntry, err error) error {
+		if err != nil || d == nil || d.IsDir() {
+			return err
+		}
+		file := strings.TrimPrefix(path, testDataPath+"/")
+
+		testName := file
+		testName = strings.TrimSuffix(testName, ".json")
+		testName = strings.TrimSuffix(testName, ".yml")
+		testName = strings.TrimSuffix(testName, ".yaml")
+
+		t.Run(testName, func(t *testing.T) {
+			a := require.New(t)
+
+			data, err := testdata.ReadFile(path)
+			a.NoError(err)
+
+			_, err = ogen.Parse(data)
+			a.NoError(err)
+		})
+		return nil
+	}); err != nil {
+		t.Fatal(err)
+	}
+}