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

Merged
merged 12 commits into from
Aug 14, 2018

Conversation

cl1337
Copy link
Contributor

@cl1337 cl1337 commented Aug 3, 2018

This PR addresses #362

@CLAassistant
Copy link

CLAassistant commented Aug 3, 2018

CLA assistant check
All committers have signed the CLA.

@cl1337 cl1337 mentioned this pull request Aug 3, 2018
@cl1337
Copy link
Contributor Author

cl1337 commented Aug 6, 2018

build failed due to

No output has been received in the last 10m0s, this potentially indicates a stalled build or something wrong with the build itself.
Check the details on how to adjust your build configuration on: https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received
The build has been terminated

should be fixed by restart, which I don't have permission to do so

@kriskowal kriskowal changed the title add go.wire.label for enum item to override on the wire name Add go.wire.label for enum item to override on the wire name Aug 6, 2018
Copy link
Contributor

@kriskowal kriskowal left a 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"
Copy link
Contributor

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 = "")
Copy link
Contributor

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.

Copy link
Contributor Author

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,
)
})
}
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

Copy link
Contributor Author

@cl1337 cl1337 Aug 6, 2018

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

Copy link
Contributor

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)
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 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

added marshal / unmarshal behavior

Copy link
Contributor

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) {
Copy link
Contributor Author

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

Copy link
Contributor

@kriskowal kriskowal left a 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.

@cl1337
Copy link
Contributor Author

cl1337 commented Aug 7, 2018

thanks @kriskowal !

@cl1337
Copy link
Contributor Author

cl1337 commented Aug 7, 2018

notes to myself:
1: add test to check label duplication enum Foo { x (label = "y"), y }
2: update the PR title and description and add an entry to the change-log

cl1337 added 3 commits August 8, 2018 12:43
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
@cl1337 cl1337 changed the title Add go.wire.label for enum item to override on the wire name Add go.label for enum item to override enum item name during code generation Aug 9, 2018
gen/enum.go Outdated
const (
// GoLabel overrides name on the wire
GoLabel = "go.label"
)
Copy link
Contributor

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
Copy link
Contributor

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.)

Copy link
Contributor Author

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
Copy link
Contributor

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))
Copy link
Contributor

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{}{}

Copy link
Contributor Author

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",
Copy link
Contributor

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)
}

Copy link
Contributor Author

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"},
Copy link
Contributor

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\"",
Copy link
Contributor

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\"",
Copy link
Contributor

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) {
Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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

@cl1337
Copy link
Contributor Author

cl1337 commented Aug 10, 2018

Thanks @abhinav for the detailed review

Copy link
Contributor

@abhinav abhinav left a 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!

Copy link
Contributor

@prashantv prashantv left a 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.
Copy link
Contributor

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?

Copy link
Contributor Author

@cl1337 cl1337 Aug 13, 2018

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

Copy link
Contributor

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

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'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
Copy link
Contributor

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"
Copy link
Contributor

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?

Copy link
Contributor Author

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.
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor

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"`,
Copy link
Contributor

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"),
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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)

Copy link
Contributor Author

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

Copy link
Contributor Author

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"),
Copy link
Contributor

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"

Copy link
Contributor

@prashantv prashantv left a 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
Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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
Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Contributor Author

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)
Copy link
Contributor

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

@abhinav abhinav changed the title Add go.label for enum item to override enum item name during code generation gen/enum: Add go.label to override item name Aug 14, 2018
@abhinav abhinav merged commit f686b85 into thriftrw:dev Aug 14, 2018
@abhinav abhinav mentioned this pull request Aug 15, 2018
11 tasks
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants