| JSON, interfaces, and go generate |
| |
| Francesc Campoy |
| Developer, Advocate, and Gopher |
| @francesc |
| campoy@golang.org |
| |
| * Your mission |
| |
| Your mission, should you choose to accept it, is to decode this message: |
| |
| .code json/unmarshaler0map.go /^{/,/^}/ |
| |
| into: |
| |
| .code json/unmarshaler0.go /type Person/,/^}/ |
| |
| * Your mission (cont.) |
| |
| Where `ShirtSize` is an enum _(1)_: |
| |
| .code json/unmarshaler0.go /type ShirtSize byte/,/^\)/ |
| |
| _(1)_: Go doesn't have enums. |
| In this talk I will refer to constants of integer types as enums. |
| |
| * Using a map |
| |
| * Using a map |
| |
| _Pros_: very simple |
| |
| _Cons_: too simple? we have to write extra code |
| |
| .code json/unmarshaler0map.go /Person.*Parse/,/p.Name/ |
| |
| * Parsing dates |
| |
| Time format based on a "magic" date: |
| |
| Mon Jan 2 15:04:05 -0700 MST 2006 |
| |
| An example: |
| |
| .play json/dates.go /func main/, |
| |
| * Why that date? |
| |
| Let's reorder: |
| |
| Mon Jan 2 15:04:05 -0700 MST 2006 |
| |
| into: |
| |
| 01/02 03:04:05 PM 2006 -07:00 MST |
| |
| which is: |
| |
| * 1 2 3 4 5 6 7! |
| |
| .image json/img/mindblown.gif 500 _ |
| |
| * Parsing the birth date: |
| |
| Since our input was: |
| |
| .code json/unmarshaler0map.go /^{/,/^}/ |
| |
| Parse the birth date: |
| |
| .code json/unmarshaler0map.go /time.Parse/,/p.Born/ |
| |
| * Parsing the shirt size |
| |
| Many ways of writing this, this is a pretty bad one: |
| |
| .code json/unmarshaler0map.go /ParseShirtSize/,/^}/ |
| |
| Use a `switch` statement, but a map is more compact. |
| |
| * Parsing the shirt size |
| |
| Our complete parsing function: |
| |
| .code json/unmarshaler0map.go /Person.*Parse/,/^}/ |
| |
| * Does this work? |
| |
| .play json/unmarshaler0map.go /func main/,/^}/ |
| |
| _Note_: `ShirtSize` is a `fmt.Stringer` |
| |
| * JSON decoding into structs |
| |
| * JSON decoding into structs |
| |
| Use tags to adapt field names: |
| |
| .code json/unmarshaler0bad.go /type Person/,/^}/ |
| |
| But this doesn't fit: |
| |
| .play json/unmarshaler0bad.go /func main/, |
| |
| * Let's use an auxiliary struct type |
| |
| Use string fields and do any decoding manually afterwards. |
| |
| .code json/unmarshaler0.go /var aux struct/,/}/ |
| |
| _Note_: the field tag for `Name` is not needed; the JSON decoder performs a case |
| insensitive match if the exact form is not found. |
| |
| * Let's use an auxiliary struct type (cont.) |
| |
| The rest of the `Parse` function doesn't change much: |
| |
| .code json/unmarshaler0.go /Person.*Parse/,/^}/ |
| |
| * Can we do better? |
| |
| * Current solution |
| |
| Repetition if other types have fields with: |
| |
| - date fields with same formatting, |
| - or t-shirt sizes. |
| |
| Let's make the types smarter so `json.Decoder` will do all the work transparently. |
| |
| *Goal*: `json.Decoder` should do all the work for me! |
| |
| * Meet Marshaler and Unmarshaler |
| |
| Types satisfying `json.Marshaler` define how to be encoded into json. |
| |
| type Marshaler interface { |
| MarshalJSON() ([]byte, error) |
| } |
| |
| And `json.Unmarshaler` for the decoding part. |
| |
| type Unmarshaler interface { |
| UnmarshalJSON([]byte) error |
| } |
| |
| * UnmarshalJSON all the things! |
| |
| * Let's make Person a json.Unmarshaler |
| |
| Replace: |
| |
| .code json/unmarshaler0.go /Person.*Parse/ |
| |
| with: |
| |
| .code json/unmarshaler1.go /Person.*UnmarshalJSON/,/rest of function/ |
| |
| * Let's make Person a json.Unmarshaler (cont.) |
| |
| And our `main` function becomes: |
| |
| .play json/unmarshaler1.go /func main/,/^}/ |
| |
| * UnmarshalJSON for enums |
| |
| Substitute `ParseShirtSize`: |
| |
| .code json/unmarshaler1.go /ParseShirtSize/ |
| |
| with `UnmarshalJSON`: |
| |
| .code json/unmarshaler2.go /ShirtSize.*UnmarshalJSON/,/^}/ |
| |
| * UnmarshalJSON for enums (cont.) |
| |
| Now use `ShirtSize` in the aux struct: |
| |
| .play json/unmarshaler2.go /Person.*UnmarshalJSON/,/rest of function/ |
| |
| Use the same trick to parse the birthdate. |
| |
| * Unmarshaling differently formatted dates |
| |
| Create a new type `Date`: |
| |
| .code json/unmarshaler3.go /type Date/ |
| |
| And make it a `json.Unmarshaler`: |
| |
| .code json/unmarshaler3.go /Date.*UnmarshalJSON/,/^}/ |
| |
| * Unmarshaling differently formatted dates (cont.) |
| |
| Now use `Date` in the aux struct: |
| |
| .play json/unmarshaler3.go /Person.*UnmarshalJSON/,/^}/ |
| |
| Can this code be shorter? |
| |
| * Yes! |
| |
| By making the `Born` field in `Person` of type `Date`. |
| |
| `Person.UnmarshalJSON` is then equivalent to the default behavior! |
| |
| It can be safely removed. |
| |
| .play json/unmarshaler4.go /func main/,/^}/ |
| |
| * Was this really better? |
| |
| - Code length: 86LoC vs 80LoC |
| |
| - Reusability of types |
| |
| - Easier to maintain |
| |
| - Usage of the standard library |
| |
| * Other ideas |
| |
| * Roman numerals |
| |
| * Roman numerals |
| |
| Because why not? |
| |
| .code json/roman_numerals.go /type romanNumeral/ |
| |
| And because Roman numerals are classier |
| |
| .code json/roman_numerals.go /type Movie/,/^}/ |
| |
| * Roman numerals (cont.) |
| |
| .play json/roman_numerals.go /func main/,/^}/ |
| |
| * Secret data |
| |
| * Secret data |
| |
| Some data is never to be encoded in clear text. |
| |
| .code json/secret.go /type Person/,/type secret/ |
| |
| Use cryptography to make sure this is safe: |
| |
| .code json/secret.go /secret.*MarshalJSON/,/^}/ |
| |
| _Note_: This solution is just a toy; don't use it for real systems. |
| |
| * Secret data (cont.) |
| |
| And use the same key to decode it when it comes back: |
| |
| .code json/secret.go /secret.*UnmarshalJSON/,/^}/ |
| |
| * Secret data (cont.) |
| |
| Let's try it: |
| |
| .play json/secret.go /func main/,/^}/ |
| |
| * But most JSON enums are boring |
| |
| * go generate to the rescue! |
| |
| `go`generate`: |
| |
| - introduced in Go 1.4 |
| - a tool for package authors |
| - an extra step before `go`build` |
| |
| You will see it as comments in the code like: |
| |
| //go:generate go tool yacc -o gopher.go -p parser gopher.y |
| |
| More information in the [[http://blog.golang.org/generate][blog post]]. |
| |
| * code generation tools: stringer |
| |
| `stringer` generates `String` methods for enum types. |
| |
| package painkiller |
| |
| //go:generate stringer -type=Pill |
| |
| type Pill int |
| |
| const ( |
| Placebo Pill = iota |
| Aspirin |
| Ibuprofen |
| Paracetamol |
| ) |
| |
| Call `go`generate`: |
| |
| $ go generate $GOPATH/src/path_to_painkiller |
| |
| which will create a new file containing the `String` definition for `Pill`. |
| |
| * jsonenums |
| |
| Around 200 lines of code. |
| |
| Parses and analyses a package using: |
| |
| - `go/{ast/build/format/parser/token}` |
| - `golang.org/x/tools/go/exact`, `golang.org/x/tools/go/types` |
| |
| And generates the code using: |
| |
| - `text/template` |
| |
| And it's on github: [[http://github.com/campoy/jsonenums][github.com/campoy/jsonenums]] |
| |
| * Demo |