Skip to content

Allow generating nullable.Nullable for nullable properties #1404

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

Merged
merged 1 commit into from
Jan 11, 2024

Conversation

sonasingh46
Copy link
Contributor

@sonasingh46 sonasingh46 commented Dec 21, 2023

This PR adds capability to use Nullable types via opt in flag for Nullable types

Ref Issue: #1039

Maintainer edit: Release notes

@sonasingh46 sonasingh46 force-pushed the implement_nullable branch 3 times, most recently from 79d1b49 to 2c4fc12 Compare December 22, 2023 15:24
@sonasingh46 sonasingh46 changed the title (wip)add optional type code generation add optional type code generation Jan 2, 2024
@sonasingh46 sonasingh46 changed the title add optional type code generation add nullable type code generation Jan 3, 2024
@sonasingh46 sonasingh46 marked this pull request as ready for review January 9, 2024 16:46
@sonasingh46 sonasingh46 force-pushed the implement_nullable branch 2 times, most recently from 952ab96 to 60ceb79 Compare January 9, 2024 17:53
@@ -13,6 +13,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9
github.com/labstack/echo/v4 v4.11.3
github.com/oapi-codegen/nullable v1.0.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a new release (docs changes only) so worth updating too:

Suggested change
github.com/oapi-codegen/nullable v1.0.0
github.com/oapi-codegen/nullable v1.0.1

if !p.Schema.SkipOptionalPointer &&
(!p.Required || p.Nullable ||
(p.ReadOnly && (!p.Required || !globalState.options.Compatibility.DisableRequiredReadOnlyAsPointer)) ||
p.WriteOnly) {

typeDef = "*" + typeDef
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Wondering if this gets caught by the linter? I remember running make lint.
I will remove it anyways.


gotJSON, err := json.Marshal(updateReq)
require.NoError(t, err)
fmt.Println(string(gotJSON))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Println(string(gotJSON))


gotJSON2, err := json.Marshal(updateReq2)
require.NoError(t, err)
fmt.Println(string(gotJSON2))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Println(string(gotJSON2))

@@ -681,10 +685,15 @@ func GenFieldsFromProperties(props []Property) []string {

field += fmt.Sprintf(" %s %s", goFieldName, p.GoTypeDef())

omitEmpty := !p.Nullable &&
(!p.Required || p.ReadOnly || p.WriteOnly) &&
commonConditions := (!p.Required || p.ReadOnly || p.WriteOnly) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I like this name - maybe shouldOmitEmpty?

Comment on lines 22 to 38
func TestIssue(t *testing.T) {
swagger, err := openapi3.NewLoader().LoadFromData(spec)
require.NoError(t, err)

opts := codegen.Configuration{
PackageName: "issue1039",
Generate: codegen.GenerateOptions{
ChiServer: true,
Client: true,
Models: true,
EmbeddedSpec: true,
NullableType: true,
},
}

_, err = codegen.Generate(swagger, opts)
require.NoError(t, err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this just to check that generating the code works? If so, this shouldn't be needed - by having the go:generates in this package, that's good enough, unless there's something else you're trying to test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Removed it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the minimal spec we could use? If we can trim it down it'd be appreciated, so it's clear for long-term maintenance what's actually important about this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Let me look into this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the spec.

@@ -35,6 +35,7 @@ type GenerateOptions struct {
Client bool `yaml:"client,omitempty"` // Client specifies whether to generate client boilerplate
Models bool `yaml:"models,omitempty"` // Models specifies whether to generate type definitions
EmbeddedSpec bool `yaml:"embedded-spec,omitempty"` // Whether to embed the swagger spec in the generated code
NullableType bool `yaml:"nullable-type,omitempty"` // Whether to generate nullable type for nullable fields
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, would this be able to move to the OutputOptions? That's a better place for it

Comment on lines 17 to 15
func ToStringPointer(str string) *string {
return &str
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: but doesn't need to be exported:

Suggested change
func ToStringPointer(str string) *string {
return &str
}
func toStringPointer(str string) *string {
return &str
}

Comment on lines 17 to 15
func ToStringPointer(str string) *string {
return &str
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively with generics:

Suggested change
func ToStringPointer(str string) *string {
return &str
}
func ptr[T any](v T) *T {
return &v
}

@@ -213,3 +213,243 @@ func TestProperty_GoTypeDef(t *testing.T) {
})
}
}

func TestPropertyNullable_GoTypeDef(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be collapsed in with the above, no? At the very least:

Suggested change
func TestPropertyNullable_GoTypeDef(t *testing.T) {
func TestProperty_GoTypeDef_nullable(t *testing.T) {

To indicate what it's testing

Copy link
Member

@jamietanna jamietanna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments, and just want to make sure that we've made sure this is the minimal set of specs + tests needed to validate the functionality?

@jamietanna
Copy link
Member

jamietanna commented Jan 11, 2024

Draft release notes:

Notable features

Nullable types,

It's possible that you want to be able to determine whether a field isn't sent, is sent as null or has a value.

For instance, if you had the following OpenAPI property:

##### TODO indentation
S:
    type: object
    properties:
        Field:
          type: string
          nullable: true
    required: []

The current behaviour in oapi-codegen is to generate:

type S struct {
	Field *string `json:"field,omitempty"`
}

However, you lose the ability to understand the three cases, as there's no way to distinguish two of the types from each other:

  • is this field not sent? (Can be checked with S.Field == nil)
  • is this field null? (Can be checked with S.Field == nil)
  • does this field have a value? (S.Field != nil && *S.Field == "123")

Therefore, as requested in #1039, this is now possible to represent with the nullable.Nullable type from our new library, oapi-codegen/nullable.

If you configure your generator's Output Options as so:

output-options:
  nullable-type: true

You will now receive the following output:

type S struct {
    Field nullable.Nullable[string] `json:"field,omitempty"`
}

Note that this is opt-in only, due to it being a break in existing signatures and behaviour.

You can find out more about how this works in a blog post with further details.

Comment on lines 63 to 50
// exclude some fields in patch request
patchReq2 := issue1039nullable.PatchRequest{
BreakoutGroup: &issue1039nullable.BreakoutGroup{
Name: ToStringPointer("test-bk"),
},
Organization: &issue1039nullable.Organization{
AliasName: ToStringPointer("org-alias-name"),
Name: ToStringPointer("org-name"),
},
Popularity: &popularity,
}
Copy link
Member

@jamietanna jamietanna Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// exclude some fields in patch request
patchReq2 := issue1039nullable.PatchRequest{
BreakoutGroup: &issue1039nullable.BreakoutGroup{
Name: ToStringPointer("test-bk"),
},
Organization: &issue1039nullable.Organization{
AliasName: ToStringPointer("org-alias-name"),
Name: ToStringPointer("org-name"),
},
Popularity: &popularity,
}
// omit some fields
patchReq2 := issue1039nullable.PatchRequest{
BreakoutGroup: &issue1039nullable.BreakoutGroup{
Name: ToStringPointer("test-bk"),
},
// HomeCountry is omitted
Organization: &issue1039nullable.Organization{
AliasName: ToStringPointer("org-alias-name"),
Name: ToStringPointer("org-name"),
},
Popularity: &popularity,
// SearchBox is omitted
}

assert.Falsef(t, obj.BreakoutGroup.IsNull(), "breakout group should not be null")

// check for non-nullable field
assert.Truef(t, obj.HomeCountry == nil, "home country should be nil")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert.Truef(t, obj.HomeCountry == nil, "home country should be nil")
assert.Nilf(t, obj.HomeCountry, "home country should be nil")

embedded-spec: true
output-options:
skip-prune: true
output: nullable-disabled/types.gen.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be clearer if this and the other package were named differently - nullabledisabled is a bit unclear at a first glance.

Maybe defaultbehaviour?

return &str
}

func TestNullableDisabled(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also recommend moving this into the package with the default behaviour, or making it clearer what this is - it was a little confusing reading this one first, then seeing the new behaviour - maybe we should keep this next to the existing behaviour's generated code?

BreakoutGroup nullable.Nullable[BreakoutGroup] `json:"breakout_group"`

// HomeCountry Home country of the user.
HomeCountry *HomeCountry `json:"home_country,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance of the tests, I wasn't sure why this was nil

It may be worth us tweaking the spec to make the property names clearer what they're testing - i.e. this could be OptionalButNotNullableProperty then it's super clear what's being tested (and why it's being checked for nil instead of expecting a nullable.Nullable)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a very good point. Let me do that

},

{
name: "when search box is null and organization has non zero value",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the key part of what we're testing, I'm not sure we should also be checking i.e. the Popularity, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checking that to ensure that because the other fields are not provided -- it should be basically not set and not null. But I think now when I am reading your comment, probably, we do not need to duplicate this in every test case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty test case should already cover that and I will remove this

Comment on lines 222 to 223
// check for nullable field
// check for nullable fields
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate?

Comment on lines 9 to 11
old-aliasing: true
old-merge-schemas: true
disable-flatten-additional-properties: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these definitely required to make this a minimal reproduction case?

package: defaultbehaviour
generate:
models: true
embedded-spec: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed any more?

package: issue1039
generate:
models: true
embedded-spec: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above

Comment on lines 9 to 12
compatibility:
old-aliasing: true
old-merge-schemas: true
disable-flatten-additional-properties: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above

Comment on lines 9 to 10
old-aliasing: true
old-merge-schemas: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed?

Comment on lines 7 to 9
compatibility:
old-aliasing: true
old-merge-schemas: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed?

Comment on lines 21 to 23
AliasName: nullable.Nullable[string]{
true: "foo-alias",
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to avoid the implementation details leaking - please use NewNullableWithValue

func TestNullableTypesMarshal(t *testing.T) {
// include all fields in patch request
patchReq := PatchRequest{
ComplexOptionalNullable: nullable.Nullable[ComplexOptionalNullable]{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the order of fields in this function mirror what's in the other file? Will make it easier to diff

@jamietanna jamietanna added the enhancement New feature or request label Jan 11, 2024
As part of oapi-codegen#1039, we've created a new library `oapi-codegen/nullable`,
which allows tracking whether:

- a field is not sent
- a field is sent with an explicit `null`
- a field is sent with an explicit value

This introduces an opt-in `output-options` flag, `nullable-type`, which
can generate the `nullable.Nullable` types.

This is opt-in, as existing code will break due to the signature change,
as well as a behaviour change.

Closes oapi-codegen#1039.

Co-authored-by: Ashutosh Kumar <ashutosh.kumar@elastic.co>
@jamietanna jamietanna changed the title add nullable type code generation Allow generating nullable.Nullable for nullable properties Jan 11, 2024
@jamietanna jamietanna merged commit 66f9bb8 into oapi-codegen:master Jan 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy