File: README.md

package info (click to toggle)
golang-github-anchore-go-struct-converter 0.0~git20240925.a088364-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 144 kB
  • sloc: makefile: 59
file content (166 lines) | stat: -rw-r--r-- 4,540 bytes parent folder | download | duplicates (4)
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).