Skip to content

Commit bd0e165

Browse files
committed
refactor(CodingPattern): introduce coding pattern
We've been running into all sorts of problems upstream when trying to send raw *Dataset's over the wire due to inconsistencies serializing to various formats. To me this highlights a tension in the two roles that structs like Dataset, Commit, Structure, etc. have to play: Serialization & business logic. To me this shows up the most when trying to decide on the proper type for struct fields when a more "exotic" type (something other than string, int, []byte, ...) is preferred to limit use cases to specific concerns. Things like our typed DataFormat consts are a great example. For the serialization concern a field like DataFormat would ideally just be a string. serialization libraries like json, cbor, and gob all know how to deal with strings, so it's less code to maintain. On the other hand DataFormat can really only be a few things right now (CSV, JSON, CBOR), and we need tight controls on what values DataFormat can have because we make lots of decisions on that value. For this reason it makes lots of sense for a DataFormat field to be a custom type that errors if it isn't a value we like. This is a "business logic" concern enforced by strong typing patterns. So I think the answer is to separate the concerns into two structs, one struct for the serialization concern, and another struct for the business logic concern. I've heard about this pattern in the wild, but haven't really implemented it myself. So, for lack of reading I'm going to call it the "coding pattern", and it works as follows: * two structs, one "standard", the other with the prefix "Coding" * the first struct gets two methods: Endode and Decode, that translate to & from the coding variant * All serialization should happen at the "Coding struct" This last bullet isn't yet fully implemented in this codebase, as it's going to take some time to migrate over. I'd like to start by switching codebases that use qri to this new pattern, then migrating all dataset subpackages once we have time.
1 parent 7d2379f commit bd0e165

9 files changed

+510
-13
lines changed

commit.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func (cm *Commit) UnmarshalJSON(data []byte) error {
133133
m := _commitMsg{}
134134
if err := json.Unmarshal(data, &m); err != nil {
135135
log.Debug(err.Error())
136-
return fmt.Errorf("error unmarshling dataset: %s", err.Error())
136+
return fmt.Errorf("error unmarshling commit: %s", err.Error())
137137
}
138138

139139
*cm = Commit(m)
@@ -158,3 +158,50 @@ func UnmarshalCommit(v interface{}) (*Commit, error) {
158158
return nil, err
159159
}
160160
}
161+
162+
// Encode creates a CodingCommit from a Commit instance
163+
func (cm Commit) Encode() *CodingCommit {
164+
return &CodingCommit{
165+
Author: cm.Author,
166+
Message: cm.Message,
167+
Path: cm.Path().String(),
168+
Qri: cm.Qri.String(),
169+
Signature: cm.Signature,
170+
Timestamp: cm.Timestamp,
171+
Title: cm.Title,
172+
}
173+
}
174+
175+
// Decode creates a Commit from a CodingCommit instance
176+
func (cm *Commit) Decode(cc *CodingCommit) error {
177+
c := Commit{
178+
path: datastore.NewKey(cc.Path),
179+
Author: cc.Author,
180+
Message: cc.Message,
181+
Signature: cc.Signature,
182+
Timestamp: cc.Timestamp,
183+
Title: cc.Title,
184+
}
185+
186+
if cc.Qri == KindCommit.String() {
187+
// TODO - this should respond to changes in CodingCommit
188+
c.Qri = KindCommit
189+
} else if cc.Qri != "" {
190+
return fmt.Errorf("invalid commit 'qri' value: %s", cc.Qri)
191+
}
192+
193+
*cm = c
194+
return nil
195+
}
196+
197+
// CodingCommit is a variant of Commit safe for serialization (encoding & decoding)
198+
// to static formats. It uses only simple go types
199+
type CodingCommit struct {
200+
Author *User `json:"author,omitempty"`
201+
Message string `json:"message,omitempty"`
202+
Path string `json:"path,omitempty"`
203+
Qri string `json:"qri,omitempty"`
204+
Signature string `json:"signature,omitempty"`
205+
Timestamp time.Time `json:"timestamp"`
206+
Title string `json:"title"`
207+
}

commit_test.go

+53-5
Original file line numberDiff line numberDiff line change
@@ -183,16 +183,18 @@ func TestCommitUnmarshalJSON(t *testing.T) {
183183
cases := []struct {
184184
data string
185185
result *Commit
186-
err error
186+
err string
187187
}{
188-
{`{}`, &Commit{}, nil},
189-
{`{ "title": "title", "message": "message"}`, &Commit{Title: "title", Message: "message"}, nil},
190-
{`{ "author" : { "id": "id", "email": "email@email.com"} }`, &Commit{Author: &User{ID: "id", Email: "email@email.com"}}, nil},
188+
{`{}`, &Commit{}, ""},
189+
{`{ "title": "title", "message": "message"}`, &Commit{Title: "title", Message: "message"}, ""},
190+
{`{ "author" : { "id": "id", "email": "email@email.com"} }`, &Commit{Author: &User{ID: "id", Email: "email@email.com"}}, ""},
191+
{`{`, &Commit{Author: &User{ID: "id", Email: "email@email.com"}}, "error unmarshling commit: unexpected end of JSON input"},
191192
}
192193

193194
for i, c := range cases {
194195
cm := &Commit{}
195-
if err := json.Unmarshal([]byte(c.data), cm); err != c.err {
196+
err := cm.UnmarshalJSON([]byte(c.data))
197+
if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
196198
t.Errorf("case %d error mismatch. expected: '%s', got: '%s'", i, c.err, err)
197199
continue
198200
}
@@ -241,3 +243,49 @@ func TestUnmarshalCommit(t *testing.T) {
241243
}
242244
}
243245
}
246+
247+
func TestCommitCoding(t *testing.T) {
248+
cases := []*Commit{
249+
{},
250+
{Author: &User{Email: "foo"}},
251+
{Message: "foo"},
252+
{path: datastore.NewKey("/foo")},
253+
{Qri: KindCommit},
254+
{Signature: "foo"},
255+
{Timestamp: time.Date(2001, 1, 1, 1, 1, 1, 1, time.UTC)},
256+
{Title: "foo"},
257+
}
258+
259+
for i, c := range cases {
260+
cd := c.Encode()
261+
got := &Commit{}
262+
if err := got.Decode(cd); err != nil {
263+
t.Errorf("case %d unexpected error '%s'", i, err)
264+
continue
265+
}
266+
267+
if err := CompareCommits(c, got); err != nil {
268+
t.Errorf("case %d mismatch: %s", i, err.Error())
269+
continue
270+
}
271+
}
272+
}
273+
274+
func TestCommitDecode(t *testing.T) {
275+
cases := []struct {
276+
cm *CodingCommit
277+
err string
278+
}{
279+
{&CodingCommit{}, ""},
280+
{&CodingCommit{Qri: "foo"}, "invalid commit 'qri' value: foo"},
281+
}
282+
283+
for i, c := range cases {
284+
got := &Commit{}
285+
err := got.Decode(c.cm)
286+
if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
287+
t.Errorf("case %d error mismatch. expected: '%s', got: '%s'", i, c.err, err)
288+
continue
289+
}
290+
}
291+
}

dataset.go

+79-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func (ds *Dataset) UnmarshalJSON(data []byte) error {
169169
d := _dataset{}
170170
if err := json.Unmarshal(data, &d); err != nil {
171171
log.Debug(err.Error())
172-
return fmt.Errorf("error unmarshaling dataset: %s", err.Error())
172+
return fmt.Errorf("unmarshaling dataset: %s", err.Error())
173173
}
174174
*ds = Dataset(d)
175175
return nil
@@ -193,3 +193,81 @@ func UnmarshalDataset(v interface{}) (*Dataset, error) {
193193
return nil, err
194194
}
195195
}
196+
197+
// Encode creates a CodingDataset from a Dataset instance
198+
func (ds Dataset) Encode() *CodingDataset {
199+
cd := &CodingDataset{
200+
DataPath: ds.DataPath,
201+
Meta: ds.Meta,
202+
Path: ds.Path().String(),
203+
PreviousPath: ds.PreviousPath,
204+
Qri: ds.Qri.String(),
205+
VisConfig: ds.VisConfig,
206+
}
207+
208+
if ds.Commit != nil {
209+
cd.Commit = ds.Commit.Encode()
210+
}
211+
if ds.Structure != nil {
212+
cd.Structure = ds.Structure.Encode()
213+
}
214+
if ds.Transform != nil {
215+
cd.Transform = ds.Transform.Encode()
216+
}
217+
218+
return cd
219+
}
220+
221+
// Decode creates a Dataset from a CodingDataset instance
222+
func (ds *Dataset) Decode(cd *CodingDataset) error {
223+
d := Dataset{
224+
path: datastore.NewKey(cd.Path),
225+
DataPath: cd.DataPath,
226+
PreviousPath: cd.PreviousPath,
227+
Meta: cd.Meta,
228+
VisConfig: cd.VisConfig,
229+
}
230+
231+
if cd.Qri != "" {
232+
// TODO - this should react to changes in cd
233+
d.Qri = KindDataset
234+
}
235+
236+
if cd.Commit != nil {
237+
d.Commit = &Commit{}
238+
if err := d.Commit.Decode(cd.Commit); err != nil {
239+
return err
240+
}
241+
}
242+
243+
if cd.Structure != nil {
244+
d.Structure = &Structure{}
245+
if err := d.Structure.Decode(cd.Structure); err != nil {
246+
return err
247+
}
248+
}
249+
250+
if cd.Transform != nil {
251+
d.Transform = &Transform{}
252+
if err := d.Transform.Decode(cd.Transform); err != nil {
253+
return err
254+
}
255+
}
256+
257+
*ds = d
258+
return nil
259+
}
260+
261+
// CodingDataset is a variant of Dataset safe for serialization (encoding & decoding)
262+
// to static formats. It uses only simple go types
263+
type CodingDataset struct {
264+
Commit *CodingCommit `json:"commit,omitempty"`
265+
DataPath string `json:"dataPath,omitempty"`
266+
Meta *Meta `json:"meta,omitempty"`
267+
Path string `json:"path,omitempty"`
268+
PreviousPath string `json:"previousPath,omitempty"`
269+
Qri string `json:"qri"`
270+
Structure *CodingStructure `json:"structure"`
271+
Transform *CodingTransform `json:"transform,omitempty"`
272+
VisConfig *VisConfig `json:"visconfig,omitempty"`
273+
}

dataset_test.go

+63-5
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ func TestDatasetUnmarshalJSON(t *testing.T) {
127127
cases := []struct {
128128
FileName string
129129
result *Dataset
130-
err error
130+
err string
131131
}{
132-
{"testdata/datasets/airport-codes.json", AirportCodes, nil},
133-
{"testdata/datasets/continent-codes.json", ContinentCodes, nil},
134-
{"testdata/datasets/hours.json", Hours, nil},
132+
{"testdata/datasets/airport-codes.json", AirportCodes, ""},
133+
{"testdata/datasets/continent-codes.json", ContinentCodes, ""},
134+
{"testdata/datasets/hours.json", Hours, ""},
135135
}
136136

137137
for i, c := range cases {
@@ -141,7 +141,8 @@ func TestDatasetUnmarshalJSON(t *testing.T) {
141141
}
142142

143143
ds := &Dataset{}
144-
if err := json.Unmarshal(data, ds); err != c.err {
144+
err = ds.UnmarshalJSON(data)
145+
if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
145146
t.Errorf("case %d error mismatch. expected: '%s', got: '%s'", i, c.err, err)
146147
continue
147148
}
@@ -163,6 +164,13 @@ func TestDatasetUnmarshalJSON(t *testing.T) {
163164
t.Errorf("unmarshal didn't set proper path: %s != %s", path, strds.path)
164165
return
165166
}
167+
168+
errDs := &Dataset{}
169+
if err := errDs.UnmarshalJSON([]byte(`{{{{`)); err != nil && err.Error() != "unmarshaling dataset: invalid character '{' looking for beginning of object key string" {
170+
t.Errorf("unexpected error: %s", err.Error())
171+
} else if err == nil {
172+
t.Errorf("expected error")
173+
}
166174
}
167175

168176
func TestDatasetIsEmpty(t *testing.T) {
@@ -242,3 +250,53 @@ func TestUnmarshalDataset(t *testing.T) {
242250
}
243251
}
244252
}
253+
254+
func TestDatasetCoding(t *testing.T) {
255+
cases := []*Dataset{
256+
{},
257+
{Commit: &Commit{Title: "foo"}},
258+
{DataPath: "foo"},
259+
{Meta: &Meta{Title: "foo"}},
260+
{PreviousPath: "foo"},
261+
{Qri: KindDataset},
262+
{Structure: &Structure{Format: CBORDataFormat}},
263+
{Transform: &Transform{AppVersion: "foo"}},
264+
{VisConfig: &VisConfig{Format: "foo"}},
265+
}
266+
267+
for i, c := range cases {
268+
cd := c.Encode()
269+
got := &Dataset{}
270+
err := got.Decode(cd)
271+
if err != nil {
272+
t.Errorf("case %d unexpected error: '%s'", i, err.Error())
273+
continue
274+
}
275+
276+
if err := CompareDatasets(c, got); err != nil {
277+
t.Errorf("case %d dataset mismatch: %s", i, err.Error())
278+
continue
279+
}
280+
}
281+
}
282+
283+
func TestDatasetDecode(t *testing.T) {
284+
cases := []struct {
285+
cd *CodingDataset
286+
err string
287+
}{
288+
{&CodingDataset{}, ""},
289+
{&CodingDataset{Commit: &CodingCommit{Qri: "foo"}}, "invalid commit 'qri' value: foo"},
290+
{&CodingDataset{Structure: &CodingStructure{Format: "foo"}}, "invalid data format: `foo`"},
291+
{&CodingDataset{Transform: &CodingTransform{Resources: []byte("foo")}}, "decoding transform resources: invalid character 'o' in literal false (expecting 'a')"},
292+
}
293+
294+
for i, c := range cases {
295+
got := &Dataset{}
296+
err := got.Decode(c.cd)
297+
if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
298+
t.Errorf("case %d error mismatch. expected: '%s', got: '%s'", i, c.err, err)
299+
continue
300+
}
301+
}
302+
}

hash.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77

8-
"github.com/jbenet/go-base58"
8+
"github.com/mr-tron/base58/base58"
99
"github.com/multiformats/go-multihash"
1010
)
1111

0 commit comments

Comments
 (0)