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 invopop/yaml#13 is merged.
  • Loading branch information
diamondburned committed Aug 12, 2024
1 parent af90e9a commit d64ca8a
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 2 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/getkin/kin-openapi

go 1.20

replace github.com/invopop/yaml => github.com/diamondburned/invopop-yaml v0.3.2-0.20240812084936-33aae275be98

require (
github.com/go-openapi/jsonpointer v0.21.0
github.com/gorilla/mux v1.8.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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/invopop-yaml v0.3.2-0.20240812084936-33aae275be98 h1:Z+YbYkmppSq0GA/nr1d8+VVf3UpALBQSyFYixw7gb44=
github.com/diamondburned/invopop-yaml v0.3.2-0.20240812084936-33aae275be98/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
40 changes: 40 additions & 0 deletions openapi3/marsh.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package openapi3

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

"github.com/invopop/yaml"
Expand Down Expand Up @@ -32,3 +34,41 @@ func unmarshal(data []byte, v any) error {
// If both unmarshaling attempts fail, return a new error that includes both errors
return fmt.Errorf("failed to unmarshal data: json error: %v, yaml error: %v", jsonErr, yamlErr)
}

// 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 }
11 changes: 11 additions & 0 deletions openapi3/marsh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,14 @@ paths:
err = doc.Validate(sl.Context)
require.NoError(t, err)
}

func TestExtractObjectKeys(t *testing.T) {
const j = `{
"z_hello": "world",
"a_foo": "bar",
}`

keys, err := extractObjectKeys([]byte(j))
require.NoError(t, err)
require.Equal(t, []string{"z_hello", "a_foo"}, keys)
}
30 changes: 30 additions & 0 deletions openapi3/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ type Schema struct {
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
// list of keys in Properties with preserved order
propertyKeys []string
}

type Types []string
Expand Down Expand Up @@ -410,6 +412,16 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}

if x.Properties != nil {
var rawProperties struct {
Properties json.RawMessage `json:"properties"`
}
if err := json.Unmarshal(data, &rawProperties); err == nil {
x.propertyKeys, _ = extractObjectKeys(rawProperties.Properties)
}
}

_ = json.Unmarshal(data, &x.Extensions)

delete(x.Extensions, "oneOf")
Expand Down Expand Up @@ -476,6 +488,24 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
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
}

// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (schema Schema) JSONLookup(token string) (any, error) {
switch token {
Expand Down
27 changes: 27 additions & 0 deletions openapi3/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1490,3 +1490,30 @@ func TestIssue751(t *testing.T) {
require.NoError(t, schema.VisitJSON(validData))
require.ErrorContains(t, schema.VisitJSON(invalidData), "duplicate items found")
}

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

pet := s.Components.Schemas["Pet"].Value
require.Equal(t, []string{"z_name", "a_ownerName"}, pet.propertyKeys)
require.Equal(t, []string{"z_name", "a_ownerName"}, pet.OrderedPropertyKeys())

pet.propertyKeys = nil
require.Equal(t, []string{"a_ownerName", "z_name"}, pet.OrderedPropertyKeys())
}

0 comments on commit d64ca8a

Please sign in to comment.