Skip to content
Open
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
972 changes: 972 additions & 0 deletions bundler/external_component_ref_test.go

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions datamodel/high/base/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,26 @@
package base

import (
"context"
"encoding/json"

"github.com/pb33f/libopenapi/datamodel/high"
"github.com/pb33f/libopenapi/datamodel/low"
lowBase "github.com/pb33f/libopenapi/datamodel/low/base"
"github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/orderedmap"
"github.com/pb33f/libopenapi/utils"
"go.yaml.in/yaml/v4"
)

// buildLowExample builds a low-level Example from a resolved YAML node.
func buildLowExample(node *yaml.Node, idx *index.SpecIndex) (*lowBase.Example, error) {
var ex lowBase.Example
low.BuildModel(node, &ex)
ex.Build(context.Background(), nil, node, idx)
return &ex, nil
}

// Example represents a high-level Example object as defined by OpenAPI 3+
//
// v3 - https://spec.openapis.org/oas/v3.1.0#example-object
Expand Down Expand Up @@ -91,6 +101,16 @@ func (e *Example) MarshalYAMLInline() (interface{}, error) {
if e.Reference != "" {
return utils.CreateRefNode(e.Reference), nil
}

// resolve external reference if present
if e.low != nil {
// buildLowExample never returns an error, so we can ignore it
rendered, _ := high.RenderExternalRef(e.low, buildLowExample, NewExample)
if rendered != nil {
return rendered, nil
}
}

return high.RenderInline(e, e.low)
}

Expand All @@ -102,6 +122,16 @@ func (e *Example) MarshalYAMLInlineWithContext(ctx any) (interface{}, error) {
if e.Reference != "" {
return utils.CreateRefNode(e.Reference), nil
}

// resolve external reference if present
if e.low != nil {
// buildLowExample never returns an error, so we can ignore it
rendered, _ := high.RenderExternalRefWithContext(e.low, buildLowExample, NewExample, ctx)
if rendered != nil {
return rendered, nil
}
}

return high.RenderInlineWithContext(e, e.low, ctx)
}

Expand Down
33 changes: 33 additions & 0 deletions datamodel/high/base/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,36 @@ func TestExample_MarshalYAMLInlineWithContext_Reference(t *testing.T) {
assert.True(t, ok)
assert.Equal(t, "$ref", yamlNode.Content[0].Value)
}

func TestBuildLowExample_Success(t *testing.T) {
yml := `summary: A test example
description: This is a test
value:
name: test`

var node yaml.Node
err := yaml.Unmarshal([]byte(yml), &node)
assert.NoError(t, err)

result, err := buildLowExample(node.Content[0], nil)

assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "A test example", result.Summary.Value)
}

func TestBuildLowExample_BuildNeverErrors(t *testing.T) {
// Example.Build never returns an error (no error return paths in the Build method)
// This test verifies the success path
yml := `summary: test
externalValue: https://example.com/example.json`

var node yaml.Node
err := yaml.Unmarshal([]byte(yml), &node)
assert.NoError(t, err)

result, err := buildLowExample(node.Content[0], nil)

assert.NoError(t, err)
assert.NotNil(t, result)
}
120 changes: 119 additions & 1 deletion datamodel/high/shared.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
// Copyright 2022 Princess Beef Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

// Package high contains a set of high-level models that represent OpenAPI 2 and 3 documents.
Expand All @@ -14,7 +14,11 @@
package high

import (
"context"
"fmt"

"github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/orderedmap"
"go.yaml.in/yaml/v4"
)
Expand Down Expand Up @@ -89,3 +93,117 @@ func UnpackExtensions[T any, R low.HasExtensions[T]](low GoesLow[R]) (*orderedma
}
return m, nil
}

// ExternalRefResolver is an interface for low-level objects that can be external references.
// This is used by ResolveExternalRef to resolve external $ref values during inline rendering.
type ExternalRefResolver interface {
IsReference() bool
GetReference() string
GetIndex() *index.SpecIndex
}

// ExternalRefBuildFunc is a function that builds a low-level object from a resolved YAML node.
// It should create a new instance of the low-level type, call BuildModel and Build on it,
// and return the constructed object along with any error encountered.
type ExternalRefBuildFunc[L any] func(node *yaml.Node, idx *index.SpecIndex) (L, error)

// ExternalRefResult contains the result of resolving an external reference.
type ExternalRefResult[H any, L any] struct {
High H
Low L
Resolved bool
}

// ResolveExternalRef attempts to resolve an external reference from a low-level object.
// If the low-level object is an external reference (IsReference() returns true), this function
// will use the index to find and resolve the referenced component, build new low and high level
// objects from the resolved content, and return them.
//
// Parameters:
// - lowObj: the low-level object that may be an external reference
// - buildLow: function to build a new low-level object from the resolved YAML node
// - buildHigh: function to create a high-level object from the resolved low-level object
//
// Returns:
// - ExternalRefResult containing the resolved high and low objects if resolution succeeded
// - error if resolution failed (malformed YAML, build errors, etc.)
//
// If the object is not a reference or cannot be resolved, Resolved will be false and the
// caller should fall back to rendering the original object.
func ResolveExternalRef[H any, L any](
lowObj ExternalRefResolver,
buildLow ExternalRefBuildFunc[L],
buildHigh func(L) H,
) (ExternalRefResult[H, L], error) {
var result ExternalRefResult[H, L]

// not a reference, nothing to resolve
if lowObj == nil || !lowObj.IsReference() {
return result, nil
}

idx := lowObj.GetIndex()
if idx == nil {
return result, nil
}

ref := lowObj.GetReference()
resolved := idx.FindComponent(context.Background(), ref)
if resolved == nil || resolved.Node == nil {
return result, nil
}

// build the low-level object from the resolved node
lowResolved, err := buildLow(resolved.Node, resolved.Index)
if err != nil {
return result, fmt.Errorf("failed to build resolved external reference '%s': %w", ref, err)
}

// build the high-level object from the resolved low-level object
highResolved := buildHigh(lowResolved)

result.High = highResolved
result.Low = lowResolved
result.Resolved = true
return result, nil
}

// RenderExternalRef is a convenience function that resolves an external reference and renders it inline.
// This combines ResolveExternalRef with RenderInline for the common case where you want to
// resolve and immediately render an external reference.
//
// If the low-level object is not a reference or resolution fails gracefully (not found),
// this returns (nil, nil) and the caller should fall back to normal rendering.
// If resolution succeeds, returns the rendered YAML node.
// If an error occurs during resolution or rendering, returns the error.
func RenderExternalRef[H any, L any](
lowObj ExternalRefResolver,
buildLow ExternalRefBuildFunc[L],
buildHigh func(L) H,
) (interface{}, error) {
result, err := ResolveExternalRef(lowObj, buildLow, buildHigh)
if err != nil {
return nil, err
}
if !result.Resolved {
return nil, nil
}
return RenderInline(result.High, result.Low)
}

// RenderExternalRefWithContext is like RenderExternalRef but passes a context for cycle detection.
func RenderExternalRefWithContext[H any, L any](
lowObj ExternalRefResolver,
buildLow ExternalRefBuildFunc[L],
buildHigh func(L) H,
ctx any,
) (interface{}, error) {
result, err := ResolveExternalRef(lowObj, buildLow, buildHigh)
if err != nil {
return nil, err
}
if !result.Resolved {
return nil, nil
}
return RenderInlineWithContext(result.High, result.Low, ctx)
}
Loading