Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

package loads

import (
"errors"
"fmt"
)

type loaderError string

func (e loaderError) Error() string {
Expand All @@ -11,7 +16,7 @@ func (e loaderError) Error() string {

const (
// ErrLoads is an error returned by the loads package.
ErrLoads loaderError = "loaderrs error"
ErrLoads loaderError = "cannot load spec"

// ErrNoLoader indicates that no configured loader matched the input.
ErrNoLoader loaderError = "no loader matched"
Expand All @@ -20,3 +25,17 @@ const (
// to a non-public address (loopback, private, link-local, or unspecified).
ErrForbiddenAddress loaderError = "blocked dial to a non-public address"
)

// errLoads marks err as an error from this package, so callers may test it with
// [errors.Is] against [ErrLoads].
//
// The cause is reported on a single line, after the sentinel. An error that already
// carries the sentinel is returned unchanged: loaders are chained, and reporting
// "cannot load spec" once per link in the chain would tell the caller nothing.
func errLoads(err error) error {
if err == nil || errors.Is(err, ErrLoads) {
return err
}

return fmt.Errorf("%w: %w", ErrLoads, err)
}
7 changes: 3 additions & 4 deletions loaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package loads

import (
"encoding/json"
"errors"
"net/url"
"slices"

Expand Down Expand Up @@ -162,7 +161,7 @@ func (l *loader) WithNext(next *loader) *loader {
func (l *loader) Load(path string) (json.RawMessage, error) {
_, erp := url.Parse(path)
if erp != nil {
return nil, errors.Join(erp, ErrLoads)
return nil, errLoads(erp)
}

var lastErr error = ErrNoLoader // default error if no match was found
Expand All @@ -180,7 +179,7 @@ func (l *loader) Load(path string) (json.RawMessage, error) {
lastErr = err
}

return nil, errors.Join(lastErr, ErrLoads)
return nil, errLoads(lastErr)
}

func (l *loader) clone() *loader {
Expand All @@ -202,7 +201,7 @@ func (l *loader) clone() *loader {
func JSONDoc(path string, opts ...loading.Option) (json.RawMessage, error) {
data, err := loading.LoadFromFileOrHTTP(path, opts...)
if err != nil {
return nil, errors.Join(err, ErrLoads)
return nil, errLoads(err)
}
return json.RawMessage(data), nil
}
Expand Down
26 changes: 26 additions & 0 deletions loaders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package loads
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -99,6 +101,30 @@ func TestLoaderChain(t *testing.T) {
require.ErrorIs(t, err, ErrLoads)
require.ErrorIs(t, err, errBoom)
})

t.Run("should report ErrLoads only once", func(t *testing.T) {
// a loader that already marked its error should not have the sentinel added again
chain := LoaderChain(NewDocLoaderWithMatch(func(string, ...loading.Option) (json.RawMessage, error) {
return nil, fmt.Errorf("%w: %w", ErrLoads, errBoom)
}, nil))

_, err := chain("x.json")
require.ErrorIs(t, err, ErrLoads)
require.ErrorIs(t, err, errBoom)
require.Equal(t, 1, strings.Count(err.Error(), ErrLoads.Error()))
})
}

func TestSpecNotFound(t *testing.T) {
// the sentinel is reported once, on a single line, with the cause of the failure
_, err := Spec(filepath.Join(t.TempDir(), "nowhere.json"))
require.Error(t, err)
require.ErrorIs(t, err, ErrLoads)
require.ErrorIs(t, err, fs.ErrNotExist)

message := err.Error()
require.Equal(t, 1, strings.Count(message, ErrLoads.Error()))
require.Equal(t, 0, strings.Count(message, "\n"))
}

func TestLoaderWithOptions(t *testing.T) {
Expand Down
11 changes: 5 additions & 6 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"bytes"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"maps"

Expand Down Expand Up @@ -110,7 +109,7 @@ func Analyzed(data json.RawMessage, version string, options ...LoaderOption) (*D
version = "2.0"
}
if version != "2.0" {
return nil, fmt.Errorf("spec version %q is not supported: %w", version, ErrLoads)
return nil, fmt.Errorf("%w: spec version %q is not supported", ErrLoads, version)
}

raw, err := trimData(data) // trim blanks, then convert yaml docs into json
Expand All @@ -120,12 +119,12 @@ func Analyzed(data json.RawMessage, version string, options ...LoaderOption) (*D

swspec := new(spec.Swagger)
if err = json.Unmarshal(raw, swspec); err != nil {
return nil, errors.Join(err, ErrLoads)
return nil, errLoads(err)
}

origsqspec, err := cloneSpec(swspec)
if err != nil {
return nil, errors.Join(err, ErrLoads)
return nil, errLoads(err)
}

d := &Document{
Expand Down Expand Up @@ -153,12 +152,12 @@ func trimData(in json.RawMessage) (json.RawMessage, error) {
// assume yaml doc: convert it to json
yml, err := yamlutils.BytesToYAMLDoc(trimmed)
if err != nil {
return nil, fmt.Errorf("analyzed: %w: %w", err, ErrLoads)
return nil, fmt.Errorf("analyzed: %w", errLoads(err))
}

d, err := yamlutils.YAMLToJSON(yml)
if err != nil {
return nil, fmt.Errorf("analyzed: %w: %w", err, ErrLoads)
return nil, fmt.Errorf("analyzed: %w", errLoads(err))
}

return d, nil
Expand Down
Loading