Skip to content

Commit

Permalink
OpenAPI: Adds slices of structs support
Browse files Browse the repository at this point in the history
  • Loading branch information
EwenQuim committed Mar 14, 2024
1 parent e1f84f3 commit 423ca86
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 96 deletions.
14 changes: 11 additions & 3 deletions examples/full-app-gourmet/controller/recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,21 @@ func (rs recipeRessource) newRecipe(c *fuego.ContextWithBody[store.CreateRecipeP
return recipe, nil
}

func (rs recipeRessource) getRecipeWithIngredients(c fuego.ContextNoBody) (store.Recipe, error) {
func (rs recipeRessource) getRecipeWithIngredients(c fuego.ContextNoBody) (store.RecipeWithDosings, error) {
recipe, err := rs.RecipeRepository.GetRecipe(c.Context(), c.PathParam("id"))
if err != nil {
return store.Recipe{}, err
return store.RecipeWithDosings{}, err
}

return recipe, nil
dosings, err := rs.IngredientRepository.GetIngredientsOfRecipe(c.Context(), recipe.ID)
if err != nil {
return store.RecipeWithDosings{}, err
}

return store.RecipeWithDosings{
Recipe: recipe,
Dosings: dosings,
}, nil
}

type RecipeRepository interface {
Expand Down
6 changes: 6 additions & 0 deletions examples/full-app-gourmet/store/models_custom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package store

type RecipeWithDosings struct {
Recipe `json:"recipe"`
Dosings []GetIngredientsOfRecipeRow `json:"dosings"`
}
1 change: 0 additions & 1 deletion openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ func RegisterOpenAPIOperation[T any, B any](s *Server, method, path string) (*op

// Request body
if method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch {

requestBody := &openapi3.RequestBody{
Required: true,
Content: make(map[openapi3.MimeType]openapi3.SchemaObject),
Expand Down
2 changes: 1 addition & 1 deletion openapi3/openapi3.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ type Info struct {
}

type Schema struct {
Type string `json:"type,omitempty" yaml:"type"`
Type OpenAPIType `json:"type,omitempty" yaml:"type"`
Format string `json:"format,omitempty" yaml:"format,omitempty"`
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
Expand Down
14 changes: 7 additions & 7 deletions openapi3/register_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ func TestDocument_RegisterType(t *testing.T) {
s := d.RegisterType(T{})
// will return a schema with a reference to the schema of T
require.Equal(t, "#/components/schemas/T", s.Ref)
require.Equal(t, "object", d.Components.Schemas["T"].Type)
require.Equal(t, "integer", d.Components.Schemas["T"].Properties["B"].Type)
require.Equal(t, "", d.Components.Schemas["T"].Properties["A"].Type)
require.Equal(t, "string", d.Components.Schemas["T"].Properties["S"].Properties["A"].Type)
require.Equal(t, Object, d.Components.Schemas["T"].Type)
require.Equal(t, Integer, d.Components.Schemas["T"].Properties["B"].Type)
require.Equal(t, OpenAPIType(""), d.Components.Schemas["T"].Properties["A"].Type)
require.Equal(t, String, d.Components.Schemas["T"].Properties["S"].Properties["A"].Type)
})

t.Run("array", func(t *testing.T) {
Expand All @@ -34,10 +34,10 @@ func TestDocument_RegisterType(t *testing.T) {
d := NewDocument()
s := d.RegisterType([]S{})
// will return a schema with a reference to the schema of T
require.Equal(t, "array", s.Type)
require.Equal(t, Array, s.Type)
require.Equal(t, "#/components/schemas/S", s.Items.Ref)
require.Equal(t, "object", d.Components.Schemas["S"].Type)
require.Equal(t, "string", d.Components.Schemas["S"].Properties["A"].Type)
require.Equal(t, Object, d.Components.Schemas["S"].Type)
require.Equal(t, String, d.Components.Schemas["S"].Properties["A"].Type)
})
}

Expand Down
117 changes: 65 additions & 52 deletions openapi3/to_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func ToSchema(v any) *Schema {
}

s := Schema{
Type: "object",
Type: Object,
Properties: make(map[string]Schema),
}

Expand All @@ -23,68 +23,48 @@ func ToSchema(v any) *Schema {
value = value.Elem()
}

if value.Kind() == reflect.Slice {
s.Type = "array"
if _, isTime := v.(time.Time); isTime {
s.Type = "string"
s.Format = "date-time"
s.Example = time.RFC3339
return &Schema{
Type: "string",
Format: "date-time",
Example: time.RFC3339,
}
}

switch value.Kind() {
case reflect.Slice, reflect.Array:
s.Type = Array
itemType := value.Type().Elem()
if itemType.Kind() == reflect.Ptr {
itemType = itemType.Elem()
}
one := reflect.New(itemType)
s.Items = ToSchema(one.Interface())
}
s.Example = "[]"
case reflect.Struct:
for i := range value.NumField() {
structField := value.Type().Field(i)

if _, isTime := value.Interface().(time.Time); isTime {
s.Type = "string"
s.Format = "date-time"
s.Example = value.Interface().(time.Time).Format(time.RFC3339)
return &s
}
fieldName := fieldName(structField)

if value.Kind() == reflect.Struct {
// Iterate on fields with reflect
for i := range value.NumField() {
field := value.Field(i)
fieldType := value.Type().Field(i)

// If the field is a struct, we need to dive into it
if field.Kind() == reflect.Struct {
s.Properties[fieldName(fieldType)] = *ToSchema(field.Interface())
} else {
// If the field is a basic type, we can just add it to the properties
fieldTypeType := fieldType.Type.Kind().String()
format := fieldType.Tag.Get("format")
if strings.Contains(fieldTypeType, "int") {
fieldTypeType = "integer"
if format != "" {
format = fieldType.Type.Name()
}
} else if fieldTypeType == "bool" {
fieldTypeType = "boolean"
}
fieldName := fieldName(fieldType)
if strings.Contains(fieldType.Tag.Get("validate"), "required") {
s.Required = append(s.Required, fieldName)
}

fieldSchema := Schema{
Type: fieldTypeType,
Example: fieldType.Tag.Get("example"),
Format: format,
}
parseValidate(&fieldSchema, fieldType.Tag.Get("validate"))
s.Properties[fieldName] = fieldSchema
fieldValue := value.Field(i)

}
}
}
fieldSchema := *ToSchema(fieldValue.Interface())

if !(value.Kind() == reflect.Struct || value.Kind() == reflect.Slice) {
s.Type = value.Kind().String()
if strings.Contains(s.Type, "int") {
s.Type = "integer"
} else if s.Type == "bool" {
s.Type = "boolean"
// Parse struct tags
if strings.Contains(structField.Tag.Get("validate"), "required") {
s.Required = append(s.Required, fieldName)
}
parseValidate(&fieldSchema, structField.Tag.Get("validate"))
fieldSchema.Example = structField.Tag.Get("example")
fieldSchema.Format = structField.Tag.Get("format")
s.Properties[fieldName] = fieldSchema
}
default:
s.Type = kindToType(value.Kind())
}

return &s
Expand All @@ -97,3 +77,36 @@ func fieldName(s reflect.StructField) string {
}
return s.Name
}

type OpenAPIType string

const (
Invalid OpenAPIType = ""
String OpenAPIType = "string"
Integer OpenAPIType = "integer"
Number OpenAPIType = "number"
Boolean OpenAPIType = "boolean"
Array OpenAPIType = "array"
Object OpenAPIType = "object"
)

func kindToType(kind reflect.Kind) OpenAPIType {
switch kind {
case reflect.String:
return String
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return Integer
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return Integer
case reflect.Float32, reflect.Float64:
return Number
case reflect.Bool:
return Boolean
case reflect.Slice, reflect.Array:
return Array
case reflect.Struct:
return Object
default:
return Invalid
}
}
93 changes: 61 additions & 32 deletions openapi3/to_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (
func TestToSchema(t *testing.T) {
t.Run("string", func(t *testing.T) {
s := ToSchema("")
require.Equal(t, "string", s.Type)
require.Equal(t, String, s.Type)
})

t.Run("alias to string", func(t *testing.T) {
type S string
s := ToSchema(S(""))
require.Equal(t, "string", s.Type)
require.Equal(t, String, s.Type)
})

t.Run("struct with a field alias to string", func(t *testing.T) {
Expand All @@ -28,23 +28,23 @@ func TestToSchema(t *testing.T) {
}

s := ToSchema(S{})
require.Equal(t, "object", s.Type)
require.Equal(t, "string", s.Properties["A"].Type)
require.Equal(t, Object, s.Type)
require.Equal(t, String, s.Properties["A"].Type)
})

t.Run("int", func(t *testing.T) {
s := ToSchema(0)
require.Equal(t, "integer", s.Type)
require.Equal(t, Integer, s.Type)
})

t.Run("bool", func(t *testing.T) {
s := ToSchema(false)
require.Equal(t, "boolean", s.Type)
require.Equal(t, Boolean, s.Type)
})

t.Run("time", func(t *testing.T) {
s := ToSchema(time.Now())
require.Equal(t, "string", s.Type)
require.Equal(t, String, s.Type)
})

t.Run("struct", func(t *testing.T) {
Expand All @@ -57,13 +57,13 @@ func TestToSchema(t *testing.T) {
}
}
s := ToSchema(S{})
require.Equal(t, "object", s.Type)
require.Equal(t, "string", s.Properties["a"].Type)
require.Equal(t, "integer", s.Properties["B"].Type)
require.Equal(t, "boolean", s.Properties["C"].Type)
require.Equal(t, Object, s.Type)
require.Equal(t, String, s.Properties["a"].Type)
require.Equal(t, Integer, s.Properties["B"].Type)
require.Equal(t, Boolean, s.Properties["C"].Type)
require.Equal(t, []string{"a"}, s.Required)
require.Equal(t, "object", s.Properties["Nested"].Type)
require.Equal(t, "string", s.Properties["Nested"].Properties["C"].Type)
require.Equal(t, Object, s.Properties["Nested"].Type)
require.Equal(t, String, s.Properties["Nested"].Properties["C"].Type)

gotSchema, err := json.Marshal(s)
require.NoError(t, err)
Expand Down Expand Up @@ -94,12 +94,12 @@ func TestToSchema(t *testing.T) {
}
}
s := ToSchema(&S{})
require.Equal(t, "object", s.Type)
require.Equal(t, "string", s.Properties["A"].Type)
require.Equal(t, "integer", s.Properties["B"].Type)
require.Equal(t, Object, s.Type)
require.Equal(t, String, s.Properties["A"].Type)
require.Equal(t, Integer, s.Properties["B"].Type)
// TODO require.Equal(t, []string{"A", "B", "Nested"}, s.Required)
require.Equal(t, "object", s.Properties["Nested"].Type)
require.Equal(t, "string", s.Properties["Nested"].Properties["C"].Type)
require.Equal(t, Object, s.Properties["Nested"].Type)
require.Equal(t, String, s.Properties["Nested"].Properties["C"].Type)

gotSchema, err := json.Marshal(s)
require.NoError(t, err)
Expand All @@ -121,28 +121,28 @@ func TestToSchema(t *testing.T) {

t.Run("slice of strings", func(t *testing.T) {
s := ToSchema([]string{})
require.Equal(t, "array", s.Type)
require.Equal(t, "string", s.Items.Type)
require.Equal(t, Array, s.Type)
require.Equal(t, String, s.Items.Type)
})

t.Run("slice of structs", func(t *testing.T) {
type S struct {
A string
}
s := ToSchema([]S{})
require.Equal(t, "array", s.Type)
require.Equal(t, "object", s.Items.Type)
require.Equal(t, "string", s.Items.Properties["A"].Type)
require.Equal(t, Array, s.Type)
require.Equal(t, Object, s.Items.Type)
require.Equal(t, String, s.Items.Properties["A"].Type)
})

t.Run("slice of ptr to struct", func(t *testing.T) {
type S struct {
A string
}
s := ToSchema([]*S{})
require.Equal(t, "array", s.Type)
require.Equal(t, "object", s.Items.Type)
require.Equal(t, "string", s.Items.Properties["A"].Type)
require.Equal(t, Array, s.Type)
require.Equal(t, Object, s.Items.Type)
require.Equal(t, String, s.Items.Properties["A"].Type)
})

t.Run("embedded struct", func(t *testing.T) {
Expand All @@ -154,16 +154,45 @@ func TestToSchema(t *testing.T) {
B int
}
s := ToSchema(T{})
require.Equal(t, "object", s.Type)
require.Equal(t, "", s.Properties["A"].Type)
require.Equal(t, "object", s.Properties["S"].Type)
require.Equal(t, "string", s.Properties["S"].Properties["A"].Type)
require.Equal(t, "integer", s.Properties["B"].Type)
require.Equal(t, Object, s.Type)
require.Equal(t, OpenAPIType(""), s.Properties["A"].Type)
require.Equal(t, Object, s.Properties["S"].Type)
require.Equal(t, String, s.Properties["S"].Properties["A"].Type)
require.Equal(t, Integer, s.Properties["B"].Type)
})

t.Run("struct of slices of structs", func(t *testing.T) {
type S struct {
A string
}

type T struct {
SliceOfS []S
}

tt := ToSchema(T{})
require.Equal(t, Object, tt.Type)
require.Equal(t, Array, tt.Properties["SliceOfS"].Type)
require.NotNil(t, tt.Properties["SliceOfS"].Items)
require.Equal(t, Array, tt.Properties["SliceOfS"].Type)
require.Equal(t, Object, tt.Properties["SliceOfS"].Items.Type)
require.Equal(t, String, tt.Properties["SliceOfS"].Items.Properties["A"].Type)
})

t.Run("struct with ptrs properties", func(t *testing.T) {
t.Skip("TODO")
type S struct {
A *string
B *int
}
s := ToSchema(S{})
require.Equal(t, Object, s.Type)
require.Equal(t, String, s.Properties["A"].Type)
require.Equal(t, Integer, s.Properties["B"].Type)
})
}

func TestFieldName(t *testing.T) {

t.Run("no tag", func(t *testing.T) {
require.Equal(t, "A", fieldName(reflect.StructField{
Name: "A",
Expand Down

0 comments on commit 423ca86

Please sign in to comment.