Skip to content

Commit

Permalink
(#34): add helpers to Marshal/Unmarshal core types
Browse files Browse the repository at this point in the history
  • Loading branch information
fogfish committed Aug 23, 2021
1 parent 2638adc commit dcb86ef
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 64 deletions.
12 changes: 1 addition & 11 deletions core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Item struct {
Tag string `json:"tag,omitempty" dynamodbav:"tag,omitempty"`
}

var fixtureLink dynamo.ID = dynamo.ID{dynamo.IRI{curie.New("foo:a/suffix")}}
var fixtureLink dynamo.ID = dynamo.ID{dynamo.IRI(curie.New("foo:a/suffix"))}

var fixtureItem Item = Item{
ID: dynamo.NewID("foo:prefix/suffix"),
Expand Down Expand Up @@ -115,13 +115,3 @@ func TestIDs(t *testing.T) {
If(a.Identity()).Should().Equal(expect).
If(b.Identity()).Should().Equal(expect)
}

func TestUnwrap(t *testing.T) {
c := curie.New("a:b/c")
a := dynamo.IRI{c}
var b *dynamo.IRI

it.Ok(t).
If(*a.Unwrap()).Should().Equal(c).
If(b.Unwrap()).Should().Equal(nil)
}
28 changes: 17 additions & 11 deletions ddb.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,12 @@ func (gen *dbGen) ID() (*ID, error) {
return nil, errors.New("Invalid DDB schema")
}

pf := aws.StringValue(prefix.S)
sf := aws.StringValue(suffix.S)
id := MkID(curie.Join(curie.New(pf), sf))
iri := curie.New(aws.StringValue(prefix.S))
if aws.StringValue(suffix.S) != "_" {
iri = curie.Join(iri, aws.StringValue(suffix.S))
}

id := MkID(iri)
return &id, nil
}

Expand Down Expand Up @@ -397,10 +399,10 @@ func (seq *dbSeq) Cursor() *curie.IRI {
val := seq.q.ExclusiveStartKey
prefix, _ := val[seq.dynamo.pkPrefix]
suffix, _ := val[seq.dynamo.skSuffix]
iri := curie.Join(
curie.New(aws.StringValue(prefix.S)),
aws.StringValue(suffix.S),
)
iri := curie.New(aws.StringValue(prefix.S))
if aws.StringValue(suffix.S) != "_" {
iri = curie.Join(iri, aws.StringValue(suffix.S))
}

return &iri
}
Expand All @@ -427,6 +429,8 @@ func (seq *dbSeq) Continue(cursor *curie.IRI) Seq {
key[seq.dynamo.pkPrefix] = &dynamodb.AttributeValue{S: aws.String(curie.Prefix(*cursor))}
if curie.Suffix(*cursor) != "" {
key[seq.dynamo.skSuffix] = &dynamodb.AttributeValue{S: aws.String(curie.Suffix(*cursor))}
} else {
key[seq.dynamo.skSuffix] = &dynamodb.AttributeValue{S: aws.String("_")}
}
seq.q.ExclusiveStartKey = key
}
Expand Down Expand Up @@ -473,6 +477,8 @@ func marshal(cfg ddbConfig, entity Thing) (map[string]*dynamodb.AttributeValue,
gen[cfg.pkPrefix] = &dynamodb.AttributeValue{S: aws.String(curie.Prefix(iri))}
if curie.Suffix(iri) != "" {
gen[cfg.skSuffix] = &dynamodb.AttributeValue{S: aws.String(curie.Suffix(iri))}
} else {
gen[cfg.skSuffix] = &dynamodb.AttributeValue{S: aws.String("_")}
}

delete(gen, "id")
Expand All @@ -499,10 +505,10 @@ func unmarshal(cfg ddbConfig, ddb map[string]*dynamodb.AttributeValue) (map[stri
return nil, errors.New("Invalid DDB schema")
}

iri := curie.Join(
curie.New(aws.StringValue(prefix.S)),
aws.StringValue(suffix.S),
)
iri := curie.New(aws.StringValue(prefix.S))
if aws.StringValue(suffix.S) != "_" {
iri = curie.Join(iri, aws.StringValue(suffix.S))
}
ddb["id"] = &dynamodb.AttributeValue{S: aws.String(iri.String())}

delete(ddb, cfg.pkPrefix)
Expand Down
42 changes: 21 additions & 21 deletions ddb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestDdbUpdate(t *testing.T) {

func TestDdbMatch(t *testing.T) {
cnt := 0
seq := apiDB().Match(dynamo.NewID("dead:"))
seq := apiDB().Match(dynamo.NewID("dead:beef"))

for seq.Tail() {
cnt++
Expand All @@ -77,7 +77,7 @@ func TestDdbMatch(t *testing.T) {
}

func TestDdbMatchHead(t *testing.T) {
seq := apiDB().Match(dynamo.NewID("dead:"))
seq := apiDB().Match(dynamo.NewID("dead:beef"))

val := person{}
err := seq.Head(&val)
Expand All @@ -102,7 +102,7 @@ func (seq *persons) Join(gen dynamo.Gen) (dynamo.Thing, error) {

func TestDdbMatchWithFMap(t *testing.T) {
pseq := persons{}
tseq, err := apiDB().Match(dynamo.NewID("dead:")).FMap(pseq.Join)
tseq, err := apiDB().Match(dynamo.NewID("dead:beef")).FMap(pseq.Join)

thing := entity()
it.Ok(t).
Expand All @@ -113,7 +113,7 @@ func TestDdbMatchWithFMap(t *testing.T) {

func TestDdbMatchIDsWithFMap(t *testing.T) {
seq := dynamo.IDs{}
_, err := apiDB().Match(dynamo.NewID("dead:")).FMap(seq.Join)
_, err := apiDB().Match(dynamo.NewID("dead:beef")).FMap(seq.Join)

thing := entity().ID
it.Ok(t).
Expand All @@ -139,17 +139,17 @@ type mockDDB struct {

func (mockDDB) GetItem(input *dynamodb.GetItemInput) (*dynamodb.GetItemOutput, error) {
expect := map[string]*dynamodb.AttributeValue{
"prefix": {S: aws.String("dead:")},
"suffix": {S: aws.String("beef")},
"prefix": {S: aws.String("dead:beef")},
"suffix": {S: aws.String("_")},
}
if !reflect.DeepEqual(expect, input.Key) {
return nil, errors.New("Unexpected entity.")
}

return &dynamodb.GetItemOutput{
Item: map[string]*dynamodb.AttributeValue{
"prefix": {S: aws.String("dead:")},
"suffix": {S: aws.String("beef")},
"prefix": {S: aws.String("dead:beef")},
"suffix": {S: aws.String("_")},
"address": {S: aws.String("Blumenstrasse 14, Berne, 3013")},
"name": {S: aws.String("Verner Pleishner")},
"age": {N: aws.String("64")},
Expand All @@ -159,8 +159,8 @@ func (mockDDB) GetItem(input *dynamodb.GetItemInput) (*dynamodb.GetItemOutput, e

func (mockDDB) PutItem(input *dynamodb.PutItemInput) (*dynamodb.PutItemOutput, error) {
expect := map[string]*dynamodb.AttributeValue{
"prefix": {S: aws.String("dead:")},
"suffix": {S: aws.String("beef")},
"prefix": {S: aws.String("dead:beef")},
"suffix": {S: aws.String("_")},
"address": {S: aws.String("Blumenstrasse 14, Berne, 3013")},
"name": {S: aws.String("Verner Pleishner")},
"age": {N: aws.String("64")},
Expand All @@ -174,8 +174,8 @@ func (mockDDB) PutItem(input *dynamodb.PutItemInput) (*dynamodb.PutItemOutput, e

func (mockDDB) DeleteItem(input *dynamodb.DeleteItemInput) (*dynamodb.DeleteItemOutput, error) {
expect := map[string]*dynamodb.AttributeValue{
"prefix": {S: aws.String("dead:")},
"suffix": {S: aws.String("beef")},
"prefix": {S: aws.String("dead:beef")},
"suffix": {S: aws.String("_")},
}
if !reflect.DeepEqual(expect, input.Key) {
return nil, errors.New("Unexpected entity.")
Expand All @@ -186,8 +186,8 @@ func (mockDDB) DeleteItem(input *dynamodb.DeleteItemInput) (*dynamodb.DeleteItem

func (mockDDB) UpdateItem(input *dynamodb.UpdateItemInput) (*dynamodb.UpdateItemOutput, error) {
expect := map[string]*dynamodb.AttributeValue{
"prefix": {S: aws.String("dead:")},
"suffix": {S: aws.String("beef")},
"prefix": {S: aws.String("dead:beef")},
"suffix": {S: aws.String("_")},
}
if !reflect.DeepEqual(expect, input.Key) {
return nil, errors.New("Unexpected entity.")
Expand All @@ -202,18 +202,18 @@ func (mockDDB) UpdateItem(input *dynamodb.UpdateItemInput) (*dynamodb.UpdateItem

return &dynamodb.UpdateItemOutput{
Attributes: map[string]*dynamodb.AttributeValue{
"prefix": {S: aws.String("dead:")},
"prefix": {S: aws.String("dead:beef")},
"address": {S: aws.String("Blumenstrasse 14, Berne, 3013")},
"name": {S: aws.String("Verner Pleishner")},
"suffix": {S: aws.String("beef")},
"suffix": {S: aws.String("_")},
"age": {N: aws.String("64")},
},
}, nil
}

func (mockDDB) Query(input *dynamodb.QueryInput) (*dynamodb.QueryOutput, error) {
expect := map[string]*dynamodb.AttributeValue{
":prefix": {S: aws.String("dead:")},
":prefix": {S: aws.String("dead:beef")},
}
if !reflect.DeepEqual(expect, input.ExpressionAttributeValues) {
return nil, errors.New("Unexpected entity.")
Expand All @@ -224,17 +224,17 @@ func (mockDDB) Query(input *dynamodb.QueryInput) (*dynamodb.QueryOutput, error)
Count: aws.Int64(2),
Items: []map[string]*dynamodb.AttributeValue{
{
"prefix": {S: aws.String("dead:")},
"prefix": {S: aws.String("dead:beef")},
"address": {S: aws.String("Blumenstrasse 14, Berne, 3013")},
"name": {S: aws.String("Verner Pleishner")},
"suffix": {S: aws.String("beef")},
"suffix": {S: aws.String("_")},
"age": {N: aws.String("64")},
},
{
"prefix": {S: aws.String("dead:")},
"prefix": {S: aws.String("dead:beef")},
"address": {S: aws.String("Blumenstrasse 14, Berne, 3013")},
"name": {S: aws.String("Verner Pleishner")},
"suffix": {S: aws.String("beef")},
"suffix": {S: aws.String("_")},
"age": {N: aws.String("64")},
},
},
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.17

require (
github.com/aws/aws-sdk-go v1.40.27
github.com/fogfish/curie v1.2.0
github.com/fogfish/curie v1.3.0
github.com/fogfish/it v0.9.1
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ github.com/aws/aws-sdk-go v1.40.27 h1:8fWW0CpmBZ8WWduNwl4vE9t07nMYFrhAsUHjPj81qU
github.com/aws/aws-sdk-go v1.40.27/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fogfish/curie v1.2.0 h1:2wVfqJY18Ud6uDgUYRua2M/ld4AKI11u8eO6enJYGbs=
github.com/fogfish/curie v1.2.0/go.mod h1:jPv7pg4hHd8Ug/USG29ZA2bAwlRfh/iinY90/30ATGg=
github.com/fogfish/curie v1.3.0 h1:FCX0Mb+ief6t93KJrGn4utlXQCdLV9mXQpCmc5V6rmc=
github.com/fogfish/curie v1.3.0/go.mod h1:jPv7pg4hHd8Ug/USG29ZA2bAwlRfh/iinY90/30ATGg=
github.com/fogfish/it v0.9.1 h1:Pu+qgqBV2ilZDzZzPIbUIhMIkdpHgbGUsdEwVQvBxNQ=
github.com/fogfish/it v0.9.1/go.mod h1:NQJG4Ygvek85y7zGj0Gny8+6ygAnHjfBORhI7TdQhp4=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
Expand Down
96 changes: 78 additions & 18 deletions id.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
package dynamo

import (
"encoding/json"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
Expand All @@ -20,31 +22,20 @@ import (
IRI is an alias to compact URI type.
The alias ensures compact URI serialization into DynamoDB schema.
*/
type IRI struct{ curie.IRI }

/*
Unwrap extract curie.IRI from dynamo.IRI
*/
func (iri *IRI) Unwrap() *curie.IRI {
if iri == nil {
return nil
}
return &iri.IRI
}
type IRI curie.IRI

/*
MarshalDynamoDBAttributeValue `IRI ⟼ "prefix:suffix"`
*/
func (iri IRI) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
if curie.Rank(curie.IRI(iri.IRI)) == 0 {
if curie.Rank(curie.IRI(iri)) == 0 {
av.NULL = aws.Bool(true)
return nil
}

// Note: we are using string representation to allow linked data in dynamo tables
val, err := dynamodbattribute.Marshal(iri.String())
val, err := dynamodbattribute.Marshal(curie.IRI(iri).String())
if err != nil {
return err
}
Expand All @@ -58,7 +49,76 @@ func (iri IRI) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error
UnmarshalDynamoDBAttributeValue `"prefix:suffix" ⟼ IRI`
*/
func (iri *IRI) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
*iri = IRI{curie.New(aws.StringValue(av.S))}
*iri = IRI(curie.New(aws.StringValue(av.S)))
return nil
}

/*
MarshalJSON `IRI ⟼ "[prefix:suffix]"`
*/
func (iri IRI) MarshalJSON() ([]byte, error) {
return json.Marshal(curie.IRI(iri))
}

/*
UnmarshalJSON `"[prefix:suffix]" ⟼ IRI`
*/
func (iri *IRI) UnmarshalJSON(b []byte) error {
var val curie.IRI

err := json.Unmarshal(b, &val)
if err != nil {
return err
}

*iri = IRI(val)
return nil
}

/*
Encode is a helper function to encode core domain types into struct.
The helper ensures compact URI serialization into DynamoDB schema.
func (x MyType) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
type tStruct MyType
return dynamo.Encode(av, x.ID, tStruct(x))
}
*/
func Encode(av *dynamodb.AttributeValue, id curie.IRI, val interface{}) error {
gen, err := dynamodbattribute.Marshal(val)
if err != nil {
return err
}

uid, err := dynamodbattribute.Marshal(IRI(id))
if err != nil {
return err
}

gen.M["id"] = uid

*av = *gen
return nil
}

/*
Decode is a helper function to decode core domain types from Dynamo DB format.
The helper ensures compact URI de-serialization from DynamoDB schema.
func (x *MyType) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
type tStruct *MyType
return dynamo.Decode(av, &x.ID, tStruct(x))
}
*/
func Decode(av *dynamodb.AttributeValue, id *curie.IRI, val interface{}) error {
dynamodbattribute.Unmarshal(av, val)

var xx IRI
dynamodbattribute.Unmarshal(av.M["id"], &xx)
*id = curie.IRI(xx)

return nil
}

Expand All @@ -82,15 +142,15 @@ type ID struct {
NewID transform category of strings to dynamo.ID.
*/
func NewID(iri string, args ...interface{}) ID {
return ID{IRI{curie.New(iri, args...)}}
return ID{IRI(curie.New(iri, args...))}
}

/*
MkID transform category of curie.IRI to dynamo.ID.
*/
func MkID(iri curie.IRI) ID {
return ID{IRI{iri}}
return ID{IRI(iri)}
}

/*
Expand All @@ -99,7 +159,7 @@ Identity makes CURIE compliant to Thing interface so that embedding ID makes any
struct to be Thing.
*/
func (id ID) Identity() curie.IRI {
return id.IRI.IRI
return curie.IRI(id.IRI)
}

/*
Expand Down
Loading

0 comments on commit dcb86ef

Please sign in to comment.