Skip to content

Commit

Permalink
openapi3: add support for extensions on the few types left
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
  • Loading branch information
fenollp committed Jun 18, 2023
1 parent 356060b commit 62b76dd
Show file tree
Hide file tree
Showing 48 changed files with 1,022 additions and 465 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ This will change the schema validation errors to return only the `Reason` field,

## Sub-v0 breaking API changes

### next
* `(openapi3.Responses).Get(int)` renamed to `(*openapi3.Responses).Status(int)`
* `Responses` field of `openapi3.Components` is now a pointer
* `Paths` field of `openapi3.T` is now a pointer
* Package `openapi3`'s `NewResponses() *Responses` function was renamed to `NewEmptyResponses`

### v0.116.0
* Dropped `openapi3filter.DefaultOptions`. Use `&openapi3filter.Options{}` directly instead.

Expand Down
2 changes: 1 addition & 1 deletion openapi2conv/issue558_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ paths:
`
doc3, err := v2v3YAML([]byte(spec))
require.NoError(t, err)
require.NotEmpty(t, doc3.Paths["/test"].Get.Deprecated)
require.NotEmpty(t, doc3.Paths.Value("/test").Get.Deprecated)
_, err = yaml.Marshal(doc3)
require.NoError(t, err)

Expand Down
4 changes: 2 additions & 2 deletions openapi2conv/issue573_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ func TestIssue573(t *testing.T) {

// Make sure the response content appears for each mime-type originally
// appeared in "produces".
pingGetContent := v3.Paths["/ping"].Get.Responses["200"].Value.Content
pingGetContent := v3.Paths.Value("/ping").Get.Responses.Value("200").Value.Content
require.Len(t, pingGetContent, 2)
require.Contains(t, pingGetContent, "application/toml")
require.Contains(t, pingGetContent, "application/xml")

// Is "produces" is not explicitly specified, default to "application/json".
pingPostContent := v3.Paths["/ping"].Post.Responses["200"].Value.Content
pingPostContent := v3.Paths.Value("/ping").Post.Responses.Value("200").Value.Content
require.Len(t, pingPostContent, 1)
require.Contains(t, pingPostContent, "application/json")
}
28 changes: 15 additions & 13 deletions openapi2conv/openapi2_conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,24 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) {
}

if paths := doc2.Paths; len(paths) != 0 {
doc3Paths := make(map[string]*openapi3.PathItem, len(paths))
doc3.Paths = openapi3.NewPathsWithCapacity(len(paths))
for path, pathItem := range paths {
r, err := ToV3PathItem(doc2, doc3.Components, pathItem, doc2.Consumes)
if err != nil {
return nil, err
}
doc3Paths[path] = r
doc3.Paths.Set(path, r)
}
doc3.Paths = doc3Paths
}

if responses := doc2.Responses; len(responses) != 0 {
doc3.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses))
doc3.Components.Responses = openapi3.NewResponsesWithCapacity(len(responses))
for k, response := range responses {
r, err := ToV3Response(response, doc2.Produces)
if err != nil {
return nil, err
}
doc3.Components.Responses[k] = r
doc3.Components.Responses.Set(k, r)
}
}

Expand Down Expand Up @@ -186,15 +185,14 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *
}

if responses := operation.Responses; responses != nil {
doc3Responses := make(openapi3.Responses, len(responses))
doc3.Responses = openapi3.NewResponsesWithCapacity(len(responses))
for k, response := range responses {
doc3, err := ToV3Response(response, operation.Produces)
responseRef3, err := ToV3Response(response, operation.Produces)
if err != nil {
return nil, err
}
doc3Responses[k] = doc3
doc3.Responses.Set(k, responseRef3)
}
doc3.Responses = doc3Responses
}
return doc3, nil
}
Expand Down Expand Up @@ -605,13 +603,16 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) {
}
}
}

if isHTTPS {
doc2.Schemes = append(doc2.Schemes, "https")
}
if isHTTP {
doc2.Schemes = append(doc2.Schemes, "http")
}
for path, pathItem := range doc3.Paths {

// TODO: add paths extensions to doc2
for path, pathItem := range doc3.Paths.Map() {
if pathItem == nil {
continue
}
Expand Down Expand Up @@ -1041,9 +1042,10 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components
return result, nil
}

func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) {
v2Responses := make(map[string]*openapi2.Response, len(responses))
for k, response := range responses {
func FromV3Responses(responses *openapi3.Responses, components *openapi3.Components) (map[string]*openapi2.Response, error) {
// TODO: add responses extensions to doc2
v2Responses := make(map[string]*openapi2.Response, responses.Len())
for k, response := range responses.Map() {
r, err := FromV3Response(response, components)
if err != nil {
return nil, err
Expand Down
136 changes: 130 additions & 6 deletions openapi3/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package openapi3

import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"

"github.com/go-openapi/jsonpointer"
)
Expand All @@ -12,7 +14,7 @@ type Callbacks map[string]*CallbackRef

var _ jsonpointer.JSONPointable = (*Callbacks)(nil)

// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable
func (c Callbacks) JSONLookup(token string) (interface{}, error) {
ref, ok := c[token]
if ref == nil || !ok {
Expand All @@ -27,19 +29,141 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) {

// Callback is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object
type Callback map[string]*PathItem
type Callback struct {
Extensions map[string]interface{} `json:"-" yaml:"-"`

m map[string]*PathItem
}

var _ jsonpointer.JSONPointable = (*Callback)(nil)

// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable
func (callback *Callback) JSONLookup(token string) (interface{}, error) {
pathItem, ok := callback.Get(token)

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This definition of ok is never used.
if true {
panic(fmt.Sprintf(">>> token=%q pathItem=%v callback=%#v", token, pathItem, callback))
}
if !ok {
return nil, fmt.Errorf("invalid token reference: %q", token)
}

if pathItem != nil {
if pathItem.Ref != "" {
return &Ref{Ref: pathItem.Ref}, nil
}
return pathItem, nil
}

v, _, err := jsonpointer.GetForToken(callback.Extensions, token)
return v, err
}

// Get returns the callback for key and the presence bit
func (callback *Callback) Get(key string) (*PathItem, bool) {
if callback == nil || callback.m == nil {
return nil, false
}
v, ok := callback.m[key]
return v, ok
}

// Value returns the callback for key or nil
func (callback *Callback) Value(key string) *PathItem {
if callback == nil || callback.m == nil {
return nil
}
return callback.m[key]
}

// Set adds or replaces the callback value for key
func (callback *Callback) Set(key string, value *PathItem) {
if callback == nil {
callback = &Callback{}
}
if callback.m == nil {
callback.m = make(map[string]*PathItem)
}
callback.m[key] = value
}

// Len returns the amount of callbacks
func (callback *Callback) Len() int {
if callback == nil || callback.m == nil {
return 0
}
return len(callback.m)
}

// Map returns callbacks as an unordered map
func (callback *Callback) Map() map[string]*PathItem {
if callback == nil {
return nil
}
return callback.m
}

// MarshalJSON returns the JSON encoding of Callback.
func (callback Callback) MarshalJSON() ([]byte, error) {
m := make(map[string]interface{}, callback.Len()+len(callback.Extensions))
for k, v := range callback.Extensions {
m[k] = v
}
for k, v := range callback.Map() {
m[k] = v
}
return json.Marshal(m)
}

// UnmarshalJSON sets Callback to a copy of data.
func (callback *Callback) UnmarshalJSON(data []byte) (err error) {
var m map[string]interface{}
if err = json.Unmarshal(data, &m); err != nil {
return
}

ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)

x := Callback{
Extensions: make(map[string]interface{}),
m: make(map[string]*PathItem, len(m)),
}

for _, k := range ks {
v := m[k]
if strings.HasPrefix(k, "x-") {
x.Extensions[k] = v
continue
}

var data []byte
if data, err = json.Marshal(v); err != nil {
return
}
var pathItem PathItem
if err = pathItem.UnmarshalJSON(data); err != nil {
return
}
x.m[k] = &pathItem
}
*callback = x
return
}

// Validate returns an error if Callback does not comply with the OpenAPI spec.
func (callback Callback) Validate(ctx context.Context, opts ...ValidationOption) error {
func (callback *Callback) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)

keys := make([]string, 0, len(callback))
for key := range callback {
keys := make([]string, 0, callback.Len())
for key := range callback.Map() {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
v := callback[key]
v := callback.m[key]
if err := v.Validate(ctx); err != nil {
return err
}
Expand Down
10 changes: 5 additions & 5 deletions openapi3/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Components struct {
Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
RequestBodies RequestBodies `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"`
Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"`
Responses *Responses `json:"responses,omitempty" yaml:"responses,omitempty"`
SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"`
Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"`
Links Links `json:"links,omitempty" yaml:"links,omitempty"`
Expand Down Expand Up @@ -46,7 +46,7 @@ func (components Components) MarshalJSON() ([]byte, error) {
if x := components.RequestBodies; len(x) != 0 {
m["requestBodies"] = x
}
if x := components.Responses; len(x) != 0 {
if x := components.Responses; x.Len() != 0 {
m["responses"] = x
}
if x := components.SecuritySchemes; len(x) != 0 {
Expand Down Expand Up @@ -134,13 +134,13 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp
}
}

responses := make([]string, 0, len(components.Responses))
for name := range components.Responses {
responses := make([]string, 0, components.Responses.Len())
for name := range components.Responses.Map() {
responses = append(responses, name)
}
sort.Strings(responses)
for _, k := range responses {
v := components.Responses[k]
v := components.Responses.Value(k)
if err = ValidateIdentifier(k); err != nil {
return fmt.Errorf("response %q: %w", k, err)
}
Expand Down
56 changes: 56 additions & 0 deletions openapi3/contact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package openapi3

import (
"context"
"encoding/json"
)

// Contact is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object
type Contact struct {
Extensions map[string]interface{} `json:"-" yaml:"-"`

Name string `json:"name,omitempty" yaml:"name,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
}

// MarshalJSON returns the JSON encoding of Contact.
func (contact Contact) MarshalJSON() ([]byte, error) {
m := make(map[string]interface{}, 3+len(contact.Extensions))
for k, v := range contact.Extensions {
m[k] = v
}
if x := contact.Name; x != "" {
m["name"] = x
}
if x := contact.URL; x != "" {
m["url"] = x
}
if x := contact.Email; x != "" {
m["email"] = x
}
return json.Marshal(m)
}

// UnmarshalJSON sets Contact to a copy of data.
func (contact *Contact) UnmarshalJSON(data []byte) error {
type ContactBis Contact
var x ContactBis
if err := json.Unmarshal(data, &x); err != nil {
return err
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, "name")
delete(x.Extensions, "url")
delete(x.Extensions, "email")
*contact = Contact(x)
return nil
}

// Validate returns an error if Contact does not comply with the OpenAPI spec.
func (contact *Contact) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)

return validateExtensions(ctx, contact.Extensions)
}
2 changes: 1 addition & 1 deletion openapi3/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Examples map[string]*ExampleRef

var _ jsonpointer.JSONPointable = (*Examples)(nil)

// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable
func (e Examples) JSONLookup(token string) (interface{}, error) {
ref, ok := e[token]
if ref == nil || !ok {
Expand Down

0 comments on commit 62b76dd

Please sign in to comment.