1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
# 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).
|