Skip to content

Commit

Permalink
Merge pull request #456 from tdakkota/fix/json-parse
Browse files Browse the repository at this point in the history
feat(parser): wrap errors with line and column
  • Loading branch information
ernado authored Jul 5, 2022
2 parents 34e48cd + 1e3fdc1 commit accc05b
Show file tree
Hide file tree
Showing 24 changed files with 779 additions and 521 deletions.
3 changes: 3 additions & 0 deletions cmd/ogen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func run() error {
if flag.NArg() == 0 || specPath == "" {
return errors.New("no spec provided")
}
specPath = filepath.Clean(specPath)

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

_, fileName := filepath.Split(specPath)
opts := gen.Options{
NoClient: *noClient,
NoServer: *noServer,
Expand All @@ -146,6 +148,7 @@ func run() error {
SkipUnimplemented: *skipUnimplemented,
InferSchemaType: *inferTypes,
AllowRemote: *allowRemote,
Filename: fileName,
Filters: gen.Filters{
PathRegex: filterPath,
Methods: filterMethods,
Expand Down
18 changes: 17 additions & 1 deletion gen/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ type unimplementedError interface {

var (
_ = []interface {
error
errors.Wrapper
errors.Formatter
fmt.Formatter
error
}{
(*ErrParseSpec)(nil),
(*ErrBuildRouter)(nil),
Expand Down Expand Up @@ -104,6 +105,11 @@ func (e *ErrParseSpec) FormatError(p errors.Printer) (next error) {
return e.err
}

// Format implements fmt.Formatter.
func (e *ErrParseSpec) Format(s fmt.State, verb rune) {
errors.FormatError(e, s, verb)
}

// Error implements error.
func (e *ErrParseSpec) Error() string {
return fmt.Sprintf("parse spec: %s", e.err)
Expand All @@ -125,6 +131,11 @@ func (e *ErrBuildRouter) FormatError(p errors.Printer) (next error) {
return e.err
}

// Format implements fmt.Formatter.
func (e *ErrBuildRouter) Format(s fmt.State, verb rune) {
errors.FormatError(e, s, verb)
}

// Error implements error.
func (e *ErrBuildRouter) Error() string {
return fmt.Sprintf("build router: %s", e.err)
Expand All @@ -146,6 +157,11 @@ func (e *ErrGoFormat) FormatError(p errors.Printer) (next error) {
return e.err
}

// Format implements fmt.Formatter.
func (e *ErrGoFormat) Format(s fmt.State, verb rune) {
errors.FormatError(e, s, verb)
}

// Error implements error.
func (e *ErrGoFormat) Error() string {
return fmt.Sprintf("goimports: %s", e.err)
Expand Down
5 changes: 5 additions & 0 deletions gen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type Options struct {
AllowRemote bool
// Remote is remote reference resolver options.
Remote RemoteOptions
// Filename is a name of the spec file.
//
// Used for error messages.
Filename string
// Filters contains filters to skip operations.
Filters Filters
// IgnoreNotImplemented contains ErrNotImplemented messages to ignore.
Expand Down Expand Up @@ -98,6 +102,7 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) {
}
api, err := parser.Parse(spec, parser.Settings{
External: external,
Filename: opts.Filename,
InferTypes: opts.InferSchemaType,
})
if err != nil {
Expand Down
7 changes: 5 additions & 2 deletions gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,15 @@ func TestGenerate(t *testing.T) {
func TestNegative(t *testing.T) {
walkTestdata(t, "_testdata/negative", func(t *testing.T, file string, data []byte) {
a := require.New(t)
_, name := path.Split(file)

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

_, err = gen.NewGenerator(spec, gen.Options{})
_, err = gen.NewGenerator(spec, gen.Options{
Filename: name,
})
a.Error(err)
t.Log(err.Error())
t.Logf("%+v", err)
})
}
91 changes: 91 additions & 0 deletions json/location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package json

import (
"bytes"
"fmt"
"strconv"

"github.com/go-json-experiment/json"
)

// LineColumn returns the line and column of the location.
//
// If offset is invalid, line and column are 0 and ok is false.
func LineColumn(offset int64, data []byte) (line, column int64, ok bool) {
if offset < 0 || int64(len(data)) <= offset {
return 0, 0, false
}

{
unread := data[offset:]
trimmed := bytes.TrimLeft(unread, "\x20\t\r\n,:")
if len(trimmed) != len(unread) {
// Skip leading whitespace, because decoder does not do it.
offset += int64(len(unread) - len(trimmed))
}
}

lines := data[:offset]
// Lines count from 1.
line = int64(bytes.Count(lines, []byte("\n"))) + 1
// Find offset from last newline.
lastNL := int64(bytes.LastIndexByte(lines, '\n'))
column = offset - lastNL
return line, column, true
}

// Location is a JSON value location.
type Location struct {
JSONPointer string `json:"-"`
Offset int64 `json:"-"`
Line, Column int64 `json:"-"`
}

// String implements fmt.Stringer.
func (l Location) String() string {
if l.Line == 0 {
return strconv.Quote(l.JSONPointer)
}
return fmt.Sprintf("%d:%d", l.Line, l.Column)
}

// Locatable is an interface for JSON value location store.
type Locatable interface {
setLocation(Location)
Location() (Location, bool)
}

// Locator stores the Location of a JSON value.
type Locator struct {
location Location
set bool
}

func (l *Locator) setLocation(loc Location) {
l.location = loc
l.set = true
}

// Location returns the location of the value if it is set.
func (l Locator) Location() (Location, bool) {
return l.location, l.set
}

// LocationUnmarshaler is json.Unmarshalers that sets the location.
func LocationUnmarshaler(data []byte) *json.Unmarshalers {
return json.UnmarshalFuncV2(func(opts json.UnmarshalOptions, d *json.Decoder, l Locatable) error {
if _, ok := l.(*Locator); ok {
return nil
}

offset := d.InputOffset()
line, column, _ := LineColumn(offset, data)
l.setLocation(Location{
JSONPointer: d.StackPointer(),
Offset: offset,
Line: line,
Column: column,
})
return json.SkipFunc
})
}
76 changes: 39 additions & 37 deletions jsonschema/raw_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,48 +36,50 @@ func (n *Num) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) err
if err != nil {
return err
}

*n = Num(val)
*n = append((*n)[:0], val...)
return nil
}

// RawSchema is unparsed JSON Schema.
type RawSchema struct {
Ref string `json:"$ref,omitzero"`
Summary string `json:"summary,omitzero"`
Description string `json:"description,omitzero"`
Type string `json:"type,omitzero"`
Format string `json:"format,omitzero"`
Properties RawProperties `json:"properties,omitzero"`
AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitzero"`
PatternProperties RawPatternProperties `json:"patternProperties,omitzero"`
Required []string `json:"required,omitzero"`
Items *RawSchema `json:"items,omitzero"`
Nullable bool `json:"nullable,omitzero"`
AllOf []*RawSchema `json:"allOf,omitzero"`
OneOf []*RawSchema `json:"oneOf,omitzero"`
AnyOf []*RawSchema `json:"anyOf,omitzero"`
Discriminator *Discriminator `json:"discriminator,omitzero"`
Enum Enum `json:"enum,omitzero"`
MultipleOf Num `json:"multipleOf,omitzero"`
Maximum Num `json:"maximum,omitzero"`
ExclusiveMaximum bool `json:"exclusiveMaximum,omitzero"`
Minimum Num `json:"minimum,omitzero"`
ExclusiveMinimum bool `json:"exclusiveMinimum,omitzero"`
MaxLength *uint64 `json:"maxLength,omitzero"`
MinLength *uint64 `json:"minLength,omitzero"`
Pattern string `json:"pattern,omitzero"`
MaxItems *uint64 `json:"maxItems,omitzero"`
MinItems *uint64 `json:"minItems,omitzero"`
UniqueItems bool `json:"uniqueItems,omitzero"`
MaxProperties *uint64 `json:"maxProperties,omitzero"`
MinProperties *uint64 `json:"minProperties,omitzero"`
Default json.RawValue `json:"default,omitzero"`
Example json.RawValue `json:"example,omitzero"`
Deprecated bool `json:"deprecated,omitzero"`
ContentEncoding string `json:"contentEncoding,omitzero"`
ContentMediaType string `json:"contentMediaType,omitzero"`
XAnnotations map[string]json.RawValue `json:",inline"`
Ref string `json:"$ref,omitzero"`
Summary string `json:"summary,omitzero"`
Description string `json:"description,omitzero"`
Type string `json:"type,omitzero"`
Format string `json:"format,omitzero"`
Properties RawProperties `json:"properties,omitzero"`
AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitzero"`
PatternProperties RawPatternProperties `json:"patternProperties,omitzero"`
Required []string `json:"required,omitzero"`
Items *RawSchema `json:"items,omitzero"`
Nullable bool `json:"nullable,omitzero"`
AllOf []*RawSchema `json:"allOf,omitzero"`
OneOf []*RawSchema `json:"oneOf,omitzero"`
AnyOf []*RawSchema `json:"anyOf,omitzero"`
Discriminator *Discriminator `json:"discriminator,omitzero"`
Enum Enum `json:"enum,omitzero"`
MultipleOf Num `json:"multipleOf,omitzero"`
Maximum Num `json:"maximum,omitzero"`
ExclusiveMaximum bool `json:"exclusiveMaximum,omitzero"`
Minimum Num `json:"minimum,omitzero"`
ExclusiveMinimum bool `json:"exclusiveMinimum,omitzero"`
MaxLength *uint64 `json:"maxLength,omitzero"`
MinLength *uint64 `json:"minLength,omitzero"`
Pattern string `json:"pattern,omitzero"`
MaxItems *uint64 `json:"maxItems,omitzero"`
MinItems *uint64 `json:"minItems,omitzero"`
UniqueItems bool `json:"uniqueItems,omitzero"`
MaxProperties *uint64 `json:"maxProperties,omitzero"`
MinProperties *uint64 `json:"minProperties,omitzero"`
Default json.RawValue `json:"default,omitzero"`
Example json.RawValue `json:"example,omitzero"`
Deprecated bool `json:"deprecated,omitzero"`
ContentEncoding string `json:"contentEncoding,omitzero"`
ContentMediaType string `json:"contentMediaType,omitzero"`

XAnnotations map[string]json.RawValue `json:",inline"`

ogenjson.Locator `json:"-"`
}

// Enum is JSON Schema enum validator description.
Expand Down
71 changes: 71 additions & 0 deletions openapi/parser/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package parser

import (
"fmt"

"github.com/go-faster/errors"

ogenjson "github.com/ogen-go/ogen/json"
)

var _ interface {
error
errors.Wrapper
errors.Formatter
} = (*LocationError)(nil)

// LocationError is a wrapper for an error that has a location.
type LocationError struct {
file string
loc ogenjson.Location
err error
}

// Unwrap implements errors.Wrapper.
func (e *LocationError) Unwrap() error {
return e.err
}

func (e *LocationError) fileName() string {
filename := e.file
if filename != "" {
switch {
case e.loc.Line != 0:
// Line is set, so return "${filename}:".
filename += ":"
case e.loc.JSONPointer != "":
// Line is not set, but JSONPointer is set, so return "${filename}#${JSONPointer}".
filename += "#"
default:
// Neither line nor JSONPointer is set, so return empty string.
return ""
}
}
return filename
}

// FormatError implements errors.Formatter.
func (e *LocationError) FormatError(p errors.Printer) (next error) {
p.Printf("at %s%s", e.fileName(), e.loc)
return e.err
}

// Error implements error.
func (e *LocationError) Error() string {
return fmt.Sprintf("at %s%s: %s", e.fileName(), e.loc, e.err)
}

func (p *parser) wrapLocation(l ogenjson.Locatable, err error) error {
if err == nil {
return nil
}
loc, ok := l.Location()
if !ok {
return err
}
return &LocationError{
file: p.filename,
loc: loc,
err: err,
}
}
5 changes: 4 additions & 1 deletion openapi/parser/parse_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/ogen-go/ogen/openapi"
)

func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Example, error) {
func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (_ *openapi.Example, rerr error) {
if e == nil {
return nil, nil
}
Expand All @@ -19,6 +19,9 @@ func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Exampl
}
return ex, nil
}
defer func() {
rerr = p.wrapLocation(e, rerr)
}()

return &openapi.Example{
Summary: e.Summary,
Expand Down
5 changes: 4 additions & 1 deletion openapi/parser/parse_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *resolveCtx)
return result, nil
}

func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (*openapi.Header, error) {
func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (_ *openapi.Header, rerr error) {
if header == nil {
return nil, errors.New("header object is empty or null")
}
Expand All @@ -34,6 +34,9 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
}
return parsed, nil
}
defer func() {
rerr = p.wrapLocation(header, rerr)
}()

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

0 comments on commit accc05b

Please sign in to comment.