Skip to content

Commit

Permalink
feat: combined properties, additional properties in named objects (#85)
Browse files Browse the repository at this point in the history
* feat: allow combined properties and additional properties in toplevel objects

* fix property names

* added tests for generated json un-/marshaling code
  • Loading branch information
megaflo committed Dec 6, 2022
1 parent 317299d commit 27c3f61
Show file tree
Hide file tree
Showing 11 changed files with 638 additions and 11 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test:
@go test -cover ./...
@cd pkg/generators/models/testdata/cases/oneof_mapped_discriminator/expected && go test -cover ./...
@cd pkg/generators/models/testdata/cases/validation/expected && go test -cover ./...
@cd pkg/generators/models/testdata/cases/objects_with_properties_and_additional_properties/expected && go test -cover ./...


${GOBINS}/${.NAME}: $(shell find . -name '*.go') go.*
Expand Down
22 changes: 15 additions & 7 deletions pkg/generators/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ type Model struct {
SpecVersion string
// PackageName is the name of the package used in the Go code
PackageName string
// AdditionalPropertiesGoType is the optional type of additional properties
// that exist _in addition_ to `Properties`
AdditionalPropertiesGoType string
}

// ConvertSpec holds all info to build one As{Type}() function
Expand All @@ -87,6 +90,8 @@ type ConvertSpec struct {
type PropSpec struct {
// Name is a property name in structs, variable name in enums, etc
Name string
// PropertyName is the original name of the property
PropertyName string
// Description used in the comment of the property
Description string
// GoType used for this property (e.g. `string`, `int`, etc)
Expand Down Expand Up @@ -156,6 +161,8 @@ func NewModelFromRef(ref *openapi3.SchemaRef) (model *Model, err error) {
if ref.Value.AdditionalProperties != nil {
model.GoType = "map[string]" + goTypeFromSpec(ref.Value.AdditionalProperties)
}
} else if ref.Value.AdditionalProperties != nil || (ref.Value.AdditionalPropertiesAllowed != nil && *ref.Value.AdditionalPropertiesAllowed) {
model.AdditionalPropertiesGoType = goTypeFromSpec(ref.Value.AdditionalProperties)
}
case len(ref.Value.OneOf) > 0:
model.Kind = OneOf
Expand Down Expand Up @@ -503,13 +510,14 @@ func structPropsFromRef(ref *openapi3.SchemaRef) (specs []PropSpec, imports []st
prop = resolveAllOf(prop, nil)

spec := PropSpec{
Name: tpl.ToPascalCase(name),
Description: prop.Value.Description,
GoType: goTypeFromSpec(prop),
IsRequired: checkIfRequired(name, ref.Value.Required),
IsEnum: len(prop.Value.Enum) > 0,
IsNullable: prop.Value.Nullable,
IsOneOf: prop.Value.OneOf != nil && len(prop.Value.OneOf) > 0,
Name: tpl.ToPascalCase(name),
PropertyName: name,
Description: prop.Value.Description,
GoType: goTypeFromSpec(prop),
IsRequired: checkIfRequired(name, ref.Value.Required),
IsEnum: len(prop.Value.Enum) > 0,
IsNullable: prop.Value.Nullable,
IsOneOf: prop.Value.OneOf != nil && len(prop.Value.OneOf) > 0,
}

if spec.GoType == "time.Time" || spec.GoType == "*time.Time" {
Expand Down
6 changes: 5 additions & 1 deletion pkg/generators/models/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ var cases = []struct {
name: "example of nested allOf to create a merged enum with validation",
directory: "testdata/cases/allOf_enum_merging_and_validation",
},
// 29
{
name: "example of objects with properties and additional properties",
directory: "testdata/cases/objects_with_properties_and_additional_properties",
},
// 30
}

func TestModels(t *testing.T) {
Expand Down
94 changes: 91 additions & 3 deletions pkg/generators/models/templates/model.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
package {{ .PackageName }}

import (
{{- if .AdditionalPropertiesGoType}}
"encoding/json"
{{- end}}

validation "github.com/go-ozzo/ozzo-validation/v4"
{{- range .Imports}}
"{{.}}"
Expand All @@ -27,6 +31,7 @@ var {{ $modelName | firstLower }}{{.Name}}Pattern = regexp.MustCompile(`{{ .Patt
{{- end}}

{{ (printf "%s is an object. %s" .Name .Description) | commentBlock }}
{{- if not .AdditionalPropertiesGoType }}
{{- if not .Properties }}
type {{.Name}} {{ .GoType }}
{{- else }}
Expand All @@ -37,10 +42,84 @@ type {{.Name}} struct {
{{- end}}
}
{{- end}}
{{- else }}
type {{.Name}} struct {
{{.Name}}Properties
AdditionalProperties map[string]{{.AdditionalPropertiesGoType}}
}

type {{.Name}}Properties struct {
{{- range .Properties}}
{{ (printf "%s: %s" .Name .Description) | commentBlock }}
{{.Name}} {{.GoType}} {{.JSONTags}}
{{- end}}
}

// Unmarshal all named properties into {{.Name}}Properties and
// the rest into the AdditionalProperties map
func (obj *{{.Name}}) UnmarshalJSON(data []byte) error {
var generic map[string]json.RawMessage
if err := json.Unmarshal(data, &generic); err != nil {
return err
}

obj.{{.Name}}Properties = {{.Name}}Properties{}

var additionalProperties = make(map[string]{{.AdditionalPropertiesGoType}})
for k, v := range generic {
{{- range .Properties}}
if k == "{{.PropertyName}}" {
if err := json.Unmarshal(v, &(obj.{{$.Name}}Properties.{{.Name}})); err != nil {
return err
}
continue
}
{{- end}}
var prop {{.AdditionalPropertiesGoType}}
if err := json.Unmarshal(v, &prop); err != nil {
return err
}
additionalProperties[k] = prop
}

obj.AdditionalProperties = additionalProperties
return nil
}

// Marshal {{.Name}} by combining the AdditionalProperties with the
// named properties in a single JSON object. Named properties take
// precedence.
func (obj {{.Name}}) MarshalJSON() ([]byte, error) {
props := make(map[string]json.RawMessage)

// start with additional properties so regular properties overwrite them
for k, v := range obj.AdditionalProperties {
if propData, err := json.Marshal(v); err == nil {
props[k] = propData
} else {
return nil, err
}
}

{{- range .Properties}}
if propData, err := json.Marshal(obj.{{$.Name}}Properties.{{.Name}}); err == nil {
props["{{.PropertyName}}"] = propData
} else {
return nil, err
}
{{- end}}

data, err := json.Marshal(props)
return data, err
}
{{- end }}

{{- $modelPropertiesName := $modelName }}
{{- if .AdditionalPropertiesGoType }}
{{- $modelPropertiesName := print $modelName "Properties" }}
{{- end }}
// Validate implements basic validation for this model
func (m {{$modelName}}) Validate() error {
func (m {{$modelPropertiesName}}) Validate() error {
return validation.Errors{
{{- range .Properties}}
{{- if .NeedsValidation }}
Expand Down Expand Up @@ -76,12 +155,21 @@ func (m {{$modelName}}) Validate() error {
}
{{ range .Properties}}
// Get{{.Name}} returns the {{.Name}} property
func (m {{$modelName}}) Get{{.Name}}() {{.GoType}} {
func (m {{$modelPropertiesName}}) Get{{.Name}}() {{.GoType}} {
return m.{{.Name}}
}

// Set{{.Name}} sets the {{.Name}} property
func (m *{{$modelName}}) Set{{.Name}}(val {{.GoType}}) {
func (m *{{$modelPropertiesName}}) Set{{.Name}}(val {{.GoType}}) {
m.{{.Name}} = val
}
{{ end}}
{{- if .AdditionalPropertiesGoType }}
func (m *{{$modelName}}) GetAdditionalProperties() map[string]{{.AdditionalPropertiesGoType}} {
return m.AdditionalProperties
}

func (m *{{$modelName}}) SetAdditionalProperties(val map[string]{{.AdditionalPropertiesGoType}}) {
m.AdditionalProperties = val
}
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
openapi: 3.0.0
info:
version: 0.1.0
title: Test

components:
schemas:
Foo:
type: object
properties:
bar:
type: integer
requiredProperties:
- bar
additionalProperties: true
FooBar:
type: object
properties:
baz:
type: string
additionalProperties:
type: array
items:
type: object
properties:
test:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// This file is auto-generated, DO NOT EDIT.
//
// Source:
//
// Title: Test
// Version: 0.1.0
package generatortest

import (
"encoding/json"

validation "github.com/go-ozzo/ozzo-validation/v4"
)

// Foo is an object.
type Foo struct {
FooProperties
AdditionalProperties map[string]interface{}
}

type FooProperties struct {
// Bar:
Bar int32 `json:"bar,omitempty"`
}

// Unmarshal all named properties into FooProperties and
// the rest into the AdditionalProperties map
func (obj *Foo) UnmarshalJSON(data []byte) error {
var generic map[string]json.RawMessage
if err := json.Unmarshal(data, &generic); err != nil {
return err
}

obj.FooProperties = FooProperties{}

var additionalProperties = make(map[string]interface{})
for k, v := range generic {
if k == "bar" {
if err := json.Unmarshal(v, &(obj.FooProperties.Bar)); err != nil {
return err
}
continue
}
var prop interface{}
if err := json.Unmarshal(v, &prop); err != nil {
return err
}
additionalProperties[k] = prop
}

obj.AdditionalProperties = additionalProperties
return nil
}

// Marshal Foo by combining the AdditionalProperties with the
// named properties in a single JSON object. Named properties take
// precedence.
func (obj Foo) MarshalJSON() ([]byte, error) {
props := make(map[string]json.RawMessage)

// start with additional properties so regular properties overwrite them
for k, v := range obj.AdditionalProperties {
if propData, err := json.Marshal(v); err == nil {
props[k] = propData
} else {
return nil, err
}
}
if propData, err := json.Marshal(obj.FooProperties.Bar); err == nil {
props["bar"] = propData
} else {
return nil, err
}

data, err := json.Marshal(props)
return data, err
}

// Validate implements basic validation for this model
func (m Foo) Validate() error {
return validation.Errors{}.Filter()
}

// GetBar returns the Bar property
func (m Foo) GetBar() int32 {
return m.Bar
}

// SetBar sets the Bar property
func (m *Foo) SetBar(val int32) {
m.Bar = val
}

func (m *Foo) GetAdditionalProperties() map[string]interface{} {
return m.AdditionalProperties
}

func (m *Foo) SetAdditionalProperties(val map[string]interface{}) {
m.AdditionalProperties = val
}
Loading

0 comments on commit 27c3f61

Please sign in to comment.