diff --git a/expander.go b/expander.go index 9edbe9a..00eb5b5 100644 --- a/expander.go +++ b/expander.go @@ -546,25 +546,28 @@ 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. @@ -572,26 +575,43 @@ func ExpandResponse(response *Response, basePath string) error { // 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) { diff --git a/expander_loader_test.go b/expander_loader_test.go index 3d0fc6d..aaaf9eb 100644 --- a/expander_loader_test.go +++ b/expander_loader_test.go @@ -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