Skip to content

Commit

Permalink
Fix bugs, rebase issues, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
percivalalb committed Jun 15, 2024
1 parent a68547e commit c3fe41b
Show file tree
Hide file tree
Showing 18 changed files with 451 additions and 341 deletions.
102 changes: 58 additions & 44 deletions .github/docs/openapi3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,36 +107,50 @@ func Float64Ptr(value float64) *float64
func Int64Ptr(value int64) *int64
Int64Ptr is a helper for defining OpenAPI schemas.

func MatchesComponentInRootDocument(doc *T, ref componentRef) (string, bool)
MatchesComponentInRootDocument returns if the given component is identical
to a component defined in the root document's '#/components/<type>'. It
returns a reference to the schema in the form '#/components/<type>/NameXXX'
func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error)
ReadFromFile is a ReadFromURIFunc which reads local file URIs.

func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool)
ReferencesComponentInRootDocument returns if the given component reference
references the same document or element as another component reference in
the root document's '#/components/<type>'. If it does, it returns the name
of it in the form '#/components/<type>/NameXXX'

Of course given it a component from the root document will always match.
Of course given a component from the root document will always match itself.

https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#relative-references-in-urls

Case 1: Directly via
Example. Take the spec with directory structure:

../openapi.yaml#/components/schemas/Record
openapi.yaml
schemas/
├─ record.yaml
├─ records.yaml

Case 2: Or indirectly by using a $ref which matches a schema in the root
document's '#/components/schemas' using the same $ref.

In schemas/record.yaml

$ref: ./record.yaml

In openapi.yaml
In openapi.yaml we have:

components:
schemas:
Record:
$ref: schemas/record.yaml

func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error)
ReadFromFile is a ReadFromURIFunc which reads local file URIs.
Case 1: records.yml references a component in the root document

$ref: ../openapi.yaml#/components/schemas/Record

This would return...

#/components/schemas/Record

Case 2: records.yml indirectly refers to the same schema as a schema the
root document's '#/components/schemas'.

$ref: ./record.yaml

This would also return...

#/components/schemas/Record

func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker)
RegisterArrayUniqueItemsChecker is used to register a customized function
Expand Down Expand Up @@ -225,9 +239,9 @@ type CallbackRef struct {
CallbackRef represents either a Callback or a $ref to a Callback. When
serializing and both fields are set, Ref is preferred over Value.

func (x *CallbackRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *CallbackRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *CallbackRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -424,9 +438,9 @@ type ExampleRef struct {
ExampleRef represents either a Example or a $ref to a Example. When
serializing and both fields are set, Ref is preferred over Value.

func (x *ExampleRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *ExampleRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *ExampleRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -522,9 +536,9 @@ type HeaderRef struct {
HeaderRef represents either a Header or a $ref to a Header. When serializing
and both fields are set, Ref is preferred over Value.

func (x *HeaderRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *HeaderRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *HeaderRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -635,9 +649,9 @@ type LinkRef struct {
LinkRef represents either a Link or a $ref to a Link. When serializing and
both fields are set, Ref is preferred over Value.

func (x *LinkRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *LinkRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *LinkRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -957,9 +971,9 @@ type ParameterRef struct {
ParameterRef represents either a Parameter or a $ref to a Parameter.
When serializing and both fields are set, Ref is preferred over Value.

func (x *ParameterRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *ParameterRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *ParameterRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -1201,9 +1215,9 @@ type RequestBodyRef struct {
RequestBodyRef represents either a RequestBody or a $ref to a RequestBody.
When serializing and both fields are set, Ref is preferred over Value.

func (x *RequestBodyRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *RequestBodyRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -1276,9 +1290,9 @@ type ResponseRef struct {
ResponseRef represents either a Response or a $ref to a Response. When
serializing and both fields are set, Ref is preferred over Value.

func (x *ResponseRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *ResponseRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *ResponseRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -1585,9 +1599,9 @@ type SchemaRef struct {
func NewSchemaRef(ref string, value *Schema) *SchemaRef
NewSchemaRef simply builds a SchemaRef

func (x *SchemaRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *SchemaRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *SchemaRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down Expand Up @@ -1743,9 +1757,9 @@ type SecuritySchemeRef struct {
SecurityScheme. When serializing and both fields are set, Ref is preferred
over Value.

func (x *SecuritySchemeRef) ComponentType() string
ComponentType returns the name of this component type used in the spec's
component section.
func (x *SecuritySchemeRef) CollectionName() string
CollectionName returns the JSON string used for a collection of these
components.

func (x *SecuritySchemeRef) JSONLookup(token string) (interface{}, error)
JSONLookup implements
Expand Down
134 changes: 106 additions & 28 deletions openapi3/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func Uint64Ptr(value uint64) *uint64 {
type componentRef interface {
RefString() string
RefPath() *url.URL
ComponentType() string
CollectionName() string
}

// refersToSameDocument returns if the $ref refers to the same document.
Expand All @@ -73,74 +73,152 @@ func refersToSameDocument(o1 componentRef, o2 componentRef) bool {
}

// refURL is relative to the working directory & base spec file.
return r1.String() == r2.String()
return referenceURIMatch(r1, r2)
}

// referencesRootDocument returns if the $ref points to the root document of the OpenAPI spec.
//
// If the document has no location, perhaps loaded from data in memory, it always returns false.
func referencesRootDocument(doc *T, ref componentRef) bool {
if doc.url == nil || ref == nil {
if doc.url == nil || ref == nil || ref.RefPath() == nil {
return false
}

refURL := *ref.RefPath()
refURL.Path, _, _ = strings.Cut(refURL.Path, "#") // remove the document element reference
refURL.Fragment = ""

// Check referenced element was in the root document.
return doc.url.String() == refURL.String()
return referenceURIMatch(doc.url, &refURL)
}

// MatchesComponentInRootDocument returns if the given component is identical
// to a component defined in the root document's '#/components/<type>'.
// It returns a reference to the schema in the form
func referenceURIMatch(u1 *url.URL, u2 *url.URL) bool {
s1, s2 := *u1, *u2
if s1.Scheme == "" {
s1.Scheme = "file"
}
if s2.Scheme == "" {
s2.Scheme = "file"
}

return s1.String() == s2.String()
}

// ReferencesComponentInRootDocument returns if the given component reference references
// the same document or element as another component reference in the root document's
// '#/components/<type>'. If it does, it returns the name of it in the form
// '#/components/<type>/NameXXX'
//
// Of course given it a component from the root document will always match.
// Of course given a component from the root document will always match itself.
//
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#relative-references-in-urls
//
// Case 1: Directly via
// Example. Take the spec with directory structure:
//
// ../openapi.yaml#/components/schemas/Record
//
// Case 2: Or indirectly by using a $ref which matches a schema
// in the root document's '#/components/schemas' using the same
// $ref.
//
// In schemas/record.yaml
//
// $ref: ./record.yaml
// openapi.yaml
// schemas/
// ├─ record.yaml
// ├─ records.yaml
//
// In openapi.yaml
// In openapi.yaml we have:
//
// components:
// schemas:
// Record:
// $ref: schemas/record.yaml
func MatchesComponentInRootDocument(doc *T, ref componentRef) (string, bool) {
//
// Case 1: records.yml references a component in the root document
//
// $ref: ../openapi.yaml#/components/schemas/Record
//
// This would return...
//
// #/components/schemas/Record
//
// Case 2: records.yml indirectly refers to the same schema
// as a schema the root document's '#/components/schemas'.
//
// $ref: ./record.yaml
//
// This would also return...
//
// #/components/schemas/Record
func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool) {
if ref == nil || ref.RefString() == "" {
return "", false
}

// Case 1:
// Something like: ../another-folder/document.json#/myElement
if isRemoteReference(ref.RefString()) && isRootComponentReference(ref.RefString(), ref.ComponentType()) {
if isRemoteReference(ref.RefString()) && isRootComponentReference(ref.RefString(), ref.CollectionName()) {
// Determine if it is *this* root doc.
if referencesRootDocument(doc, ref) {
_, name, _ := strings.Cut(ref.RefString(), path.Join("#/components/", ref.ComponentType()))
_, name, _ := strings.Cut(ref.RefString(), path.Join("#/components/", ref.CollectionName()))

return path.Join("#/components/", ref.ComponentType(), name), true
return path.Join("#/components/", ref.CollectionName(), name), true
}
}

// If there are no schemas defined in the root document return early.
if doc.Components == nil || doc.Components.Schemas == nil {
if doc.Components == nil {
return "", false
}

var components map[string]componentRef

switch ref.CollectionName() {
case "callbacks":
components = make(map[string]componentRef, len(doc.Components.Callbacks))
for k, x := range doc.Components.Callbacks {
components[k] = x
}
case "examples":
components = make(map[string]componentRef, len(doc.Components.Examples))
for k, x := range doc.Components.Examples {
components[k] = x
}
case "headers":
components = make(map[string]componentRef, len(doc.Components.Headers))
for k, x := range doc.Components.Headers {
components[k] = x
}
case "links":
components = make(map[string]componentRef, len(doc.Components.Links))
for k, x := range doc.Components.Links {
components[k] = x
}
case "parameters":
components = make(map[string]componentRef, len(doc.Components.Parameters))
for k, x := range doc.Components.Parameters {
components[k] = x
}
case "requestBodies":
components = make(map[string]componentRef, len(doc.Components.RequestBodies))
for k, x := range doc.Components.RequestBodies {
components[k] = x
}
case "responses":
components = make(map[string]componentRef, len(doc.Components.Responses))
for k, x := range doc.Components.Responses {
components[k] = x
}
case "schemas":
components = make(map[string]componentRef, len(doc.Components.Schemas))
for k, x := range doc.Components.Schemas {
components[k] = x
}
case "securitySchemes":
components = make(map[string]componentRef, len(doc.Components.SecuritySchemes))
for k, x := range doc.Components.SecuritySchemes {
components[k] = x
}
}

// Case 2:
// Something like: ../openapi.yaml#/components/schemas/myElement
for name, s := range doc.Components.Schemas {
for name, s := range components {
// Must be a reference to a YAML file.
if !isWholeDocumentReference(s.Ref) {
if !isWholeDocumentReference(s.RefString()) {
continue
}

Expand All @@ -150,7 +228,7 @@ func MatchesComponentInRootDocument(doc *T, ref componentRef) (string, bool) {
}

// Transform the remote ref to the equivalent schema in the root document.
return path.Join("#/components/", ref.ComponentType(), name), true
return path.Join("#/components/", ref.CollectionName(), name), true
}

return "", false
Expand Down
Loading

0 comments on commit c3fe41b

Please sign in to comment.