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 21, 2023
1 parent 98141fa commit 272b1de
Show file tree
Hide file tree
Showing 39 changed files with 1,108 additions and 331 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ jobs:

- uses: actions/checkout@v2

- name: Check codegen
run: |
./refs.sh | tee openapi3/refs.go
git --no-pager diff --exit-code
- run: ./refs.sh | tee openapi3/refs.go
- run: git --no-pager diff --exit-code

- run: ./maps.sh
- run: git --no-pager diff --exit-code

- name: Check docsgen
run: ./docs.sh
- run: ./docs.sh

- run: go mod download && go mod tidy && go mod verify
- run: git --no-pager diff --exit-code
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,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
203 changes: 203 additions & 0 deletions maps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/bin/bash -eux
set -o pipefail

maplike=./openapi3/maplike.go
maplike_test=./openapi3/maplike_test.go

types=()
types+=('*Responses')
types+=('*Callback')
types+=('*Paths')

value_types=()
value_types+=('*ResponseRef')
value_types+=('*PathItem')
value_types+=('*PathItem')

names=()
names+=('responses')
names+=('callback')
names+=('paths')

[[ "${#types[@]}" = "${#value_types[@]}" ]]
[[ "${#types[@]}" = "${#names[@]}" ]]
[[ "${#types[@]}" = "$(git grep -InF ' m map[string]*' -- openapi3/loader.go | wc -l)" ]]

cat <<EOF >"$maplike"
package openapi3
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/go-openapi/jsonpointer"
)
EOF


cat <<EOF >"$maplike_test"
package openapi3
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMaplikeMethods(t *testing.T) {
t.Parallel()
EOF

for i in "${!types[@]}"; do
type=${types[$i]}
value_type=${value_types[$i]}
name=${names[$i]}

cat <<EOF >>"$maplike"
// Get returns the ${name} for key and the presence bit
func (${name} ${type}) Get(key string) (${value_type}, bool) {
if ${name}.Len() == 0 {
return nil, false
}
v, ok := ${name}.m[key]
return v, ok
}
// Value returns the ${name} for key or nil
func (${name} ${type}) Value(key string) ${value_type} {
if ${name}.Len() == 0 {
return nil
}
return ${name}.m[key]
}
// Set adds or replaces key 'key' of '${name}' with 'value'.
// Note: '${name}' MUST be non-nil
func (${name} ${type}) Set(key string, value ${value_type}) {
if ${name}.m == nil {
${name}.m = make(map[string]${value_type})
}
${name}.m[key] = value
}
// Len returns the amount of keys in ${name} excluding ${name}.Extensions.
func (${name} ${type}) Len() int {
if ${name} == nil {
return 0
}
return len(${name}.m)
}
// Map returns ${name} as a 'map'.
// Note: iteration on Go maps is not ordered.
func (${name} ${type}) Map() map[string]${value_type} {
if ${name}.Len() == 0 {
return nil
}
return ${name}.m
}
var _ jsonpointer.JSONPointable = (${type})(nil)
// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable
func (${name} ${type}) JSONLookup(token string) (interface{}, error) {
v, ok := ${name}.Get(token)
if !ok {
return nil, fmt.Errorf("invalid token reference: %q", token)
}
if v != nil {
if v.Ref != "" {
return &Ref{Ref: v.Ref}, nil
}
return v, nil
}
vv, _, err := jsonpointer.GetForToken(${name}.Extensions, token)
return vv, err
}
// MarshalJSON returns the JSON encoding of ${type#'*'}.
func (${name} ${type#'*'}) MarshalJSON() ([]byte, error) {
m := make(map[string]interface{}, ${name}.Len()+len(${name}.Extensions))
for k, v := range ${name}.Extensions {
m[k] = v
}
for k, v := range ${name}.Map() {
m[k] = v
}
return json.Marshal(m)
}
// UnmarshalJSON sets ${type#'*'} to a copy of data.
func (${name} ${type}) 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 := ${type#'*'}{
Extensions: make(map[string]interface{}),
m: make(map[string]${value_type}, 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 vv ${value_type#'*'}
if err = vv.UnmarshalJSON(data); err != nil {
return
}
x.m[k] = &vv
}
*${name} = x
return
}
EOF

cat <<EOF >>"$maplike_test"
t.Run("${type}", func(t *testing.T) {
t.Parallel()
t.Run("nil", func(t *testing.T) {
x := (${type})(nil)
require.Equal(t, 0, x.Len())
require.Equal(t, (map[string]${value_type})(nil), x.Map())
require.Equal(t, (${value_type})(nil), x.Value("key"))
require.Panics(t, func() { x.Set("key", &${value_type#'*'}{}) })
})
t.Run("nonnil", func(t *testing.T) {
x := &${type#'*'}{}
require.Equal(t, 0, x.Len())
require.Equal(t, (map[string]${value_type})(nil), x.Map())
require.Equal(t, (${value_type})(nil), x.Value("key"))
x.Set("key", &${value_type#'*'}{})
require.Equal(t, 1, x.Len())
require.Equal(t, map[string]${value_type}{"key": {}}, x.Map())
require.Equal(t, &${value_type#'*'}{}, x.Value("key"))
})
})
EOF

done

cat <<EOF >>"$maplike_test"
}
EOF
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
14 changes: 9 additions & 5 deletions openapi3/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,23 @@ 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
}

// 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

0 comments on commit 272b1de

Please sign in to comment.