Skip to content
This repository has been archived by the owner on Jun 12, 2020. It is now read-only.

Commit

Permalink
add some public marshalling to mappings, built-in mappings again requ…
Browse files Browse the repository at this point in the history
…ire struct instances on init
  • Loading branch information
carloscm committed May 6, 2012
1 parent 4baea69 commit 6e03459
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 120 deletions.
20 changes: 5 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The low level interface is based on passing []byte values for everything, mirror
### Struct mapping
The Mapping interface and its implementations allow to convert Go structs into Rows, and they have support of advanced features like composites or overriding column names and types. NewSparse() returns a Mapping for the new CQL 3.0 pattern of composite "primary keys":
The Mapping interface and its implementations allow to convert Go structs into Rows, and they have support of advanced features like composites or overriding column names and types. Built-in NewMapping() returns a Mapping implementation that can map and unmap Go structs from Cassandra rows, serialized in classic key/value rows or in composited column names, with support for both sparse and compact storage. For example:
```Go
/*
Expand All @@ -87,30 +87,20 @@ CREATE TABLE Timeline (

// In Gossie:
type Tweet struct {
UserID string
UserID string `cf:"Timeline" key:"UserID" cols:"TweetID"`
TweetID int64
Author string
Body string
}

mapping := gossie.NewSparse("Timeline", "UserID", "TweetID")
mapping := gossie.NewMapping(&Tweet{})
row, err = mapping.Map(&Tweet{"userid", 10000000000004, "Author Name", "Hey this thing rocks!"})
err = pool.Writer().Insert("Timeline", row).Run()
````

When calling Mapping.Map() you can tag your struct fiels with `name`, `type` and `skip`. The `name` field tag will change the column name to its value when the field it appears on is (un)marhsaled to/from a Cassandra row column. The `type` field tag allows to override the default type Go<->Cassandra type mapping used by Gossie for the field it appears on. If `skip:"true"` is present the field will be ignored by Gossie.

The tags `cf`, `key` and `cols` can be used in any field in the struct to document a mapping. It can later be extracted with `MappingFromTags()` by passing any instance of the struct, even an empty one. For example this is equivalent to the mapping created with `NewSparse()` in the previous example:
When calling NewMapping() you can tag your struct fiels with `name`, `type` and `skip`. The `name` field tag will change the column name to its value when the field it appears on is (un)marhsaled to/from a Cassandra row column. The `type` field tag allows to override the default type Go<->Cassandra type mapping used by Gossie for the field it appears on. If `skip:"true"` is present the field will be ignored by Gossie.

```Go
type Tweet struct {
UserID string `cf:"Timeline" key:"UserID" cols:"TweetID"`
TweetID int64
Author string
Body string
}
mapping := gossie.MappingFromTags(&Tweet{})
```
The tags `cf`, `key`, `cols` and `value` can be used in any field in the struct to document a mapping. `cf` is the column family name. `key` is the field name in the struct that stores the Cassandra row key value. `cols` is a list of struct fiels that build up the composite column name, if there is any. `value` is the field that stores the column value for compact storage rows.

### Query and Result interfaces (planned)

Expand Down
108 changes: 70 additions & 38 deletions src/gossie/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import (
mapping for Go slices (N slices?)
*/

// Mapping maps the type of a Go object to/from (a slice of) a Cassandra row.
// Mapping maps the type of a Go object to/from a Cassandra row.
type Mapping interface {

// Cf returns the column family name
Cf() string

// MinColumns returns the minimal number of columns required by the mapped Go
// object
MinColumns(source interface{}) int
MarshalKey(key interface{}) ([]byte, error)

MarshalComponent(component interface{}, position int) ([]byte, error)

// Map converts a Go object compatible with this Mapping into a Row
Map(source interface{}) (*Row, error)
Expand All @@ -32,56 +32,78 @@ type Mapping interface {
Unmap(destination interface{}, offset int, row *Row) (int, error)
}

// MappingFromTags looks up the field tag 'mapping' in the passed struct type
var (
noMoreComponents = errors.New("No more components allowed")
)

// NewMapping looks up the field tag 'mapping' in the passed struct type
// to decide which mapping it is using, then builds a mapping using the 'cf',
// 'key', 'cols' and 'value' field tags.
func MappingFromTags(source interface{}) (Mapping, error) {
func NewMapping(source interface{}) (Mapping, error) {
_, si, err := validateAndInspectStruct(source)
if err != nil {
return nil, err
}
// mandatory tags for all the provided mappings
for _, t := range []string{"cf", "key"} {
_, found := si.globalTags[t]
if !found {
return nil, errors.New(fmt.Sprint("Mandatory struct tag ", t, " not found in passed struct of type ", si.rtype.Name()))
}

cf, found := si.globalTags["cf"]
if !found {
return nil, errors.New(fmt.Sprint("Mandatory struct tag 'cf' not found in passed struct of type ", si.rtype.Name()))
}

key, found := si.globalTags["key"]
if !found {
return nil, errors.New(fmt.Sprint("Mandatory struct tag 'key' not found in passed struct of type ", si.rtype.Name()))
}
_, found = si.goFields[key]
if !found {
return nil, errors.New(fmt.Sprint("Key field ", key, " not found in passed struct of type ", si.rtype.Name()))
}
// optional tags

colsS := []string{}
cols, found := si.globalTags["cols"]
if found {
colsS = strings.Split(cols, ",")
}
for _, c := range colsS {
_, found := si.goFields[c]
if !found {
return nil, errors.New(fmt.Sprint("Composite field ", c, " not found in passed struct of type ", si.rtype.Name()))
}
}

value, found := si.globalTags["value"]
if found {
_, found := si.goFields[value]
if !found {
return nil, errors.New(fmt.Sprint("Value field ", value, " not found in passed struct of type ", si.rtype.Name()))
}
}

mapping, found := si.globalTags["mapping"]
if !found {
mapping = "sparse"
}
value := si.globalTags["value"]

switch mapping {
case "sparse":
return NewSparse(si.globalTags["cf"], si.globalTags["key"], colsS...), nil
return newSparseMapping(si, cf, key, colsS...), nil
case "compact":
if value == "" {
return nil, errors.New(fmt.Sprint("Mandatory struct tag value for compact mapping not found in passed struct of type ", si.rtype.Name()))
}
return NewCompact(si.globalTags["cf"], si.globalTags["key"], si.globalTags["value"], colsS...), nil
return newCompactMapping(si, cf, key, value, colsS...), nil
}

return nil, errors.New(fmt.Sprint("Unrecognized mapping type ", mapping, " in passed struct of type ", si.rtype.Name()))
}

// Sparse returns a mapping for Go structs that represents a Cassandra row key
// as a struct field, zero or more composite column names as zero or more
// struct fields, and the rest of the struct fields as extra columns with the
// name being the last composite column name, and the value the column value.
func NewSparse(cf string, keyField string, componentFields ...string) Mapping {
func newSparseMapping(si *structInspection, cf string, keyField string, componentFields ...string) Mapping {
cm := make(map[string]bool, 0)
for _, f := range componentFields {
cm[f] = true
}
return &sparseMapping{
si: si,
cf: cf,
key: keyField,
components: componentFields,
Expand All @@ -90,6 +112,7 @@ func NewSparse(cf string, keyField string, componentFields ...string) Mapping {
}

type sparseMapping struct {
si *structInspection
cf string
key string
components []string
Expand All @@ -100,13 +123,25 @@ func (m *sparseMapping) Cf() string {
return m.cf
}

func (m *sparseMapping) MinColumns(source interface{}) int {
_, si, err := validateAndInspectStruct(source)
func (m *sparseMapping) MarshalKey(key interface{}) ([]byte, error) {
f := m.si.goFields[m.key]
b, err := Marshal(key, f.cassandraType)
if err != nil {
return -1
return nil, errors.New(fmt.Sprint("Error marshaling passed value for the key in field ", f.name, ":", err))
}
// struct fields minus the components fields minus one field for the key
return len(si.goFields) - len(m.components) - 1
return b, nil
}

func (m *sparseMapping) MarshalComponent(component interface{}, position int) ([]byte, error) {
if position >= len(m.components) {
return nil, errors.New(fmt.Sprint("The mapping has a component length of ", len(m.components), " and the passed position is ", position))
}
f := m.si.goFields[m.components[position]]
b, err := Marshal(component, f.cassandraType)
if err != nil {
return nil, errors.New(fmt.Sprint("Error marshaling passed value for a composite component in field ", f.name, ":", err))
}
return b, nil
}

func (m *sparseMapping) startMap(source interface{}) (*Row, *reflect.Value, *structInspection, []byte, error) {
Expand Down Expand Up @@ -233,11 +268,16 @@ func (m *sparseMapping) Unmap(destination interface{}, offset int, row *Row) (in

compositeFieldsAreSet := false

// unmarshal col/values
min := m.MinColumns(destination)
// FIXME: change this code to NOT expect a fixed number of columns and
// instead adapt itself to the data by assuming the first column composite
// to be uniform for all the struct values (except field name), then
// request column by column with some kind of interface that does
// buffering reads on demand from an underlying query
min := len(si.goFields) - len(m.components) - 1
if min > len(row.Columns) {
return -1, nil
}

columns := row.Columns[offset : offset+min]
for _, column := range columns {
readColumns++
Expand Down Expand Up @@ -275,13 +315,9 @@ func (m *sparseMapping) Unmap(destination interface{}, offset int, row *Row) (in
return readColumns, nil
}

// NewCompact returns a mapping for Go structs that represents a Cassandra row
// column as a full Go struct. The field named by the value is mapped to the
// column value. Each passed component field name is mapped, in order, to the
// column composite values.
func NewCompact(cf string, keyField string, valueField string, componentFields ...string) Mapping {
func newCompactMapping(si *structInspection, cf string, keyField string, valueField string, componentFields ...string) Mapping {
return &compactMapping{
sparseMapping: *(NewSparse(cf, keyField, componentFields...).(*sparseMapping)),
sparseMapping: *(newSparseMapping(si, cf, keyField, componentFields...).(*sparseMapping)),
value: valueField,
}
}
Expand All @@ -295,10 +331,6 @@ func (m *compactMapping) Cf() string {
return m.cf
}

func (m *compactMapping) MinColumns(source interface{}) int {
return 1
}

func (m *compactMapping) Map(source interface{}) (*Row, error) {
row, v, si, composite, err := m.startMap(source)
if err != nil {
Expand Down
68 changes: 12 additions & 56 deletions src/gossie/mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (

/*
todo:
deeper tests
deeper tests, over more methods, and over all internal types
*/

type everythingComp struct {
Key string
Key string `cf:"1" key:"Key" cols:"FBytes,FBool,FInt8,FInt16,FInt32,FInt,FInt64,FFloat32,FFloat64,FString,FUUID"`
FBytes []byte
FBool bool
FInt8 int8
Expand All @@ -35,9 +35,9 @@ type tagsA struct {

type tagsB struct {
A int `cf:"1" key:"A" cols:"B"`
B int
C int
D int
B int `type:"AsciiType"`
C int `skip:"true"`
D int `name:"Z"`
}

type tagsC struct {
Expand Down Expand Up @@ -86,52 +86,12 @@ func (shell *structTestShell) checkFullMap(t *testing.T) {
}

func TestMap(t *testing.T) {
mA, _ := MappingFromTags(&tagsA{})
mB, _ := MappingFromTags(&tagsB{})
mC, _ := MappingFromTags(&tagsC{})
mA, _ := NewMapping(&tagsA{})
mB, _ := NewMapping(&tagsB{})
mC, _ := NewMapping(&tagsC{})
mE, _ := NewMapping(&everythingComp{})

t.Log(mC)
shells := []*structTestShell{
&structTestShell{
mapping: NewSparse("cfname", "A"),
name: "noErrA",
expectedStruct: &noErrA{1, 2, 3},
resultStruct: &noErrA{},
expectedRow: &Row{
Key: []byte{0, 0, 0, 0, 0, 0, 0, 1},
Columns: []*Column{
&Column{
Name: []byte{'B'},
Value: []byte{0, 0, 0, 0, 0, 0, 0, 2},
},
&Column{
Name: []byte{'C'},
Value: []byte{0, 0, 0, 0, 0, 0, 0, 3},
},
},
},
},

&structTestShell{
mapping: NewSparse("cfname", "A", "B"),
name: "noErrB",
expectedStruct: &noErrB{1, 2, 3, 4},
resultStruct: &noErrB{},
expectedRow: &Row{
Key: []byte{0, 0, 0, 0, 0, 0, 0, 1},
Columns: []*Column{
&Column{
Name: []byte{0, 8, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 'C', 0},
Value: []byte{'3'},
},
&Column{
Name: []byte{0, 8, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 'Z', 0},
Value: []byte{0, 0, 0, 0, 0, 0, 0, 4},
},
},
},
},

&structTestShell{
mapping: mA,
name: "tagsA",
Expand Down Expand Up @@ -159,17 +119,13 @@ func TestMap(t *testing.T) {
&structTestShell{
mapping: mB,
name: "tagsB",
expectedStruct: &tagsB{1, 2, 3, 4},
expectedStruct: &tagsB{1, 2, 0, 4},
resultStruct: &tagsB{},
expectedRow: &Row{
Key: []byte{0, 0, 0, 0, 0, 0, 0, 1},
Columns: []*Column{
&Column{
Name: []byte{0, 8, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 'C', 0},
Value: []byte{0, 0, 0, 0, 0, 0, 0, 3},
},
&Column{
Name: []byte{0, 8, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 'D', 0},
Name: []byte{0, 1, '2', 0, 0, 1, 'Z', 0},
Value: []byte{0, 0, 0, 0, 0, 0, 0, 4},
},
},
Expand All @@ -193,7 +149,7 @@ func TestMap(t *testing.T) {
},

&structTestShell{
mapping: NewSparse("cfname", "Key", "FBytes", "FBool", "FInt8", "FInt16", "FInt32", "FInt", "FInt64", "FFloat32", "FFloat64", "FString", "FUUID"),
mapping: mE,
name: "everythingComp",
expectedStruct: &everythingComp{"a", []byte{1, 2}, true, 3, 4, 5, 6, 7, 8.0, 9.0, "b",
[16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, "c"},
Expand Down
Loading

0 comments on commit 6e03459

Please sign in to comment.