blob: 06d8e4311ea378d0f3083096fd3cc0eaba960472 [file] [view] [edit]
# 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).