Skip to content

Commit d54840d

Browse files
authored
feat: cache resolved refs, improve URI reader extensibility (#469)
1 parent 69874b2 commit d54840d

File tree

2 files changed

+106
-19
lines changed

2 files changed

+106
-19
lines changed

openapi3/loader.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8-
"io/ioutil"
9-
"net/http"
108
"net/url"
119
"path"
1210
"path/filepath"
@@ -31,7 +29,7 @@ type Loader struct {
3129
IsExternalRefsAllowed bool
3230

3331
// ReadFromURIFunc allows overriding the any file/URL reading func
34-
ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
32+
ReadFromURIFunc ReadFromURIFunc
3533

3634
Context context.Context
3735

@@ -121,22 +119,7 @@ func (loader *Loader) readURL(location *url.URL) ([]byte, error) {
121119
if f := loader.ReadFromURIFunc; f != nil {
122120
return f(loader, location)
123121
}
124-
125-
if location.Scheme != "" && location.Host != "" {
126-
resp, err := http.Get(location.String())
127-
if err != nil {
128-
return nil, err
129-
}
130-
defer resp.Body.Close()
131-
if resp.StatusCode > 399 {
132-
return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode)
133-
}
134-
return ioutil.ReadAll(resp.Body)
135-
}
136-
if location.Scheme != "" || location.Host != "" || location.RawQuery != "" {
137-
return nil, fmt.Errorf("unsupported URI: %q", location.String())
138-
}
139-
return ioutil.ReadFile(location.Path)
122+
return DefaultReadFromURI(loader, location)
140123
}
141124

142125
// LoadFromData loads a spec from a byte array

openapi3/loader_uri_reader.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package openapi3
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/ioutil"
7+
"net/http"
8+
"net/url"
9+
"path/filepath"
10+
)
11+
12+
// ReadFromURIFunc defines a function which reads the contents of a resource
13+
// located at a URI.
14+
type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
15+
16+
// ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a
17+
// given URI.
18+
var ErrURINotSupported = errors.New("unsupported URI")
19+
20+
// ReadFromURIs returns a ReadFromURIFunc which tries to read a URI using the
21+
// given reader functions, in the same order. If a reader function does not
22+
// support the URI and returns ErrURINotSupported, the next function is checked
23+
// until a match is found, or the URI is not supported by any.
24+
func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc {
25+
return func(loader *Loader, url *url.URL) ([]byte, error) {
26+
for i := range readers {
27+
buf, err := readers[i](loader, url)
28+
if err == ErrURINotSupported {
29+
continue
30+
} else if err != nil {
31+
return nil, err
32+
}
33+
return buf, nil
34+
}
35+
return nil, ErrURINotSupported
36+
}
37+
}
38+
39+
// DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote
40+
// HTTP URIs and local file URIs.
41+
var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile))
42+
43+
// ReadFromHTTP returns a ReadFromURIFunc which uses the given http.Client to
44+
// read the contents from a remote HTTP URI. This client may be customized to
45+
// implement timeouts, RFC 7234 caching, etc.
46+
func ReadFromHTTP(cl *http.Client) ReadFromURIFunc {
47+
return func(loader *Loader, location *url.URL) ([]byte, error) {
48+
if location.Scheme == "" || location.Host == "" {
49+
return nil, ErrURINotSupported
50+
}
51+
req, err := http.NewRequest("GET", location.String(), nil)
52+
if err != nil {
53+
return nil, err
54+
}
55+
resp, err := cl.Do(req)
56+
if err != nil {
57+
return nil, err
58+
}
59+
defer resp.Body.Close()
60+
if resp.StatusCode > 399 {
61+
return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode)
62+
}
63+
return ioutil.ReadAll(resp.Body)
64+
}
65+
}
66+
67+
// ReadFromFile is a ReadFromURIFunc which reads local file URIs.
68+
func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) {
69+
if location.Host != "" {
70+
return nil, ErrURINotSupported
71+
}
72+
if location.Scheme != "" && location.Scheme != "file" {
73+
return nil, ErrURINotSupported
74+
}
75+
return ioutil.ReadFile(location.Path)
76+
}
77+
78+
// URIMapCache returns a ReadFromURIFunc that caches the contents read from URI
79+
// locations in a simple map. This cache implementation is suitable for
80+
// short-lived processes such as command-line tools which process OpenAPI
81+
// documents.
82+
func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc {
83+
cache := map[string][]byte{}
84+
return func(loader *Loader, location *url.URL) (buf []byte, err error) {
85+
if location.Scheme == "" || location.Scheme == "file" {
86+
if !filepath.IsAbs(location.Path) {
87+
// Do not cache relative file paths; this can cause trouble if
88+
// the current working directory changes when processing
89+
// multiple top-level documents.
90+
return reader(loader, location)
91+
}
92+
}
93+
uri := location.String()
94+
var ok bool
95+
if buf, ok = cache[uri]; ok {
96+
return
97+
}
98+
if buf, err = reader(loader, location); err != nil {
99+
return
100+
}
101+
cache[uri] = buf
102+
return
103+
}
104+
}

0 commit comments

Comments
 (0)