-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
gen/enum: Add go.label
to override item name
#363
Conversation
build failed due to
should be fixed by restart, which I don't have permission to do so |
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.
Thank you for proposing this change. I have some design and code nits I would like you to answer or address.
gen/enum.go
Outdated
@@ -26,6 +26,11 @@ import ( | |||
"go.uber.org/thriftrw/compile" | |||
) | |||
|
|||
const ( | |||
// GoWireLabel overrides name on the wire | |||
GoWireLabel = "go.wire.label" |
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.
Let’s call this just "label"
or "go.label"
since it affects the JSON and log representation of the in memory struct and not the TBinary (wire) protocol.
enum EnumWithWireLabel { | ||
username (go.wire.label = "surname"), | ||
password (go.wire.label = "hashed_password"), | ||
salt (go.wire.label = "") |
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.
Can we also test an annotation without a value, for whether the behavior is equivalent to empty string? salt (label)
is valid Thrift IDL.
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.
good catch
gen/enum_test.go
Outdated
"test roundtrip "+tt.wireValue, | ||
) | ||
}) | ||
} |
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.
Can we also confirm through an integration test that this effects the desired behavior for JSON marshaling?
Can we also test the behavior of serializing an unrecognized enum case?
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.
👍
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 want to make sure I understand your comment correctly, are you suggesting something here like:
...
// marshal
label := te.EnumWithWireLabel(te.EnumWithWireLabelUsername)
b, err := json.Marshal(label)
assert.Equal(t, "username", string(b))
// unmarlshal
var testLabel te.EnumWithWireLabel
err = json.Unmarshal(b, testLabel)
assert.Equal(t, testLabel, label)
...
or you're suggesting a specific kind of integration tests in this repo? Thanks
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 is fine.
gen/enum_test.go
Outdated
} { | ||
invalidEnumItem := te.EnumWithLabel(tt.value) | ||
b, err := json.Marshal(invalidEnumItem) | ||
assert.NoError(t, err) |
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 is default marshal behavior: if not matching any key, just marshal as string representation of int value, without raising error
gen/enum_test.go
Outdated
} | ||
for _, tt := range tests { | ||
t.Run("compare label:"+tt.wireValue, func(t *testing.T) { | ||
// marshal |
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.
added marshal / unmarshal behavior
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.
Can we put the JSON behavior check in a subtest since this is also testing roundtripping.
t.Run("JSON round trip", func(t *testing.T) {
...
})
assertRoundTrip(...)
gen/enum_test.go
Outdated
} | ||
} | ||
|
||
func TestEnumLabelInvalid(t *testing.T) { |
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.
added test cases for invalid enum item marshal / unmarshal
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 looks good to me. Please let @abhinav have a run at it.
thanks @kriskowal ! |
notes to myself: |
enum item label name introduced possibility of conflict enum item, that bypasses compile and idl parse, add error for such case in codegen, and add tests accordingly thriftrw#362
go.label
for enum item to override enum item name during code generation
gen/enum.go
Outdated
const ( | ||
// GoLabel overrides name on the wire | ||
GoLabel = "go.label" | ||
) |
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.
styling nit: const group unnecessary if it's just one constant.
// GoLabel is the annotation expected on enum items to override their label.
const GoLabel = "go.label"
gen/enum.go
Outdated
@@ -265,6 +275,34 @@ func enumItemName(enumName string, spec *compile.EnumItem) (string, error) { | |||
return enumName + name, err | |||
} | |||
|
|||
// enumItemLabelName returns the actual name used for serialization/deserialization | |||
// default to EnumItem.Name, override by the value of GoLabel |
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.
nit:
// enumItemLabelName returns the label we use for this enum item in the generated code.
(I'm trying to make it clear that the label actually has nothing to do with
serialization for the use cases that ThriftRW supports. Using ThriftRW objects
as JSON is technically unsupported; it just happens to work.)
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.
agreed, the serialization/deserialization is pretty misleading given ThriftRW's context here
gen/enum.go
Outdated
labelName := spec.Name | ||
val, ok := spec.Annotations[GoLabel] | ||
if ok && len(val) > 0 { | ||
labelName = val |
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.
nit: If you use the single assignment form for a map, it returns the zero
value for that type (which is an empty string in this case).
So this can be simplified to,
if val := spec.Annotations[GoLabel]; len(val) > 0 {
labelName = val
}
gen/enum.go
Outdated
// duplicates in resolved enum item names | ||
func validateEnumUniqueNames(spec *compile.EnumSpec) error { | ||
items := spec.Items | ||
used := make(map[string]bool, len(items)) |
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.
When using a map
as a set, we prefer to use struct{}
instead of bool so
that you don't accidentally rely on the value of the boolean. Plus struct{}
has zero runtime cost.
used := make(map[string]struct{}, len(items))
...
used[itemName] = struct{}{}
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.
thanks! Pretty neat trick, and good to know about the zero runtime cost part :)
func BenchmarkBool(b *testing.B) {
mp := make(map[string]bool)
for i := 0; i < b.N; i++ {
if _, ok := mp[string(i)]; ok {
mp[string(i)] = true
}
}
}
func BenchmarkStruct(b *testing.B) {
mp := make(map[string]struct{})
for i := 0; i < b.N; i++ {
if _, ok := mp[string(i)]; ok {
mp[string(i)] = struct{}{}
}
}
}
▶ go test -bench=Benchmark -count=1 -benchmem bench_test.go
goos: darwin
goarch: amd64
BenchmarkBool-8 200000000 7.00 ns/op 0 B/op 0 allocs/op
BenchmarkStruct-8 200000000 7.31 ns/op 0 B/op 0 allocs/op
PASS
gen/enum.go
Outdated
itemName := enumItemLabelName(&i) | ||
if _, isUsed := used[itemName]; isUsed { | ||
return fmt.Errorf( | ||
"duplicated item name %q found in enum %q", |
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.
nit: If we find a conflict, it would be nice to know what it's with. The above
map could be changed to a map[string]string
, mapping label to original item.
map[string]EnumItem
Then this can be,
if conflict, isUsed := used[itemName]; isUsed {
return fmt.Errorf(
"item %q with label %q conflicts with item %q in enum %q",
i.Name, itemName, conflict.Name, spec.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.
makes sense
gen/enum_test.go
Outdated
rep string | ||
}{ | ||
{-1, "-1"}, | ||
{1 << 10, "1024"}, |
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 isn't technically invalid. These are valid enum items. They're just unknown. You can fold these cases into the valid case above.
gen/enum_test.go
Outdated
}, | ||
{ | ||
[]byte("\"; drop table users;\""), | ||
"unknown enum value \"; drop table users;\" for \"EnumWithLabel\"", |
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.
nit: use backticks to avoid quoting "
gen/enum_test.go
Outdated
}, | ||
}, | ||
}, | ||
"duplicated item name \"A\" found in enum \"duplicate item 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.
nit: backticks
gen/enum_test.go
Outdated
} | ||
} | ||
|
||
func TestGenInvalidEnumFailure(t *testing.T) { |
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.
nit: TestEnumLabelConflict
username (go.label = "surname"), | ||
password (go.label = "hashed_password"), | ||
salt (go.label = ""), | ||
sugar (go.label) |
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.
Can the items be upper-case (the labels can be lower-case)? That matches our
Thrift Style Guide.
USERNAME (go.label = "surname")
And we should have one case where the label is a Thrift keyword since that's
the motivation behind this change.
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.
good advice, enum name and its label is actually case sensitive, I added upper/lower cases for item name and label, also added one reserved keyword label
Thanks @abhinav for the detailed review |
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.
Great! Thanks for the change!
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.
Change looks good, would like to see more documentation (since this doesn't just affect the generated code but interoperability with external tools/systems that rely on the text output).
Also have some suggestions on tests
CHANGELOG.md
Outdated
- No changes yet. | ||
### Added | ||
- gen: Added `go.label` used in enum item to override enum | ||
item name in codegen. |
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 think we should be more explicit about what exactly go.label
affects here. as someone without much context on this change, it's unclear what "item name in codegen" actually means. does it mean the name of the Go identifier? Does it mean the name in String
? What about JSON/YAML?
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.
good point, what do you think of:
- gen: Added `go.label="<TAGGED_NAME>"` annotation for enum items.
Corresponding items of the generated Go structs will be using
<TAGGED_NAME> for JSON marshal/unmarshal instead of origin item
name, this change does not apply to YAML
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 think this change applies to all text outputs -- String()
, JSON, YAML and anything else that uses TextMarshaler
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'll revise it to
- gen: Added `go.label="<TAGGED_NAME>"` annotation for enum items.
Corresponding items of the generated Go structs will be using
<TAGGED_NAME> for text marshal / unmarshal instead of its origin item
name
gen/enum.go
Outdated
@@ -265,6 +273,32 @@ func enumItemName(enumName string, spec *compile.EnumItem) (string, error) { | |||
return enumName + name, err | |||
} | |||
|
|||
// enumItemLabelName returns the label we use for this enum item in the generated code. | |||
func enumItemLabelName(spec *compile.EnumItem) string { | |||
labelName := spec.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.
optional: can be simplified:
if val := spec.Annotations[GoLabel]; len(val) > 0 {
return val
}
return spec.Name
gen/enum_test.go
Outdated
@@ -25,11 +25,12 @@ import ( | |||
"fmt" | |||
"testing" | |||
|
|||
"github.com/stretchr/testify/assert" |
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.
think imports were grouped in this repo before -- can we keep the third party imports at the bottom?
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.
ops, ide linter didnt' capture this
gen/enum.go
Outdated
"strings" | ||
|
||
"go.uber.org/thriftrw/compile" | ||
) | ||
|
||
// GoLabel is the annotation expected on enum items to override their label. |
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.
similar to changelog, can we expand documentation here on what exactly this means and how this affects users relying on the JSON output from thriftrw-go
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.
how about:
// GoLabel is the annotation expected on enum items to override their label.
// Enum items tagged with `go.label=<TAGGED_NAME>` will generate go struct
// using `TAGGED_NAME` for JSON Marshal/Unmarshal
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.
There's no go struct involved for an enum, so I think it's a little confusing. I think it would be better as:
// GoLabel allows overriding the text formatting of an enum.
// Enum items will use the annotation value when serialized as
// a string. This affects String(), as well as text marshalling, used by JSON/YAML.
}, | ||
}, | ||
}, | ||
`item "B" with label "A" conflicts with item "A" in enum "duplicate item 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.
suggest an extra test where 2 different enums specify the same go.label
gen/enum_test.go
Outdated
errMsg string | ||
}{ | ||
{ | ||
[]byte("some-random-str"), |
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 test isn't testing the UnmarshalText
at all -- this is testing the json.Unmarshal
. Not sure that's useful?
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.
https://github.com/thriftrw/thriftrw-go/blob/dev/gen/internal/tests/enums/types.go#L98
json.Unmarshal
will call UnmarshalText
in this test case.
I was testing against UnmarshalText
, and was suggested to test json.Unmarshal
in previous revision, let me know if we want to also add UnmarshalText
here
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.
The issue is that this code never hits UnmarshalText
.
https://play.golang.org/p/B1SIZYDGr_4
Using json.Unmarshal
is fine, but the test case is incorrect since it probably should be "some-random-str"
(e.g., the quotes should be part of the value passed to Unmarshal
)
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.
ah, good catch! Thanks for this comment, I'll fix the test cases here
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.
actually looked at it again I added that first test case intentionally, you are right it didn't hit UnmarshalText
at all like the rest of test cases, I added it as an invalid case for json Unmarshal, but has nothing to do with this label PR (generic invalid case), I'll make it "some-randome-str"
so it hits unmarshalText
gen/enum_test.go
Outdated
errMsg string | ||
}{ | ||
{ | ||
[]byte("some-random-str"), |
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.
we should have a test for the original enum name, and ensure that it's no longer recognized. e.g., "USERNAME"
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.
LGTM with comments
CHANGELOG.md
Outdated
### Added | ||
- gen: Added `go.label="<TAGGED_NAME>"` annotation for enum items. | ||
Corresponding items of the generated Go structs will be using | ||
<TAGGED_NAME> for text marshal / unmarshal instead of its origin item |
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.
nit: some minor fixes, marshal -> marshalling, origin -> original
for text marshalling/unmarshaling instead of the original item name.
This allows overriding the String/JSON/YAML output for an enum.
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.
👍
gen/enum.go
Outdated
"strings" | ||
|
||
"go.uber.org/thriftrw/compile" | ||
) | ||
|
||
// GoLabel is the annotation expected on enum items to override their label. |
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.
There's no go struct involved for an enum, so I think it's a little confusing. I think it would be better as:
// GoLabel allows overriding the text formatting of an enum.
// Enum items will use the annotation value when serialized as
// a string. This affects String(), as well as text marshalling, used by JSON/YAML.
gen/enum_test.go
Outdated
|
||
func TestEnumLabelInvalidUnmarshal(t *testing.T) { | ||
tests := []struct { | ||
errVal []byte |
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.
nit: since you are using strings for all the cases, make this string
, and then convert to []byte
when calling Unmarshal
removes some noise from tests
gen/enum_test.go
Outdated
t.Run(string(tt.errVal), func(t *testing.T) { | ||
var expectedLabel te.EnumWithLabel | ||
err := json.Unmarshal(tt.errVal, &expectedLabel) | ||
assert.Equal(t, err.Error(), tt.errMsg) |
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.
use assert.EqualError
, otherwise, if it doesn't return an error, this will panic
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.
neat! Thanks for the advice
gen/enum_test.go
Outdated
for _, tt := range testCases { | ||
t.Run(tt.spec.Name, func(t *testing.T) { | ||
err := enum(nil, &tt.spec) | ||
assert.Error(t, err) |
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.
use assert.EqualError
instead of assert.Error
+ assert.Equal
, which could panic if err
is nil
This changes the generated MarshalLogObject for enums (thriftrw#366) to support the `go.label` annotation for enum items (thriftrw#363).
TextMarshaler cannot be round-tripped because of thriftrw#368.
go.label
for enum item to override enum item name during code generationgo.label
to override item name
This PR addresses #362