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
33 changes: 27 additions & 6 deletions expander.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,41 @@ func baseForRoot(root any, cache ResolutionCache) string {
// (use ExpandSchemaWithBasePath to resolve external references).
//
// Setting the cache is optional and this parameter may safely be left to nil.
//
// ExpandSchema uses the package default document loader, which is not sandboxed. To expand a
// schema whose $ref may derive from untrusted input, use [ExpandSchemaWithOptions] with a confined
// loader — see the package "Security" section.
func ExpandSchema(schema *Schema, root any, cache ResolutionCache) error {
return ExpandSchemaWithOptions(schema, root, cache, nil)
}

// ExpandSchemaWithOptions expands the refs in the schema object with reference to the root object,
// honoring the provided expand options. It is the option-aware form of [ExpandSchema].
//
// In particular, set opts.PathLoaderWithOptions (or opts.PathLoader) to inject a confined document
// loader when expanding a schema whose $ref may derive from an untrusted source (see the package
// "Security" section). opts.ContinueOnError, opts.AbsoluteCircularRef and opts.MaxExpansionNodes
// are honored as well.
//
// The base path is always derived from root (as with [ExpandSchema]), so opts.RelativeBase and
// opts.SkipSchemas are ignored. Passing nil opts is equivalent to [ExpandSchema].
//
// Setting the cache is optional and this parameter may safely be left to nil.
func ExpandSchemaWithOptions(schema *Schema, root any, cache ResolutionCache, opts *ExpandOptions) error {
cache = cacheOrDefault(cache)
if root == nil {
root = schema
}

opts := &ExpandOptions{
// when a root is specified, cache the root as an in-memory document for $ref retrieval
RelativeBase: baseForRoot(root, cache),
SkipSchemas: false,
ContinueOnError: false,
effective := ExpandOptions{}
if opts != nil {
effective = *opts // preserve caller options (loader, ContinueOnError, budget, ...)
}
// when a root is specified, cache the root as an in-memory document for $ref retrieval
effective.RelativeBase = baseForRoot(root, cache)
effective.SkipSchemas = false

return ExpandSchemaWithBasePath(schema, cache, opts)
return ExpandSchemaWithBasePath(schema, cache, &effective)
}

// ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options.
Expand Down
39 changes: 39 additions & 0 deletions expander_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,45 @@ import (
// errUnexpectedLoad is returned by a test loader asked to load a path it does not expect.
var errUnexpectedLoad = errors.New("unexpected load")

func TestExpandSchemaWithOptions(t *testing.T) {
// A schema whose $ref points into an external document is expanded through the injected
// option-aware loader, exactly as flatten/analysis and validate need for confined expansion.
const external = `{"definitions":{"Thing":{"type":"string"}}}`

root := map[string]any{"swagger": "2.0", "definitions": map[string]any{}}
schema := RefSchema("external.json#/definitions/Thing")

var loaderCalls int
err := ExpandSchemaWithOptions(schema, root, nil, &ExpandOptions{
PathLoaderWithOptions: 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)
},
})
require.NoError(t, err)
assert.TrueT(t, loaderCalls > 0, "expected the injected loader to resolve the external $ref")

out, err := json.Marshal(schema)
require.NoError(t, err)
assert.StringContainsT(t, string(out), `"type":"string"`)
assert.StringNotContainsT(t, string(out), `"$ref"`)

t.Run("nil options behaves like ExpandSchema (in-memory root, fragment ref)", func(t *testing.T) {
inMemRoot := map[string]any{
"definitions": map[string]any{"Local": map[string]any{"type": "integer"}},
}
sch := RefSchema("#/definitions/Local")
require.NoError(t, ExpandSchemaWithOptions(sch, inMemRoot, nil, nil))

out, err := json.Marshal(sch)
require.NoError(t, err)
assert.StringContainsT(t, string(out), `"type":"integer"`)
})
}

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