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
25 changes: 23 additions & 2 deletions bundler/bundler_composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,27 @@ func rootSupportsPathItemComponents(rootIdx *index.SpecIndex) bool {
return rootIdx.GetConfig().SpecInfo.VersionNumeric >= 3.1
}

func detectImportedComponentType(ref *index.Reference) (string, bool) {
if ref == nil {
return "", false
}
if refUsesJSONSyntax(ref.FullDefinition) {
return DetectOpenAPIComponentTypeForJSON(ref.Node)
}
return DetectOpenAPIComponentType(ref.Node)
}

func refUsesJSONSyntax(fullDefinition string) bool {
refPath := strings.SplitN(fullDefinition, "#", 2)[0]
if refPath == "" {
return false
}
if parsed, err := url.Parse(refPath); err == nil && parsed.Path != "" {
refPath = parsed.Path
}
return strings.EqualFold(filepath.Ext(refPath), ".json")
}

// processReference will extract a reference from the current index, and transform it into a first class
// top-level component in the root OpenAPI document.
func processReference(model *v3.Document, pr *processRef, cf *handleIndexConfig) error {
Expand Down Expand Up @@ -193,7 +214,7 @@ func processReference(model *v3.Document, pr *processRef, cf *handleIndexConfig)
pr.ref.FullDefinition = pr.seqRef.FullDefinition
// this is a root document reference, there is no way to get the location from the fragment.
// first, lets try to determine the type of the import, if we can.
if importType, ok := DetectOpenAPIComponentType(pr.ref.Node); ok {
if importType, ok := detectImportedComponentType(pr.ref); ok {
// cool, using the filename as the reference name, check if we have any collisions.
switch importType {
case v3low.SchemasLabel:
Expand Down Expand Up @@ -319,7 +340,7 @@ func processReference(model *v3.Document, pr *processRef, cf *handleIndexConfig)
// preserve original name before collision handling
pr.originalName = componentName

if importType, ok := DetectOpenAPIComponentType(pr.ref.Node); ok {
if importType, ok := detectImportedComponentType(pr.ref); ok {
switch importType {
case v3low.SchemasLabel:
if components.Schemas != nil {
Expand Down
170 changes: 170 additions & 0 deletions bundler/bundler_composer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/datamodel"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
v3low "github.com/pb33f/libopenapi/datamodel/low/v3"
"github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/orderedmap"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -150,6 +151,49 @@ func TestRenameReference(t *testing.T) {
assert.Equal(t, "#/_oh_#/_yeah", renameRef(nil, "#/_oh_#/_yeah", nil))
}

func TestDetectImportedComponentType(t *testing.T) {
parseNode := func(t *testing.T, input string) *yaml.Node {
t.Helper()

var node yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(input), &node))
require.NotEmpty(t, node.Content)
return node.Content[0]
}

componentType, detected := detectImportedComponentType(nil)
assert.Equal(t, "", componentType)
assert.False(t, detected)

componentType, detected = detectImportedComponentType(&index.Reference{
FullDefinition: "User.yaml",
Node: parseNode(t, `
type: object
properties:
id:
type: string
`),
})
assert.Equal(t, v3low.SchemasLabel, componentType)
assert.True(t, detected)

componentType, detected = detectImportedComponentType(&index.Reference{
FullDefinition: "https://example.com/schemas/User.JSON#/User",
Node: parseNode(t, `{"type":"object","properties":{"id":{"type":"string"}}}`),
})
assert.Equal(t, v3low.SchemasLabel, componentType)
assert.True(t, detected)
}

func TestRefUsesJSONSyntax(t *testing.T) {
assert.False(t, refUsesJSONSyntax(""))
assert.True(t, refUsesJSONSyntax("User.json"))
assert.True(t, refUsesJSONSyntax("User.json#/User"))
assert.True(t, refUsesJSONSyntax("https://example.com/schemas/User.JSON#/User"))
assert.False(t, refUsesJSONSyntax("https://example.com"))
assert.False(t, refUsesJSONSyntax("User.yaml#/User"))
}

func TestBuildSchema(t *testing.T) {
_, err := buildSchema(nil, nil)
assert.Error(t, err)
Expand Down Expand Up @@ -1267,6 +1311,132 @@ properties:
}
}

func TestBundleBytesComposed_BareJSONSchemaFile(t *testing.T) {
rootSpec := `openapi: 3.0.3
info: {title: t, version: '1'}
paths:
/x:
get:
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: 'User.json'
`

childSpec := `{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}`

tmp := t.TempDir()
write := func(name, src string) {
require.NoError(t, os.WriteFile(filepath.Join(tmp, name), []byte(src), 0644))
}
write("main.yaml", rootSpec)
write("User.json", childSpec)

mainBytes, err := os.ReadFile(filepath.Join(tmp, "main.yaml"))
require.NoError(t, err)

docConfig := datamodel.DocumentConfiguration{
BasePath: tmp,
AllowFileReferences: true,
RecomposeRefs: true,
}

bundleConfig := BundleCompositionConfig{
StrictValidation: true,
}

bundled, err := BundleBytesComposed(mainBytes, &docConfig, &bundleConfig)
require.NoError(t, err)

var doc map[string]any
require.NoError(t, yaml.Unmarshal(bundled, &doc))

schema := doc["paths"].(map[string]any)["/x"].(map[string]any)["get"].(map[string]any)["responses"].(map[string]any)["200"].(map[string]any)["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)
ref, hasRef := schema["$ref"].(string)
require.True(t, hasRef, "Schema should have $ref pointing to component")
assert.Equal(t, "#/components/schemas/User", ref)

components := doc["components"].(map[string]any)
schemas := components["schemas"].(map[string]any)
user, ok := schemas["User"].(map[string]any)
require.True(t, ok, "User schema should be added to components")
assert.Equal(t, "object", user["type"])
}

func TestBundleBytesComposed_JSONFileRefWithJSONPointer(t *testing.T) {
rootSpec := `openapi: 3.1.0
paths:
/nonreq:
get:
operationId: getNonReq
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: 'child.json#/NonRequired'
`

childSpec := `{
"NonRequired": {
"type": "object",
"properties": {
"str": {
"type": "string",
"pattern": ".+"
}
}
}
}`

tmp := t.TempDir()
write := func(name, src string) {
require.NoError(t, os.WriteFile(filepath.Join(tmp, name), []byte(src), 0644))
}
write("main.yaml", rootSpec)
write("child.json", childSpec)

mainBytes, err := os.ReadFile(filepath.Join(tmp, "main.yaml"))
require.NoError(t, err)

docConfig := datamodel.DocumentConfiguration{
BasePath: tmp,
AllowFileReferences: true,
RecomposeRefs: true,
}

bundleConfig := BundleCompositionConfig{
StrictValidation: true,
}

bundled, err := BundleBytesComposed(mainBytes, &docConfig, &bundleConfig)
require.NoError(t, err)

var doc map[string]any
require.NoError(t, yaml.Unmarshal(bundled, &doc))

schema := doc["paths"].(map[string]any)["/nonreq"].(map[string]any)["get"].(map[string]any)["responses"].(map[string]any)["200"].(map[string]any)["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)
ref, hasRef := schema["$ref"].(string)
require.True(t, hasRef, "Schema should have $ref pointing to component")
assert.Equal(t, "#/components/schemas/NonRequired", ref)

components := doc["components"].(map[string]any)
schemas := components["schemas"].(map[string]any)
_, ok := schemas["NonRequired"].(map[string]any)
assert.True(t, ok, "NonRequired schema should be added to components")
}

func TestBundleBytesComposed_BarePathItemFile_OAS30Inlines(t *testing.T) {
rootSpec := `openapi: 3.0.3
paths:
Expand Down
Loading
Loading