Improvements to jsonld.Marshal - now with tags

This commit is contained in:
Marius Orcsik 2017-10-02 16:19:23 +02:00
parent ca076e7185
commit b2d595e31d
No known key found for this signature in database
GPG key ID: C36D1EBE93A6EEAE
4 changed files with 172 additions and 69 deletions

View file

@ -9,19 +9,19 @@ import (
type Ref string type Ref string
type Context struct { type Context struct {
URL Ref `jsonld:"_"` URL Ref `jsonld:"@url"`
Language activitypub.NaturalLanguageValue `jsonld:"@language,omitempty,collapsible"` Language activitypub.NaturalLanguageValue `jsonld:"@language,omitempty,collapsible"`
} }
func (c *Context) Ref() Ref { func (c *Context) Ref() Ref {
return Ref(c.URL) return Ref(c.URL)
} }
func (r *Ref) MarshalText() ([]byte, error) { func (r *Ref) MarshalText() ([]byte, error) {
return []byte(*r), nil return []byte(*r), nil
} }
func (c *Context) MarshalJSON() ([]byte, error) { func (c *Context) MarshalJSON() ([]byte, error) {
var a map[string]interface{} var a map[string]interface{}
a = getMap(c) a = reflectToJsonLdMap(c)
return json.Marshal(a) return json.Marshal(a)
} }

View file

@ -2,42 +2,54 @@ package jsonld
import ( import (
"encoding/json" "encoding/json"
"fmt"
"reflect" "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 { type payloadWithContext struct {
Context *Context `json:"@context"` Context *Context `jsonld:"@context,omitempty,collapsible"`
Obj *interface{} Obj *interface{}
} }
func IsEmpty(v reflect.Value) bool { func isEmptyValue(v reflect.Value) bool {
var ret bool
switch v.Kind() { switch v.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func: case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
ret = v.IsNil() return v.Len() == 0
case reflect.Slice, reflect.Map: case reflect.Bool:
ret = v.Len() == 0 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: case reflect.Struct:
ret = func(reflect.Value) bool { return func(reflect.Value) bool {
var ret bool = true var ret bool = true
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
ret = ret && IsEmpty(v.Field(i)) ret = ret && isEmptyValue(v.Field(i))
} }
return ret return ret
}(v) }(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{}) a := make(map[string]interface{})
typ := reflect.TypeOf(v) typ := reflect.TypeOf(v)
val := reflect.ValueOf(v) val := reflect.ValueOf(v)
@ -49,14 +61,22 @@ func getMap(v interface{}) map[string]interface{} {
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
cField := typ.Field(i) cField := typ.Field(i)
cValue := val.Field(i) cValue := val.Field(i)
cTag := cField.Tag
jsonLdTag, ok := loadJsonLdTag(cTag)
omitEmpty := ok && jsonLdTag.omitEmpty
if jsonLdTag.ignore {
continue
}
if cField.Anonymous { if cField.Anonymous {
for k, v := range getMap(cValue.Interface()) { for k, v := range reflectToJsonLdMap(cValue.Interface()) {
a[k] = v a[k] = v
} }
} else { continue
if !IsEmpty(cValue) { }
a[cField.Name] = cValue.Interface() 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) { func (p *payloadWithContext) MarshalJSON() ([]byte, error) {
a := make(map[string]interface{}) a := make(map[string]interface{})
if p.Context != nil { 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) { con := reflectToJsonLdMap(p.Context)
a[k] = v 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) return json.Marshal(a)
} }
@ -80,7 +118,45 @@ func (p *payloadWithContext) UnmarshalJSON() {}
type Encoder struct{} type Encoder struct{}
func Marshal(v interface{}, c *Context) ([]byte, error) { type jsonLdTag struct {
p := payloadWithContext{c, &v} name string
return p.MarshalJSON() 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
} }

View file

@ -1,11 +1,11 @@
package jsonld package jsonld
import ( import (
"bytes"
"encoding/json" "encoding/json"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"bytes"
) )
type mockBase struct { type mockBase struct {
@ -61,13 +61,13 @@ func TestMarshalNullContext(t *testing.T) {
var a = struct { var a = struct {
PropA string PropA string
PropB float64 PropB float64
} {"test", 0.0004} }{"test", 0.0004}
outL, errL := Marshal(a, nil ) outL, errL := Marshal(a, nil)
if errL != nil { if errL != nil {
t.Errorf("%s", errL) t.Errorf("%s", errL)
} }
outJ, errJ := Marshal(a, nil ) outJ, errJ := Marshal(a, nil)
if errJ != nil { if errJ != nil {
t.Errorf("%s", errJ) t.Errorf("%s", errJ)
} }
@ -78,34 +78,42 @@ func TestMarshalNullContext(t *testing.T) {
func TestIsEmpty(t *testing.T) { func TestIsEmpty(t *testing.T) {
var a int = 0 var a int = 0
if !IsEmpty(reflect.ValueOf(a)) { if !isEmptyValue(reflect.ValueOf(a)) {
t.Errorf("Invalid empty valid %s", a) t.Errorf("Invalid empty value %s", a)
} }
if !IsEmpty(reflect.ValueOf(uint(a))) { if !isEmptyValue(reflect.ValueOf(uint(a))) {
t.Errorf("Invalid empty valid %s", uint(a)) t.Errorf("Invalid empty value %s", uint(a))
} }
var b float64 = 0 var b float64 = 0
if !IsEmpty(reflect.ValueOf(b)) { if !isEmptyValue(reflect.ValueOf(b)) {
t.Errorf("Invalid empty valid %s", b) t.Errorf("Invalid empty value %s", b)
} }
var c string = "" var c string = ""
if !IsEmpty(reflect.ValueOf(c)) { if !isEmptyValue(reflect.ValueOf(c)) {
t.Errorf("Invalid empty valid %s", c) t.Errorf("Invalid empty value %s", c)
} }
var d []byte = nil var d []byte = nil
if !IsEmpty(reflect.ValueOf(d)) { if !isEmptyValue(reflect.ValueOf(d)) {
t.Errorf("Invalid empty valid %v", d) t.Errorf("Invalid empty value %v", d)
} }
var e *interface{} = nil var e *interface{} = nil
if !IsEmpty(reflect.ValueOf(e)) { if !isEmptyValue(reflect.ValueOf(e)) {
t.Errorf("Invalid empty valid %v", e) t.Errorf("Invalid empty value %v", e)
} }
f := struct { f := struct {
a string a string
b int b int
}{} }{}
if !IsEmpty(reflect.ValueOf(f)) { if !isEmptyValue(reflect.ValueOf(f)) {
t.Errorf("Invalid empty valid %v", 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)
} }
} }

View file

@ -1,42 +1,61 @@
package tests package tests
import ( import (
"testing"
"activitypub" "activitypub"
"jsonld" "jsonld"
"testing" "strings"
) )
func TestAcceptSerialization(t *testing.T) { func TestAcceptSerialization(t *testing.T) {
o := activitypub.AcceptNew("https://localhost/myactivity") obj := activitypub.AcceptNew("https://localhost/myactivity")
o.Name = make(activitypub.NaturalLanguageValue, 1) obj.Name = make(activitypub.NaturalLanguageValue, 1)
o.Name["en"] = "test" obj.Name["en"] = "test"
ctx := jsonld.Context{URL: "https://www.w3.org/ns/activitystreams"} 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 { if err != nil {
t.Errorf("Error: %v", err) 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) { func TestCreateActivityHTTPSerialization(t *testing.T) {
id := activitypub.ObjectId("test_object") id := activitypub.ObjectId("test_object")
o := activitypub.AcceptNew(id) obj := activitypub.AcceptNew(id)
o.Name["en"] = "Accept New" obj.Name["en"] = "Accept New"
baseUri := string(activitypub.ActivityBaseURI) baseUri := string(activitypub.ActivityBaseURI)
c := jsonld.Context{ ctx := jsonld.Context{
URL: jsonld.Ref(baseUri + string(o.Type)), URL: jsonld.Ref(baseUri + string(obj.Type)),
} }
data, err := jsonld.Marshal(obj, &ctx)
out, err := jsonld.Marshal(o, &c)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
outNoC, errNoC := jsonld.Marshal(o, nil) if !strings.Contains(string(data), string(ctx.URL)) {
if errNoC != nil { t.Errorf("Could not find context url %#v in output %s", ctx.URL, data)
t.Error(errNoC) }
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)
} }