From 4599863fae8bdb15a198cd66e5fa4dc99c8f151c Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Wed, 18 Dec 2019 17:34:47 +0100 Subject: [PATCH] Added Activity/IntransitiveActivity/Question Json marshaling and tests --- activity.go | 25 ++ activity_test.go | 485 +++++++++++++++++++++++++++++++++++++++ encoding.go | 348 ++++++++++++++++++++++++++++ intransitive_activity.go | 9 + object.go | 263 +-------------------- 5 files changed, 868 insertions(+), 262 deletions(-) create mode 100644 encoding.go diff --git a/activity.go b/activity.go index e88d54c..e387135 100644 --- a/activity.go +++ b/activity.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "time" "unsafe" @@ -793,3 +794,27 @@ func FlattenActivityProperties(act *Activity) *Activity { act.Instrument = Flatten(act.Instrument) return act } + +// MarshalJSON +func (a Activity) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.Write([]byte{'{'}) + + if !writeActivity(&b, a) { + return nil, nil + } + b.Write([]byte{'}'}) + return b.Bytes(), nil +} + +// MarshalJSON +func (i IntransitiveActivity) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.Write([]byte{'{'}) + + if !writeIntransitiveActivity(&b, i) { + return nil, nil + } + b.Write([]byte{'}'}) + return b.Bytes(), nil +} diff --git a/activity_test.go b/activity_test.go index 635cfa2..d860d28 100644 --- a/activity_test.go +++ b/activity_test.go @@ -2,7 +2,9 @@ package activitypub import ( "fmt" + "reflect" "testing" + "time" ) func TestActivityNew(t *testing.T) { @@ -976,4 +978,487 @@ func TestActivity_GetLink(t *testing.T) { func TestActivity_GetType(t *testing.T) { t.Skipf("TODO") + +func TestActivity_MarshalJSON(t *testing.T) { + type fields struct { + ID ID + Type ActivityVocabularyType + Name NaturalLanguageValues + Attachment Item + AttributedTo Item + Audience ItemCollection + Content NaturalLanguageValues + Context Item + MediaType MimeType + EndTime time.Time + Generator Item + Icon Item + Image Item + InReplyTo Item + Location Item + Preview Item + Published time.Time + Replies Item + StartTime time.Time + Summary NaturalLanguageValues + Tag ItemCollection + Updated time.Time + URL LinkOrIRI + To ItemCollection + Bto ItemCollection + CC ItemCollection + BCC ItemCollection + Duration time.Duration + Likes Item + Shares Item + Source Source + Actor Item + Target Item + Result Item + Origin Item + Instrument Item + Object Item + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Empty", + fields: fields{}, + want: nil, + wantErr: false, + }, + { + name: "JustID", + fields: fields{ + ID: ID("example.com"), + }, + want: []byte(`{"id":"example.com"}`), + wantErr: false, + }, + { + name: "JustType", + fields: fields{ + Type: ActivityVocabularyType("myType"), + }, + want: []byte(`{"type":"myType"}`), + wantErr: false, + }, + { + name: "JustOneName", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "ana"}, + }, + }, + want: []byte(`{"name":"ana"}`), + wantErr: false, + }, + { + name: "MoreNames", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: "en", Value: "anna"}, + {Ref: "fr", Value: "anne"}, + }, + }, + want: []byte(`{"nameMap":{"en":"anna","fr":"anne"}}`), + wantErr: false, + }, + { + name: "JustOneSummary", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test summary"}, + }, + }, + want: []byte(`{"summary":"test summary"}`), + wantErr: false, + }, + { + name: "MoreSummaryEntries", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: "en", Value: "test summary"}, + {Ref: "fr", Value: "teste summary"}, + }, + }, + want: []byte(`{"summaryMap":{"en":"test summary","fr":"teste summary"}}`), + wantErr: false, + }, + { + name: "JustOneContent", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test content"}, + }, + }, + want: []byte(`{"content":"test content"}`), + wantErr: false, + }, + { + name: "MoreContentEntries", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: "en", Value: "test content"}, + {Ref: "fr", Value: "teste content"}, + }, + }, + want: []byte(`{"contentMap":{"en":"test content","fr":"teste content"}}`), + wantErr: false, + }, + { + name: "MediaType", + fields: fields{ + MediaType: MimeType("text/stupid"), + }, + 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{ + Source: Source{ + MediaType: MimeType("text/plain"), + Content: NaturalLanguageValues{}, + }, + }, + want: []byte(`{"source":{"mediaType":"text/plain"}}`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := Activity{ + ID: tt.fields.ID, + Type: tt.fields.Type, + Name: tt.fields.Name, + Attachment: tt.fields.Attachment, + AttributedTo: tt.fields.AttributedTo, + Audience: tt.fields.Audience, + Content: tt.fields.Content, + Context: tt.fields.Context, + MediaType: tt.fields.MediaType, + EndTime: tt.fields.EndTime, + Generator: tt.fields.Generator, + Icon: tt.fields.Icon, + Image: tt.fields.Image, + InReplyTo: tt.fields.InReplyTo, + Location: tt.fields.Location, + Preview: tt.fields.Preview, + Published: tt.fields.Published, + Replies: tt.fields.Replies, + StartTime: tt.fields.StartTime, + Summary: tt.fields.Summary, + Tag: tt.fields.Tag, + Updated: tt.fields.Updated, + URL: tt.fields.URL, + To: tt.fields.To, + Bto: tt.fields.Bto, + CC: tt.fields.CC, + BCC: tt.fields.BCC, + Duration: tt.fields.Duration, + Likes: tt.fields.Likes, + Shares: tt.fields.Shares, + Source: tt.fields.Source, + Actor: tt.fields.Actor, + Target: tt.fields.Target, + Result: tt.fields.Result, + Origin: tt.fields.Origin, + Instrument: tt.fields.Instrument, + Object: tt.fields.Object, + } + got, err := a.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() got = %s, want %s", got, tt.want) + } + }) + } +} + +func TestIntransitiveActivity_MarshalJSON(t *testing.T) { + type fields struct { + ID ID + Type ActivityVocabularyType + Name NaturalLanguageValues + Attachment Item + AttributedTo Item + Audience ItemCollection + Content NaturalLanguageValues + Context Item + MediaType MimeType + EndTime time.Time + Generator Item + Icon Item + Image Item + InReplyTo Item + Location Item + Preview Item + Published time.Time + Replies Item + StartTime time.Time + Summary NaturalLanguageValues + Tag ItemCollection + Updated time.Time + URL LinkOrIRI + To ItemCollection + Bto ItemCollection + CC ItemCollection + BCC ItemCollection + Duration time.Duration + Likes Item + Shares Item + Source Source + Actor CanReceiveActivities + Target Item + Result Item + Origin Item + Instrument Item + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Empty", + fields: fields{}, + want: nil, + wantErr: false, + }, + { + name: "JustID", + fields: fields{ + ID: ID("example.com"), + }, + want: []byte(`{"id":"example.com"}`), + wantErr: false, + }, + { + name: "JustType", + fields: fields{ + Type: ActivityVocabularyType("myType"), + }, + want: []byte(`{"type":"myType"}`), + wantErr: false, + }, + { + name: "JustOneName", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "ana"}, + }, + }, + want: []byte(`{"name":"ana"}`), + wantErr: false, + }, + { + name: "MoreNames", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: "en", Value: "anna"}, + {Ref: "fr", Value: "anne"}, + }, + }, + want: []byte(`{"nameMap":{"en":"anna","fr":"anne"}}`), + wantErr: false, + }, + { + name: "JustOneSummary", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test summary"}, + }, + }, + want: []byte(`{"summary":"test summary"}`), + wantErr: false, + }, + { + name: "MoreSummaryEntries", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: "en", Value: "test summary"}, + {Ref: "fr", Value: "teste summary"}, + }, + }, + want: []byte(`{"summaryMap":{"en":"test summary","fr":"teste summary"}}`), + wantErr: false, + }, + { + name: "JustOneContent", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test content"}, + }, + }, + want: []byte(`{"content":"test content"}`), + wantErr: false, + }, + { + name: "MoreContentEntries", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: "en", Value: "test content"}, + {Ref: "fr", Value: "teste content"}, + }, + }, + want: []byte(`{"contentMap":{"en":"test content","fr":"teste content"}}`), + wantErr: false, + }, + { + name: "MediaType", + fields: fields{ + MediaType: MimeType("text/stupid"), + }, + 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{ + Source: Source{ + MediaType: MimeType("text/plain"), + Content: NaturalLanguageValues{}, + }, + }, + want: []byte(`{"source":{"mediaType":"text/plain"}}`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := IntransitiveActivity{ + ID: tt.fields.ID, + Type: tt.fields.Type, + Name: tt.fields.Name, + Attachment: tt.fields.Attachment, + AttributedTo: tt.fields.AttributedTo, + Audience: tt.fields.Audience, + Content: tt.fields.Content, + Context: tt.fields.Context, + MediaType: tt.fields.MediaType, + EndTime: tt.fields.EndTime, + Generator: tt.fields.Generator, + Icon: tt.fields.Icon, + Image: tt.fields.Image, + InReplyTo: tt.fields.InReplyTo, + Location: tt.fields.Location, + Preview: tt.fields.Preview, + Published: tt.fields.Published, + Replies: tt.fields.Replies, + StartTime: tt.fields.StartTime, + Summary: tt.fields.Summary, + Tag: tt.fields.Tag, + Updated: tt.fields.Updated, + URL: tt.fields.URL, + To: tt.fields.To, + Bto: tt.fields.Bto, + CC: tt.fields.CC, + BCC: tt.fields.BCC, + Duration: tt.fields.Duration, + Likes: tt.fields.Likes, + Shares: tt.fields.Shares, + Source: tt.fields.Source, + Actor: tt.fields.Actor, + Target: tt.fields.Target, + Result: tt.fields.Result, + Origin: tt.fields.Origin, + Instrument: tt.fields.Instrument, + } + got, err := i.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() got = %s, want %s", got, tt.want) + } + }) + } } diff --git a/encoding.go b/encoding.go new file mode 100644 index 0000000..5559b1b --- /dev/null +++ b/encoding.go @@ -0,0 +1,348 @@ +package activitypub + +import ( + "bytes" + "fmt" + "time" +) + +func writeProp(b *bytes.Buffer, name string, val []byte) (notEmpty bool) { + if len(val) == 0 { + return false + } + writePropName(b, name) + return writeValue(b, val) +} + +func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + b.Write([]byte{'"'}) + b.WriteString(s) + b.Write([]byte{'"', ':'}) + return true +} + +func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { + if len(s) == 0 { + return false + } + b.Write(s) + return true +} + +func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValues) (notEmpty bool) { + l := nl.Count() + if l > 1 { + n += "Map" + } + if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 { + return writeProp(b, n, v) + } + return false +} + +func writeBoolProp(b *bytes.Buffer, n string, t bool) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf("%t", t))) +} +func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { + if v, err := t.MarshalJSON(); err == nil { + return writeProp(b, n, v) + } + return false +} + +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 { + return false + } + writePropName(b, n) + b.Write([]byte{'"'}) + b.Write([]byte(url)) + b.Write([]byte{'"'}) + return true +} + +func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { + if i == nil { + return notEmpty + } + if i.IsObject() { + OnObject(i, func(o *Object) error { + v, err := o.MarshalJSON() + if err != nil { + return nil + } + notEmpty = writeProp(b, n, v) + return nil + }) + } else if i.IsCollection() { + OnCollection(i, func(c CollectionInterface) error { + notEmpty = writeItemCollectionProp(b, n, c.Collection()) || notEmpty + return nil + }) + } + return notEmpty +} + +func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { + 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) || notEmpty + return nil + }) + } else if i.IsLink() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeValue(b, []byte(i.GetLink())) || notEmpty + } + } + b.Write([]byte{']'}) + return notEmpty +} + +func writeObject(b *bytes.Buffer, o Object) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(b, "id", v) || notEmpty + } + if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(b, "type", v) || notEmpty + } + if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(b, "mediaType", v) || notEmpty + } + if len(o.Name) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(b, "name", o.Name) || notEmpty + } + if len(o.Summary) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(b, "summary", o.Summary) || notEmpty + } + if len(o.Content) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(b, "content", o.Content) || notEmpty + } + if o.Attachment != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "attachment", o.Attachment) || notEmpty + } + if o.AttributedTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "attributedTo", o.AttributedTo) || notEmpty + } + if o.Audience != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "audience", o.Audience) || notEmpty + } + if o.Context != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "context", o.Context) || notEmpty + } + if o.Generator != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "generator", o.Generator) || notEmpty + } + if o.Icon != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "icon", o.Icon) || notEmpty + } + if o.Image != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "image", o.Image) || notEmpty + } + if o.InReplyTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "inReplyTo", o.InReplyTo) || notEmpty + } + if o.Location != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "location", o.Location) || notEmpty + } + if o.Preview != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "preview", o.Preview) || notEmpty + } + if o.Replies != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "replies", o.Replies) || notEmpty + } + if o.Tag != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "tag", o.Tag) || notEmpty + } + if o.URL != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIRIProp(b, "url", o.URL) || notEmpty + } + if o.To != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "to", o.To) || notEmpty + } + if o.Bto != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "bto", o.Bto) || notEmpty + } + if o.CC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "cc", o.CC) || notEmpty + } + if o.BCC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "bcc", o.BCC) || notEmpty + } + if !o.Published.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "published", o.Published) || notEmpty + } + if !o.Updated.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "updated", o.Updated) || notEmpty + } + if !o.StartTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "startTime", o.StartTime) || notEmpty + } + if !o.EndTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "endTime", o.EndTime) || notEmpty + } + 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) || notEmpty + } + if o.Likes != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "likes", o.Likes) || notEmpty + } + if o.Shares != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "shares", o.Shares) || notEmpty + } + if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(b, "source", v) || notEmpty + } + return notEmpty +} + +func writeActivity(b *bytes.Buffer, a Activity) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + + OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = writeIntransitiveActivity(b, *i) || notEmpty + return nil + }) + if a.Object != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "object", a.Object) || notEmpty + } + return notEmpty +} + +func writeIntransitiveActivity(b *bytes.Buffer, i IntransitiveActivity) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + OnObject(i, func(o *Object) error { + if o == nil { + return nil + } + notEmpty = writeObject(b, *o) || notEmpty + return nil + }) + if i.Actor != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "actor", i.Actor) || notEmpty + } + if i.Target != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "target", i.Target) || notEmpty + } + if i.Result != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "result", i.Result) || notEmpty + } + if i.Origin != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "origin", i.Origin) || notEmpty + } + if i.Instrument != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "instrument", i.Instrument) || notEmpty + } + return notEmpty +} + +func writeQuestion(b *bytes.Buffer, q Question) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + + OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = writeIntransitiveActivity(b, *i) || notEmpty + return nil + }) + if q.OneOf != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty + } else if q.AnyOf != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty + } + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeBoolProp(b, "closed", q.Closed) || notEmpty + return notEmpty +} diff --git a/intransitive_activity.go b/intransitive_activity.go index 9c96217..e5fc945 100644 --- a/intransitive_activity.go +++ b/intransitive_activity.go @@ -3,6 +3,7 @@ package activitypub import ( "errors" "time" + "unsafe" ) // IntransitiveActivity Instances of IntransitiveActivity are a subtype of Activity representing intransitive actions. @@ -256,6 +257,14 @@ func IntransitiveActivityNew(id ID, typ ActivityVocabularyType) *IntransitiveAct // ToIntransitiveActivity func ToIntransitiveActivity(it Item) (*IntransitiveActivity, error) { switch i := it.(type) { + case *Activity: + return (*IntransitiveActivity)(unsafe.Pointer(i)), nil + case Activity: + return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil + case *Question: + return (*IntransitiveActivity)(unsafe.Pointer(i)), nil + case Question: + return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil case *IntransitiveActivity: return i, nil case IntransitiveActivity: diff --git a/object.go b/object.go index 5e13017..e987461 100644 --- a/object.go +++ b/object.go @@ -320,275 +320,14 @@ func (o *Object) UnmarshalJSON(data []byte) error { return nil } -func writeProp(b *bytes.Buffer, name string, val []byte) (notEmpty bool) { - notEmpty = false - if len(val) == 0 { - return notEmpty - } - writePropName(b, name) - return writeValue(b, val) -} - -func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { - if len(s) == 0 { - return false - } - b.Write([]byte{'"'}) - l, err := b.WriteString(s) - b.Write([]byte{'"', ':'}) - if err != nil { - return false - } - if l <= 0 { - return false - } - return true -} - -func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { - l, err := b.Write(s) - if err != nil { - return false - } - if l <= 0 { - return false - } - return true -} - -func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValues) (notEmpty bool) { - l := nl.Count() - if l > 1 { - n += "Map" - } - if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 { - notEmpty = writeProp(b, n, v) - } - 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 { - return notEmpty - } - if i.IsObject() { - OnObject(i, func(o *Object) error { - v, err := o.MarshalJSON() - if err != nil { - return nil - } - notEmpty = writeProp(b, n, v) - return nil - }) - } else if i.IsCollection() { - OnCollection(i, func(c CollectionInterface) error { - notEmpty = writeItemCollectionProp(b, n, c.Collection()) - return nil - }) - } - return notEmpty -} - -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 func (o Object) MarshalJSON() ([]byte, error) { b := bytes.Buffer{} notEmpty := false b.Write([]byte{'{'}) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } - if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { - notEmpty = writeProp(&b, "id", v) - } - if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeProp(&b, "type", v) - } - if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeProp(&b, "mediaType", v) - } - if len(o.Name) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeNaturalLanguageProp(&b, "name", o.Name) - } - if len(o.Summary) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeNaturalLanguageProp(&b, "summary", o.Summary) - } - if len(o.Content) > 0 { - 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) - } + notEmpty = writeObject(&b, o) - 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) - notEmpty = writeProp(&b, "source", v) - } if notEmpty { b.Write([]byte{'}'}) return b.Bytes(), nil