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
70 changes: 45 additions & 25 deletions expander.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,52 +546,72 @@ func expandOperation(op *Operation, resolver *schemaLoader, basePath string) err
//
// Setting the cache is optional and this parameter may safely be left to nil.
func ExpandResponseWithRoot(response *Response, root any, cache ResolutionCache) error {
cache = cacheOrDefault(cache)
opts := &ExpandOptions{
RelativeBase: baseForRoot(root, cache),
}
resolver := defaultSchemaLoader(root, opts, cache, nil)

return expandParameterOrResponse(response, resolver, opts.RelativeBase)
return ExpandResponseWithOptions(response, root, cache, nil)
}

// ExpandResponse expands a response based on a basepath
//
// All refs inside response will be resolved relative to basePath.
func ExpandResponse(response *Response, basePath string) error {
opts := optionsOrDefault(&ExpandOptions{
RelativeBase: basePath,
})
resolver := defaultSchemaLoader(nil, opts, nil, nil)
return ExpandResponseWithOptions(response, nil, nil, &ExpandOptions{RelativeBase: basePath})
}

return expandParameterOrResponse(response, resolver, opts.RelativeBase)
// ExpandResponseWithOptions expands a response, honoring the provided expand options.
//
// It is the option-aware form of [ExpandResponse] and [ExpandResponseWithRoot]. When root is
// non-nil, refs resolve against the in-memory root document; otherwise they resolve relative to
// opts.RelativeBase.
//
// Set opts.PathLoaderWithOptions (or opts.PathLoader) to inject a confined document loader when
// the response's $ref may derive from an untrusted source — see the package "Security" section.
//
// Setting the cache is optional and this parameter may safely be left to nil.
func ExpandResponseWithOptions(response *Response, root any, cache ResolutionCache, opts *ExpandOptions) error {
return expandRefableWithOptions(response, root, cache, opts)
}

// ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document.
//
// Notice that it is impossible to reference a json schema in a different document other than root
// (use ExpandParameter to resolve external references).
func ExpandParameterWithRoot(parameter *Parameter, root any, cache ResolutionCache) error {
cache = cacheOrDefault(cache)

opts := &ExpandOptions{
RelativeBase: baseForRoot(root, cache),
}
resolver := defaultSchemaLoader(root, opts, cache, nil)

return expandParameterOrResponse(parameter, resolver, opts.RelativeBase)
return ExpandParameterWithOptions(parameter, root, cache, nil)
}

// ExpandParameter expands a parameter based on a basepath.
// This is the exported version of expandParameter
// all refs inside parameter will be resolved relative to basePath.
func ExpandParameter(parameter *Parameter, basePath string) error {
opts := optionsOrDefault(&ExpandOptions{
RelativeBase: basePath,
})
resolver := defaultSchemaLoader(nil, opts, nil, nil)
return ExpandParameterWithOptions(parameter, nil, nil, &ExpandOptions{RelativeBase: basePath})
}

// ExpandParameterWithOptions expands a parameter, honoring the provided expand options.
//
// It is the option-aware form of [ExpandParameter] and [ExpandParameterWithRoot]. When root is
// non-nil, refs resolve against the in-memory root document; otherwise they resolve relative to
// opts.RelativeBase.
//
// Set opts.PathLoaderWithOptions (or opts.PathLoader) to inject a confined document loader when
// the parameter's $ref may derive from an untrusted source — see the package "Security" section.
//
// Setting the cache is optional and this parameter may safely be left to nil.
func ExpandParameterWithOptions(parameter *Parameter, root any, cache ResolutionCache, opts *ExpandOptions) error {
return expandRefableWithOptions(parameter, root, cache, opts)
}

// expandRefableWithOptions is the shared implementation for the option-aware parameter/response
// expanders. When root is non-nil, refs resolve against the in-memory root (base derived from
// root); otherwise they resolve relative to opts.RelativeBase. opts carries the loader and other
// expand options.
func expandRefableWithOptions(input any, root any, cache ResolutionCache, opts *ExpandOptions) error {
cache = cacheOrDefault(cache)
effective := optionsOrDefault(opts) // clones and normalizes RelativeBase; preserves the loader
if root != nil {
effective.RelativeBase = baseForRoot(root, cache)
}
resolver := defaultSchemaLoader(root, effective, cache, nil)

return expandParameterOrResponse(parameter, resolver, opts.RelativeBase)
return expandParameterOrResponse(input, resolver, effective.RelativeBase)
}

func getRefAndSchema(input any) (*Ref, *Schema, error) {
Expand Down
37 changes: 37 additions & 0 deletions expander_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,43 @@ func TestExpandSchemaWithOptions(t *testing.T) {
})
}

func TestExpandParameterResponseWithOptions(t *testing.T) {
// Parameter and response $ref pointing into an external document are expanded through the
// injected option-aware loader — the path go-openapi/validate needs for confined validation.
const external = `{
"parameters":{"Foo":{"name":"foo","in":"query","type":"string"}},
"responses":{"Bar":{"description":"ok"}}
}`

var loaderCalls int
loader := func(pth string, _ ...loading.Option) (json.RawMessage, error) {
if strings.Contains(pth, "external.json") {
loaderCalls++
return json.RawMessage(external), nil
}
return nil, fmt.Errorf("%w: %s", errUnexpectedLoad, pth)
}
opts := &ExpandOptions{RelativeBase: "spec.json", PathLoaderWithOptions: loader}

t.Run("parameter", func(t *testing.T) {
param := new(Parameter)
param.Ref = MustCreateRef("external.json#/parameters/Foo")
require.NoError(t, ExpandParameterWithOptions(param, nil, nil, opts))
assert.EqualT(t, "foo", param.Name)
assert.EqualT(t, "", param.Ref.String())
})

t.Run("response", func(t *testing.T) {
resp := new(Response)
resp.Ref = MustCreateRef("external.json#/responses/Bar")
require.NoError(t, ExpandResponseWithOptions(resp, nil, nil, opts))
assert.EqualT(t, "ok", resp.Description)
assert.EqualT(t, "", resp.Ref.String())
})

assert.TrueT(t, loaderCalls >= 2, "expected the injected loader to resolve both external $ref")
}

func TestPathLoaderSelection(t *testing.T) {
t.Run("option-aware loader is used when set", func(t *testing.T) {
var called string
Expand Down
Loading