1// Copyright 2015 go-swagger maintainers 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package spec 16 17import ( 18 "encoding/json" 19 "fmt" 20 "log" 21 "net/url" 22 "reflect" 23 "strings" 24 25 "github.com/go-openapi/swag" 26) 27 28// PathLoader is a function to use when loading remote refs. 29// 30// This is a package level default. It may be overridden or bypassed by 31// specifying the loader in ExpandOptions. 32// 33// NOTE: if you are using the go-openapi/loads package, it will override 34// this value with its own default (a loader to retrieve YAML documents as 35// well as JSON ones). 36var PathLoader = func(pth string) (json.RawMessage, error) { 37 data, err := swag.LoadFromFileOrHTTP(pth) 38 if err != nil { 39 return nil, err 40 } 41 return json.RawMessage(data), nil 42} 43 44// resolverContext allows to share a context during spec processing. 45// At the moment, it just holds the index of circular references found. 46type resolverContext struct { 47 // circulars holds all visited circular references, to shortcircuit $ref resolution. 48 // 49 // This structure is privately instantiated and needs not be locked against 50 // concurrent access, unless we chose to implement a parallel spec walking. 51 circulars map[string]bool 52 basePath string 53 loadDoc func(string) (json.RawMessage, error) 54 rootID string 55} 56 57func newResolverContext(options *ExpandOptions) *resolverContext { 58 expandOptions := optionsOrDefault(options) 59 60 // path loader may be overridden by options 61 var loader func(string) (json.RawMessage, error) 62 if expandOptions.PathLoader == nil { 63 loader = PathLoader 64 } else { 65 loader = expandOptions.PathLoader 66 } 67 68 return &resolverContext{ 69 circulars: make(map[string]bool), 70 basePath: expandOptions.RelativeBase, // keep the root base path in context 71 loadDoc: loader, 72 } 73} 74 75type schemaLoader struct { 76 root interface{} 77 options *ExpandOptions 78 cache ResolutionCache 79 context *resolverContext 80} 81 82func (r *schemaLoader) transitiveResolver(basePath string, ref Ref) *schemaLoader { 83 if ref.IsRoot() || ref.HasFragmentOnly { 84 return r 85 } 86 87 baseRef := MustCreateRef(basePath) 88 currentRef := normalizeRef(&ref, basePath) 89 if strings.HasPrefix(currentRef.String(), baseRef.String()) { 90 return r 91 } 92 93 // set a new root against which to resolve 94 rootURL := currentRef.GetURL() 95 rootURL.Fragment = "" 96 root, _ := r.cache.Get(rootURL.String()) 97 98 // shallow copy of resolver options to set a new RelativeBase when 99 // traversing multiple documents 100 newOptions := r.options 101 newOptions.RelativeBase = rootURL.String() 102 103 return defaultSchemaLoader(root, newOptions, r.cache, r.context) 104} 105 106func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) string { 107 if transitive != r { 108 if transitive.options != nil && transitive.options.RelativeBase != "" { 109 return normalizeBase(transitive.options.RelativeBase) 110 } 111 } 112 113 return basePath 114} 115 116func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) error { 117 tgt := reflect.ValueOf(target) 118 if tgt.Kind() != reflect.Ptr { 119 return ErrResolveRefNeedsAPointer 120 } 121 122 if ref.GetURL() == nil { 123 return nil 124 } 125 126 var ( 127 res interface{} 128 data interface{} 129 err error 130 ) 131 132 // Resolve against the root if it isn't nil, and if ref is pointing at the root, or has a fragment only which means 133 // it is pointing somewhere in the root. 134 root := r.root 135 if (ref.IsRoot() || ref.HasFragmentOnly) && root == nil && basePath != "" { 136 if baseRef, erb := NewRef(basePath); erb == nil { 137 root, _, _, _ = r.load(baseRef.GetURL()) 138 } 139 } 140 141 if (ref.IsRoot() || ref.HasFragmentOnly) && root != nil { 142 data = root 143 } else { 144 baseRef := normalizeRef(ref, basePath) 145 data, _, _, err = r.load(baseRef.GetURL()) 146 if err != nil { 147 return err 148 } 149 } 150 151 res = data 152 if ref.String() != "" { 153 res, _, err = ref.GetPointer().Get(data) 154 if err != nil { 155 return err 156 } 157 } 158 return swag.DynamicJSONToStruct(res, target) 159} 160 161func (r *schemaLoader) load(refURL *url.URL) (interface{}, url.URL, bool, error) { 162 debugLog("loading schema from url: %s", refURL) 163 toFetch := *refURL 164 toFetch.Fragment = "" 165 166 var err error 167 pth := toFetch.String() 168 normalized := normalizeBase(pth) 169 debugLog("loading doc from: %s", normalized) 170 171 unescaped, err := url.PathUnescape(normalized) 172 if err != nil { 173 return nil, url.URL{}, false, err 174 } 175 176 u := url.URL{Path: unescaped} 177 178 data, fromCache := r.cache.Get(u.RequestURI()) 179 if fromCache { 180 return data, toFetch, fromCache, nil 181 } 182 183 b, err := r.context.loadDoc(normalized) 184 if err != nil { 185 return nil, url.URL{}, false, err 186 } 187 188 var doc interface{} 189 if err := json.Unmarshal(b, &doc); err != nil { 190 return nil, url.URL{}, false, err 191 } 192 r.cache.Set(normalized, doc) 193 194 return doc, toFetch, fromCache, nil 195} 196 197// isCircular detects cycles in sequences of $ref. 198// 199// It relies on a private context (which needs not be locked). 200func (r *schemaLoader) isCircular(ref *Ref, basePath string, parentRefs ...string) (foundCycle bool) { 201 normalizedRef := normalizeURI(ref.String(), basePath) 202 if _, ok := r.context.circulars[normalizedRef]; ok { 203 // circular $ref has been already detected in another explored cycle 204 foundCycle = true 205 return 206 } 207 foundCycle = swag.ContainsStrings(parentRefs, normalizedRef) // normalized windows url's are lower cased 208 if foundCycle { 209 r.context.circulars[normalizedRef] = true 210 } 211 return 212} 213 214// Resolve resolves a reference against basePath and stores the result in target. 215// 216// Resolve is not in charge of following references: it only resolves ref by following its URL. 217// 218// If the schema the ref is referring to holds nested refs, Resolve doesn't resolve them. 219// 220// If basePath is an empty string, ref is resolved against the root schema stored in the schemaLoader struct 221func (r *schemaLoader) Resolve(ref *Ref, target interface{}, basePath string) error { 222 return r.resolveRef(ref, target, basePath) 223} 224 225func (r *schemaLoader) deref(input interface{}, parentRefs []string, basePath string) error { 226 var ref *Ref 227 switch refable := input.(type) { 228 case *Schema: 229 ref = &refable.Ref 230 case *Parameter: 231 ref = &refable.Ref 232 case *Response: 233 ref = &refable.Ref 234 case *PathItem: 235 ref = &refable.Ref 236 default: 237 return fmt.Errorf("unsupported type: %T: %w", input, ErrDerefUnsupportedType) 238 } 239 240 curRef := ref.String() 241 if curRef == "" { 242 return nil 243 } 244 245 normalizedRef := normalizeRef(ref, basePath) 246 normalizedBasePath := normalizedRef.RemoteURI() 247 248 if r.isCircular(normalizedRef, basePath, parentRefs...) { 249 return nil 250 } 251 252 if err := r.resolveRef(ref, input, basePath); r.shouldStopOnError(err) { 253 return err 254 } 255 256 if ref.String() == "" || ref.String() == curRef { 257 // done with rereferencing 258 return nil 259 } 260 261 parentRefs = append(parentRefs, normalizedRef.String()) 262 return r.deref(input, parentRefs, normalizedBasePath) 263} 264 265func (r *schemaLoader) shouldStopOnError(err error) bool { 266 if err != nil && !r.options.ContinueOnError { 267 return true 268 } 269 270 if err != nil { 271 log.Println(err) 272 } 273 274 return false 275} 276 277func (r *schemaLoader) setSchemaID(target interface{}, id, basePath string) (string, string) { 278 debugLog("schema has ID: %s", id) 279 280 // handling the case when id is a folder 281 // remember that basePath has to point to a file 282 var refPath string 283 if strings.HasSuffix(id, "/") { 284 // ensure this is detected as a file, not a folder 285 refPath = fmt.Sprintf("%s%s", id, "placeholder.json") 286 } else { 287 refPath = id 288 } 289 290 // updates the current base path 291 // * important: ID can be a relative path 292 // * registers target to be fetchable from the new base proposed by this id 293 newBasePath := normalizeURI(refPath, basePath) 294 295 // store found IDs for possible future reuse in $ref 296 r.cache.Set(newBasePath, target) 297 298 // the root document has an ID: all $ref relative to that ID may 299 // be rebased relative to the root document 300 if basePath == r.context.basePath { 301 debugLog("root document is a schema with ID: %s (normalized as:%s)", id, newBasePath) 302 r.context.rootID = newBasePath 303 } 304 305 return newBasePath, refPath 306} 307 308func defaultSchemaLoader( 309 root interface{}, 310 expandOptions *ExpandOptions, 311 cache ResolutionCache, 312 context *resolverContext) *schemaLoader { 313 314 if expandOptions == nil { 315 expandOptions = &ExpandOptions{} 316 } 317 318 cache = cacheOrDefault(cache) 319 320 if expandOptions.RelativeBase == "" { 321 // if no relative base is provided, assume the root document 322 // contains all $ref, or at least, that the relative documents 323 // may be resolved from the current working directory. 324 expandOptions.RelativeBase = baseForRoot(root, cache) 325 } 326 debugLog("effective expander options: %#v", expandOptions) 327 328 if context == nil { 329 context = newResolverContext(expandOptions) 330 } 331 332 return &schemaLoader{ 333 root: root, 334 options: expandOptions, 335 cache: cache, 336 context: context, 337 } 338} 339