Skip to content

Commit

Permalink
Add openapi3.Schema.OrderedPropertyKeys
Browse files Browse the repository at this point in the history
This commit adds the `OrderedPropertyKeys` method to the
`openapi3.Schema`:

    OrderedPropertyKeys returns the keys of the properties in the order
    they were defined. This is useful for generating code that needs to
    iterate over the properties in a consistent order. If the keys could
    not be extracted for some reason, then this method automatically
    sorts the keys to be deterministic.

This is done via a temporary fork of the YAML-to-JSON transformation library.
It will not be ready until these PRs are merged in this order:

    - silasdavis/yaml#1
    - ghodss/yaml#62
  • Loading branch information
diamondburned committed Aug 12, 2024
1 parent e9b36da commit f9deb57
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 3 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ module github.com/getkin/kin-openapi

go 1.14

replace github.com/ghodss/yaml/v2 => github.com/diamondburned/yaml/v2 v2.0.0-20240812065612-baf990d70122

require (
github.com/ghodss/yaml v1.0.0
github.com/ghodss/yaml/v2 v2.0.0-00010101000000-000000000000
github.com/go-openapi/jsonpointer v0.19.5
github.com/gorilla/mux v1.8.0
github.com/stretchr/testify v1.5.1
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/yaml/v2 v2.0.0-20240812065612-baf990d70122 h1:hOA7Z6xhY5sn50zMsuY9JhA0A1QMiO0z/Ltx7ZcqUCM=
github.com/diamondburned/yaml/v2 v2.0.0-20240812065612-baf990d70122/go.mod h1:KkR1H6NtyEqVsGChMAaRwn4BkIX0dG683i7NgqX947Y=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
Expand Down Expand Up @@ -29,3 +31,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
46 changes: 46 additions & 0 deletions jsoninfo/orderedmap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package jsoninfo

import (
"bytes"
"encoding/json"
"fmt"
"log"
)

// ExtractObjectKeys extracts the keys of an object in a JSON string. The keys
// are returned in the order they appear in the JSON string.
func ExtractObjectKeys(b []byte) ([]string, error) {
if !bytes.HasPrefix(b, []byte{'{'}) {
return nil, fmt.Errorf("expected '{' at start of JSON object")
}

dec := json.NewDecoder(bytes.NewReader(b))
var keys []string

for dec.More() {
// Read prop name
t, err := dec.Token()
if err != nil {
log.Printf("Err: %v", err)
break
}

name, ok := t.(string)
if !ok {
continue // May be a delimeter
}

keys = append(keys, name)

var whatever nullMessage
dec.Decode(&whatever)
}

return keys, nil
}

// nullMessage implements json.Unmarshaler and does nothing with the given
// value.
type nullMessage struct{}

func (*nullMessage) UnmarshalJSON(data []byte) error { return nil }
23 changes: 23 additions & 0 deletions jsoninfo/orderedmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package jsoninfo

import (
"reflect"
"testing"
)

func TestExtractObjectKeys(t *testing.T) {
const j = `{
"foo": {"bar": 1},
"baz": "qux",
"quux": "quuz"
}`

keys, err := ExtractObjectKeys([]byte(j))
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(keys, []string{"foo", "baz", "quux"}) {
t.Fatalf("expected %v, got %v", []string{"foo", "baz", "quux"}, keys)
}
}
2 changes: 1 addition & 1 deletion openapi3/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"strconv"
"strings"

"github.com/ghodss/yaml"
"github.com/ghodss/yaml/v2"
)

func foundUnresolvedRef(ref string) error {
Expand Down
44 changes: 42 additions & 2 deletions openapi3/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"math"
"math/big"
"regexp"
"sort"
"strconv"
"unicode/utf16"

Expand Down Expand Up @@ -150,14 +151,18 @@ type Schema struct {
// Object
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
propertyKeys []string // order kept
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order...
AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // ...for multijson
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
}

var _ jsonpointer.JSONPointable = (*Schema)(nil)
var (
_ jsonpointer.JSONPointable = (*Schema)(nil)
_ json.Unmarshaler = (*Schema)(nil)
)

func NewSchema() *Schema {
return &Schema{}
Expand All @@ -168,7 +173,42 @@ func (schema *Schema) MarshalJSON() ([]byte, error) {
}

func (schema *Schema) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, schema)
if err := jsoninfo.UnmarshalStrictStruct(data, schema); err != nil {
return err
}

var rawProperties struct {
Properties json.RawMessage `json:"properties"`
}

if err := json.Unmarshal(data, &rawProperties); err != nil {
return fmt.Errorf("failed to extract raw schema properties: %w", err)
}

if schema.Type == "object" && rawProperties.Properties != nil {
keys, _ := jsoninfo.ExtractObjectKeys(rawProperties.Properties)
schema.propertyKeys = keys
}

return nil
}

// OrderedPropertyKeys returns the keys of the properties in the order they were
// defined. This is useful for generating code that needs to iterate over the
// properties in a consistent order. If the keys could not be extracted for some
// reason, then this method automatically sorts the keys to be deterministic.
func (schema Schema) OrderedPropertyKeys() []string {
if schema.propertyKeys != nil {
return schema.propertyKeys
}

keys := make([]string, 0, len(schema.Properties))
for k := range schema.Properties {
keys = append(keys, k)
}

sort.Strings(keys)
return keys
}

func (schema Schema) JSONLookup(token string) (interface{}, error) {
Expand Down
20 changes: 20 additions & 0 deletions openapi3/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,26 @@ components:
require.Contains(t, err.Error(), `Error at "/ownerName": Doesn't match schema "not"`)
}

func TestSchemaOrderedProperties(t *testing.T) {
const api = `
openapi: "3.0.1"
components:
schemas:
Pet:
properties:
z_name:
type: string
a_ownerName:
not:
type: boolean
type: object
`
s, err := NewLoader().LoadFromData([]byte(api))
require.NoError(t, err)
require.NotNil(t, s)
require.Equal(t, []string{"z_name", "a_ownerName"}, s.Components.Schemas["Pet"].Value.propertyKeys)
}

func TestValidationFailsOnInvalidPattern(t *testing.T) {
schema := Schema{
Pattern: "[",
Expand Down

0 comments on commit f9deb57

Please sign in to comment.