Skip to content

Commit

Permalink
update README
Browse files Browse the repository at this point in the history
  • Loading branch information
fogfish committed Oct 1, 2021
1 parent 73db7e0 commit 3ffb5fe
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 51 deletions.
107 changes: 71 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The latest version of the library is available at its `main` branch. All develop
- [Hierarchical structures](#hierarchical-structures)
- [Sequences and Pagination](#sequences-and-pagination)
- [Linked data](#linked-data)
- [Type composition](#type-composition)
- [Type projections](#type-projections)
- [Custom codecs for core domain types](#custom-codecs-for-core-domain-types)
- [Optimistic Locking](#optimistic-locking)
- [Configure DynamoDB](#configure-dynamodb)
Expand All @@ -53,24 +53,31 @@ The latest version of the library is available at its `main` branch. All develop

Data types definition is an essential part of development with `dynamo` library. Golang structs declares domain of your application. Public fields are serialized into DynamoDB attributes, the field tag `dynamodbav` controls marshal/unmarshal process.

The library demands from each structure embedding of `dynamo.ID` type. This type acts as struct annotation -- Golang compiler raises an error at compile time if other data type is supplied for DynamoDB I/O. Secondly, this type facilitates linked-data, hierarchical structures and cheap relations between data elements.
The library demands from each structure implementation of `Thing` interface. This type acts as struct annotation -- Golang compiler raises an error at compile time if other data type is supplied for DynamoDB I/O. Secondly, each structure defines unique "composite primary key". The library encourages definition of both partition and sort keys, which facilitates linked-data, hierarchical structures and cheap relations between data elements.

```go
import "github.com/fogfish/dynamo"

type Person struct {
dynamo.ID
Org string `dynamodbav:"prefix,omitempty"`
ID string `dynamodbav:"suffix,omitempty"`
Name string `dynamodbav:"name,omitempty"`
Age int `dynamodbav:"age,omitempty"`
Address string `dynamodbav:"address,omitempty"`
}

//
// Identity implements thing interface
func (p Person) Identity() (string, string) {
return p.Org, p.ID
}

//
// this data type is a normal Golang struct
// just create an instance, fill required fields
// ID is own data type thus use dynamo.NewfID(...)
var person := Person{
ID: dynamo.NewfID("8980789222")
Org: "University",
ID: "8980789222",
Name: "Verner Pleishner",
Age: 64,
Address: "Blumenstrasse 14, Berne, 3013",
Expand Down Expand Up @@ -109,7 +116,10 @@ if err := db.Put(person); err != nil {
// Lookup the struct using Get. This function takes "empty" structure as
// a placeholder and fill it with a data upon the completion. The only
// requirement - ID has to be defined.
person := Person{ID: dynamo.NewfID("8980789222")}
person := Person{
Org: "University",
ID: "8980789222",
}
switch err := db.Get(&person).(type) {
case nil:
// success
Expand All @@ -124,15 +134,20 @@ default:
// a partially defined structure, patches the instance at storage and
// returns remaining attributes.
person := Person{
ID: dynamo.NewfID("8980789222"),
Org: "University",
ID: "8980789222",
Address: "Viktoriastrasse 37, Berne, 3013",
}
if err := db.Update(&person); err != nil {
}

//
// Remove the struct using Remove. Either give struct or ID to it
if err := db.Remove(dynamo.NewfID("8980789222")); err != nil {
// Remove the struct using Remove give partially defined struct with ID
person := Person{
Org: "University",
ID: "8980789222",
}
if err := db.Remove(person); err != nil {
}
```

Expand All @@ -150,28 +165,28 @@ A
└ G
```

The data type `dynamo.ID` is core type to organize hierarchies. This data type is a synonym to compact Internationalized Resource Identifiers (`curie.IRI`), which facilitates linked-data, hierarchical structures and cheap relations between data items. An application declares node path using composite sort key design pattern. For example, the root is `dynamo.NewfID("thread:A")`, 2nd rank node `dynamo.NewfID("thread:A#C")`, 3rd rank node `dynamo.NewfID("thread:A#C/E")` and so on `dynamo.NewID("thread:A#C/E/F")`. Each `id` declares partition and sub nodes. The library implement a `Match` function, supply the node identity and it returns sequence of child elements.
Composite sort key is core concept to organize hierarchies. It facilitates linked-data, hierarchical structures and cheap relations between data items. An application declares node path using composite sort key design pattern. For example, the root is `thread:A`, 2nd rank node `thread:A, C⟩`, 3rd rank node `thread:A, C/E` and so on `thread:A, C/E/F`. Each `id` declares partition and sub nodes. The library implement a `Match` function, supply the node identity and it returns sequence of child elements.

```go
//
// Match uses dynamo.ID to match DynamoDB entries. It returns a sequence of
// Match uses partition key to match DynamoDB entries. It returns a sequence of
// generic representations that has to be transformed into actual data types
// FMap is an utility it takes a closure function that lifts generic to
// the struct.
db.Match(dynamo.NewfID("thread:A")).FMap(
db.Match(Message{Thread: "thread:A"}).FMap(
func(gen dynamo.Gen) error {
p := person{}
return gen.To(&p)
m := Message{}
return gen.To(&m)
}
)

//
// Type aliases is the best approach to lift generic sequence in type safe one.
type persons []person
type Messages []Message

// Join is a monoid to append generic element into sequence
func (seq *persons) Join(gen dynamo.Gen) error {
val := person{}
func (seq *Messages) Join(gen dynamo.Gen) error {
val := Message{}
if err := gen.To(&val); err != nil {
return err
}
Expand All @@ -180,8 +195,8 @@ func (seq *persons) Join(gen dynamo.Gen) error {
}

// and final magic to discover hierarchy of elements
seq := persons{}
db.Match(dynamo.NewfID("thread:A#C/E")).FMap(seq.Join)
seq := Messages{}
db.Match(Message{Thread: "thread:A", ID: "C/E"}).FMap(seq.Join)
```

See the [go doc](https://pkg.go.dev/github.com/fogfish/dynamo?tab=doc) for api spec and [advanced example](example) app.
Expand All @@ -193,48 +208,67 @@ Hierarchical structures is the way to organize collections, lists, sets, etc. Th

```go
// 1. Set the limit on the stream
seq := db.Match(dynamo.NewfID("thread:A#C")).Limit(25)
seq := db.Match(Message{Thread: "thread:A", ID: "C"}).Limit(25)
// 2. Consume the stream
seq.FMap(persons.Join)
// 3. Read cursor value
cursor := seq.Cursor()


// 4. Continue I/O with a new stream, supply the cursor
seq := db.Match(dynamo.NewfID("thread:A#C")).Limit(25).Continue(cursor)
seq := db.Match(Message{Thread: "thread:A", ID: "C"}).Limit(25).Continue(cursor)
```


### Linked data

Cross-linking of structured data is an essential part of type safe domain driven design. The library helps developers to model relations between data instances using familiar data type:
Cross-linking of structured data is an essential part of type safe domain driven design. The library helps developers to model relations between data instances using familiar data type.

```go
type Person struct {
dynamo.ID
Account *dynamo.IRI `dynamodbav:"account,omitempty"`
Org string `dynamodbav:"prefix,omitempty"`
ID string `dynamodbav:"suffix,omitempty"`
Leader string `dynamodbav:"leader,omitempty"`
}
```

`dynamo.ID` and `dynamo.IRI` are sibling, equivalent data types. `ID` is only used as primary identity, `IRI` is a "pointer" to linked-data.
`ID` and `Leader` are sibling, equivalent data types. `ID` is only used as primary identity, `Leader` is a "pointer" to linked-data. Some of examples, supplied with the library, uses compact Internationalized Resource Identifiers (`curie.IRI`) for this purpose. Semantic Web publishes structured data using this type so that it can be interlinked by applications.


### Type composition
### Type projections

Often, there is an established system of the types in the application.
It is not convenient either to inject `dynamo.ID` or re-define a new type just to facilitate storage I/O. The composition of types is the solution.
Often, there is an established system of the types in the application. It is not convenient to inject dependencies to the `dynamo` library. Then usage of secondary indexes requires multiple projections of core type. The composition of types is the solution.

```go
//
// original core type
type Person struct {
Org string `dynamodbav:"prefix,omitempty"`
ID string `dynamodbav:"suffix,omitempty"`
Name string `dynamodbav:"name,omitempty"`
Age int `dynamodbav:"age,omitempty"`
Address string `dynamodbav:"address,omitempty"`
Country string `dynamodbav:"country,omitempty"`
}

type dbPerson struct{
dynamo.ID
Person
}
//
// the core type projection that uses ⟨Org, ID⟩ as composite key
// e.g. this projection supports writes to DynamoDB table
type dbPerson Person

func (p dbPerson) Identity() (string, string) { return p.Org, p.ID }

//
// the core type projection that uses ⟨Org, Name⟩ as composite key
// e.g. the projection support lookup of employer
type dbNamedPerson Person

func (p dbNamedPerson) Identity() (string, string) { return p.Org, p.Name }

//
// the core type projection that uses ⟨Country, Name⟩ as composite key
type dbCitizen Person

func (p dbCitizen) Identity() (string, string) { return p.Country, p.Name }
```

### Custom codecs for core domain types
Expand All @@ -244,7 +278,8 @@ Development of complex Golang application might lead developers towards [Standar
```go
// core.go
type Person struct {
ID curie.IRI `dynamodbav:"-"`
Org curie.IRI `dynamodbav:"prefix,omitempty"`
ID curie.IRI `dynamodbav:"suffix,omitempty"`
Account *curie.Safe `dynamodbav:"account,omitempty"`
}

Expand All @@ -253,12 +288,12 @@ type dbPerson Person

func (x dbPerson) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
type tStruct dbPerson
return dynamo.Encode(av, x.ID, tStruct(x))
return dynamo.Encode(av, dynamo.IRI(x.Org), dynamo.IRI(x.ID), tStruct(x))
}

func (x *dbPerson) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
type tStruct *dbPerson
return dynamo.Decode(av, &x.ID, tStruct(x))
return dynamo.Decode(av, (*dynamo.IRI)(&x.Org), (*dynamo.IRI)(&x.ID), tStruct(x))
}
```

Expand Down
30 changes: 15 additions & 15 deletions example/keyval/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,26 @@ import (
//
// Person type demonstrates composition of core type with db one
type Person struct {
HKey curie.IRI `dynamodbav:"prefix,omitempty"`
SKey curie.IRI `dynamodbav:"suffix,omitempty"`
Org curie.IRI `dynamodbav:"prefix,omitempty"`
ID curie.IRI `dynamodbav:"suffix,omitempty"`
Name string `dynamodbav:"name,omitempty"`
Age int `dynamodbav:"age,omitempty"`
Address string `dynamodbav:"address,omitempty"`
}

//
func (p Person) Identity() (string, string) { return p.HKey.String(), p.SKey.String() }
func (p Person) Identity() (string, string) { return p.Org.String(), p.ID.String() }

//
func (p Person) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
type tStruct Person
return dynamo.Encode(av, dynamo.IRI(p.HKey), dynamo.IRI(p.SKey), tStruct(p))
return dynamo.Encode(av, dynamo.IRI(p.Org), dynamo.IRI(p.ID), tStruct(p))
}

//
func (p *Person) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
type tStruct *Person
return dynamo.Decode(av, (*dynamo.IRI)(&p.HKey), (*dynamo.IRI)(&p.SKey), tStruct(p))
return dynamo.Decode(av, (*dynamo.IRI)(&p.Org), (*dynamo.IRI)(&p.ID), tStruct(p))
}

//
Expand Down Expand Up @@ -75,8 +75,8 @@ const n = 5
func examplePut(db KeyVal) {
for i := 0; i < n; i++ {
val := &Person{
HKey: curie.New("test:"),
SKey: curie.New("person:%d", i),
Org: curie.New("test:"),
ID: curie.New("person:%d", i),
Name: "Verner Pleishner",
Age: 64,
Address: "Blumenstrasse 14, Berne, 3013",
Expand All @@ -90,14 +90,14 @@ func examplePut(db KeyVal) {
func exampleGet(db KeyVal) {
for i := 0; i < n; i++ {
val := &Person{
HKey: curie.New("test:"),
SKey: curie.New("person:%d", i),
Org: curie.New("test:"),
ID: curie.New("person:%d", i),
}
switch err := db.Get(val).(type) {
case nil:
fmt.Printf("=[ get ]=> %+v\n", val)
case dynamo.NotFound:
fmt.Printf("=[ get ]=> Not found: (%v, %v)\n", val.HKey, val.SKey)
fmt.Printf("=[ get ]=> Not found: (%v, %v)\n", val.Org, val.ID)
default:
fmt.Printf("=[ get ]=> Fail: %v\n", err)
}
Expand All @@ -107,8 +107,8 @@ func exampleGet(db KeyVal) {
func exampleUpdate(db KeyVal) {
for i := 0; i < n; i++ {
val := &Person{
HKey: curie.New("test:"),
SKey: curie.New("person:%d", i),
Org: curie.New("test:"),
ID: curie.New("person:%d", i),
Address: "Viktoriastrasse 37, Berne, 3013",
}
err := db.Update(val)
Expand All @@ -119,7 +119,7 @@ func exampleUpdate(db KeyVal) {

func exampleMatch(db KeyVal) {
seq := Persons{}
err := db.Match(Person{HKey: curie.New("test:")}).FMap(seq.Join)
err := db.Match(Person{Org: curie.New("test:")}).FMap(seq.Join)

if err == nil {
fmt.Printf("=[ match ]=> %+v\n", seq)
Expand All @@ -131,8 +131,8 @@ func exampleMatch(db KeyVal) {
func exampleRemove(db KeyVal) {
for i := 0; i < n; i++ {
val := &Person{
HKey: curie.New("test:"),
SKey: curie.New("person:%d", i),
Org: curie.New("test:"),
ID: curie.New("person:%d", i),
}
err := db.Remove(val)

Expand Down

0 comments on commit 3ffb5fe

Please sign in to comment.