-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add support for default values #140
base: master
Are you sure you want to change the base?
Conversation
@LucasRoesler pinging you, just make sure you saw this. Thanks!! |
@injeniero I am so sorry, i saw this and then it slipped off my list. There were a few offline events happening and it distracted me. I am going to read through this today |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple questions, mostly about the json marshal/unmarshal.
{{ if .HasDefault }} | ||
type alias {{.Name}} | ||
err := json.Unmarshal([]byte(`{{.DefaultValue}}`), (*alias)({{if .IsMap}}&{{end}}m)) | ||
if err != nil { | ||
panic(fmt.Errorf("could not unmarshal default values for {{.Name}}: %w", err)) | ||
} | ||
{{- end}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we already know the defaults and the fields, why can't we template Range over the default config and set the defaults directlly instead of needing another allocation for json Unmarshal?
// UnmarshalJSON all named properties into {{.Name}} and the rest into the AdditionalProperties map | ||
func (m *{{.Name}}) UnmarshalJSON(data []byte) error { | ||
// Set default values | ||
*m = *New{{.Name}}() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure this is correct or expected behavior in UnmarshalJSON
. It certainly differs from the stdlib.
If the object m
is already initialized, you should be able to unmarshal json into it while preserving pre-existing values. For example
package main
import (
"encoding/json"
"fmt"
)
type Data struct {
A int
B string
}
func main() {
sample := Data{10, "sample data"}
fmt.Printf("init: %+v\n", sample)
// init: {A:10 B:sample data}
input_data := []byte(`{"A": 5}`)
_ = json.Unmarshal(input_data, &sample)
fmt.Printf("load: %+v\n", sample)
// load: {A:5 B:sample data}
}
https://play.golang.com/p/mxpH_x4rGvl
The behavior here makes Patch
requests that support several optional fields more difficult to use. Imagine the workflow for an Update endpoint:
- get existing value from the database (or return error)
- validate the input payload
- marshal the input payload on top of the values from the database.
The PR implementation would break this flow by throwing away any pre-existing values. Not saying this is the perfect workflow, but it is one i have seen in the wild.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am trying to think of how it should behave and I am not 100% sure. Perhaps one approach is to use mapstructure a bit more.
- unmarshal to a
inputData := map[string]interface{}
- loop over the defaults (in template or using a Go loop over a dict), for each default check if the key exists
_, ok := inputData[name]
, if it does not, push the default value into the payload - now map this combined input into the object
m
.
This has the benefit of not emptying fields that don't have defaults but it will still reset fields that do have a default.
The next alternative would be similar but modifying 2
- unmarshal to a
inputData := map[string]interface{}
- loop over the defaults (in template or using a Go loop over a dict), for each default check if the key exists
_, ok := inputData[name]
also check ifm.Name
is the empty (Go) value, if the key is not in theinputData
and the current value is not "empty", then we set theinputData[Name]
value to the default - now map this combined input into the object
m
.
I think a case could be made for either approach, but approach 1 is probably more predictable/expected: "treat missing fields as if the default was sent". But this only makes sense if the field is non-optional/non-nillable.
// MarshalJSON {{.Name}} by combining the AdditionalProperties with the named properties in a single JSON object. | ||
// An error will be returned if there are duplicate keys. | ||
func (m {{.Name}}) MarshalJSON() ([]byte, error) { | ||
type alias {{.Name}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a moment to understand why we needed the alias type, this just let's us use the original unmarshal behavior, right? It might be worth adding a comment in the code so that anyone else using it can follow along a bit more easily.
@@ -47,6 +47,11 @@ type {{$modelName}} struct { | |||
{{- end }} | |||
} | |||
|
|||
// New{{$modelName}} creates a new {{$modelName}} instance with no internal value. | |||
func New{{$modelName}}() *{{$modelName}} { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A default could still be specified at the schema level right? For example:
Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
- $ref: '#/components/schemas/Lizard'
default: '{"petType": "cat", "name": "Felix"}'
// HasDefault is true when the property has a default value | ||
HasDefault bool | ||
// DefaultValue is the default value of the property | ||
DefaultValue string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be a map[string]interface{}
or a json.RawMessage
// passedSchemas is a map of pointers to a schema. | ||
// Used for marking already visited types when recursively resolve `allOf` definitions and merging types. | ||
type passedSchemas map[*openapi3.SchemaRef]something | ||
type passedSchemas map[*openapi3.SchemaRef]bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why move to a boolean? the empty "something" was intended as a meaningless, but non-empty value, vs the bool
which has implied meaning.
inlineStruct: | ||
type: object | ||
properties: | ||
field1: | ||
type: string | ||
field2: | ||
type: integer | ||
default: 13 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be interesting to have multiple nested inline structs, just to have a solid example of what happens with naming. This is hopefully a very rare case, but worth at least showing what happens. We actually forbid unnamed inline objects like this, because there is always a possibility for a weird name conflict.
inlineStruct: | |
type: object | |
properties: | |
field1: | |
type: string | |
field2: | |
type: integer | |
default: 13 | |
inlineStruct: | |
type: object | |
properties: | |
field1: | |
type: string | |
field2: | |
type: integer | |
default: 13 | |
nestedInline: | |
type: object | |
properties: | |
name: | |
type: string | |
cost: | |
type: integer | |
default: 100 |
This PR implements support for the "default" property. The implementation details are as follow:
Examples
For a full example please go to: Default
Important
A side effect of this PR is moving inline structs to named types. This was required to properly support validation and default handling without needing to have a special case for them.
Ticket
#136
How Has This Been Verified?
Unit tests
Checklist: