Skip to content

Commit 8306348

Browse files
percivalalbAlex Barter
authored and
Alex Barter
committed
Add function to check if a schema matches the root document's
It does this by keeping track of the source file paths of schemas in multi-file'd specs. This enables the amalgamation of schemas which reference the same underlying model which removes *very annoying* to use anonymous structs in generated models when using codegen tools like oapi-codegen.
1 parent 9d820c9 commit 8306348

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

openapi3/loader.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
186186
loader.resetVisitedPathItemRefs()
187187
}
188188

189+
if location != nil {
190+
specURL := *location
191+
doc.url = &specURL // shallow-copy
192+
}
193+
189194
if components := doc.Components; components != nil {
190195
for _, component := range components.Headers {
191196
if err = loader.resolveHeaderRef(doc, component, location); err != nil {
@@ -787,6 +792,12 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
787792
}
788793

789794
if ref := component.Ref; ref != "" {
795+
if documentPath != nil {
796+
refURL := *documentPath // shallow-clone
797+
refURL.Path = path.Join(path.Dir(documentPath.Path), ref)
798+
component.refURL = &refURL
799+
}
800+
790801
if isSingleRefElement(ref) {
791802
var schema Schema
792803
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil {

openapi3/openapi3.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"net/url"
9+
"path"
10+
"strings"
811

912
"github.com/go-openapi/jsonpointer"
1013
)
@@ -24,6 +27,96 @@ type T struct {
2427
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
2528

2629
visited visitedComponent
30+
url *url.URL
31+
}
32+
33+
// MatchesSchemaInRootDocument returns if the given schema is identical
34+
// to a schema defined in the root document's '#/components/schemas'.
35+
// It returns a reference to the schema in the form
36+
// '#/components/schemas/NameXXX'
37+
//
38+
// Of course given it a schema from the root document will always match.
39+
//
40+
// https://swagger.io/docs/specification/using-ref/#syntax
41+
//
42+
// Case 1: Directly via
43+
//
44+
// ../openapi.yaml#/components/schemas/Record
45+
//
46+
// Case 2: Or indirectly by using a $ref which matches a schema
47+
// in the root document's '#/components/schemas' using the same
48+
// $ref.
49+
//
50+
// In schemas/record.yaml
51+
//
52+
// $ref: ./record.yaml
53+
//
54+
// In openapi.yaml
55+
//
56+
// components:
57+
// schemas:
58+
// Record:
59+
// $ref: schemas/record.yaml
60+
func (doc *T) MatchesSchemaInRootDocument(sch *SchemaRef) (string, bool) {
61+
// Case 1:
62+
// Something like: ../another-folder/document.json#/myElement
63+
if isRemoteReference(sch.Ref) && isSchemaReference(sch.Ref) {
64+
// Determine if it is *this* root doc.
65+
if sch.referencesRootDocument(doc) {
66+
_, name, _ := strings.Cut(sch.Ref, "#/components/schemas")
67+
68+
return path.Join("#/components/schemas", name), true
69+
}
70+
}
71+
72+
// If there are no schemas defined in the root document return early.
73+
if doc.Components == nil || doc.Components.Schemas == nil {
74+
return "", false
75+
}
76+
77+
// Case 2:
78+
// Something like: ../openapi.yaml#/components/schemas/myElement
79+
for name, s := range doc.Components.Schemas {
80+
// Must be a reference to a YAML file.
81+
if !isWholeDocumentReference(s.Ref) {
82+
continue
83+
}
84+
85+
// Is the schema a ref to the same resource.
86+
if !sch.refersToSameDocument(s) {
87+
continue
88+
}
89+
90+
// Transform the remote ref to the equivalent schema in the root document.
91+
return path.Join("#/components/schemas", name), true
92+
}
93+
94+
return "", false
95+
}
96+
97+
// isElementReference takes a $ref value and checks if it references a specific element.
98+
func isElementReference(ref string) bool {
99+
return ref != "" && !isWholeDocumentReference(ref)
100+
}
101+
102+
// isSchemaReference takes a $ref value and checks if it references a schema element.
103+
func isSchemaReference(ref string) bool {
104+
return isElementReference(ref) && strings.Contains(ref, "#/components/schemas")
105+
}
106+
107+
// isWholeDocumentReference takes a $ref value and checks if it is whole document reference.
108+
func isWholeDocumentReference(ref string) bool {
109+
return ref != "" && !strings.ContainsAny(ref, "#")
110+
}
111+
112+
// isRemoteReference takes a $ref value and checks if it is remote reference.
113+
func isRemoteReference(ref string) bool {
114+
return ref != "" && !strings.HasPrefix(ref, "#") && !isURLReference(ref)
115+
}
116+
117+
// isURLReference takes a $ref value and checks if it is URL reference.
118+
func isURLReference(ref string) bool {
119+
return strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") || strings.HasPrefix(ref, "//")
27120
}
28121

29122
var _ jsonpointer.JSONPointable = (*T)(nil)

openapi3/refs.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"net/url"
78
"sort"
9+
"strings"
810

911
"github.com/go-openapi/jsonpointer"
1012
"github.com/perimeterx/marshmallow"
@@ -562,10 +564,50 @@ type SchemaRef struct {
562564
Ref string
563565
Value *Schema
564566
extra []string
567+
568+
// Only non-nil if Ref is non-empty, might be nil even if Ref is non-empty
569+
// if loaded from a in-memory schema that has been merged.
570+
refURL *url.URL
565571
}
566572

567573
var _ jsonpointer.JSONPointable = (*SchemaRef)(nil)
568574

575+
// refersToSameDocument returns if the $ref refers to the same document.
576+
//
577+
// Documents in different directories will have distinct $ref values that resolve to
578+
// the same document.
579+
// For example, consider the 3 files:
580+
//
581+
// /records.yaml
582+
// /root.yaml $ref: records.yaml
583+
// /schema/other.yaml $ref: ../records.yaml
584+
//
585+
// The records.yaml reference in the 2 latter refers to the same document.
586+
func (x *SchemaRef) refersToSameDocument(o *SchemaRef) bool {
587+
if x == nil || x.refURL == nil || o == nil || o.refURL == nil {
588+
return false
589+
}
590+
591+
// refURL is relative to the working directory & base spec file.
592+
return x.refURL.String() == o.refURL.String()
593+
}
594+
595+
// referencesRootDocument returns if the given schema matches the root document of the OpenAPI spec.
596+
//
597+
// If the document has no location, perhaps loaded from data in memory, it always returns false.
598+
func (x *SchemaRef) referencesRootDocument(doc *T) bool {
599+
if doc.url == nil || x == nil || x.refURL == nil {
600+
return false
601+
}
602+
603+
refURL := *x.refURL
604+
605+
refURL.Path, _, _ = strings.Cut(refURL.Path, "#") // remove the document element reference
606+
607+
// Check referenced element was in the root document.
608+
return doc.url.String() == refURL.String()
609+
}
610+
569611
func (x *SchemaRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
570612

571613
// MarshalYAML returns the YAML encoding of SchemaRef.

0 commit comments

Comments
 (0)