From 8af476849fc9dd0bd7b2577f41640fd11dc0f863 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Wed, 18 Dec 2019 13:09:13 +0100 Subject: [PATCH] Completed functionality of Object.MarshalJSON Added a couple more tests Added proper time.Duration to xsd:duration encoding --- natural_language_values.go | 4 +- object.go | 164 +++++++++++++++++++++++++++++++++++-- object_test.go | 39 +++++++++ unmarshal.go | 1 + xsdduration.go | 84 +++++++++++++++++++ xsdduration_test.go | 48 +++++++++++ 6 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 xsdduration.go create mode 100644 xsdduration_test.go diff --git a/natural_language_values.go b/natural_language_values.go index b952921..52b06a5 100644 --- a/natural_language_values.go +++ b/natural_language_values.go @@ -73,7 +73,7 @@ func (n *NaturalLanguageValues) Set(ref LangRef, v string) error { // MarshalJSON serializes the NaturalLanguageValues into JSON func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) { l := len(n) - if l == 0 { + if l <= 0 { return nil, nil } @@ -86,7 +86,7 @@ func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - if ll == 0 { + if ll <= 0 { return nil, nil } return b.Bytes(), nil diff --git a/object.go b/object.go index 8f43ddb..5e13017 100644 --- a/object.go +++ b/object.go @@ -339,7 +339,7 @@ func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { if err != nil { return false } - if l == 0 { + if l <= 0 { return false } return true @@ -350,7 +350,7 @@ func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { if err != nil { return false } - if l == 0 { + if l <= 0 { return false } return true @@ -367,6 +367,32 @@ func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValue return notEmpty } +func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { + v, err := t.MarshalJSON() + if err != nil { + return false + } + return writeProp(b, n, v) +} +func writeDurationProp(b *bytes.Buffer, n string, d time.Duration) (notEmpty bool) { + if v, err := marshalXSD(d); err == nil { + return writeProp(b, n, v) + } + return false +} + +func writeIRIProp(b *bytes.Buffer, n string, i LinkOrIRI) (notEmpty bool) { + url := i.GetLink() + if len(url) > 0 { + writePropName(b, n) + b.Write([]byte{'"'}) + b.Write([]byte(url)) + b.Write([]byte{'"'}) + return true + } + return false +} + func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { notEmpty = false if i == nil { @@ -390,8 +416,37 @@ func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { return notEmpty } -func writeItemCollectionProp(b *bytes.Buffer, n string, i ItemCollection) (notEmpty bool) { - return false +func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { + notEmpty = false + if len(col) == 0 { + return notEmpty + } + writePropName(b, n) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + b.Write([]byte{'['}) + for _, i := range col { + if i.IsObject() { + OnObject(i, func(o *Object) error { + v, err := o.MarshalJSON() + if err != nil { + return nil + } + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeValue(b, v) + return nil + }) + } else if i.IsLink() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeValue(b, []byte(i.GetLink())) + } + } + b.Write([]byte{']'}) + return notEmpty } // MarshalJSON @@ -406,7 +461,6 @@ func (o Object) MarshalJSON() ([]byte, error) { writeComma() } } - if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { notEmpty = writeProp(&b, "id", v) } @@ -430,6 +484,106 @@ func (o Object) MarshalJSON() ([]byte, error) { writeCommaIfNotEmpty(notEmpty) notEmpty = writeNaturalLanguageProp(&b, "content", o.Content) } + if o.Attachment != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "attachment", o.Attachment) + } + if o.AttributedTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "attributedTo", o.AttributedTo) + } + if o.Audience != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "audience", o.Audience) + } + if o.Context != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "context", o.Context) + } + if o.Generator != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "generator", o.Generator) + } + if o.Icon != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "icon", o.Icon) + } + if o.Image != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "image", o.Image) + } + if o.InReplyTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "inReplyTo", o.InReplyTo) + } + if o.Location != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "location", o.Location) + } + if o.Preview != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "preview", o.Preview) + } + if o.Replies != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "replies", o.Replies) + } + if o.Tag != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "tag", o.Tag) + } + if o.URL != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIRIProp(&b, "url", o.URL) + } + if o.To != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "to", o.To) + } + if o.Bto != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "bto", o.Bto) + } + if o.CC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "cc", o.CC) + } + if o.BCC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "bcc", o.BCC) + } + + if !o.Published.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "published", o.Published) + } + if !o.Updated.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "updated", o.Updated) + } + if !o.StartTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "startTime", o.StartTime) + } + if !o.EndTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "endTime", o.EndTime) + } + if o.Duration != 0 { + // TODO(marius): maybe don't use 0 as a nil value for Object types + // which can have a valid duration of 0 - (Video, Audio, etc) + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeDurationProp(&b, "duration", o.Duration) + } + + if o.Likes != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "likes", o.Likes) + } + if o.Shares != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "shares", o.Shares) + } if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { writeCommaIfNotEmpty(notEmpty) diff --git a/object_test.go b/object_test.go index 4725411..d846d1c 100644 --- a/object_test.go +++ b/object_test.go @@ -635,6 +635,45 @@ func TestObject_MarshalJSON(t *testing.T) { want: []byte(`{"mediaType":"text/stupid"}`), wantErr: false, }, + { + name: "Attachment", + fields: fields{ + Attachment: &Object{ + ID: "some example", + Type: VideoType, + }, + }, + want: []byte(`{"attachment":{"id":"some example","type":"Video"}}`), + wantErr: false, + }, + { + name: "AttributedTo", + fields: fields{ + AttributedTo: &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + }, + want: []byte(`{"attributedTo":{"id":"http://example.com/ana","type":"Person"}}`), + wantErr: false, + }, + { + name: "AttributedToDouble", + fields: fields{ + AttributedTo: ItemCollection{ + &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + &Actor{ + ID: "http://example.com/GGG", + Type: GroupType, + }, + }, + }, + want: []byte(`{"attributedTo":[{"id":"http://example.com/ana","type":"Person"},{"id":"http://example.com/GGG","type":"Group"}]}`), + wantErr: false, + }, { name: "Source", fields: fields{ diff --git a/unmarshal.go b/unmarshal.go index 3cc78e3..a368fc1 100644 --- a/unmarshal.go +++ b/unmarshal.go @@ -123,6 +123,7 @@ func JSONGetTime(data []byte, prop string) time.Time { func JSONGetDuration(data []byte, prop string) time.Duration { str, _ := jsonparser.GetUnsafeString(data, prop) + // TODO(marius): this needs to be replaced to be compatible with xsd:duration d, _ := time.ParseDuration(str) return d } diff --git a/xsdduration.go b/xsdduration.go new file mode 100644 index 0000000..31351d1 --- /dev/null +++ b/xsdduration.go @@ -0,0 +1,84 @@ +package activitypub + +import ( + "bytes" + "fmt" + "time" +) + +const day = time.Hour * 24 +const week = day * 7 +const month = week * 4 +const year = month * 12 + +func Days(d time.Duration) float64 { + dd := d / day + h := d % day + return float64(dd) + float64(h)/(24*60*60*1e9) +} +func Weeks(d time.Duration) float64 { + w := d / week + dd := d % week + return float64(w) + float64(dd)/(7*24*60*60*1e9) +} +func Months(d time.Duration) float64 { + m := d / month + w := d % month + return float64(m) + float64(w)/(4*7*24*60*60*1e9) +} +func Years(d time.Duration) float64 { + y := d / year + m := d % year + return float64(y) + float64(m)/(12*4*7*24*60*60*1e9) +} + +func marshalXSD(d time.Duration) ([]byte, error) { + if d == 0 { + return []byte{'P','T','0','S'}, nil + } + + neg := d < 0 + if neg { + d = -d + } + y := Years(d) + d -= time.Duration(y) * year + m := Months(d) + d -= time.Duration(m) * month + dd := Days(d) + d -= time.Duration(dd) * day + H := d.Hours() + d -= time.Duration(H) * time.Hour + M := d.Minutes() + d -= time.Duration(M) * time.Minute + s := d.Seconds() + d -= time.Duration(s) * time.Second + b := bytes.Buffer{} + if neg { + b.Write([]byte{'-'}) + } + b.Write([]byte{'P'}) + if y > 0 { + b.WriteString(fmt.Sprintf("%dY", int64(y))) + } + if m > 0 { + b.WriteString(fmt.Sprintf("%dM", int64(m))) + } + if dd > 0 { + b.WriteString(fmt.Sprintf("%dD", int64(dd))) + } + + if H + M + s > 0 { + b.Write([]byte{'T'}) + if H > 0 { + b.WriteString(fmt.Sprintf("%dH", int64(H))) + } + if M > 0 { + b.WriteString(fmt.Sprintf("%dM", int64(M))) + } + if s > 0 { + b.WriteString(fmt.Sprintf("%dS", int64(s))) + } + } + return b.Bytes(), nil +} diff --git a/xsdduration_test.go b/xsdduration_test.go new file mode 100644 index 0000000..f8e9720 --- /dev/null +++ b/xsdduration_test.go @@ -0,0 +1,48 @@ +package activitypub + +import ( + "reflect" + "testing" + "time" +) + +func Test_marshalXSD(t *testing.T) { + tests := []struct { + name string + d time.Duration + want []byte + wantErr bool + }{ + { + name: "Zero duration", + d: 0, + want: []byte("PT0S"), + wantErr: false, + }, + { + name: "One year", + d: year, + want: []byte("P1Y"), + wantErr: false, + }, + { + name: "XSD:duration example 1st", + d: 2*year+6*month+5*day+12*time.Hour+35*time.Minute+30*time.Second, + want: []byte("P2Y6M5DT12H35M30S"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := marshalXSD(tt.d) + if (err != nil) != tt.wantErr { + t.Errorf("marshalXSD() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("marshalXSD() got = %s, want %s", got, tt.want) + } + }) + } +} +