From b2d595e31d52258637bc3f8ffa747d8f1bd202fb Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Mon, 2 Oct 2017 16:19:23 +0200 Subject: [PATCH] Improvements to jsonld.Marshal - now with tags --- src/jsonld/context.go | 6 +- src/jsonld/encode.go | 140 +++++++++++++++++++++++++++++--------- src/jsonld/encode_test.go | 44 +++++++----- tests/integration_test.go | 51 +++++++++----- 4 files changed, 172 insertions(+), 69 deletions(-) diff --git a/src/jsonld/context.go b/src/jsonld/context.go index b0e3db9..cf6c085 100644 --- a/src/jsonld/context.go +++ b/src/jsonld/context.go @@ -9,19 +9,19 @@ import ( type Ref string type Context struct { - URL Ref `jsonld:"_"` + URL Ref `jsonld:"@url"` Language activitypub.NaturalLanguageValue `jsonld:"@language,omitempty,collapsible"` } func (c *Context) Ref() Ref { return Ref(c.URL) } + func (r *Ref) MarshalText() ([]byte, error) { return []byte(*r), nil } - func (c *Context) MarshalJSON() ([]byte, error) { var a map[string]interface{} - a = getMap(c) + a = reflectToJsonLdMap(c) return json.Marshal(a) } diff --git a/src/jsonld/encode.go b/src/jsonld/encode.go index f1999be..a53264b 100644 --- a/src/jsonld/encode.go +++ b/src/jsonld/encode.go @@ -2,42 +2,54 @@ package jsonld import ( "encoding/json" + "fmt" "reflect" + "strings" ) +const ( + tagLabel = "jsonld" + tagOmitEmpty = "omitempty" + tagCollapsible = "collapsible" +) + +func Marshal(v interface{}, c *Context) ([]byte, error) { + p := payloadWithContext{c, &v} + return p.MarshalJSON() +} + type payloadWithContext struct { - Context *Context `json:"@context"` + Context *Context `jsonld:"@context,omitempty,collapsible"` Obj *interface{} } -func IsEmpty(v reflect.Value) bool { - var ret bool +func isEmptyValue(v reflect.Value) bool { switch v.Kind() { - case reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func: - ret = v.IsNil() - case reflect.Slice, reflect.Map: - ret = v.Len() == 0 + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() case reflect.Struct: - ret = func(reflect.Value) bool { + return func(reflect.Value) bool { var ret bool = true for i := 0; i < v.NumField(); i++ { - ret = ret && IsEmpty(v.Field(i)) + ret = ret && isEmptyValue(v.Field(i)) } return ret }(v) - case reflect.String: - ret = v.String() == "" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - ret = v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - ret = v.Uint() == 0 - case reflect.Float32, reflect.Float64: - ret = v.Float() == 0.0 } - return ret + return false } -func getMap(v interface{}) map[string]interface{} { +func reflectToJsonLdMap(v interface{}) map[string]interface{} { a := make(map[string]interface{}) typ := reflect.TypeOf(v) val := reflect.ValueOf(v) @@ -49,14 +61,22 @@ func getMap(v interface{}) map[string]interface{} { for i := 0; i < typ.NumField(); i++ { cField := typ.Field(i) cValue := val.Field(i) + cTag := cField.Tag + + jsonLdTag, ok := loadJsonLdTag(cTag) + omitEmpty := ok && jsonLdTag.omitEmpty + if jsonLdTag.ignore { + continue + } if cField.Anonymous { - for k, v := range getMap(cValue.Interface()) { + for k, v := range reflectToJsonLdMap(cValue.Interface()) { a[k] = v } - } else { - if !IsEmpty(cValue) { - a[cField.Name] = cValue.Interface() - } + continue + } + empty := isEmptyValue(cValue) + if !empty || empty && !omitEmpty { + a[jsonLdName(cField.Name, jsonLdTag)] = cValue.Interface() } } @@ -66,13 +86,31 @@ func getMap(v interface{}) map[string]interface{} { func (p *payloadWithContext) MarshalJSON() ([]byte, error) { a := make(map[string]interface{}) if p.Context != nil { - a["@context"] = p.Context.Ref() - } + typ := reflect.TypeOf(*p) + cMirror, _ := typ.FieldByName("Context") + jsonLdTag, ok := loadJsonLdTag(cMirror.Tag) + omitEmpty := ok && jsonLdTag.omitEmpty + collapsible := ok && jsonLdTag.collapsible - for k, v := range getMap(*p.Obj) { - a[k] = v + con := reflectToJsonLdMap(p.Context) + if len(con) > 0 || !omitEmpty { + for _, v := range con { + a[jsonLdName(cMirror.Name, jsonLdTag)] = v + if len(con) == 1 && collapsible { + break + } + } + } + } + if *p.Obj != nil { + oMap := reflectToJsonLdMap(*p.Obj) + if len(oMap) == 0 { + return nil, fmt.Errorf("invalid object to marshall") + } + for k, v := range oMap { + a[k] = v + } } - return json.Marshal(a) } @@ -80,7 +118,45 @@ func (p *payloadWithContext) UnmarshalJSON() {} type Encoder struct{} -func Marshal(v interface{}, c *Context) ([]byte, error) { - p := payloadWithContext{c, &v} - return p.MarshalJSON() +type jsonLdTag struct { + name string + ignore bool + omitEmpty bool + collapsible bool +} + +func loadJsonLdTag(tag reflect.StructTag) (jsonLdTag, bool) { + jlTag, ok := tag.Lookup(tagLabel) + if !ok { + return jsonLdTag{}, false + } + val := strings.Split(jlTag, ",") + cont := func(arr []string, s string) bool { + for _, v := range arr { + if v == s { + return true + } + } + return false + } + t := jsonLdTag{ + omitEmpty: cont(val, tagOmitEmpty), + collapsible: cont(val, tagCollapsible), + } + t.name, t.ignore = func(v string) (string, bool) { + if len(v) > 0 && v != "_" { + return v, false + } else { + return "", true + } + }(val[0]) + + return t, true +} + +func jsonLdName(n string, tag jsonLdTag) string { + if len(tag.name) > 0 { + return tag.name + } + return n } diff --git a/src/jsonld/encode_test.go b/src/jsonld/encode_test.go index f552bf0..3840ba2 100644 --- a/src/jsonld/encode_test.go +++ b/src/jsonld/encode_test.go @@ -1,11 +1,11 @@ package jsonld import ( + "bytes" "encoding/json" "reflect" "strings" "testing" - "bytes" ) type mockBase struct { @@ -61,13 +61,13 @@ func TestMarshalNullContext(t *testing.T) { var a = struct { PropA string PropB float64 - } {"test", 0.0004} + }{"test", 0.0004} - outL, errL := Marshal(a, nil ) + outL, errL := Marshal(a, nil) if errL != nil { t.Errorf("%s", errL) } - outJ, errJ := Marshal(a, nil ) + outJ, errJ := Marshal(a, nil) if errJ != nil { t.Errorf("%s", errJ) } @@ -78,34 +78,42 @@ func TestMarshalNullContext(t *testing.T) { func TestIsEmpty(t *testing.T) { var a int = 0 - if !IsEmpty(reflect.ValueOf(a)) { - t.Errorf("Invalid empty valid %s", a) + if !isEmptyValue(reflect.ValueOf(a)) { + t.Errorf("Invalid empty value %s", a) } - if !IsEmpty(reflect.ValueOf(uint(a))) { - t.Errorf("Invalid empty valid %s", uint(a)) + if !isEmptyValue(reflect.ValueOf(uint(a))) { + t.Errorf("Invalid empty value %s", uint(a)) } var b float64 = 0 - if !IsEmpty(reflect.ValueOf(b)) { - t.Errorf("Invalid empty valid %s", b) + if !isEmptyValue(reflect.ValueOf(b)) { + t.Errorf("Invalid empty value %s", b) } var c string = "" - if !IsEmpty(reflect.ValueOf(c)) { - t.Errorf("Invalid empty valid %s", c) + if !isEmptyValue(reflect.ValueOf(c)) { + t.Errorf("Invalid empty value %s", c) } var d []byte = nil - if !IsEmpty(reflect.ValueOf(d)) { - t.Errorf("Invalid empty valid %v", d) + if !isEmptyValue(reflect.ValueOf(d)) { + t.Errorf("Invalid empty value %v", d) } var e *interface{} = nil - if !IsEmpty(reflect.ValueOf(e)) { - t.Errorf("Invalid empty valid %v", e) + if !isEmptyValue(reflect.ValueOf(e)) { + t.Errorf("Invalid empty value %v", e) } f := struct { a string b int }{} - if !IsEmpty(reflect.ValueOf(f)) { - t.Errorf("Invalid empty valid %v", f) + if !isEmptyValue(reflect.ValueOf(f)) { + t.Errorf("Invalid empty value %v", f) + } + g := false + if !isEmptyValue(reflect.ValueOf(g)) { + t.Errorf("Invalid empty value %v", g) + } + h := true + if isEmptyValue(reflect.ValueOf(h)) { + t.Errorf("Invalid empty value %v", h) } } diff --git a/tests/integration_test.go b/tests/integration_test.go index a2d6912..2b7cc29 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -1,42 +1,61 @@ package tests import ( + "testing" + "activitypub" "jsonld" - "testing" + "strings" ) func TestAcceptSerialization(t *testing.T) { - o := activitypub.AcceptNew("https://localhost/myactivity") - o.Name = make(activitypub.NaturalLanguageValue, 1) - o.Name["en"] = "test" + obj := activitypub.AcceptNew("https://localhost/myactivity") + obj.Name = make(activitypub.NaturalLanguageValue, 1) + obj.Name["en"] = "test" ctx := jsonld.Context{URL: "https://www.w3.org/ns/activitystreams"} - bytes, err := jsonld.Marshal(o, &ctx) + data, err := jsonld.Marshal(obj, &ctx) if err != nil { t.Errorf("Error: %v", err) } - t.Logf("%s", bytes) + if !strings.Contains(string(data), string(ctx.URL)) { + t.Errorf("Could not find context url %#v in output %s", ctx.URL, data) + } + if !strings.Contains(string(data), string(obj.Id)) { + t.Errorf("Could not find id %#v in output %s", string(obj.Id), data) + } + if !strings.Contains(string(data), string(obj.Name["en"])) { + t.Errorf("Could not find name %#v in output %s", string(obj.Name["en"]), data) + } + if !strings.Contains(string(data), string(obj.Type)) { + t.Errorf("Could not find activity type %#v in output %s", obj.Type, data) + } } func TestCreateActivityHTTPSerialization(t *testing.T) { id := activitypub.ObjectId("test_object") - o := activitypub.AcceptNew(id) - o.Name["en"] = "Accept New" + obj := activitypub.AcceptNew(id) + obj.Name["en"] = "Accept New" baseUri := string(activitypub.ActivityBaseURI) - c := jsonld.Context{ - URL: jsonld.Ref(baseUri + string(o.Type)), + ctx := jsonld.Context{ + URL: jsonld.Ref(baseUri + string(obj.Type)), } - - out, err := jsonld.Marshal(o, &c) + data, err := jsonld.Marshal(obj, &ctx) if err != nil { t.Error(err) } - outNoC, errNoC := jsonld.Marshal(o, nil) - if errNoC != nil { - t.Error(errNoC) + if !strings.Contains(string(data), string(ctx.URL)) { + t.Errorf("Could not find context url %#v in output %s", ctx.URL, data) + } + if !strings.Contains(string(data), string(obj.Id)) { + t.Errorf("Could not find id %#v in output %s", string(obj.Id), data) + } + if !strings.Contains(string(data), string(obj.Name["en"])) { + t.Errorf("Could not find name %#v in output %s", string(obj.Name["en"]), data) + } + if !strings.Contains(string(data), string(obj.Type)) { + t.Errorf("Could not find activity type %#v in output %s", obj.Type, data) } - t.Logf("%s\n\n%s", out, outNoC) }