Skip to content

Commit 10b968f

Browse files
authored
Remove lib state (#134)
* Removed the global state of the resolution cache * the cache at the package level is only used for static entries (json schema, swagger schema) * all subsequent utilizations of this cache are taken from a shallow clone of this initial cache which will therefore not mutate Signed-off-by: Frederic BIDON <[email protected]> * Added PathLoader to ExpandOptions This allows to bypass package level settings. In particular, go-openapi/loads can be changed to remove the subtle race conditions when altering PathLoader at the package level from the Spec() function. Signed-off-by: Frederic BIDON <[email protected]> * * adapted package tests to avoid switching the package level variable * ran tests with go-openapi/validate and go-openapi/loads Signed-off-by: Frederic BIDON <[email protected]>
1 parent a490b92 commit 10b968f

File tree

6 files changed

+180
-145
lines changed

6 files changed

+180
-145
lines changed

cache.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
package spec
1616

17-
import "sync"
17+
import (
18+
"sync"
19+
)
1820

1921
// ResolutionCache a cache for resolving urls
2022
type ResolutionCache interface {
@@ -27,6 +29,19 @@ type simpleCache struct {
2729
store map[string]interface{}
2830
}
2931

32+
func (s *simpleCache) ShallowClone() ResolutionCache {
33+
store := make(map[string]interface{}, len(s.store))
34+
s.lock.RLock()
35+
for k, v := range s.store {
36+
store[k] = v
37+
}
38+
s.lock.RUnlock()
39+
40+
return &simpleCache{
41+
store: store,
42+
}
43+
}
44+
3045
// Get retrieves a cached URI
3146
func (s *simpleCache) Get(uri string) (interface{}, bool) {
3247
debugLog("getting %q from resolution cache", uri)
@@ -46,18 +61,40 @@ func (s *simpleCache) Set(uri string, data interface{}) {
4661
}
4762

4863
var (
49-
resCache ResolutionCache
64+
// resCache is a package level cache for $ref resolution and expansion.
65+
// It is initialized lazily by methods that have the need for it: no
66+
// memory is allocated unless some expander methods are called.
67+
//
68+
// It is initialized with JSON schema and swagger schema,
69+
// which do not mutate during normal operations.
70+
//
71+
// All subsequent utilizations of this cache are produced from a shallow
72+
// clone of this initial version.
73+
resCache *simpleCache
5074
onceCache sync.Once
75+
76+
_ ResolutionCache = &simpleCache{}
5177
)
5278

5379
// initResolutionCache initializes the URI resolution cache. To be wrapped in a sync.Once.Do call.
5480
func initResolutionCache() {
5581
resCache = defaultResolutionCache()
5682
}
5783

58-
func defaultResolutionCache() ResolutionCache {
84+
func defaultResolutionCache() *simpleCache {
5985
return &simpleCache{store: map[string]interface{}{
6086
"http://swagger.io/v2/schema.json": MustLoadSwagger20Schema(),
6187
"http://json-schema.org/draft-04/schema": MustLoadJSONSchemaDraft04(),
6288
}}
6389
}
90+
91+
func cacheOrDefault(cache ResolutionCache) ResolutionCache {
92+
onceCache.Do(initResolutionCache)
93+
94+
if cache != nil {
95+
return cache
96+
}
97+
98+
// get a shallow clone of the base cache with swagger and json schema
99+
return resCache.ShallowClone()
100+
}

expander.go

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type ExpandOptions struct {
2626
SkipSchemas bool
2727
ContinueOnError bool
2828
AbsoluteCircularRef bool
29+
PathLoader func(string) (json.RawMessage, error) `json:"-"`
2930
}
3031

3132
// ResolveRefWithBase resolves a reference against a context root with preservation of base path
@@ -81,6 +82,7 @@ func ResolveParameterWithBase(root interface{}, ref Ref, opts *ExpandOptions) (*
8182
if err != nil {
8283
return nil, err
8384
}
85+
8486
specBasePath := ""
8587
if opts != nil && opts.RelativeBase != "" {
8688
specBasePath, _ = absPath(opts.RelativeBase)
@@ -126,10 +128,12 @@ func ResolveItems(root interface{}, ref Ref, opts *ExpandOptions) (*Items, error
126128
if err != nil {
127129
return nil, err
128130
}
131+
129132
basePath := ""
130133
if opts.RelativeBase != "" {
131134
basePath = opts.RelativeBase
132135
}
136+
133137
result := new(Items)
134138
if err := resolver.Resolve(&ref, result, basePath); err != nil {
135139
return nil, err
@@ -143,10 +147,12 @@ func ResolvePathItem(root interface{}, ref Ref, opts *ExpandOptions) (*PathItem,
143147
if err != nil {
144148
return nil, err
145149
}
150+
146151
basePath := ""
147152
if opts.RelativeBase != "" {
148153
basePath = opts.RelativeBase
149154
}
155+
150156
result := new(PathItem)
151157
if err := resolver.Resolve(&ref, result, basePath); err != nil {
152158
return nil, err
@@ -157,7 +163,7 @@ func ResolvePathItem(root interface{}, ref Ref, opts *ExpandOptions) (*PathItem,
157163
// ExpandSpec expands the references in a swagger spec
158164
func ExpandSpec(spec *Swagger, options *ExpandOptions) error {
159165
resolver, err := defaultSchemaLoader(spec, options, nil, nil)
160-
// Just in case this ever returns an error.
166+
// just in case this ever returns an error
161167
if resolver.shouldStopOnError(err) {
162168
return err
163169
}
@@ -212,28 +218,31 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error {
212218

213219
const rootBase = ".root"
214220

215-
// baseForRoot loads in the cache the root document and produces a fake "root" base path entry
221+
// baseForRoot loads in the cache the root document and produces a fake ".root" base path entry
216222
// for further $ref resolution
223+
//
224+
// Setting the cache is optional and this parameter may safely be left to nil.
217225
func baseForRoot(root interface{}, cache ResolutionCache) string {
226+
if root == nil {
227+
return ""
228+
}
229+
218230
// cache the root document to resolve $ref's
219-
if root != nil {
220-
base, _ := absPath(rootBase)
221-
normalizedBase := normalizeAbsPath(base)
222-
debugLog("setting root doc in cache at: %s", normalizedBase)
223-
if cache == nil {
224-
onceCache.Do(initResolutionCache)
225-
cache = resCache
226-
}
227-
cache.Set(normalizedBase, root)
228-
return normalizedBase
229-
}
230-
return ""
231+
base, _ := absPath(rootBase)
232+
normalizedBase := normalizeAbsPath(base)
233+
debugLog("setting root doc in cache at: %s", normalizedBase)
234+
cache.Set(normalizedBase, root)
235+
236+
return normalizedBase
231237
}
232238

233239
// ExpandSchema expands the refs in the schema object with reference to the root object
234240
// go-openapi/validate uses this function
235241
// notice that it is impossible to reference a json schema in a different file other than root
242+
//
243+
// Setting the cache is optional and this parameter may safely be left to nil.
236244
func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error {
245+
cache = cacheOrDefault(cache)
237246
opts := &ExpandOptions{
238247
// when a root is specified, cache the root as an in-memory document for $ref retrieval
239248
RelativeBase: baseForRoot(root, cache),
@@ -245,12 +254,16 @@ func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error
245254
return ExpandSchemaWithBasePath(schema, cache, opts)
246255
}
247256

248-
// ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options
257+
// ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options.
258+
//
259+
// Setting the cache is optional and this parameter may safely be left to nil.
249260
func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *ExpandOptions) error {
250261
if schema == nil {
251262
return nil
252263
}
253264

265+
cache = cacheOrDefault(cache)
266+
254267
var basePath string
255268
if opts.RelativeBase != "" {
256269
basePath, _ = absPath(opts.RelativeBase)
@@ -314,7 +327,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba
314327
basePath = normalizePaths(refPath, basePath)
315328

316329
// store found IDs for possible future reuse in $ref
317-
resCache.Set(basePath, target)
330+
resolver.cache.Set(basePath, target)
318331
}
319332

320333
var t *Schema
@@ -529,15 +542,18 @@ func expandOperation(op *Operation, resolver *schemaLoader, basePath string) err
529542
}
530543

531544
// ExpandResponseWithRoot expands a response based on a root document, not a fetchable document
545+
//
546+
// Setting the cache is optional and this parameter may safely be left to nil.
532547
func ExpandResponseWithRoot(response *Response, root interface{}, cache ResolutionCache) error {
548+
cache = cacheOrDefault(cache)
533549
opts := &ExpandOptions{
534550
RelativeBase: baseForRoot(root, cache),
535551
SkipSchemas: false,
536552
ContinueOnError: false,
537553
// when no base path is specified, remaining $ref (circular) are rendered with an absolute path
538554
AbsoluteCircularRef: false,
539555
}
540-
resolver, err := defaultSchemaLoader(root, opts, nil, nil)
556+
resolver, err := defaultSchemaLoader(root, opts, cache, nil)
541557
if err != nil {
542558
return err
543559
}
@@ -566,14 +582,15 @@ func ExpandResponse(response *Response, basePath string) error {
566582

567583
// ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document
568584
func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache ResolutionCache) error {
585+
cache = cacheOrDefault(cache)
569586
opts := &ExpandOptions{
570587
RelativeBase: baseForRoot(root, cache),
571588
SkipSchemas: false,
572589
ContinueOnError: false,
573590
// when no base path is specified, remaining $ref (circular) are rendered with an absolute path
574591
AbsoluteCircularRef: false,
575592
}
576-
resolver, err := defaultSchemaLoader(root, opts, nil, nil)
593+
resolver, err := defaultSchemaLoader(root, opts, cache, nil)
577594
if err != nil {
578595
return err
579596
}

0 commit comments

Comments
 (0)