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
7 changes: 5 additions & 2 deletions bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ func BundleDocument(model *v3.Document) ([]byte, error) {

// BundleCompositionConfig is used to configure the composition of OpenAPI documents when using BundleDocumentComposed.
type BundleCompositionConfig struct {
Delimiter string // Delimiter is used to separate clashing names. Defaults to `__`.
Delimiter string // Delimiter is used to separate clashing names. Defaults to `__`.
StrictValidation bool // StrictValidation will cause bundling to fail on invalid OpenAPI specs (e.g. $ref with siblings)
}

// BundleDocumentComposed will take a v3.Document and return a composed bundled version of it. Composed means
Expand Down Expand Up @@ -134,7 +135,9 @@ func compose(model *v3.Document, compositionConfig *BundleCompositionConfig) ([]
compositionConfig: compositionConfig,
discriminatorMappings: discriminatorMappings,
}
handleIndex(cf)
if err := handleIndex(cf); err != nil {
return nil, err
}

processedNodes := orderedmap.New[string, *processRef]()
var errs []error
Expand Down
18 changes: 16 additions & 2 deletions bundler/bundler_composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package bundler

import (
"context"
"fmt"
"strings"
"sync"

Expand Down Expand Up @@ -38,14 +39,24 @@ type handleIndexConfig struct {
// handleIndex will recursively explore the indexes and their references, building a map of references
// to be processed later. It will also check for circular references and avoid infinite loops.
// everything is stored in the handleIndexConfig, which is passed around to avoid passing too many parameters.
func handleIndex(c *handleIndexConfig) {
func handleIndex(c *handleIndexConfig) error {
mappedReferences := c.idx.GetMappedReferences()
sequencedReferences := c.idx.GetRawReferencesSequenced()
var indexesToExplore []*index.SpecIndex

for _, sequenced := range sequencedReferences {
mappedReference := mappedReferences[sequenced.FullDefinition]

// Check for invalid sibling properties if strict validation is enabled
if c.compositionConfig.StrictValidation && sequenced.HasSiblingProperties {
siblingKeys := make([]string, 0, len(sequenced.SiblingProperties))
for key := range sequenced.SiblingProperties {
siblingKeys = append(siblingKeys, key)
}
return fmt.Errorf("invalid OpenAPI specification: $ref cannot have sibling properties. Found $ref '%s' with siblings %v at line %d, column %d",
sequenced.FullDefinition, siblingKeys, sequenced.Node.Line, sequenced.Node.Column)
}

// if we're in the root document, don't bundle anything.
refExp := strings.Split(sequenced.FullDefinition, "#/")
var foundIndex *index.SpecIndex
Expand Down Expand Up @@ -90,8 +101,11 @@ func handleIndex(c *handleIndexConfig) {

for _, idx := range indexesToExplore {
c.idx = idx
handleIndex(c)
if err := handleIndex(c); err != nil {
return err
}
}
return nil
}

// processReference will extract a reference from the current index, and transform it into a first class
Expand Down
136 changes: 136 additions & 0 deletions bundler/bundler_strict_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package bundler

import (
"testing"

"github.com/pb33f/libopenapi/datamodel"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStrictValidation_RefWithSiblings_ShouldError(t *testing.T) {
spec := `openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TestSchema'
description: This is invalid - $ref cannot have siblings
components:
schemas:
TestSchema:
type: object
properties:
name:
type: string`

config := &BundleCompositionConfig{
StrictValidation: true,
}

docConfig := &datamodel.DocumentConfiguration{
AllowFileReferences: false,
}

_, err := BundleBytesComposed([]byte(spec), docConfig, config)

require.Error(t, err, "Strict validation must fail on invalid $ref siblings")
assert.Contains(
t,
err.Error(),
"invalid OpenAPI specification: $ref cannot have sibling properties",
)
assert.Contains(t, err.Error(), "siblings [description]")
assert.Contains(t, err.Error(), "line 14")
assert.Contains(t, err.Error(), "column 17")
}

func TestStrictValidation_RefWithoutSiblings_ShouldSucceed(t *testing.T) {
spec := `openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TestSchema'
components:
schemas:
TestSchema:
type: object
properties:
name:
type: string`

config := &BundleCompositionConfig{
StrictValidation: true,
}

docConfig := &datamodel.DocumentConfiguration{
AllowFileReferences: false,
}

result, err := BundleBytesComposed([]byte(spec), docConfig, config)

require.NoError(t, err, "Valid $ref without siblings should succeed")
assert.NotNil(t, result)
assert.True(t, len(result) > 0)
}

func TestStrictValidation_Disabled_ShouldNotError(t *testing.T) {
spec := `openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TestSchema'
description: This would be invalid with strict validation
components:
schemas:
TestSchema:
type: object
properties:
name:
type: string`

config := &BundleCompositionConfig{
StrictValidation: false, // Disabled - should not error
}

docConfig := &datamodel.DocumentConfiguration{
AllowFileReferences: false,
}

result, err := BundleBytesComposed([]byte(spec), docConfig, config)

require.NoError(t, err, "Disabled strict validation should allow invalid siblings")
assert.NotNil(t, result)
}

func TestBundleCompositionConfig_DefaultValues(t *testing.T) {
config := &BundleCompositionConfig{}
assert.False(t, config.StrictValidation)
assert.Empty(t, config.Delimiter)
}
Loading