| # Go `struct` Converter | |
| A library for converting between Go structs. | |
| ```go | |
| chain := converter.NewChain(V1{}, V2{}, V3{}) | |
| chain.Convert(myV1struct, &myV3struct) | |
| ``` | |
| ## Details | |
| At its core, this library provides a `Convert` function, which automatically | |
| handles converting fields with the same name, and "convertable" | |
| types. Some examples are: | |
| * `string` -> `string` | |
| * `string` -> `*string` | |
| * `int` -> `string` | |
| * `string` -> `[]string` | |
| The automatic conversions are implemented when there is an obvious way | |
| to convert between the types. A lot more automatic conversions happen | |
| -- see [the converter tests](converter_test.go) for a more comprehensive | |
| list of what is currently supported. | |
| Not everything can be handled automatically, however, so there is also | |
| a `ConvertFrom` interface any struct in the graph can implement to | |
| perform custom conversion, similar to how the stdlib `MarshalJSON` and | |
| `UnmarshalJSON` would be implemented. | |
| Additionally, and maybe most importantly, there is a `converter.Chain` available, | |
| which orchestrates conversions between _multiple versions_ of structs. This could | |
| be thought of similar to database migrations: given a starting struct and a target | |
| struct, the `chain.Convert` function iterates through every intermediary migration | |
| in order to arrive at the target struct. | |
| ## Basic Usage | |
| To illustrate usage we'll start with a few basic structs, some of which | |
| implement the `ConvertFrom` interface due to breaking changes: | |
| ```go | |
| // --------- V1 struct definition below --------- | |
| type V1 struct { | |
| Name string | |
| OldField string | |
| } | |
| // --------- V2 struct definition below --------- | |
| type V2 struct { | |
| Name string | |
| NewField string // this was a renamed field | |
| } | |
| func (to *V2) ConvertFrom(from interface{}) error { | |
| if from, ok := from.(V1); ok { // forward migration | |
| to.NewField = from.OldField | |
| } | |
| return nil | |
| } | |
| // --------- V3 struct definition below --------- | |
| type V3 struct { | |
| Name []string | |
| FinalField []string // this field was renamed and the type was changed | |
| } | |
| func (to *V3) ConvertFrom(from interface{}) error { | |
| if from, ok := from.(V2); ok { // forward migration | |
| to.FinalField = []string{from.NewField} | |
| } | |
| return nil | |
| } | |
| ``` | |
| Given these type definitions, we can easily set up a conversion chain | |
| like this: | |
| ```go | |
| chain := converter.NewChain(V1{}, V2{}, V3{}) | |
| ``` | |
| This chain can then be used to convert from an _older version_ to a _newer | |
| version_. This is because our `ConvertFrom` definitions are only handling | |
| _forward_ migrations. | |
| This chain can be used to convert from a `V1` struct to a `V3` struct easily, | |
| like this: | |
| ```go | |
| v1 := // somehow get a populated v1 struct | |
| v3 := V3{} | |
| chain.Convert(v1, &v3) | |
| ``` | |
| Since we've defined our chain as `V1` → `V2` → `V3`, the chain will execute | |
| conversions to all intermediary structs (`V2`, in this case) and ultimately end | |
| when we've populated the `v3` instance. | |
| Note we haven't needed to define any conversions on the `Name` field of any structs | |
| since this one is convertible between structs: `string` → `string` → `[]string`. | |
| ## Backwards Migrations | |
| If we wanted to _also_ provide backwards migrations, we could also easily add a case | |
| to the `ConvertFrom` methods. The whole set of structs would look something like this: | |
| ```go | |
| // --------- V1 struct definition below --------- | |
| type V1 struct { | |
| Name string | |
| OldField string | |
| } | |
| func (to *V1) ConvertFrom(from interface{}) error { | |
| if from, ok := from.(V2); ok { // backward migration | |
| to.OldField = from.NewField | |
| } | |
| return nil | |
| } | |
| // --------- V2 struct definition below --------- | |
| type V2 struct { | |
| Name string | |
| NewField string | |
| } | |
| func (to *V2) ConvertFrom(from interface{}) error { | |
| if from, ok := from.(V1); ok { // forward migration | |
| to.NewField = from.OldField | |
| } | |
| if from, ok := from.(V3); ok { // backward migration | |
| to.NewField = from.FinalField[0] | |
| } | |
| return nil | |
| } | |
| // --------- V3 struct definition below --------- | |
| type V3 struct { | |
| Name []string | |
| FinalField []string | |
| } | |
| func (to *V3) ConvertFrom(from interface{}) error { | |
| if from, ok := from.(V2); ok { // forward migration | |
| to.FinalField = []string{from.NewField} | |
| } | |
| return nil | |
| } | |
| ``` | |
| At this point we could convert in either direction, for example a | |
| `V3` struct could convert to a `V1` struct, with the caveat that there | |
| may be data loss, as might need to happen due to changes in the data shapes. | |
| ## Contributing | |
| If you would like to contribute to this repository, please see the | |
| [CONTRIBUTING.md](CONTRIBUTING.md). |