diff --git a/go.mod b/go.mod index e908977..57b2f69 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/phoops/ngsiv2 go 1.13 -require github.com/paulmach/go.geojson v1.4.0 +require ( + github.com/mitchellh/mapstructure v1.4.2 + github.com/paulmach/go.geojson v1.4.0 +) diff --git a/go.sum b/go.sum index 0ff2c9e..4bb2338 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY= github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs= diff --git a/model/model.go b/model/model.go index 0605171..1ee41e1 100644 --- a/model/model.go +++ b/model/model.go @@ -10,6 +10,7 @@ import ( "time" "unicode" + "github.com/mitchellh/mapstructure" geojson "github.com/paulmach/go.geojson" ) @@ -41,20 +42,22 @@ type Metadata struct { type AttributeType string +// Constants representing NGSIv2 special data types const ( - StringType AttributeType = "String" - TextType AttributeType = "Text" - NumberType AttributeType = "Number" - FloatType AttributeType = "Float" - IntegerType AttributeType = "Integer" - BooleanType AttributeType = "Boolean" - PercentageType AttributeType = "Percentage" - DateTimeType AttributeType = "DateTime" - GeoPointType AttributeType = "geo:point" - GeoLineType AttributeType = "geo:line" - GeoPolygonType AttributeType = "geo:polygon" - GeoBoxType AttributeType = "geo:box" - GeoJSONType AttributeType = "geo:json" + StringType AttributeType = "String" + TextType AttributeType = "Text" + NumberType AttributeType = "Number" + FloatType AttributeType = "Float" + IntegerType AttributeType = "Integer" + BooleanType AttributeType = "Boolean" + PercentageType AttributeType = "Percentage" + DateTimeType AttributeType = "DateTime" + GeoPointType AttributeType = "geo:point" + GeoLineType AttributeType = "geo:line" + GeoPolygonType AttributeType = "geo:polygon" + GeoBoxType AttributeType = "geo:box" + GeoJSONType AttributeType = "geo:json" + StructuredValueType AttributeType = "StructuredValue" ) const ( @@ -683,6 +686,19 @@ func (e *Entity) SetAttributeAsGeoJSON(name string, value *geojson.Geometry) err return nil } +func (e *Entity) SetAttributeAsStructuredValue(name string, value interface{}) error { + if err := validateAttributeName(name); err != nil { + return err + } + e.Attributes[name] = &Attribute{ + typeValue: typeValue{ + Type: StructuredValueType, + Value: value, + }, + } + return nil +} + func (a *Attribute) GetAsString() (string, error) { if a.Type != StringType && a.Type != TextType { return "", fmt.Errorf("Attribute is nor String or Text, but %s", a.Type) @@ -765,6 +781,16 @@ func (a *Attribute) GetAsGeoJSON() (*geojson.Geometry, error) { return g, nil } +// DecodeStructuredValue decodes the attribute into output if attribute type is StructuredValue. +// output must be a pointer to a map or struct. +func (a *Attribute) DecodeStructuredValue(output interface{}) error { + if a.Type != StructuredValueType { + return fmt.Errorf("Attribute is not %s, but '%s'", StructuredValueType, a.Type) + } + + return mapstructure.Decode(a.Value, output) +} + func (e *Entity) GetAttributeAsString(attributeName string) (string, error) { if a, err := e.GetAttribute(attributeName); err != nil { return "", err @@ -845,6 +871,16 @@ func (e *Entity) GetAttributeAsGeoJSON(attributeName string) (*geojson.Geometry, return a.GetAsGeoJSON() } +// DecodeStructuredValueAttribute decodes the attribute named attributeName into output if +// attribute type is StructuredValue. output must be a pointer to a map or struct. +func (e *Entity) DecodeStructuredValueAttribute(attributeName string, output interface{}) error { + a, err := e.GetAttribute(attributeName) + if err != nil { + return err + } + return a.DecodeStructuredValue(output) +} + func NewBatchUpdate(action ActionType) *BatchUpdate { b := &BatchUpdate{ActionType: action} return b diff --git a/model/model_test.go b/model/model_test.go index a29db1a..045d2bc 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -321,6 +321,75 @@ func TestEntityUnmarshal(t *testing.T) { } else { t.Fatal("Expected error on getting a nasty boolean, got nil") } + + structuredVal := ` + { + "id": "Structured1", + "manufacturers": { + "metadata": {}, + "type": "StructuredValue", + "value": [ + "Audi", + "Alfa Romeo", + "BMW" + ] + }, + "seller": { + "metadata": {}, + "type": "StructuredValue", + "value": { + "name": "Jane", + "age": 25 + } + } + } + ` + + structuredEntity := &model.Entity{} + if err := json.Unmarshal([]byte(structuredVal), structuredEntity); err != nil { + t.Fatalf("Error unmarshaling entity: %v", err) + } + + manufacturersAttr, err := structuredEntity.GetAttribute("manufacturers") + if err != nil { + t.Fatalf("Unexpected error: '%v'", err) + } + if manufacturersAttr.Type != model.StructuredValueType { + t.Fatalf("Expected '%s' for manufacturers attribute type, got '%s'", model.StructuredValueType, manufacturersAttr.Type) + } + + type intarray []int + type stringarray []string + manufacturersInt := make(intarray, 0) + manufacturersString := make(stringarray, 0) + if err := manufacturersAttr.DecodeStructuredValue(&manufacturersInt); err == nil { + t.Fatal("Expected a failure on non integers 'manufacturers'") + } + err = manufacturersAttr.DecodeStructuredValue(&manufacturersString) + if err != nil { + t.Fatalf("Unexpected error: '%v'", err) + } + if len(manufacturersString) != 3 { + t.Fatalf("Expected '%d' manufacturers, got '%d'", 3, len(manufacturersString)) + } + if manufacturersString[2] != "BMW" { + t.Fatalf("Expected 'BMW' as third manufacturer, got '%s'", manufacturersString[2]) + } + + type Person struct { + Name string + Age int + } + + person := new(Person) + err = structuredEntity.DecodeStructuredValueAttribute("seller", person) + if err != nil { + t.Fatalf("Unexpected error: '%v'", err) + } + + if person.Name != "Jane" || person.Age != 25 { + t.Fatalf("Expected Jane, got '%v'", *person) + } } func TestEntityMarshal(t *testing.T) { @@ -352,6 +421,16 @@ func TestEntityMarshal(t *testing.T) { geoJsonPoint := geojson.NewPointGeometry([]float64{4.1, 2.3}) office.SetAttributeAsGeoJSON("position", geoJsonPoint) + type Dog struct { + Name string + Age int + } + + ambra := &Dog{"Ambra", 4} + if err := office.SetAttributeAsStructuredValue("mascot", ambra); err != nil { + t.Fatalf("Unexpected error: '%v'", err) + } + bytes, err := json.Marshal(office) if err != nil { t.Fatalf("Unexpected error: '%v'", err) @@ -481,6 +560,21 @@ func TestEntityMarshal(t *testing.T) { } } } + + mascotAttr, err := unmarshaled.GetAttribute("mascot") + if err != nil { + t.Fatalf("Unexpected error: '%v'", err) + } + if mascotAttr.Type != model.StructuredValueType { + t.Fatalf("Expected %s type, got '%s'", model.StructuredValueType, mascotAttr.Type) + } + mascot := new(Dog) + if err := mascotAttr.DecodeStructuredValue(mascot); err != nil { + t.Fatalf("Unexpected error: '%v'", err) + } + if mascot.Name != "Ambra" || mascot.Age != 4 { + t.Fatalf("Expected Ambra as mascot, got '%v'", mascot) + } } func TestDirectEntityAttributeAccess(t *testing.T) {