Skip to content
Browse files

struct mapping cleanup

  • Loading branch information...
1 parent e49f29d commit 39cfa7fc12b2664c149f4a632f27626b9dcd745a @carloscm committed Apr 22, 2012
Showing with 302 additions and 637 deletions.
  1. +25 −10 README.md
  2. +10 −36 src/gossie/cursor.go
  3. +1 −1 src/gossie/cursor_test.go
  4. +194 −405 src/gossie/struct.go
  5. +71 −183 src/gossie/struct_test.go
  6. +1 −2 src/gossie/types.go
View
35 README.md
@@ -1,6 +1,7 @@
# About
-Gossie is a Go library with a low level wrapper for the Cassandra 1.0 Thrift bindings with utilities for connection pooling, primitive type marshaling and easy query building. It also includes a higher level layer that allows mapping structs to Cassandra column famlilies, with support for advanced features like composite column names.
+Gossie is a Go library for Apache Cassandra. It includes a wrapper for the Cassandra 1.0 Thrift bindings with utilities for connection pooling, primitive type marshaling and easy query building. It also includes a higher level layer that allows mapping structs to Cassandra column famlilies, with support for advanced features like composite column names.
+
# Requeriments
@@ -15,6 +16,7 @@ Installing thrift4go under GOPATH in Go 1:
3) go install thrift
```
+
# Installing
There is no need to generate a Cassandra Thrift biding, I am providing one with Gossie (and the whole point is not to have to use it!)
@@ -40,6 +42,7 @@ GOPATH=$GOPATH:`pwd` go test gossie
Launch a Cassandra instance in localhost:9160, create a keyspace named TestGossie, and execute the provided schema-test.txt to create the test column families. Now you can run the Gossie tests.
+
# Quickstart
### Connection pooling
@@ -68,23 +71,23 @@ The low level interface is based on passing []byte values for everything, mirror
### Struct maping
-The first part of the high level Gossie interface is the Map/Unmap functions. These functions allow to convert Go structs into Row-s, and they have support of advanced features like composites or overriding column names and types.
+The first part of the high level Gossie interface is the Map/Unmap functions. These functions allow to convert Go structs into Rows, and they have support of advanced features like composites or overriding column names and types.
```Go
/*
In CQL 3.0:
-CREATE TABLE timeline (
- user_id varchar,
- tweet_id bigint,
- author varchar,
- body varchar,
- PRIMARY KEY (user_id, tweet_id)
+CREATE TABLE Timeline (
+ UserID varchar,
+ TweetID bigint,
+ Author varchar,
+ Body varchar,
+ PRIMARY KEY (UserID, TweetID)
);
*/
// In Gossie:
type Timeline struct {
- UserID string `cf:"Timeline" key:"UserID" col:"TweetID,*name" val:"*value"`
+ UserID string `cf:"Timeline" key:"UserID,TweetID"`
TweetID int64
Author string
Body string
@@ -94,6 +97,10 @@ row, err = gossie.Map(&Timeline{"userid", 10000000000004, "Author Name", "Hey th
err = pool.Mutation().Insert("Timeline", row).Run()
````
+The `cf` field tag names the column family of this struct, and the `key` field tag starts by naming the field that represents the row key, followed by zero or more fields that represent the components of a composite column name. Any other field not referenced in the `key` will be used as a column value, and its name used as a the column name, or appended to the end of the composite as the last component, if the struct had a composite. The `cf` and `key` field tags can appear at any field in the struct.
+
+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.
+
### Cursors
As a convenient wrapper over Map/Unmap and the Query/Mutation interfaces Gossie provides the Cursor interface. This wrapper implements a classic database cursor over rows and composite row slices. Example:
@@ -117,7 +124,15 @@ err = cursor.Read(tweet2)
// 10000000000004, or gossie.ErrorNotFound was returned in case it was not found
````
-Comming soon: range reads for composites with buffering and paging
+
+# Planned features
+
+- Cursor: range reads for composites with buffering and paging
+- Cursor: secondary index read with buffering and paging
+- Cursor: multiget reads with buffering and paging
+- Cursor: batching writes
+- High level mapping for Go slices
+- High level mapping for Go maps
# License
View
46 src/gossie/cursor.go
@@ -2,7 +2,6 @@ package gossie
import (
"errors"
- "fmt"
)
/*
@@ -12,8 +11,6 @@ todo:
buffering for slicing and range get
Next/Prev for the buffering with autopaging
- isSliceColumn == true
-
Search() and interface(s) for indexed get
multiget
@@ -57,12 +54,12 @@ func makeCursor(cp *connectionPool) (c *cursor) {
func (c *cursor) Write(source interface{}) error {
- row, ms, err := internalMap(source)
+ row, mi, err := internalMap(source)
if err != nil {
return err
}
- if err = c.pool.Mutation().Insert(ms.sm.cf, row).Run(); err != nil {
+ if err = c.pool.Mutation().Insert(mi.m.cf, row).Run(); err != nil {
return err
}
@@ -72,61 +69,38 @@ func (c *cursor) Write(source interface{}) error {
func (c *cursor) Read(source interface{}) error {
// deconstruct the source struct into a reflect.Value and a (cached) struct mapping
- ms, err := newMappedStruct(source)
+ mi, err := newMappedInstance(source)
if err != nil {
return err
}
// sanity checks
- if ms.sm.isSliceColumn {
- return errors.New(fmt.Sprint("Slice field in col tag is unsuported in Cursor for now, check back soon!"))
- }
-
// marshal the key field
- key, err := ms.marshalKey()
+ key, err := mi.m.key.marshalValue(&mi.v)
if err != nil {
return err
}
// start building the query
- q := c.pool.Query().Cf(ms.sm.cf)
+ q := c.pool.Query().Cf(mi.m.cf)
// build a slice composite comparator if needed
- if ms.sm.isCompositeColumn {
+ if len(mi.m.composite) > 0 {
// iterate over the components and set an equality comparison for every simple field
start := make([]byte, 0)
end := make([]byte, 0)
- var component int
- for component = 0; component < len(ms.sm.columns); component++ {
- fm := ms.sm.columns[component]
- if fm.fieldKind != baseTypeField {
- break
- }
- b, err := ms.mapColumn(baseTypeField, fm, 0)
+ for _, f := range mi.m.composite {
+ b, err := f.marshalValue(&mi.v)
if err != nil {
return err
}
- start = packComposite(start, b, eocEquals)
- end = packComposite(end, b, eocGreater)
+ start = append(start, packComposite(b, eocEquals)...)
+ end = append(end, packComposite(b, eocGreater)...)
}
-
- /*if component < len(ms.sm.columns) {
- // we still got one to go, this means the last one was an iterable non-fixed type (*name or go slice)
- //fm := ms.sm.columns[component]
- // TODO: this will only work for *name
- b := make([]byte, 0)
- start = packComposite(start, b, o[6], o[7], o[8])
- end = packComposite(end, b, o[9], o[10], o[11])
- }*/
-
// TODO: fix hardcoded number of columns
q.Slice(&Slice{Start: start, End: end, Count: 100})
}
- //isCompositeColumn bool
- //isSliceColumn bool
- //isStarNameColumn bool
-
row, err := q.Get(key)
if err != nil {
View
2 src/gossie/cursor_test.go
@@ -14,7 +14,7 @@ todo:
*/
type ReasonableOne struct {
- Username string `cf:"Reasonable" key:"Username" col:"TweetID,*name" val:"*value"`
+ Username string `cf:"Reasonable" key:"Username,TweetID"`
TweetID int64
Lat float32
Lon float32
View
599 src/gossie/struct.go
@@ -9,245 +9,212 @@ import (
)
/*
- NOTE: currently this works but it is in dire need of a cleanup/refactoring
- the external interfaces (Map/Unmap) are final and won't change
-*/
-
-/*
-
todo:
allow some form of "custom composite" to support common use cases like row keys in the form (read as ascii) 111:222:333.
-> maybe support it via optional callables over the passed interface? OnMapKey/etc
- allow to reuse the same fields as both key and col component
-
- OR: introduce special key field tags to use during sharding, since the "row key" concept losses meaning in this case,
- since it is managed by the lib
-
- go maps support for things like
- type s struct {
- a int `cf:"cfname" key:"a" col:"atts" val:"atts"`
- atts map[string]string
- }
- type s2 struct {
- a int `cf:"cfname" key:"a" col:"b,atts" val:"atts"`
- b UUID
- atts map[string]string
- }
- --> then think about slicing/pagging this, oops
-
- support composite key and composite values, not just composite column names (are those actually in use by anybody???)
*/
-const (
- _ = iota
- baseTypeField
- baseTypeSliceField
- starNameField
- starValueField
-)
+type mapping struct {
+ cf string
+ key *field
+ composite []*field
+ values []*field
+ namedValues map[string]*field
+}
-// mapping stores how to map from/to a struct
-type fieldMapping struct {
- fieldKind int
- position int
+type field struct {
name string
+ index []int
cassandraName string
cassandraType TypeDesc
}
-type structMapping struct {
- cf string
- key *fieldMapping
- columns []*fieldMapping
- value *fieldMapping
- others map[string]*fieldMapping
- isCompositeColumn bool
- isSliceColumn bool
- isStarNameColumn bool
-}
-func defaultCassandraType(t reflect.Type) (TypeDesc, int) {
+func defaultType(t reflect.Type) TypeDesc {
switch t.Kind() {
case reflect.Bool:
- return BooleanType, baseTypeField
+ return BooleanType
case reflect.String:
- return UTF8Type, baseTypeField
+ return UTF8Type
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return LongType, baseTypeField
+ return LongType
case reflect.Float32:
- return FloatType, baseTypeField
+ return FloatType
case reflect.Float64:
- return DoubleType, baseTypeField
+ return DoubleType
case reflect.Array:
if t.Name() == "UUID" && t.Size() == 16 {
- return UUIDType, baseTypeField
+ return UUIDType
}
- return UnknownType, baseTypeField
+ return UnknownType
case reflect.Slice:
if et := t.Elem(); et.Kind() == reflect.Uint8 {
- return BytesType, baseTypeField
- } else {
- if subTD, subKind := defaultCassandraType(et); subTD != UnknownType && subKind == baseTypeField {
- return subTD, baseTypeSliceField
- }
- return UnknownType, baseTypeField
+ return BytesType
}
- return UnknownType, baseTypeField
+ return UnknownType
}
- return UnknownType, baseTypeField
+ return UnknownType
}
-func newFieldMapping(pos int, sf reflect.StructField, overrideName, overrideType string) *fieldMapping {
- fm := &fieldMapping{}
- fm.cassandraType, fm.fieldKind = defaultCassandraType(sf.Type)
- // signal an invalid Go base type
- if fm.cassandraType == UnknownType {
- return nil
+func newField(sf reflect.StructField) (*field, error) {
+ // ignore anon fields
+ if sf.Anonymous || sf.Name == "" {
+ return nil, nil
}
- if overrideType != "" {
- fm.cassandraType = parseTypeDesc(overrideType)
+
+ name := sf.Name
+ index := sf.Index
+
+ cassandraType := defaultType(sf.Type)
+ if cassandraType == UnknownType {
+ return nil, errors.New(fmt.Sprint("Field ", name, " has unsupported type"))
}
- fm.position = pos
- if overrideName != "" {
- fm.cassandraName = overrideName
- } else {
- fm.cassandraName = sf.Name
+ if tagType := sf.Tag.Get("type"); tagType != "" {
+ cassandraType = parseTypeDesc(tagType)
+ }
+
+ cassandraName := name
+ if tagName := sf.Tag.Get("name"); tagName != "" {
+ cassandraName = tagName
}
- fm.name = sf.Name
- return fm
+
+ return &field{name, index, cassandraName, cassandraType}, nil
}
-func newStructMapping(t reflect.Type) (*structMapping, error) {
- sm := &structMapping{}
- n := t.NumField()
- found := false
- // globally recognized meta fields in the struct tag
- meta := map[string]string{
- "cf": "",
- "key": "",
- "col": "",
- "val": "",
+func (f *field) marshalName() ([]byte, error) {
+ b, err := Marshal(f.cassandraName, UTF8Type)
+ if err != nil {
+ return nil, errors.New(fmt.Sprint("Error marshaling field name for field ", f.name, ":", err))
}
- // hold a field mapping for every candidate field
- fields := make(map[string]*fieldMapping)
+ return b, nil
+}
+
+func (f *field) marshalValue(structValue *reflect.Value) ([]byte, error) {
+ v := structValue.FieldByIndex(f.index)
+ b, err := Marshal(v.Interface(), f.cassandraType)
+ if err != nil {
+ return nil, errors.New(fmt.Sprint("Error marshaling filed value for field ", f.name, ":", err))
+ }
+ return b, nil
+}
+
+func (f *field) unmarshalValue(b []byte, structValue *reflect.Value) error {
+ v := structValue.FieldByIndex(f.index)
+ if !v.CanAddr() {
+ return errors.New(fmt.Sprint("Cannot obtain pointer to field ", f.name))
+ }
+ vp := v.Addr()
+ err := Unmarshal(b, f.cassandraType, vp.Interface())
+ if err != nil {
+ return errors.New(fmt.Sprint("Error unmarshaling field ", f.name, ":", err))
+ }
+ return nil
+}
+
+func newMapping(t reflect.Type) (*mapping, error) {
+ m := &mapping{}
+ n := t.NumField()
+
+ cf := ""
+ keyAndComposite := ""
+ fields := make(map[string]*field, 0)
// pass 1: gather field metadata
for i := 0; i < n; i++ {
sf := t.Field(i)
- // find the field tags
- for key, _ := range meta {
- if tagValue := sf.Tag.Get(key); tagValue != "" {
- meta[key] = tagValue
- }
+ if tagValue := sf.Tag.Get("cf"); tagValue != "" {
+ cf = tagValue
}
- // build a field mapping for all non-anon named fields with a suitable Go type
- if sf.Name != "" && !sf.Anonymous {
- if fm := newFieldMapping(i, sf, sf.Tag.Get("name"), sf.Tag.Get("type")); fm == nil {
- continue
- } else {
- fields[sf.Name] = fm
- }
+ if tagValue := sf.Tag.Get("key"); tagValue != "" {
+ keyAndComposite = tagValue
+ }
+ f, err := newField(sf)
+ if err != nil {
+ return nil, errors.New(fmt.Sprint("Error in struct ", t.Name(), ": ", err))
+ }
+ if f != nil {
+ fields[sf.Name] = f
}
}
- // pass 2: struct data for each meta field
- if name := meta["cf"]; name != "" {
- sm.cf = meta["cf"]
- } else {
+ // pass 2: build mapping
+
+ if cf == "" {
return nil, errors.New(fmt.Sprint("No cf field in struct ", t.Name()))
}
+ m.cf = cf
- if name := meta["key"]; name != "" {
- if sm.key, found = fields[name]; !found {
- return nil, errors.New(fmt.Sprint("Referenced key field ", name, " does not exist in struct ", t.Name()))
- }
- if sm.key.fieldKind != baseTypeField {
- return nil, errors.New(fmt.Sprint("Referenced key field ", name, " in struct ", t.Name(), " has invalid type"))
- }
- delete(fields, name)
- } else {
+ if keyAndComposite == "" {
return nil, errors.New(fmt.Sprint("No key field in struct ", t.Name()))
}
+ keyAndCompositeCols := strings.Split(keyAndComposite, ",")
+ if len(keyAndCompositeCols) < 1 {
+ return nil, errors.New(fmt.Sprint("Not enough key/composite fields given in struct ", t.Name()))
+ }
+ key, compositeCols := keyAndCompositeCols[0], keyAndCompositeCols[1:]
- if name := meta["val"]; (name != "") || (name == "*value") {
- if name == "*value" {
- sm.value = &fieldMapping{fieldKind: starValueField, name: "*value"}
- } else if sm.value, found = fields[name]; !found {
- return nil, errors.New(fmt.Sprint("Referenced value field ", name, " does not exist in struct ", t.Name()))
- }
- delete(fields, name)
+ if keyField, found := fields[key]; !found {
+ return nil, errors.New(fmt.Sprint("Referenced key field ", key, " does not exist in struct ", t.Name()))
} else {
- return nil, errors.New(fmt.Sprint("No val field in struct ", t.Name()))
+ m.key = keyField
+ delete(fields, key)
}
- if meta["col"] != "" {
- colNames := strings.Split(meta["col"], ",")
- for i, name := range colNames {
- isLast := i == (len(colNames) - 1)
- var fm *fieldMapping
- if name == "*name" {
- if !isLast {
- return nil, errors.New(fmt.Sprint("*name can only be used in the last position of a composite, error in struct ", t.Name()))
- } else {
- sm.isStarNameColumn = true
- fm = &fieldMapping{fieldKind: starNameField, name: "*name"}
- }
- } else if fm, found = fields[name]; !found {
- return nil, errors.New(fmt.Sprint("Referenced column field ", name, " does not exist in struct ", t.Name()))
- }
- if fm.fieldKind == baseTypeSliceField {
- sm.isSliceColumn = true
- if !isLast {
- return nil, errors.New(fmt.Sprint("Slice struct fields can only be used in the last position of a composite, error in struct ", t.Name()))
- }
- }
- delete(fields, name)
- sm.columns = append(sm.columns, fm)
+ m.composite = make([]*field, 0)
+ for _, name := range compositeCols {
+ f, found := fields[name]
+ if !found {
+ return nil, errors.New(fmt.Sprint("Referenced composite field ", name, " does not exist in struct ", t.Name()))
}
- sm.isCompositeColumn = len(sm.columns) > 1
- sm.others = make(map[string]*fieldMapping)
- for _, fm := range fields {
- sm.others[fm.cassandraName] = fm
+ m.composite = append(m.composite, f)
+ delete(fields, name)
+ }
+
+ m.values = make([]*field, 0)
+ m.namedValues = make(map[string]*field, 0)
+ for i := 0; i < n; i++ {
+ sf := t.Field(i)
+ if f, found := fields[sf.Name]; found {
+ m.values = append(m.values, f)
+ m.namedValues[f.cassandraName] = f
}
- } else {
- return nil, errors.New(fmt.Sprint("No col field in struct ", t.Name()))
}
- return sm, nil
+ return m, nil
}
-var mapCache map[reflect.Type]*structMapping
+var mapCache map[reflect.Type]*mapping
var mapCacheMutex *sync.Mutex = new(sync.Mutex)
-func getMapping(v reflect.Value) (*structMapping, error) {
- var sm *structMapping
+func getMapping(v reflect.Value) (*mapping, error) {
+ var m *mapping
var err error
found := false
t := v.Type()
mapCacheMutex.Lock()
if mapCache == nil {
- mapCache = make(map[reflect.Type]*structMapping)
+ mapCache = make(map[reflect.Type]*mapping)
}
- if sm, found = mapCache[t]; !found {
- sm, err = newStructMapping(t)
+ if m, found = mapCache[t]; !found {
+ m, err = newMapping(t)
if err != nil {
- mapCache[t] = sm
+ mapCache[t] = m
}
}
mapCacheMutex.Unlock()
- return sm, err
+ return m, err
}
-// mappedStruct stores the reflect.Value and mapping for a particular instance of an struct
-type mappedStruct struct {
+// mappedInstance stores the reflect.Value and mapping for a particular instance of a struct
+type mappedInstance struct {
source interface{}
v reflect.Value
- sm *structMapping
+ m *mapping
}
-func newMappedStruct(source interface{}) (*mappedStruct, error) {
- ms := &mappedStruct{source: source}
+func newMappedInstance(source interface{}) (*mappedInstance, error) {
+ mi := &mappedInstance{source: source}
var err error
// always work with a pointer to struct
@@ -258,34 +225,26 @@ func newMappedStruct(source interface{}) (*mappedStruct, error) {
if vp.IsNil() {
return nil, errors.New("Passed source is not a pointer to a struct")
}
- ms.v = reflect.Indirect(vp)
- if ms.v.Kind() != reflect.Struct {
+ mi.v = reflect.Indirect(vp)
+ if mi.v.Kind() != reflect.Struct {
return nil, errors.New("Passed source is not a pointer to a struct")
}
- if !ms.v.CanSet() {
+ if !mi.v.CanSet() {
return nil, errors.New("Cannot modify the passed struct instance")
}
- ms.sm, err = getMapping(ms.v)
+ mi.m, err = getMapping(mi.v)
if err != nil {
return nil, err
}
- return ms, nil
+ return mi, nil
}
-func (ms *mappedStruct) marshalKey() ([]byte, error) {
- vk := ms.v.Field(ms.sm.key.position)
- b, err := Marshal(vk.Interface(), ms.sm.key.cassandraType)
- if err != nil {
- return nil, errors.New(fmt.Sprint("Error marshaling key field ", ms.sm.key.name, ":", err))
- }
- return b, nil
-}
+func internalMap(source interface{}) (*Row, *mappedInstance, error) {
-func internalMap(source interface{}) (*Row, *mappedStruct, error) {
// deconstruct the source struct into a reflect.Value and a (cached) struct mapping
- ms, err := newMappedStruct(source)
+ mi, err := newMappedInstance(source)
if err != nil {
return nil, nil, err
}
@@ -294,268 +253,98 @@ func internalMap(source interface{}) (*Row, *mappedStruct, error) {
row := &Row{}
// marshal the key field
- b, err := ms.marshalKey()
+ b, err := mi.m.key.marshalValue(&mi.v)
if err != nil {
return nil, nil, err
}
row.Key = b
- // marshal columns and values
- return row, ms, ms.mapField(row, 0, make([]byte, 0), make([]byte, 0), 0)
-}
-
-func Map(source interface{}) (*Row, error) {
- row, _, err := internalMap(source)
- return row, err
-}
-
-func (ms *mappedStruct) mapColumn(fieldKind int, fm *fieldMapping, i int) ([]byte, error) {
- var b []byte
- var err error
-
- switch fieldKind {
- case baseTypeField:
- // single value from a single field
- v := ms.v.Field(fm.position)
- b, err = Marshal(v.Interface(), fm.cassandraType)
-
- case baseTypeSliceField:
- // the field is a slice, get the falue from position i
- v := ms.v.Field(fm.position)
- vi := v.Index(i)
- b, err = Marshal(vi.Interface(), fm.cassandraType)
-
- case starNameField:
- // set value to the name of the field
- b, err = Marshal(fm.cassandraName, UTF8Type)
- }
-
- if err != nil {
- return nil, errors.New(fmt.Sprint("Error marshaling field ", fm.name, ":", err))
- }
-
- return b, nil
-}
-
-func (ms *mappedStruct) mapColumnForRow(fieldKind int, fm *fieldMapping, i int, composite []byte) ([]byte, error) {
- b, err := ms.mapColumn(fieldKind, fm, i)
- if err != nil {
- return nil, err
- }
-
- // add to composite, if required
- if ms.sm.isCompositeColumn {
- b = packComposite(composite, b, eocEquals)
+ // prepare composite, if needed
+ composite := make([]byte, 0)
+ for _, f := range mi.m.composite {
+ b, err := f.marshalValue(&mi.v)
+ if err != nil {
+ return nil, nil, err
+ }
+ composite = append(composite, packComposite(b, eocEquals)...)
}
- return b, nil
-}
-
-func (ms *mappedStruct) mapField(row *Row, component int, composite []byte, value []byte, valueIndex int) error {
- // check if there are components left
- if component < len(ms.sm.columns) {
-
- fm := ms.sm.columns[component]
-
- // switch type of field named by component
- switch fm.fieldKind {
-
- // base type
- case baseTypeField:
- composite, err := ms.mapColumnForRow(baseTypeField, fm, 0, composite)
- if err != nil {
- return err
- }
- return ms.mapField(row, component+1, composite, value, valueIndex)
-
- // slice of base type
- case baseTypeSliceField:
- // iterate slice and map more columns
- v := ms.v.Field(fm.position)
- n := v.Len()
- for i := 0; i < n; i++ {
- subComposite, err := ms.mapColumnForRow(baseTypeSliceField, fm, i, composite)
- if err != nil {
- return err
- }
- err = ms.mapField(row, component+1, subComposite, value, i)
- if err != nil {
- return err
- }
- }
-
- // *name
- case starNameField:
- // iterate over non-key/col/val-referenced struct fields and map more columns
- for _, fm := range ms.sm.others {
- subComposite, err := ms.mapColumnForRow(starNameField, fm, 0, composite)
- if err != nil {
- return err
- }
- // marshal field value and pass it to next field mapper in case it is *value
- v := ms.v.Field(fm.position)
- b, err := Marshal(v.Interface(), fm.cassandraType)
- if err != nil {
- return errors.New(fmt.Sprint("Error marshaling field ", fm.name, ":", err))
- }
-
- err = ms.mapField(row, component+1, subComposite, b, valueIndex)
- if err != nil {
- return err
- }
- }
+ // add columns
+ for _, f := range mi.m.values {
+ columnName, err := f.marshalName()
+ if err != nil {
+ return nil, nil, err
}
-
- } else {
- // no components left, emit column
-
- fm := ms.sm.value
-
- // switch type of value field
- switch fm.fieldKind {
-
- case starValueField:
- // use passed value
- row.Columns = append(row.Columns, &Column{Name: composite, Value: value})
-
- case baseTypeSliceField:
- // set value to the passed value index in this slice
- vs := ms.v.Field(fm.position)
- v := vs.Index(valueIndex)
- b, err := Marshal(v.Interface(), fm.cassandraType)
- if err != nil {
- return errors.New(fmt.Sprint("Error marshaling field ", fm.name, ":", err))
- }
- row.Columns = append(row.Columns, &Column{Name: composite, Value: b})
-
- case baseTypeField:
- // set value to the field value
- v := ms.v.Field(fm.position)
- b, err := Marshal(v.Interface(), fm.cassandraType)
- if err != nil {
- return errors.New(fmt.Sprint("Error marshaling field ", fm.name, ":", err))
- }
- row.Columns = append(row.Columns, &Column{Name: composite, Value: b})
-
- // support literal case?
-
- // support zero, non-set case?
+ if len(composite) > 0 {
+ columnName = append(composite, packComposite(columnName, eocEquals)...)
+ }
+ columnValue, err := f.marshalValue(&mi.v)
+ if err != nil {
+ return nil, nil, err
}
+ row.Columns = append(row.Columns, &Column{Name: columnName, Value: columnValue})
}
- return nil
+ return row, mi, nil
+}
+
+func Map(source interface{}) (*Row, error) {
+ row, _, err := internalMap(source)
+ return row, err
}
func Unmap(row *Row, destination interface{}) error {
// deconstruct the source struct into a reflect.Value and a (cached) struct mapping
- ms, err := newMappedStruct(destination)
+ mi, err := newMappedInstance(destination)
if err != nil {
return err
}
- // unmarshal key
- vk := ms.v.Field(ms.sm.key.position)
- if !vk.CanAddr() {
- return errors.New("Cannot obtain pointer to key field")
- }
- vkp := vk.Addr()
- err = Unmarshal(row.Key, ms.sm.key.cassandraType, vkp.Interface())
+ // unmarshal key field
+ err = mi.m.key.unmarshalValue(row.Key, &mi.v)
if err != nil {
- return errors.New(fmt.Sprint("Error unmarshaling key field ", ms.sm.key.name, ":", err))
- }
-
- // unmarshal col/values
-
- setField := func(fm *fieldMapping, b []byte, index int) error {
- vfield := ms.v.Field(fm.position)
- if index >= 0 {
- vfield = vfield.Index(index)
- }
- if !vfield.CanAddr() {
- return errors.New(fmt.Sprint("Cannot obtain pointer to field ", vfield.Type().Name(), " in struct ", ms.v.Type().Name()))
- }
- vfieldp := vfield.Addr()
-
- err = Unmarshal(b, fm.cassandraType, vfieldp.Interface())
- if err != nil {
- return errors.New(fmt.Sprint("Error unmarshaling composite field ", vfield.Type().Name(), " in struct ", ms.v.Type().Name(), ", error: ", err))
- }
- return nil
- }
-
- prepareSlice := func(fm *fieldMapping, n int) {
- vfield := ms.v.Field(fm.position)
- t := vfield.Type()
- s := reflect.MakeSlice(t, n, n)
- vfield.Set(s)
+ return err
}
- rowLength := len(row.Columns)
-
- // prepare slice components and value
- for _, fm := range ms.sm.columns {
- if fm.fieldKind == baseTypeSliceField {
- prepareSlice(fm, rowLength)
- }
- }
- if ms.sm.value.fieldKind == baseTypeSliceField {
- prepareSlice(ms.sm.value, rowLength)
- }
+ compositeFieldsAreSet := false
- for i, column := range row.Columns {
+ // unmarshal col/values
+ for _, column := range row.Columns {
var components [][]byte
- if ms.sm.isCompositeColumn {
+ if len(mi.m.composite) > 0 {
components = unpackComposite(column.Name)
} else {
components = [][]byte{column.Name}
}
- if len(components) != len(ms.sm.columns) {
- return errors.New(fmt.Sprint("Returned number of components in composite column name does not match struct col: component in struct ", ms.v.Type().Name()))
+ if len(components) != (len(mi.m.composite) + 1) {
+ return errors.New(fmt.Sprint("Returned number of components in composite column name does not match struct key: composite in struct ", mi.v.Type().Name()))
}
- // iterate over column name components and set them, plus values
- for j, b := range components {
- fm := ms.sm.columns[j]
- switch fm.fieldKind {
-
- case baseTypeField:
- if err = setField(fm, b, -1); err != nil {
- return err
- }
-
- case starNameField:
- var name string
- err = Unmarshal(b, UTF8Type, &name)
+ // FIXME: it is possible for a row to contain multiple composite values instead of an uniform one. assume
+ // that is not the case for now!
+ // iterate over composite components, just once, to set the composite fields
+ if !compositeFieldsAreSet {
+ for i, f := range mi.m.composite {
+ b := components[i]
+ err := f.unmarshalValue(b, &mi.v)
if err != nil {
- return errors.New(fmt.Sprint("Error unmarshaling composite field as UTF8Type for *name in struct ", ms.v.Type().Name(), ", error: ", err))
- }
- if valueFM, found := ms.sm.others[name]; found {
- if err = setField(valueFM, column.Value, -1); err != nil {
- return err
- }
- }
-
- case baseTypeSliceField:
- if err = setField(fm, b, i); err != nil {
- return err
+ return errors.New(fmt.Sprint("Error unmarshaling composite field: ", err))
}
}
+ compositeFieldsAreSet = true
}
- // set value field for the non-*name cases
- if ms.sm.value != nil {
- switch ms.sm.value.fieldKind {
-
- case baseTypeField:
- if err = setField(ms.sm.value, column.Value, -1); err != nil {
- return err
- }
-
- case baseTypeSliceField:
- if err = setField(ms.sm.value, column.Value, i); err != nil {
- return err
- }
+ // lookup field by name
+ var name string
+ err = Unmarshal(components[len(components)-1], UTF8Type, &name)
+ if err != nil {
+ return errors.New(fmt.Sprint("Error unmarshaling composite field as UTF8Type for field name in struct ", mi.v.Type().Name(), ", error: ", err))
+ }
+ if f, found := mi.m.namedValues[name]; found {
+ err := f.unmarshalValue(column.Value, &mi.v)
+ if err != nil {
+ return errors.New(fmt.Sprint("Error unmarshaling column value: ", err))
}
}
}
View
254 src/gossie/struct_test.go
@@ -18,71 +18,27 @@ todo:
type errNoMeta struct {
A int
}
-type errNoMetaKeyColVal struct {
+type errNoMetaKey struct {
A int `cf:"cfname"`
}
-type errNoMetaColVal struct {
- A int `cf:"cfname" key:"A"`
-}
-type errNoMetaVal struct {
- A int `cf:"cfname" key:"A" col:"B"`
- B int
-}
type errInvKey struct {
- A int `cf:"cfname" key:"Z" col:"B" val:"C"`
- B int
- C int
-}
-type errInvCol struct {
- A int `cf:"cfname" key:"A" col:"Z" val:"C"`
- B int
- C int
-}
-type errInvVal struct {
- A int `cf:"cfname" key:"A" col:"B" val:"Z"`
- B int
- C int
-}
-type errStarNameNotLast struct {
- A int `cf:"cfname" key:"A" col:"*name,B" val:"*value"`
+ A int `cf:"cfname" key:"Z"`
B int
C int
}
-type errSliceNotLast struct {
- A int `cf:"cfname" key:"A" col:"C,B" val:"D"`
- B int
- C []int
- D []int
-}
type noErrA struct {
- A int `cf:"cfname" key:"A" col:"B" val:"C"`
+ A int `cf:"cfname" key:"A"`
B int
C int
}
type noErrB struct {
- A int `cf:"cfname" key:"A" col:"*name" val:"*value"`
- B int `name:"Z"`
- C int `type:"AsciiType"`
- D int
-}
-type noErrC struct {
- A int `cf:"cfname" key:"A" col:"B,*name" val:"*value"`
- B int
- C int
-}
-type noErrD struct {
- A int `cf:"cfname" key:"A" col:"B" val:"C"`
- B []int
- C []int
-}
-type noErrE struct {
- A int `cf:"cfname" key:"A" col:"B,C" val:"D"`
+ A int `cf:"cfname" key:"A,B"`
B int
- C []int
- D []int
+ C int `type:"AsciiType"`
+ D int `name:"Z"`
}
type everythingComp struct {
- Key string `cf:"cfname" key:"Key" col:"FBytes,FBool,FInt8,FInt16,FInt32,FInt,FInt64,FFloat32,FFloat64,FString,FUUID,*name" val:"*value"`
+ Key string `cf:"cfname" key:"Key,FBytes,FBool,FInt8,FInt16,FInt32,FInt,FInt64,FFloat32,FFloat64,FString,FUUID"`
FBytes []byte
FBool bool
FInt8 int8
@@ -97,17 +53,17 @@ type everythingComp struct {
Val string
}
-func buildMappingFromPtr(instance interface{}) (*structMapping, error) {
+func buildMappingFromPtr(instance interface{}) (*mapping, error) {
valuePtr := reflect.ValueOf(instance)
value := reflect.Indirect(valuePtr)
typ := value.Type()
- return newStructMapping(typ)
+ return newMapping(typ)
}
func structMapMustError(t *testing.T, instance interface{}) {
_, err := buildMappingFromPtr(instance)
if err == nil {
- t.Error("Expected error calling newStructMapping, got none")
+ t.Error("Expected error calling newMapping, got none")
}
}
@@ -119,122 +75,76 @@ func checkMapping(t *testing.T, expected, actual interface{}, name string) {
func TestStructMapping(t *testing.T) {
structMapMustError(t, &errNoMeta{})
- structMapMustError(t, &errNoMetaKeyColVal{})
- structMapMustError(t, &errNoMetaColVal{})
- structMapMustError(t, &errNoMetaVal{})
+ structMapMustError(t, &errNoMetaKey{})
structMapMustError(t, &errInvKey{})
- structMapMustError(t, &errInvCol{})
- structMapMustError(t, &errInvVal{})
- structMapMustError(t, &errStarNameNotLast{})
- structMapMustError(t, &errSliceNotLast{})
-
- mapA, _ := buildMappingFromPtr(&noErrA{1, 2, 3})
- goodA := &structMapping{
- cf: "cfname",
- key: &fieldMapping{fieldKind: baseTypeField, position: 0, name: "A", cassandraType: LongType, cassandraName: "A"},
- columns: []*fieldMapping{
- &fieldMapping{fieldKind: baseTypeField, position: 1, name: "B", cassandraType: LongType, cassandraName: "B"},
- },
- value: &fieldMapping{fieldKind: baseTypeField, position: 2, name: "C", cassandraType: LongType, cassandraName: "C"},
- others: make(map[string]*fieldMapping, 0),
- isCompositeColumn: false,
- }
- checkMapping(t, goodA, mapA, "mapA")
- mapB, _ := buildMappingFromPtr(&noErrB{1, 2, 3, 4})
- goodB := &structMapping{
- cf: "cfname",
- key: &fieldMapping{fieldKind: baseTypeField, position: 0, name: "A", cassandraType: LongType, cassandraName: "A"},
- columns: []*fieldMapping{
- &fieldMapping{fieldKind: starNameField, position: 0, name: "*name", cassandraType: 0, cassandraName: ""},
+ mapA, err := buildMappingFromPtr(&noErrA{1, 2, 3})
+ goodA := &mapping{
+ cf: "cfname",
+ key: &field{index: []int{0}, name: "A", cassandraType: LongType, cassandraName: "A"},
+ composite: []*field{},
+ values: []*field{
+ &field{index: []int{1}, name: "B", cassandraType: LongType, cassandraName: "B"},
+ &field{index: []int{2}, name: "C", cassandraType: LongType, cassandraName: "C"},
},
- value: &fieldMapping{fieldKind: starValueField, position: 0, name: "*value", cassandraType: 0, cassandraName: ""},
- others: map[string]*fieldMapping{
- "Z": &fieldMapping{fieldKind: baseTypeField, position: 1, name: "B", cassandraType: LongType, cassandraName: "Z"},
- "C": &fieldMapping{fieldKind: baseTypeField, position: 2, name: "C", cassandraType: AsciiType, cassandraName: "C"},
- "D": &fieldMapping{fieldKind: baseTypeField, position: 3, name: "D", cassandraType: LongType, cassandraName: "D"},
+ namedValues: map[string]*field{
+ "B": &field{index: []int{1}, name: "B", cassandraType: LongType, cassandraName: "B"},
+ "C": &field{index: []int{2}, name: "C", cassandraType: LongType, cassandraName: "C"},
},
- isCompositeColumn: false,
- isSliceColumn: false,
- isStarNameColumn: true,
}
- checkMapping(t, goodB, mapB, "mapB")
+ if err != nil {
+ t.Fatal("Unexpected error calling mapA newMapping:", err)
+ }
+ checkMapping(t, goodA, mapA, "mapA")
- mapC, _ := buildMappingFromPtr(&noErrC{1, 2, 3})
- goodC := &structMapping{
+ mapB, err := buildMappingFromPtr(&noErrB{1, 2, 3, 4})
+ goodB := &mapping{
cf: "cfname",
- key: &fieldMapping{fieldKind: baseTypeField, position: 0, name: "A", cassandraType: LongType, cassandraName: "A"},
- columns: []*fieldMapping{
- &fieldMapping{fieldKind: baseTypeField, position: 1, name: "B", cassandraType: LongType, cassandraName: "B"},
- &fieldMapping{fieldKind: starNameField, position: 0, name: "*name", cassandraType: 0, cassandraName: ""},
+ key: &field{index: []int{0}, name: "A", cassandraType: LongType, cassandraName: "A"},
+ composite: []*field{
+ &field{index: []int{1}, name: "B", cassandraType: LongType, cassandraName: "B"},
},
- value: &fieldMapping{fieldKind: starValueField, position: 0, name: "*value", cassandraType: 0, cassandraName: ""},
- others: map[string]*fieldMapping{
- "C": &fieldMapping{fieldKind: baseTypeField, position: 2, name: "C", cassandraType: LongType, cassandraName: "C"},
+ values: []*field{
+ &field{index: []int{2}, name: "C", cassandraType: AsciiType, cassandraName: "C"},
+ &field{index: []int{3}, name: "D", cassandraType: LongType, cassandraName: "Z"},
},
- isCompositeColumn: true,
- isSliceColumn: false,
- isStarNameColumn: true,
- }
- checkMapping(t, goodC, mapC, "mapC")
-
- mapD, _ := buildMappingFromPtr(&noErrD{1, []int{2, 3}, []int{4, 5}})
- goodD := &structMapping{
- cf: "cfname",
- key: &fieldMapping{fieldKind: baseTypeField, position: 0, name: "A", cassandraType: LongType, cassandraName: "A"},
- columns: []*fieldMapping{
- &fieldMapping{fieldKind: baseTypeSliceField, position: 1, name: "B", cassandraType: LongType, cassandraName: "B"},
+ namedValues: map[string]*field{
+ "C": &field{index: []int{2}, name: "C", cassandraType: AsciiType, cassandraName: "C"},
+ "Z": &field{index: []int{3}, name: "D", cassandraType: LongType, cassandraName: "Z"},
},
- value: &fieldMapping{fieldKind: baseTypeSliceField, position: 2, name: "C", cassandraType: LongType, cassandraName: "C"},
- others: make(map[string]*fieldMapping, 0),
- isCompositeColumn: false,
- isSliceColumn: true,
- isStarNameColumn: false,
}
- checkMapping(t, goodD, mapD, "mapD")
-
- mapE, _ := buildMappingFromPtr(&noErrE{1, 2, []int{3, 4}, []int{5, 6}})
- goodE := &structMapping{
- cf: "cfname",
- key: &fieldMapping{fieldKind: baseTypeField, position: 0, name: "A", cassandraType: LongType, cassandraName: "A"},
- columns: []*fieldMapping{
- &fieldMapping{fieldKind: baseTypeField, position: 1, name: "B", cassandraType: LongType, cassandraName: "B"},
- &fieldMapping{fieldKind: baseTypeSliceField, position: 2, name: "C", cassandraType: LongType, cassandraName: "C"},
- },
- value: &fieldMapping{fieldKind: baseTypeSliceField, position: 3, name: "D", cassandraType: LongType, cassandraName: "D"},
- others: make(map[string]*fieldMapping, 0),
- isCompositeColumn: true,
- isSliceColumn: true,
- isStarNameColumn: false,
+ if err != nil {
+ t.Fatal("Unexpected error calling mapB newMapping:", err)
}
- checkMapping(t, goodE, mapE, "mapE")
+ checkMapping(t, goodB, mapB, "mapB")
- eComp, _ := buildMappingFromPtr(&everythingComp{"A", []byte{1, 2}, true, 3, 4, 5, 6, 7, 8.0, 9.0, "B",
+ eComp, err := buildMappingFromPtr(&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"})
- goodEComp := &structMapping{
+ goodEComp := &mapping{
cf: "cfname",
- key: &fieldMapping{fieldKind: baseTypeField, position: 0, name: "Key", cassandraType: UTF8Type, cassandraName: "Key"},
- columns: []*fieldMapping{
- &fieldMapping{fieldKind: baseTypeField, position: 1, name: "FBytes", cassandraType: BytesType, cassandraName: "FBytes"},
- &fieldMapping{fieldKind: baseTypeField, position: 2, name: "FBool", cassandraType: BooleanType, cassandraName: "FBool"},
- &fieldMapping{fieldKind: baseTypeField, position: 3, name: "FInt8", cassandraType: LongType, cassandraName: "FInt8"},
- &fieldMapping{fieldKind: baseTypeField, position: 4, name: "FInt16", cassandraType: LongType, cassandraName: "FInt16"},
- &fieldMapping{fieldKind: baseTypeField, position: 5, name: "FInt32", cassandraType: LongType, cassandraName: "FInt32"},
- &fieldMapping{fieldKind: baseTypeField, position: 6, name: "FInt", cassandraType: LongType, cassandraName: "FInt"},
- &fieldMapping{fieldKind: baseTypeField, position: 7, name: "FInt64", cassandraType: LongType, cassandraName: "FInt64"},
- &fieldMapping{fieldKind: baseTypeField, position: 8, name: "FFloat32", cassandraType: FloatType, cassandraName: "FFloat32"},
- &fieldMapping{fieldKind: baseTypeField, position: 9, name: "FFloat64", cassandraType: DoubleType, cassandraName: "FFloat64"},
- &fieldMapping{fieldKind: baseTypeField, position: 10, name: "FString", cassandraType: UTF8Type, cassandraName: "FString"},
- &fieldMapping{fieldKind: baseTypeField, position: 11, name: "FUUID", cassandraType: UUIDType, cassandraName: "FUUID"},
- &fieldMapping{fieldKind: starNameField, position: 0, name: "*name", cassandraType: 0, cassandraName: ""},
+ key: &field{index: []int{0}, name: "Key", cassandraType: UTF8Type, cassandraName: "Key"},
+ composite: []*field{
+ &field{index: []int{1}, name: "FBytes", cassandraType: BytesType, cassandraName: "FBytes"},
+ &field{index: []int{2}, name: "FBool", cassandraType: BooleanType, cassandraName: "FBool"},
+ &field{index: []int{3}, name: "FInt8", cassandraType: LongType, cassandraName: "FInt8"},
+ &field{index: []int{4}, name: "FInt16", cassandraType: LongType, cassandraName: "FInt16"},
+ &field{index: []int{5}, name: "FInt32", cassandraType: LongType, cassandraName: "FInt32"},
+ &field{index: []int{6}, name: "FInt", cassandraType: LongType, cassandraName: "FInt"},
+ &field{index: []int{7}, name: "FInt64", cassandraType: LongType, cassandraName: "FInt64"},
+ &field{index: []int{8}, name: "FFloat32", cassandraType: FloatType, cassandraName: "FFloat32"},
+ &field{index: []int{9}, name: "FFloat64", cassandraType: DoubleType, cassandraName: "FFloat64"},
+ &field{index: []int{10}, name: "FString", cassandraType: UTF8Type, cassandraName: "FString"},
+ &field{index: []int{11}, name: "FUUID", cassandraType: UUIDType, cassandraName: "FUUID"},
+ },
+ values: []*field{
+ &field{index: []int{12}, name: "Val", cassandraType: UTF8Type, cassandraName: "Val"},
},
- value: &fieldMapping{fieldKind: starValueField, position: 0, name: "*value", cassandraType: 0, cassandraName: ""},
- others: map[string]*fieldMapping{
- "Val": &fieldMapping{fieldKind: baseTypeField, position: 12, name: "Val", cassandraType: UTF8Type, cassandraName: "Val"},
+ namedValues: map[string]*field{
+ "Val": &field{index: []int{12}, name: "Val", cassandraType: UTF8Type, cassandraName: "Val"},
},
- isCompositeColumn: true,
- isSliceColumn: false,
- isStarNameColumn: true,
+ }
+ if err != nil {
+ t.Fatal("Unexpected error calling eComp newMapping:", err)
}
checkMapping(t, goodEComp, eComp, "eComp")
@@ -276,9 +186,6 @@ func (shell *structTestShell) checkFullMap(t *testing.T) {
func TestMap(t *testing.T) {
- // CAVEAT: column ordering is not deterministic for this kind of Map/Unmap roundtrip
- // something needs to be done to test these structs with independent order over the columns
-
shells := []*structTestShell{
&structTestShell{
name: "noErrA",
@@ -288,7 +195,11 @@ func TestMap(t *testing.T) {
Key: []byte{0, 0, 0, 0, 0, 0, 0, 1},
Columns: []*Column{
&Column{
- Name: []byte{0, 0, 0, 0, 0, 0, 0, 2},
+ 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},
},
},
@@ -303,36 +214,13 @@ func TestMap(t *testing.T) {
Key: []byte{0, 0, 0, 0, 0, 0, 0, 1},
Columns: []*Column{
&Column{
- Name: []byte{67},
- Value: []byte{51},
+ Name: []byte{0, 8, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 'C', 0},
+ Value: []byte{'3'},
},
&Column{
- Name: []byte{68},
+ 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},
},
- &Column{
- Name: []byte{90},
- Value: []byte{0, 0, 0, 0, 0, 0, 0, 2},
- },
- },
- },
- },
-
- &structTestShell{
- name: "noErrE",
- expectedStruct: &noErrE{1, 2, []int{5, 6}, []int{7, 8}},
- resultStruct: &noErrE{},
- 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, 8, 0, 0, 0, 0, 0, 0, 0, 5, 0},
- Value: []byte{0, 0, 0, 0, 0, 0, 0, 7},
- },
- &Column{
- Name: []byte{0, 8, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 6, 0},
- Value: []byte{0, 0, 0, 0, 0, 0, 0, 8},
- },
},
},
},
View
3 src/gossie/types.go
@@ -631,10 +631,9 @@ const (
eocLower byte = 0xff
)
-func packComposite(current, component []byte, eoc byte) []byte {
+func packComposite(component []byte, eoc byte) []byte {
r := make([]byte, 2)
enc.BigEndian.PutUint16(r, uint16(len(component)))
- r = append(current, r...)
r = append(r, component...)
return append(r, eoc)
}

0 comments on commit 39cfa7f

Please sign in to comment.
Something went wrong with that request. Please try again.