Skip to content

Commit 77f34c8

Browse files
committed
feat: cache resolved refs, improve URI reader extensibility
This improves extensibility of how URI references are read when loading OpenAPI 3 documents, and introduces simple URI-based caching. One could, for example, integate new URI readers, such as one backed by embed.FS. A basic map-based cache implementation is provided, which may cover many simple use cases. The cache interface introduced here may be implemented with a third-party backend such as an LRU cache or Redis for more advanced use cases. RFC 7234 HTTP caching may also be implemented by customizing the HTTP client used to read remote URI references. Customizing the HTTP client also allows further customization of the transport, timeouts, etc.
1 parent 2a1c4b1 commit 77f34c8

File tree

2 files changed

+117
-19
lines changed

2 files changed

+117
-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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package openapi3
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"net/http"
7+
"net/url"
8+
)
9+
10+
// ReadFromURIFunc defines a function which reads the contents of a resource
11+
// located at a URI.
12+
type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
13+
14+
// ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a
15+
// given URI.
16+
var ErrURINotSupported = fmt.Errorf("unsupported URI")
17+
18+
// ReadFromURIs returns a ReadFromURIFunc which tries to read a URI using the
19+
// given reader functions, in the same order. If a reader function does not
20+
// support the URI and returns ErrURINotSupported, the next function is checked
21+
// until a match is found, or the URI is not supported by any.
22+
func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc {
23+
return func(loader *Loader, url *url.URL) ([]byte, error) {
24+
for i := range readers {
25+
buf, err := readers[i](loader, url)
26+
if err == ErrURINotSupported {
27+
continue
28+
} else if err != nil {
29+
return nil, err
30+
}
31+
return buf, nil
32+
}
33+
return nil, ErrURINotSupported
34+
}
35+
}
36+
37+
// DefaultReadFromURI returns a ReadFromURIFunc which can read remote HTTP URIs and
38+
// local file URIs.
39+
var DefaultReadFromURI = ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile)
40+
41+
// ReadFromHTTP returns a ReadFromURIFunc which uses the given http.Client to
42+
// read the contents from a remote HTTP URI. This client may be customized to
43+
// implement timeouts, RFC 7234 caching, etc.
44+
func ReadFromHTTP(cl *http.Client) ReadFromURIFunc {
45+
return func(loader *Loader, location *url.URL) ([]byte, error) {
46+
if location.Scheme == "" || location.Host == "" {
47+
return nil, ErrURINotSupported
48+
}
49+
req, err := http.NewRequest("GET", location.String(), nil)
50+
if err != nil {
51+
return nil, err
52+
}
53+
resp, err := cl.Do(req)
54+
if err != nil {
55+
return nil, err
56+
}
57+
defer resp.Body.Close()
58+
if resp.StatusCode > 399 {
59+
return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode)
60+
}
61+
return ioutil.ReadAll(resp.Body)
62+
}
63+
}
64+
65+
// ReadFromFile is a ReadFromURIFunc which reads local file URIs.
66+
func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) {
67+
if location.Host != "" {
68+
return nil, ErrURINotSupported
69+
}
70+
if location.Scheme != "" && location.Scheme != "file" {
71+
return nil, ErrURINotSupported
72+
}
73+
return ioutil.ReadFile(location.Path)
74+
}
75+
76+
// URIReadCache defines a cache for the contents read from URI references.
77+
type URIReadCache interface {
78+
Get(location *url.URL) ([]byte, bool)
79+
Set(location *url.URL, contents []byte)
80+
}
81+
82+
// MapURIReadCache implements URIReadCache with a simple map.
83+
type MapURIReadCache map[string][]byte
84+
85+
// Get implements URIReadCache.
86+
func (m MapURIReadCache) Get(location *url.URL) ([]byte, bool) {
87+
contents, ok := m[location.String()]
88+
return contents, ok
89+
}
90+
91+
// Set implements URIReadCache.
92+
func (m MapURIReadCache) Set(location *url.URL, contents []byte) {
93+
m[location.String()] = contents
94+
}
95+
96+
// ReadFromCache returns a cached ReadFromURIFunc. If cache is nil, a new
97+
// internal cache is allocated, scoped to the given reader.
98+
func ReadFromCache(cache URIReadCache, r ReadFromURIFunc) ReadFromURIFunc {
99+
if cache == nil {
100+
cache = MapURIReadCache{}
101+
}
102+
return func(loader *Loader, location *url.URL) ([]byte, error) {
103+
var err error
104+
cached, ok := cache.Get(location)
105+
if ok {
106+
return cached, nil
107+
}
108+
cached, err = r(loader, location)
109+
if err != nil {
110+
return nil, err
111+
}
112+
cache.Set(location, cached)
113+
return cached, nil
114+
}
115+
}

0 commit comments

Comments
 (0)