forked from TrueCloudLab/distribution
Add field type for serialization
Since reference itself may be represented by multiple types which implement the reference inteface, serialization can lead to ambiguous type which cannot be deserialized. Field wraps the reference object to ensure that the correct type is always deserialized, requiring an extra unwrap of the reference after deserialization. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
parent
31a448a628
commit
bcda04d6cd
2 changed files with 168 additions and 0 deletions
|
@ -57,6 +57,44 @@ type Reference interface {
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field provides a wrapper type for resolving correct reference types when
|
||||||
|
// working with encoding.
|
||||||
|
type Field struct {
|
||||||
|
reference Reference
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsField wraps a reference in a Field for encoding.
|
||||||
|
func AsField(reference Reference) Field {
|
||||||
|
return Field{reference}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference unwraps the reference type from the field to
|
||||||
|
// return the Reference object. This object should be
|
||||||
|
// of the appropriate type to further check for different
|
||||||
|
// reference types.
|
||||||
|
func (f Field) Reference() Reference {
|
||||||
|
return f.reference
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText serializes the field to byte text which
|
||||||
|
// is the string of the reference.
|
||||||
|
func (f Field) MarshalText() (p []byte, err error) {
|
||||||
|
return []byte(f.reference.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText parses text bytes by invoking the
|
||||||
|
// reference parser to ensure the appropriately
|
||||||
|
// typed reference object is wrapped by field.
|
||||||
|
func (f *Field) UnmarshalText(p []byte) error {
|
||||||
|
r, err := Parse(string(p))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.reference = r
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Named is an object with a full name
|
// Named is an object with a full name
|
||||||
type Named interface {
|
type Named interface {
|
||||||
Name() string
|
Name() string
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package reference
|
package reference
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -265,3 +266,132 @@ func TestSplitHostname(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type serializationType struct {
|
||||||
|
Description string
|
||||||
|
Field Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerialization(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
description string
|
||||||
|
input string
|
||||||
|
name string
|
||||||
|
tag string
|
||||||
|
digest string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "empty value",
|
||||||
|
err: ErrNameEmpty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "just a name",
|
||||||
|
input: "example.com:8000/named",
|
||||||
|
name: "example.com:8000/named",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "name with a tag",
|
||||||
|
input: "example.com:8000/named:tagged",
|
||||||
|
name: "example.com:8000/named",
|
||||||
|
tag: "tagged",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "name with digest",
|
||||||
|
input: "other.com/named@sha256:1234567890098765432112345667890098765",
|
||||||
|
name: "other.com/named",
|
||||||
|
digest: "sha256:1234567890098765432112345667890098765",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
failf := func(format string, v ...interface{}) {
|
||||||
|
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string]string{
|
||||||
|
"Description": testcase.description,
|
||||||
|
"Field": testcase.input,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
failf("error marshalling: %v", err)
|
||||||
|
}
|
||||||
|
t := serializationType{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &t); err != nil {
|
||||||
|
if testcase.err == nil {
|
||||||
|
failf("error unmarshalling: %v", err)
|
||||||
|
}
|
||||||
|
if err != testcase.err {
|
||||||
|
failf("wrong error, expected %v, got %v", testcase.err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
} else if testcase.err != nil {
|
||||||
|
failf("expected error unmarshalling: %v", testcase.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Description != testcase.description {
|
||||||
|
failf("wrong description, expected %q, got %q", testcase.description, t.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := t.Field.Reference()
|
||||||
|
|
||||||
|
if named, ok := ref.(Named); ok {
|
||||||
|
if named.Name() != testcase.name {
|
||||||
|
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name)
|
||||||
|
}
|
||||||
|
} else if testcase.name != "" {
|
||||||
|
failf("expected named type, got %T", ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
tagged, ok := ref.(Tagged)
|
||||||
|
if testcase.tag != "" {
|
||||||
|
if ok {
|
||||||
|
if tagged.Tag() != testcase.tag {
|
||||||
|
failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failf("expected tagged type, got %T", ref)
|
||||||
|
}
|
||||||
|
} else if ok {
|
||||||
|
failf("unexpected tagged type")
|
||||||
|
}
|
||||||
|
|
||||||
|
digested, ok := ref.(Digested)
|
||||||
|
if testcase.digest != "" {
|
||||||
|
if ok {
|
||||||
|
if digested.Digest().String() != testcase.digest {
|
||||||
|
failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failf("expected digested type, got %T", ref)
|
||||||
|
}
|
||||||
|
} else if ok {
|
||||||
|
failf("unexpected digested type")
|
||||||
|
}
|
||||||
|
|
||||||
|
t = serializationType{
|
||||||
|
Description: testcase.description,
|
||||||
|
Field: AsField(ref),
|
||||||
|
}
|
||||||
|
|
||||||
|
b2, err := json.Marshal(t)
|
||||||
|
if err != nil {
|
||||||
|
failf("error marshing serialization type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(b) != string(b2) {
|
||||||
|
failf("unexpected serialized value: expected %q, got %q", string(b), string(b2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure t.Field is not implementing "Reference" directly, getting
|
||||||
|
// around the Reference type system
|
||||||
|
var fieldInterface interface{} = t.Field
|
||||||
|
if _, ok := fieldInterface.(Reference); ok {
|
||||||
|
failf("field should not implement Reference interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue