Skip to content

Commit accc05b

Browse files
authored
Merge pull request #456 from tdakkota/fix/json-parse
feat(parser): wrap errors with line and column
2 parents 34e48cd + 1e3fdc1 commit accc05b

24 files changed

+779
-521
lines changed

cmd/ogen/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ func run() error {
114114
if flag.NArg() == 0 || specPath == "" {
115115
return errors.New("no spec provided")
116116
}
117+
specPath = filepath.Clean(specPath)
117118

118119
switch files, err := os.ReadDir(*targetDir); {
119120
case os.IsNotExist(err):
@@ -138,6 +139,7 @@ func run() error {
138139
_ = logger.Sync()
139140
}()
140141

142+
_, fileName := filepath.Split(specPath)
141143
opts := gen.Options{
142144
NoClient: *noClient,
143145
NoServer: *noServer,
@@ -146,6 +148,7 @@ func run() error {
146148
SkipUnimplemented: *skipUnimplemented,
147149
InferSchemaType: *inferTypes,
148150
AllowRemote: *allowRemote,
151+
Filename: fileName,
149152
Filters: gen.Filters{
150153
PathRegex: filterPath,
151154
Methods: filterMethods,

gen/errors.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ type unimplementedError interface {
1414

1515
var (
1616
_ = []interface {
17-
error
1817
errors.Wrapper
1918
errors.Formatter
19+
fmt.Formatter
20+
error
2021
}{
2122
(*ErrParseSpec)(nil),
2223
(*ErrBuildRouter)(nil),
@@ -104,6 +105,11 @@ func (e *ErrParseSpec) FormatError(p errors.Printer) (next error) {
104105
return e.err
105106
}
106107

108+
// Format implements fmt.Formatter.
109+
func (e *ErrParseSpec) Format(s fmt.State, verb rune) {
110+
errors.FormatError(e, s, verb)
111+
}
112+
107113
// Error implements error.
108114
func (e *ErrParseSpec) Error() string {
109115
return fmt.Sprintf("parse spec: %s", e.err)
@@ -125,6 +131,11 @@ func (e *ErrBuildRouter) FormatError(p errors.Printer) (next error) {
125131
return e.err
126132
}
127133

134+
// Format implements fmt.Formatter.
135+
func (e *ErrBuildRouter) Format(s fmt.State, verb rune) {
136+
errors.FormatError(e, s, verb)
137+
}
138+
128139
// Error implements error.
129140
func (e *ErrBuildRouter) Error() string {
130141
return fmt.Sprintf("build router: %s", e.err)
@@ -146,6 +157,11 @@ func (e *ErrGoFormat) FormatError(p errors.Printer) (next error) {
146157
return e.err
147158
}
148159

160+
// Format implements fmt.Formatter.
161+
func (e *ErrGoFormat) Format(s fmt.State, verb rune) {
162+
errors.FormatError(e, s, verb)
163+
}
164+
149165
// Error implements error.
150166
func (e *ErrGoFormat) Error() string {
151167
return fmt.Sprintf("goimports: %s", e.err)

gen/generator.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ type Options struct {
4949
AllowRemote bool
5050
// Remote is remote reference resolver options.
5151
Remote RemoteOptions
52+
// Filename is a name of the spec file.
53+
//
54+
// Used for error messages.
55+
Filename string
5256
// Filters contains filters to skip operations.
5357
Filters Filters
5458
// IgnoreNotImplemented contains ErrNotImplemented messages to ignore.
@@ -98,6 +102,7 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) {
98102
}
99103
api, err := parser.Parse(spec, parser.Settings{
100104
External: external,
105+
Filename: opts.Filename,
101106
InferTypes: opts.InferSchemaType,
102107
})
103108
if err != nil {

gen_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,15 @@ func TestGenerate(t *testing.T) {
119119
func TestNegative(t *testing.T) {
120120
walkTestdata(t, "_testdata/negative", func(t *testing.T, file string, data []byte) {
121121
a := require.New(t)
122+
_, name := path.Split(file)
122123

123124
spec, err := ogen.Parse(data)
124125
a.NoError(err)
125126

126-
_, err = gen.NewGenerator(spec, gen.Options{})
127+
_, err = gen.NewGenerator(spec, gen.Options{
128+
Filename: name,
129+
})
127130
a.Error(err)
128-
t.Log(err.Error())
131+
t.Logf("%+v", err)
129132
})
130133
}

json/location.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package json
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/go-json-experiment/json"
9+
)
10+
11+
// LineColumn returns the line and column of the location.
12+
//
13+
// If offset is invalid, line and column are 0 and ok is false.
14+
func LineColumn(offset int64, data []byte) (line, column int64, ok bool) {
15+
if offset < 0 || int64(len(data)) <= offset {
16+
return 0, 0, false
17+
}
18+
19+
{
20+
unread := data[offset:]
21+
trimmed := bytes.TrimLeft(unread, "\x20\t\r\n,:")
22+
if len(trimmed) != len(unread) {
23+
// Skip leading whitespace, because decoder does not do it.
24+
offset += int64(len(unread) - len(trimmed))
25+
}
26+
}
27+
28+
lines := data[:offset]
29+
// Lines count from 1.
30+
line = int64(bytes.Count(lines, []byte("\n"))) + 1
31+
// Find offset from last newline.
32+
lastNL := int64(bytes.LastIndexByte(lines, '\n'))
33+
column = offset - lastNL
34+
return line, column, true
35+
}
36+
37+
// Location is a JSON value location.
38+
type Location struct {
39+
JSONPointer string `json:"-"`
40+
Offset int64 `json:"-"`
41+
Line, Column int64 `json:"-"`
42+
}
43+
44+
// String implements fmt.Stringer.
45+
func (l Location) String() string {
46+
if l.Line == 0 {
47+
return strconv.Quote(l.JSONPointer)
48+
}
49+
return fmt.Sprintf("%d:%d", l.Line, l.Column)
50+
}
51+
52+
// Locatable is an interface for JSON value location store.
53+
type Locatable interface {
54+
setLocation(Location)
55+
Location() (Location, bool)
56+
}
57+
58+
// Locator stores the Location of a JSON value.
59+
type Locator struct {
60+
location Location
61+
set bool
62+
}
63+
64+
func (l *Locator) setLocation(loc Location) {
65+
l.location = loc
66+
l.set = true
67+
}
68+
69+
// Location returns the location of the value if it is set.
70+
func (l Locator) Location() (Location, bool) {
71+
return l.location, l.set
72+
}
73+
74+
// LocationUnmarshaler is json.Unmarshalers that sets the location.
75+
func LocationUnmarshaler(data []byte) *json.Unmarshalers {
76+
return json.UnmarshalFuncV2(func(opts json.UnmarshalOptions, d *json.Decoder, l Locatable) error {
77+
if _, ok := l.(*Locator); ok {
78+
return nil
79+
}
80+
81+
offset := d.InputOffset()
82+
line, column, _ := LineColumn(offset, data)
83+
l.setLocation(Location{
84+
JSONPointer: d.StackPointer(),
85+
Offset: offset,
86+
Line: line,
87+
Column: column,
88+
})
89+
return json.SkipFunc
90+
})
91+
}

jsonschema/raw_schema.go

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,48 +36,50 @@ func (n *Num) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) err
3636
if err != nil {
3737
return err
3838
}
39-
40-
*n = Num(val)
39+
*n = append((*n)[:0], val...)
4140
return nil
4241
}
4342

4443
// RawSchema is unparsed JSON Schema.
4544
type RawSchema struct {
46-
Ref string `json:"$ref,omitzero"`
47-
Summary string `json:"summary,omitzero"`
48-
Description string `json:"description,omitzero"`
49-
Type string `json:"type,omitzero"`
50-
Format string `json:"format,omitzero"`
51-
Properties RawProperties `json:"properties,omitzero"`
52-
AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitzero"`
53-
PatternProperties RawPatternProperties `json:"patternProperties,omitzero"`
54-
Required []string `json:"required,omitzero"`
55-
Items *RawSchema `json:"items,omitzero"`
56-
Nullable bool `json:"nullable,omitzero"`
57-
AllOf []*RawSchema `json:"allOf,omitzero"`
58-
OneOf []*RawSchema `json:"oneOf,omitzero"`
59-
AnyOf []*RawSchema `json:"anyOf,omitzero"`
60-
Discriminator *Discriminator `json:"discriminator,omitzero"`
61-
Enum Enum `json:"enum,omitzero"`
62-
MultipleOf Num `json:"multipleOf,omitzero"`
63-
Maximum Num `json:"maximum,omitzero"`
64-
ExclusiveMaximum bool `json:"exclusiveMaximum,omitzero"`
65-
Minimum Num `json:"minimum,omitzero"`
66-
ExclusiveMinimum bool `json:"exclusiveMinimum,omitzero"`
67-
MaxLength *uint64 `json:"maxLength,omitzero"`
68-
MinLength *uint64 `json:"minLength,omitzero"`
69-
Pattern string `json:"pattern,omitzero"`
70-
MaxItems *uint64 `json:"maxItems,omitzero"`
71-
MinItems *uint64 `json:"minItems,omitzero"`
72-
UniqueItems bool `json:"uniqueItems,omitzero"`
73-
MaxProperties *uint64 `json:"maxProperties,omitzero"`
74-
MinProperties *uint64 `json:"minProperties,omitzero"`
75-
Default json.RawValue `json:"default,omitzero"`
76-
Example json.RawValue `json:"example,omitzero"`
77-
Deprecated bool `json:"deprecated,omitzero"`
78-
ContentEncoding string `json:"contentEncoding,omitzero"`
79-
ContentMediaType string `json:"contentMediaType,omitzero"`
80-
XAnnotations map[string]json.RawValue `json:",inline"`
45+
Ref string `json:"$ref,omitzero"`
46+
Summary string `json:"summary,omitzero"`
47+
Description string `json:"description,omitzero"`
48+
Type string `json:"type,omitzero"`
49+
Format string `json:"format,omitzero"`
50+
Properties RawProperties `json:"properties,omitzero"`
51+
AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitzero"`
52+
PatternProperties RawPatternProperties `json:"patternProperties,omitzero"`
53+
Required []string `json:"required,omitzero"`
54+
Items *RawSchema `json:"items,omitzero"`
55+
Nullable bool `json:"nullable,omitzero"`
56+
AllOf []*RawSchema `json:"allOf,omitzero"`
57+
OneOf []*RawSchema `json:"oneOf,omitzero"`
58+
AnyOf []*RawSchema `json:"anyOf,omitzero"`
59+
Discriminator *Discriminator `json:"discriminator,omitzero"`
60+
Enum Enum `json:"enum,omitzero"`
61+
MultipleOf Num `json:"multipleOf,omitzero"`
62+
Maximum Num `json:"maximum,omitzero"`
63+
ExclusiveMaximum bool `json:"exclusiveMaximum,omitzero"`
64+
Minimum Num `json:"minimum,omitzero"`
65+
ExclusiveMinimum bool `json:"exclusiveMinimum,omitzero"`
66+
MaxLength *uint64 `json:"maxLength,omitzero"`
67+
MinLength *uint64 `json:"minLength,omitzero"`
68+
Pattern string `json:"pattern,omitzero"`
69+
MaxItems *uint64 `json:"maxItems,omitzero"`
70+
MinItems *uint64 `json:"minItems,omitzero"`
71+
UniqueItems bool `json:"uniqueItems,omitzero"`
72+
MaxProperties *uint64 `json:"maxProperties,omitzero"`
73+
MinProperties *uint64 `json:"minProperties,omitzero"`
74+
Default json.RawValue `json:"default,omitzero"`
75+
Example json.RawValue `json:"example,omitzero"`
76+
Deprecated bool `json:"deprecated,omitzero"`
77+
ContentEncoding string `json:"contentEncoding,omitzero"`
78+
ContentMediaType string `json:"contentMediaType,omitzero"`
79+
80+
XAnnotations map[string]json.RawValue `json:",inline"`
81+
82+
ogenjson.Locator `json:"-"`
8183
}
8284

8385
// Enum is JSON Schema enum validator description.

openapi/parser/errors.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package parser
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/go-faster/errors"
7+
8+
ogenjson "github.com/ogen-go/ogen/json"
9+
)
10+
11+
var _ interface {
12+
error
13+
errors.Wrapper
14+
errors.Formatter
15+
} = (*LocationError)(nil)
16+
17+
// LocationError is a wrapper for an error that has a location.
18+
type LocationError struct {
19+
file string
20+
loc ogenjson.Location
21+
err error
22+
}
23+
24+
// Unwrap implements errors.Wrapper.
25+
func (e *LocationError) Unwrap() error {
26+
return e.err
27+
}
28+
29+
func (e *LocationError) fileName() string {
30+
filename := e.file
31+
if filename != "" {
32+
switch {
33+
case e.loc.Line != 0:
34+
// Line is set, so return "${filename}:".
35+
filename += ":"
36+
case e.loc.JSONPointer != "":
37+
// Line is not set, but JSONPointer is set, so return "${filename}#${JSONPointer}".
38+
filename += "#"
39+
default:
40+
// Neither line nor JSONPointer is set, so return empty string.
41+
return ""
42+
}
43+
}
44+
return filename
45+
}
46+
47+
// FormatError implements errors.Formatter.
48+
func (e *LocationError) FormatError(p errors.Printer) (next error) {
49+
p.Printf("at %s%s", e.fileName(), e.loc)
50+
return e.err
51+
}
52+
53+
// Error implements error.
54+
func (e *LocationError) Error() string {
55+
return fmt.Sprintf("at %s%s: %s", e.fileName(), e.loc, e.err)
56+
}
57+
58+
func (p *parser) wrapLocation(l ogenjson.Locatable, err error) error {
59+
if err == nil {
60+
return nil
61+
}
62+
loc, ok := l.Location()
63+
if !ok {
64+
return err
65+
}
66+
return &LocationError{
67+
file: p.filename,
68+
loc: loc,
69+
err: err,
70+
}
71+
}

openapi/parser/parse_example.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/ogen-go/ogen/openapi"
88
)
99

10-
func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Example, error) {
10+
func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (_ *openapi.Example, rerr error) {
1111
if e == nil {
1212
return nil, nil
1313
}
@@ -19,6 +19,9 @@ func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Exampl
1919
}
2020
return ex, nil
2121
}
22+
defer func() {
23+
rerr = p.wrapLocation(e, rerr)
24+
}()
2225

2326
return &openapi.Example{
2427
Summary: e.Summary,

openapi/parser/parse_header.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *resolveCtx)
2323
return result, nil
2424
}
2525

26-
func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (*openapi.Header, error) {
26+
func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (_ *openapi.Header, rerr error) {
2727
if header == nil {
2828
return nil, errors.New("header object is empty or null")
2929
}
@@ -34,6 +34,9 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
3434
}
3535
return parsed, nil
3636
}
37+
defer func() {
38+
rerr = p.wrapLocation(header, rerr)
39+
}()
3740

3841
if header.In != "" {
3942
return nil, errors.Errorf(`"in" MUST NOT be specified, got %q`, header.In)

0 commit comments

Comments
 (0)