Skip to content
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

Use norman:"pointer" for nullable fields #400

Merged
merged 1 commit into from
Jun 8, 2021

Conversation

cmurphy
Copy link
Contributor

@cmurphy cmurphy commented May 19, 2021

Add a new field attribute "pointer" to indicate that the generated
client code for the field must be a pointer. This allows clients to
differentiate between sending nil/leaving the value unset and sending an
empty map or slice.

This change also removes the nullablestring norman type introduced in
30f8d18 since schemas that need a pointer to a string can now use this
field attribute. There are no libraries currently using this feature so
it should be safe to remove.

Example usage:

Labels map[string]string `json:"labels" norman:"pointer"`

Resulting API schema:

"labels": {
  "create": true,
  "nullable": true,
  "pointer": true,
  "type": "map[string]",
  "update": true
}

Generated client code:

Labels *map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`

rancher/rancher#32553

@cmurphy
Copy link
Contributor Author

cmurphy commented May 19, 2021

This is my other idea for fixing the issue we're having in the KEv2 operators: cmurphy@c685c62 - it's more direct about the problem, but I don't feel great about the extra attribute in the schema when it only has meaning in the generated client code.

@cmurphy
Copy link
Contributor Author

cmurphy commented May 19, 2021

@ibuildthecloud thoughts on this?

@thedadams
Copy link

I like this solution. I think it create a little more cumbersome client behavior, but I don't really see another way around the nil/empty issue without writing custom unmarshalers.

mjura
mjura previously approved these changes May 26, 2021
Copy link

@mjura mjura left a comment

Choose a reason for hiding this comment

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

LGTM

@@ -46,6 +46,8 @@ func getTypeString(nullable bool, typeName string, schema *types.Schema, schemas
return "map[string]" + getTypeString(false, typeName[len("map["):len(typeName)-1], schema, schemas)
case strings.HasPrefix(typeName, "array["):
return "[]" + getTypeString(false, typeName[len("array["):len(typeName)-1], schema, schemas)
case strings.HasPrefix(typeName, "nullable"):
return "*" + getTypeString(false, typeName[len("nullable"):len(typeName)], schema, schemas)

Choose a reason for hiding this comment

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

Suggested change
return "*" + getTypeString(false, typeName[len("nullable"):len(typeName)], schema, schemas)
return "*" + getTypeString(false, strings.TrimPrefix(typeName, "nullable"), schema, schemas)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

@rmweir rmweir left a comment

Choose a reason for hiding this comment

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

I did a PR for a similar issue I believe: https://github.com/rancher/norman/pull/382/files. It looks like maps could use the same fix in order to preserve the nil value. By reading the description for this PR it sounds similar but when reading the original issue it sounds like the problem is that omitting a value makes it convert to nil? Could I get some clarification on the issue?

@cmurphy
Copy link
Contributor Author

cmurphy commented Jun 1, 2021

It looks like maps could use the same fix in order to preserve the nil value.

@rmweir The problem happens for arrays too. That case is not getting hit because the JSON blob in the request is not just setting those fields to nil, it's omitting them entirely. So when the map is iterated over here, it never hits e.g. the locations field because it was never sent, because it was set to an empty slice.

By reading the description for this PR it sounds similar but when reading the original issue it sounds like the problem is that omitting a value makes it convert to nil? Could I get some clarification on the issue?

It's similar but it's a different layer in the parsing. Those array and map fields never run through the conversion because they're not even there. The result of that is the spec object gets created with those fields set to nil.

Copy link
Contributor

@rmweir rmweir left a comment

Choose a reason for hiding this comment

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

This is probably fine. Ideally we wouldn't have to say it's a map pointer but this fix has a lot less potential to create undesirable side effects in other places so I think it is worth it. Was going to say to leave a comment but by the time someone get's to this logic it will probably be self explanatory. I worry a bit about it being generalized, but null int and similar types are already supported right?

Comment on lines 10 to 14
return (strings.HasPrefix(fieldType, "map[") || strings.HasPrefix(fieldType, "nullablemap[")) && strings.HasSuffix(fieldType, "]")
}

func IsArrayType(fieldType string) bool {
return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]")
return (strings.HasPrefix(fieldType, "array[") || strings.HasPrefix(fieldType, "nullablearray[")) && strings.HasSuffix(fieldType, "]")
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like the nullable logic applies to any type now? Should this logic be added to IsReferenceType in that 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.

I guess it should, for the sake of completeness. I'm not 100% sure what the implications would be for generalizing it to the reference[] type. It doesn't exactly map to a regular Go type like slices and maps, I can't picture a case where it would make sense to use a nullablereference[]. But it has to be opted into so it shouldn't hurt anything.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe if a field is optional but otherwise should reference an existing type? If we don't intend for it to be used that way let's just remove it; removing it should cause an error to be returned when the type can't be parsed. We can always add the functionality if we end up wanting it. My bad.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed it

thedadams
thedadams previously approved these changes Jun 2, 2021
Copy link
Contributor Author

@cmurphy cmurphy left a comment

Choose a reason for hiding this comment

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

I worry a bit about it being generalized, but null int and similar types are already supported right?

These already fall into the regular case of being nullable primitives so there's no need to explicitly use a nullableint type:

case "boolean":
name = "bool"
case "float":
name = "float64"
case "int":
name = "int64"

if nullable {
return "*" + name
}

Comment on lines 10 to 14
return (strings.HasPrefix(fieldType, "map[") || strings.HasPrefix(fieldType, "nullablemap[")) && strings.HasSuffix(fieldType, "]")
}

func IsArrayType(fieldType string) bool {
return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]")
return (strings.HasPrefix(fieldType, "array[") || strings.HasPrefix(fieldType, "nullablearray[")) && strings.HasSuffix(fieldType, "]")
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 guess it should, for the sake of completeness. I'm not 100% sure what the implications would be for generalizing it to the reference[] type. It doesn't exactly map to a regular Go type like slices and maps, I can't picture a case where it would make sense to use a nullablereference[]. But it has to be opted into so it shouldn't hurt anything.

thedadams
thedadams previously approved these changes Jun 3, 2021
Comment on lines 10 to 14
return (strings.HasPrefix(fieldType, "map[") || strings.HasPrefix(fieldType, "nullablemap[")) && strings.HasSuffix(fieldType, "]")
}

func IsArrayType(fieldType string) bool {
return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]")
return (strings.HasPrefix(fieldType, "array[") || strings.HasPrefix(fieldType, "nullablearray[")) && strings.HasSuffix(fieldType, "]")
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe if a field is optional but otherwise should reference an existing type? If we don't intend for it to be used that way let's just remove it; removing it should cause an error to be returned when the type can't be parsed. We can always add the functionality if we end up wanting it. My bad.

thedadams
thedadams previously approved these changes Jun 4, 2021
@cmurphy cmurphy requested a review from rmweir June 4, 2021 21:41
rmweir
rmweir previously approved these changes Jun 6, 2021
Copy link
Contributor

@rmweir rmweir left a comment

Choose a reason for hiding this comment

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

LGTM

Add a new field attribute "pointer" to indicate that the generated
client code for the field must be a pointer. This allows clients to
differentiate between sending nil/leaving the value unset and sending an
empty map or slice.

This change also removes the `nullablestring` norman type introduced in
30f8d18 since schemas that need a pointer to a string can now use this
field attribute. There are no libraries currently using this feature so
it should be safe to remove.

Example usage:

```
Labels map[string]string `json:"labels" norman:"pointer"`
```

Resulting API schema:

```
"labels": {
  "create": true,
  "nullable": true,
  "pointer": true,
  "type": "map[string]",
  "update": true
}
```

Generated client code:

```
Labels *map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
```
@cmurphy cmurphy dismissed stale reviews from rmweir and thedadams via 6e4b2ba June 8, 2021 00:04
@cmurphy cmurphy force-pushed the nullablecomplex branch from d677f21 to 6e4b2ba Compare June 8, 2021 00:04
@cmurphy
Copy link
Contributor Author

cmurphy commented Jun 8, 2021

After talking with @ibuildthecloud it made more sense to make this an attribute of the field instead of its type. It is still used to generate a pointer type, as opposed to messing with the omitempty in the template.

Use in gke-operator: rancher/gke-operator#49
Rancher: rancher/rancher#32502

@cmurphy cmurphy changed the title Add complex nullable types Use norman:"pointer" for nullable fields Jun 8, 2021
@ibuildthecloud
Copy link
Contributor

The approach is good. LGTM on the design.

@ibuildthecloud ibuildthecloud merged commit 59b3523 into rancher:master Jun 8, 2021
cmurphy added a commit to cmurphy/aks-operator that referenced this pull request Jun 8, 2021
norman removed the 'nullablestring' type in favor of a 'pointer'
attribute for field types[1]. In addition to string pointers, this can
be used on maps and slices to indicate the generated client code should
become a pointer, even if the original type was not.

[1] rancher/norman#400
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants