diff --git a/activity.go b/activity.go index e88d54c..531849d 100644 --- a/activity.go +++ b/activity.go @@ -793,3 +793,27 @@ func FlattenActivityProperties(act *Activity) *Activity { act.Instrument = Flatten(act.Instrument) return act } + +// MarshalJSON +func (a Activity) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + write(&b, '{') + + if !writeActivity(&b, a) { + return nil, nil + } + write(&b, '}') + return b, nil +} + +// MarshalJSON +func (i IntransitiveActivity) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + write(&b, '{') + + if !writeIntransitiveActivity(&b, i) { + return nil, nil + } + write(&b, '}') + return b, nil +} diff --git a/activity_test.go b/activity_test.go index 635cfa2..2fe378a 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) { @@ -977,3 +979,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/actor.go b/actor.go index 5a7e66c..1243a3d 100644 --- a/actor.go +++ b/actor.go @@ -209,6 +209,28 @@ func (p *PublicKey) UnmarshalJSON(data []byte) error { } return nil } + +func (p PublicKey) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := true + write(&b, '{') + if v, err := p.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = !writeProp(&b, "id", v) + } + if p.Owner != nil { + notEmpty = writeIRIProp(&b, "owner", p.Owner) || notEmpty + } + if len(p.PublicKeyPem) > 0 { + notEmpty = writeIRIProp(&b, "publicKeyPem", p.Owner) || notEmpty + } + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + type ( // Application describes a software application. Application = Actor @@ -364,6 +386,51 @@ func (a *Actor) UnmarshalJSON(data []byte) error { return nil } +func (a Actor) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(a, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if a.Inbox != nil { + notEmpty = writeItemProp(&b, "inbox", a.Inbox) || notEmpty + } + if a.Outbox != nil { + notEmpty = writeItemProp(&b, "outbox", a.Outbox) || notEmpty + } + if a.Following != nil { + notEmpty = writeItemProp(&b, "following", a.Following) || notEmpty + } + if a.Followers != nil { + notEmpty = writeItemProp(&b, "followers", a.Followers) || notEmpty + } + if a.Liked != nil { + notEmpty = writeItemProp(&b, "liked", a.Liked) || notEmpty + } + if a.Endpoints != nil { + if v, err := a.Endpoints.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(&b, "endpoints", v) || notEmpty + } + } + if len(a.Streams) > 0 { + writePropName(&b, "streams") + lNotEmpty := true + for _, ss := range a.Streams { + lNotEmpty = writeItemCollection(&b, ss) || lNotEmpty + } + notEmpty = lNotEmpty || notEmpty + } + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // Endpoints a json object which maps additional (typically server/domain-wide) // endpoints which may be useful either for this actor or someone referencing this actor. // This mapping may be nested inside the actor document as the value or may be a link to @@ -402,6 +469,37 @@ func (e *Endpoints) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (e Endpoints) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + + write(&b, '{') + if e.OauthAuthorizationEndpoint != nil { + notEmpty = writeItemProp(&b, "oauthAuthorizationEndpoint", e.OauthAuthorizationEndpoint) || notEmpty + } + if e.OauthTokenEndpoint != nil { + notEmpty = writeItemProp(&b, "oauthTokenEndpoint", e.OauthTokenEndpoint) || notEmpty + } + if e.ProvideClientKey != nil { + notEmpty = writeItemProp(&b, "provideClientKey", e.ProvideClientKey) || notEmpty + } + if e.SignClientKey != nil { + notEmpty = writeItemProp(&b, "signClientKey", e.SignClientKey) || notEmpty + } + if e.SharedInbox != nil { + notEmpty = writeItemProp(&b, "sharedInbox", e.SharedInbox) || notEmpty + } + if e.UploadMedia != nil { + notEmpty = writeItemProp(&b, "uploadMedia", e.UploadMedia) || notEmpty + } + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // ToActor func ToActor(it Item) (*Actor, error) { switch i := it.(type) { @@ -410,6 +508,8 @@ func ToActor(it Item) (*Actor, error) { case Actor: return &i, nil case *Object: + // TODO(marius): this is unsafe as Actor has a different memory layout than Actor + // Everything should be fine as long as you don't try to read the Actor specific collections return (*Actor)(unsafe.Pointer(i)), nil case Object: return (*Actor)(unsafe.Pointer(&i)), nil diff --git a/collection.go b/collection.go index 7580e68..bffe714 100644 --- a/collection.go +++ b/collection.go @@ -131,6 +131,22 @@ type Collection struct { Items ItemCollection `jsonld:"items,omitempty"` } +type ( + // FollowersCollection is a collection of followers + FollowersCollection = Followers + + // Followers is a Collection type + Followers = Collection + + // FollowingCollection is a list of everybody that the actor has followed, added as a side effect. + // The following collection MUST be either an OrderedCollection or a Collection and MAY + // be filtered on privileges of an authenticated user or as appropriate when no authentication is given. + FollowingCollection = Following + + // Following is a type alias for a simple Collection + Following = Collection +) + // CollectionNew initializes a new Collection func CollectionNew(id ID) *Collection { c := Collection{ID: id, Type: CollectionType} @@ -279,6 +295,37 @@ func (c *Collection) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (c Collection) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(c, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if c.Current != nil { + notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + notEmpty = writeItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty + } + if c.Items != nil { + notEmpty = writeItemCollectionProp(&b, "items", c.Items) || notEmpty + } + notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // ToCollection func ToCollection(it Item) (*Collection, error) { switch i := it.(type) { @@ -293,3 +340,17 @@ func ToCollection(it Item) (*Collection, error) { } return nil, errors.New("unable to convert to collection") } + +// FollowingNew initializes a new Following +func FollowingNew() *Following { + id := ID("following") + + i := Following{ID: id, Type: CollectionType} + i.Name = NaturalLanguageValuesNew() + i.Content = NaturalLanguageValuesNew() + i.Summary = NaturalLanguageValuesNew() + + i.TotalItems = 0 + + return &i +} diff --git a/collection_page.go b/collection_page.go index 474089a..04615a0 100644 --- a/collection_page.go +++ b/collection_page.go @@ -254,6 +254,47 @@ func (c *CollectionPage) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (c CollectionPage) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(c, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if c.Current != nil { + notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + notEmpty = writeItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty + } + if c.Items != nil { + notEmpty = writeItemCollectionProp(&b, "items", c.Items) || notEmpty + } + if c.PartOf != nil { + notEmpty = writeItemProp(&b, "partOf", c.PartOf) || notEmpty + } + if c.Next != nil { + notEmpty = writeItemProp(&b, "next", c.Next) || notEmpty + } + if c.Prev != nil { + notEmpty = writeItemProp(&b, "prev", c.Prev) || notEmpty + } + + notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // CollectionNew initializes a new CollectionPage func CollectionPageNew(parent CollectionInterface) *CollectionPage { p := CollectionPage{ diff --git a/collection_test.go b/collection_test.go index 59a5f6f..d611576 100644 --- a/collection_test.go +++ b/collection_test.go @@ -174,3 +174,10 @@ func TestCollection_IsCollection(t *testing.T) { t.Skipf("TODO") } +func TestFollowersNew(t *testing.T) { + t.Skipf("TODO") +} + +func TestFollowingNew(t *testing.T) { + t.Skipf("TODO") +} diff --git a/encoding.go b/encoding.go new file mode 100644 index 0000000..fdfdc27 --- /dev/null +++ b/encoding.go @@ -0,0 +1,310 @@ +package activitypub + +import ( + "encoding/json" + "fmt" + "time" +) + +func writeComma(b *[]byte) { + if len(*b) > 1 && (*b)[len(*b)-1] != ',' { + *b = append(*b, ',') + } +} +func writeProp(b *[]byte, name string, val []byte) (notEmpty bool) { + if len(val) == 0 { + return false + } + writeComma(b) + success := writePropName(b, name) && writeValue(b, val) + if !success { + *b = (*b)[:len(*b)-1] + } + return success +} + +func write(b *[]byte, c ...byte) { + *b = append(*b, c...) +} + +func writeS(b *[]byte, s string) { + *b = append(*b, s...) +} + +func writePropName(b *[]byte, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + write(b, '"') + writeS(b, s) + write(b, '"', ':') + return true +} + +func writeValue(b *[]byte, s []byte) (notEmpty bool) { + if len(s) == 0 { + return false + } + write(b, s...) + return true +} + +func writeNaturalLanguageProp(b *[]byte, 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 writeStringProp(b *[]byte, n string, s string) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf(`"%s"`, s))) +} +func writeBoolProp(b *[]byte, n string, t bool) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf(`"%t"`, t))) +} +func writeIntProp(b *[]byte, n string, d int64) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf("%d", d))) +} +func writeFloatProp(b *[]byte, n string, f float64) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf("%f", f))) +} +func writeTimeProp(b *[]byte, n string, t time.Time) (notEmpty bool) { + if v, err := t.MarshalJSON(); err == nil { + return writeProp(b, n, v) + } + return false +} + +func writeDurationProp(b *[]byte, n string, d time.Duration) (notEmpty bool) { + if v, err := marshalXSD(d); err == nil { + return writeProp(b, n, v) + } + return false +} + +func writeIRIProp(b *[]byte, n string, i LinkOrIRI) (notEmpty bool) { + url := i.GetLink().String() + if len(url) == 0 { + return false + } + writeStringProp(b, n, url) + return true +} + +func writeItemProp(b *[]byte, n string, i Item) (notEmpty bool) { + if i == nil { + return notEmpty + } + if im, ok := i.(json.Marshaler); ok { + v, err := im.MarshalJSON() + if err != nil { + return false + } + return writeProp(b, n, v) + } + return notEmpty +} + +func writeString(b *[]byte, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + write(b, '"') + writeS(b, s) + write(b, '"') + return true +} + +func writeItemCollection(b *[]byte, col ItemCollection) (notEmpty bool) { + if len(col) == 0 { + return notEmpty + } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + write(b, ',') + } + } + write(b, '[') + for i, it := range col { + if im, ok := it.(json.Marshaler); ok { + v, err := im.MarshalJSON() + if err != nil { + return false + } + writeCommaIfNotEmpty(i > 0) + write(b, v...) + } + } + write(b, ']') + return true +} +func writeItemCollectionProp(b *[]byte, n string, col ItemCollection) (notEmpty bool) { + if len(col) == 0 { + return notEmpty + } + writeComma(b) + success := writePropName(b, n) && writeItemCollection(b, col) + if !success { + *b = (*b)[:len(*b)-1] + } + return success +} + +func writeObject(b *[]byte, o Object) (notEmpty bool) { + 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 { + notEmpty = writeProp(b, "type", v) || notEmpty + } + if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(b, "mediaType", v) || notEmpty + } + if len(o.Name) > 0 { + notEmpty = writeNaturalLanguageProp(b, "name", o.Name) || notEmpty + } + if len(o.Summary) > 0 { + notEmpty = writeNaturalLanguageProp(b, "summary", o.Summary) || notEmpty + } + if len(o.Content) > 0 { + notEmpty = writeNaturalLanguageProp(b, "content", o.Content) || notEmpty + } + if o.Attachment != nil { + notEmpty = writeItemProp(b, "attachment", o.Attachment) || notEmpty + } + if o.AttributedTo != nil { + notEmpty = writeItemProp(b, "attributedTo", o.AttributedTo) || notEmpty + } + if o.Audience != nil { + notEmpty = writeItemProp(b, "audience", o.Audience) || notEmpty + } + if o.Context != nil { + notEmpty = writeItemProp(b, "context", o.Context) || notEmpty + } + if o.Generator != nil { + notEmpty = writeItemProp(b, "generator", o.Generator) || notEmpty + } + if o.Icon != nil { + notEmpty = writeItemProp(b, "icon", o.Icon) || notEmpty + } + if o.Image != nil { + notEmpty = writeItemProp(b, "image", o.Image) || notEmpty + } + if o.InReplyTo != nil { + notEmpty = writeItemProp(b, "inReplyTo", o.InReplyTo) || notEmpty + } + if o.Location != nil { + notEmpty = writeItemProp(b, "location", o.Location) || notEmpty + } + if o.Preview != nil { + notEmpty = writeItemProp(b, "preview", o.Preview) || notEmpty + } + if o.Replies != nil { + notEmpty = writeItemProp(b, "replies", o.Replies) || notEmpty + } + if o.Tag != nil { + notEmpty = writeItemProp(b, "tag", o.Tag) || notEmpty + } + if o.URL != nil { + notEmpty = writeIRIProp(b, "url", o.URL) || notEmpty + } + if o.To != nil { + notEmpty = writeItemProp(b, "to", o.To) || notEmpty + } + if o.Bto != nil { + notEmpty = writeItemProp(b, "bto", o.Bto) || notEmpty + } + if o.CC != nil { + notEmpty = writeItemProp(b, "cc", o.CC) || notEmpty + } + if o.BCC != nil { + notEmpty = writeItemProp(b, "bcc", o.BCC) || notEmpty + } + if !o.Published.IsZero() { + notEmpty = writeTimeProp(b, "published", o.Published) || notEmpty + } + if !o.Updated.IsZero() { + notEmpty = writeTimeProp(b, "updated", o.Updated) || notEmpty + } + if !o.StartTime.IsZero() { + notEmpty = writeTimeProp(b, "startTime", o.StartTime) || notEmpty + } + if !o.EndTime.IsZero() { + 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) + notEmpty = writeDurationProp(b, "duration", o.Duration) || notEmpty + } + if o.Likes != nil { + notEmpty = writeItemProp(b, "likes", o.Likes) || notEmpty + } + if o.Shares != nil { + notEmpty = writeItemProp(b, "shares", o.Shares) || notEmpty + } + if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(b, "source", v) || notEmpty + } + return notEmpty +} + +func writeActivity(b *[]byte, a Activity) (notEmpty bool) { + OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = writeIntransitiveActivity(b, *i) || notEmpty + return nil + }) + if a.Object != nil { + notEmpty = writeItemProp(b, "object", a.Object) || notEmpty + } + return notEmpty +} + +func writeIntransitiveActivity(b *[]byte, i IntransitiveActivity) (notEmpty bool) { + OnObject(i, func(o *Object) error { + if o == nil { + return nil + } + notEmpty = writeObject(b, *o) || notEmpty + return nil + }) + if i.Actor != nil { + notEmpty = writeItemProp(b, "actor", i.Actor) || notEmpty + } + if i.Target != nil { + notEmpty = writeItemProp(b, "target", i.Target) || notEmpty + } + if i.Result != nil { + notEmpty = writeItemProp(b, "result", i.Result) || notEmpty + } + if i.Origin != nil { + notEmpty = writeItemProp(b, "origin", i.Origin) || notEmpty + } + if i.Instrument != nil { + notEmpty = writeItemProp(b, "instrument", i.Instrument) || notEmpty + } + return notEmpty +} + +func writeQuestion(b *[]byte, q Question) (notEmpty bool) { + OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = writeIntransitiveActivity(b, *i) || notEmpty + return nil + }) + if q.OneOf != nil { + notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty + } else if q.AnyOf != nil { + notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty + } + notEmpty = writeBoolProp(b, "closed", q.Closed) || notEmpty + return notEmpty +} diff --git a/followers.go b/followers.go deleted file mode 100644 index 425c37b..0000000 --- a/followers.go +++ /dev/null @@ -1,9 +0,0 @@ -package activitypub - -type ( - // FollowersCollection is a collection of followers - FollowersCollection = Followers - - // Followers is a Collection type - Followers = Collection -) diff --git a/followers_test.go b/followers_test.go deleted file mode 100644 index faeee6c..0000000 --- a/followers_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package activitypub - -import "testing" - -func TestFollowersNew(t *testing.T) { - t.Skipf("TODO") -} diff --git a/following.go b/following.go deleted file mode 100644 index 82c1d7a..0000000 --- a/following.go +++ /dev/null @@ -1,25 +0,0 @@ -package activitypub - -type ( - // FollowingCollection is a list of everybody that the actor has followed, added as a side effect. - // The following collection MUST be either an OrderedCollection or a Collection and MAY - // be filtered on privileges of an authenticated user or as appropriate when no authentication is given. - FollowingCollection = Following - - // Following is a type alias for a simple Collection - Following = Collection -) - -// FollowingNew initializes a new Following -func FollowingNew() *Following { - id := ID("following") - - i := Following{ID: id, Type: CollectionType} - i.Name = NaturalLanguageValuesNew() - i.Content = NaturalLanguageValuesNew() - i.Summary = NaturalLanguageValuesNew() - - i.TotalItems = 0 - - return &i -} diff --git a/following_test.go b/following_test.go deleted file mode 100644 index 55a333c..0000000 --- a/following_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package activitypub - -import "testing" - -func TestFollowingNew(t *testing.T) { - t.Skipf("TODO") -} diff --git a/inbox.go b/inbox.go deleted file mode 100644 index 70e481e..0000000 --- a/inbox.go +++ /dev/null @@ -1,26 +0,0 @@ -package activitypub - -type ( - // InboxStream contains all activities received by the actor. - // The server SHOULD filter content according to the requester's permission. - // In general, the owner of an inbox is likely to be able to access all of their inbox contents. - // Depending on access control, some other content may be public, whereas other content may - // require authentication for non-owner users, if they can access the inbox at all. - InboxStream = Inbox - - // Inbox is a type alias for an Ordered Collection - Inbox = OrderedCollection -) - -// InboxNew initializes a new Inbox -func InboxNew() *OrderedCollection { - id := ID("inbox") - - i := OrderedCollection{ID: id, Type: CollectionType} - i.Name = NaturalLanguageValuesNew() - i.Content = NaturalLanguageValuesNew() - - i.TotalItems = 0 - - return &i -} diff --git a/inbox_test.go b/inbox_test.go deleted file mode 100644 index 5257260..0000000 --- a/inbox_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package activitypub - -import ( - "testing" -) - -func TestInboxNew(t *testing.T) { - i := InboxNew() - - id := ID("inbox") - if i.ID != id { - t.Errorf("%T should be initialized with %q as %T", i, id, id) - } - if len(i.Name) != 0 { - t.Errorf("%T should be initialized with 0 length Name", i) - } - if len(i.Content) != 0 { - t.Errorf("%T should be initialized with 0 length Content", i) - } - if len(i.Summary) != 0 { - t.Errorf("%T should be initialized with 0 length Summary", i) - } - if i.TotalItems != 0 { - t.Errorf("%T should be initialized with 0 TotalItems", i) - } -} 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/iri.go b/iri.go index 3acd9e3..36aa8a8 100644 --- a/iri.go +++ b/iri.go @@ -47,6 +47,15 @@ func (i *IRI) UnmarshalJSON(s []byte) error { return nil } +// MarshalJSON +func (i IRI) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + write(&b, '"') + writeS(&b, i.String()) + write(&b, '"') + return b, nil +} + // GetID func (i IRI) GetID() ID { return ID(i) @@ -80,6 +89,29 @@ func FlattenToIRI(it Item) Item { return it } +func (i IRIs) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + if len(i) == 0 { + return nil, nil + } + notEmpty := false + writeComma := func() { writeS(&b, ",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + write(&b, '[') + for _, iri := range i { + writeCommaIfNotEmpty(notEmpty) + write(&b, '"') + writeS(&b, iri.String()) + write(&b, '"') + } + write(&b, ']') + return b, nil +} + // Contains verifies if IRIs array contains the received one func (i IRIs) Contains(r IRI) bool { if len(i) == 0 { diff --git a/item_collection.go b/item_collection.go index f378825..362f672 100644 --- a/item_collection.go +++ b/item_collection.go @@ -33,6 +33,12 @@ func (i ItemCollection) IsObject() bool { return false } +func (i ItemCollection) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + writeItemCollection(&b, i) + return b, nil +} + // Append facilitates adding elements to Item arrays // and ensures ItemCollection implements the Collection interface func (i *ItemCollection) Append(o Item) error { diff --git a/liked.go b/liked.go deleted file mode 100644 index dab8fa9..0000000 --- a/liked.go +++ /dev/null @@ -1,25 +0,0 @@ -package activitypub - -type ( - // LikedCollection is a list of every object from all of the actor's Like activities, - // added as a side effect. The liked collection MUST be either an OrderedCollection or - // a Collection and MAY be filtered on privileges of an authenticated user or as - // appropriate when no authentication is given. - LikedCollection = Liked - - // Liked is a type alias for an Ordered Collection - Liked = OrderedCollection -) - -// LikedCollection initializes a new Outbox -func LikedNew() *OrderedCollection { - id := ID("liked") - - l := OrderedCollection{ID: id, Type: CollectionType} - l.Name = NaturalLanguageValuesNew() - l.Content = NaturalLanguageValuesNew() - - l.TotalItems = 0 - - return &l -} diff --git a/liked_test.go b/liked_test.go deleted file mode 100644 index 313b0a3..0000000 --- a/liked_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package activitypub - -import ( - "testing" -) - -func TestLikedNew(t *testing.T) { - l := LikedNew() - - id := ID("liked") - if l.ID != id { - t.Errorf("%T should be initialized with %q as %T", l, id, id) - } - if len(l.Name) != 0 { - t.Errorf("%T should be initialized with 0 length Name", l) - } - if len(l.Content) != 0 { - t.Errorf("%T should be initialized with 0 length Content", l) - } - if len(l.Summary) != 0 { - t.Errorf("%T should be initialized with 0 length Summary", l) - } - if l.TotalItems != 0 { - t.Errorf("%T should be initialized with 0 TotalItems", l) - } -} diff --git a/likes.go b/likes.go deleted file mode 100644 index 0baa141..0000000 --- a/likes.go +++ /dev/null @@ -1,25 +0,0 @@ -package activitypub - -type ( - // LikesCollection is a list of all Like activities with this object as the object property, - // added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection - // and MAY be filtered on privileges of an authenticated user or as appropriate when - // no authentication is given. - LikesCollection = Likes - - // Likes is a type alias for an Ordered Collection - Likes = OrderedCollection -) - -// LikesCollection initializes a new Outbox -func LikesNew() *Likes { - id := ID("likes") - - l := Likes{ID: id, Type: CollectionType} - l.Name = NaturalLanguageValuesNew() - l.Content = NaturalLanguageValuesNew() - - l.TotalItems = 0 - - return &l -} diff --git a/likes_test.go b/likes_test.go deleted file mode 100644 index d030dc8..0000000 --- a/likes_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package activitypub - -import "testing" - -func TestLikesNew(t *testing.T) { - t.Skipf("TODO") -} diff --git a/natural_language_values.go b/natural_language_values.go index 661e7d8..52b06a5 100644 --- a/natural_language_values.go +++ b/natural_language_values.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "github.com/buger/jsonparser" - json "github.com/go-ap/jsonld" "strconv" "strings" ) @@ -73,21 +72,47 @@ func (n *NaturalLanguageValues) Set(ref LangRef, v string) error { // MarshalJSON serializes the NaturalLanguageValues into JSON func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) { - if len(n) == 0 { - return json.Marshal(nil) + l := len(n) + if l <= 0 { + return nil, nil } - if len(n) == 1 { + + b := bytes.Buffer{} + if l == 1 { v := n[0] - if v.Ref == NilLangRef { - return v.MarshalJSON() + if len(v.Value) > 0 { + v.Value = string(unescape([]byte(v.Value))) + ll, err := b.WriteString(strconv.Quote(v.Value)) + if err != nil { + return nil, err + } + if ll <= 0 { + return nil, nil + } + return b.Bytes(), nil } } - mm := make(map[LangRef]string) + b.Write([]byte{'{'}) + empty := true for _, val := range n { - mm[val.Ref] = val.Value + if len(val.Ref) == 0 || len(val.Value) == 0 { + continue + } + if !empty { + b.Write([]byte{','}) + } + if v, err := val.MarshalJSON(); err == nil && len(v) > 0 { + l, err := b.Write(v) + if err == nil && l > 0 { + empty = false + } + } } - - return json.Marshal(mm) + b.Write([]byte{'}'}) + if !empty { + return b.Bytes(), nil + } + return nil, nil } // First returns the first element in the array @@ -165,7 +190,7 @@ func (l *LangRefValue) UnmarshalText(data []byte) error { // MarshalJSON serializes the LangRefValue into JSON func (l LangRefValue) MarshalJSON() ([]byte, error) { buf := bytes.Buffer{} - if l.Ref != NilLangRef { + if l.Ref != NilLangRef && len(l.Ref) > 0{ if l.Value == "" { return nil, nil } diff --git a/natural_language_values_test.go b/natural_language_values_test.go index 2690c66..488a832 100644 --- a/natural_language_values_test.go +++ b/natural_language_values_test.go @@ -36,7 +36,7 @@ func TestNaturalLanguageValue_MarshalJSON(t *testing.T) { if err1 != nil { t.Errorf("Error: '%s'", err1) } - txt := `{"en":"the test"}` + txt := `"the test"` if txt != string(out1) { t.Errorf("Different marshal result '%s', instead of '%s'", out1, txt) } @@ -291,7 +291,7 @@ func TestNaturalLanguageValues_MarshalJSON(t *testing.T) { if err != nil { t.Errorf("Failed marshaling '%v'", err) } - mRes := "{\"de\":\"test\",\"en\":\"test\"}" + mRes := "{\"en\":\"test\",\"de\":\"test\"}" if string(result) != mRes { t.Errorf("Different results '%v' vs. '%v'", string(result), mRes) } @@ -307,7 +307,7 @@ func TestNaturalLanguageValues_MarshalJSON(t *testing.T) { if err1 != nil { t.Errorf("Failed marshaling '%v'", err1) } - mRes1 := `{"en":"test"}` + mRes1 := `"test"` if string(result1) != mRes1 { t.Errorf("Different results '%v' vs. '%v'", string(result1), mRes1) } @@ -343,7 +343,7 @@ func TestNaturalLanguageValues_MarshalJSON(t *testing.T) { if j == nil { t.Errorf("Error marshaling: nil value returned") } - expected := fmt.Sprintf("{\"%s\":\"%s\"}", nlv.Ref, nlv.Value) + expected := fmt.Sprintf("\"%s\"", nlv.Value) if string(j) != expected { t.Errorf("Wrong value: %s, expected %s", j, expected) } diff --git a/object.go b/object.go index 7d1478b..9747fa2 100644 --- a/object.go +++ b/object.go @@ -106,6 +106,15 @@ type ( MimeType string ) +func (a ActivityVocabularyType) MarshalJSON() ([]byte, error) { + if len(a) == 0 { + return nil, nil + } + b := make([]byte, 0) + writeString(&b, string(a)) + return b, nil +} + // Object describes an ActivityPub object of any kind. // It serves as the base type for most of the other kinds of objects defined in the Activity // Vocabulary, including other Core types such as Activity, IntransitiveActivity, Collection and OrderedCollection. @@ -308,6 +317,21 @@ func (o *Object) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (o Object) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + notEmpty = writeObject(&b, o) + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // Recipients performs recipient de-duplication on the Object's To, Bto, CC and BCC properties func (o *Object) Recipients() ItemCollection { var aud ItemCollection @@ -346,6 +370,16 @@ func (c *MimeType) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (m MimeType) MarshalJSON() ([]byte, error) { + if len(m) == 0 { + return nil, nil + } + b := make([]byte, 0) + writeString(&b, string(m)) + return b, nil +} + // ToObject returns an Object pointer to the data in the current Item // It relies on the fact that all the types in this package have a data layout compatible with Object. func ToObject(it Item) (*Object, error) { @@ -464,3 +498,23 @@ func (s *Source) UnmarshalJSON(data []byte) error { *s = GetAPSource(data) return nil } + +// MarshalJSON +func (s Source) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + empty := true + write(&b, '{') + if len(s.MediaType) > 0 { + if v, err := s.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + empty = !writeProp(&b, "mediaType", v) + } + } + if len(s.Content) > 0 { + empty = !writeNaturalLanguageProp(&b, "content", s.Content) + } + if !empty { + write(&b, '}') + return b, nil + } + return nil, nil +} diff --git a/object_id.go b/object_id.go index 26588a7..bd97ce8 100644 --- a/object_id.go +++ b/object_id.go @@ -1,6 +1,9 @@ package activitypub -import "strings" +import ( + "bytes" + "strings" +) // ID designates an unique global identifier. // All Objects in [ActivityStreams] should have unique global identifiers. @@ -22,6 +25,18 @@ func (i *ID) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (i ID) MarshalJSON() ([]byte, error) { + if len(i) == 0 { + return nil, nil + } + b := bytes.Buffer{} + b.Write([]byte{'"'}) + b.WriteString(string(i)) + b.Write([]byte{'"'}) + return b.Bytes(), nil +} + func (i *ID) IsValid() bool { return i != nil && len(*i) > 0 } diff --git a/object_id_test.go b/object_id_test.go index a2001dd..45bc35d 100644 --- a/object_id_test.go +++ b/object_id_test.go @@ -11,3 +11,11 @@ func TestID_UnmarshalJSON(t *testing.T) { t.Errorf("Unmarshaled object %T should be an empty string, received %q", o, o) } } + +func TestID_IsValid(t *testing.T) { + t.Skip("TODO") +} + +func TestID_MarshalJSON(t *testing.T) { + t.Skip("TODO") +} diff --git a/object_test.go b/object_test.go index a0cb234..d846d1c 100644 --- a/object_test.go +++ b/object_test.go @@ -3,6 +3,7 @@ package activitypub import ( "reflect" "testing" + "time" ) func TestObjectNew(t *testing.T) { @@ -488,3 +489,317 @@ func TestGetAPSource(t *testing.T) { t.Errorf("Content didn't match test value. Received %q, expecting %q", a.MediaType, "text/plain") } } + +func TestObject_Clean(t *testing.T) { + t.Skip("TODO") +} + +func TestObject_IsCollection(t *testing.T) { + t.Skip("TODO") +} + +func TestActivityVocabularyType_MarshalJSON(t *testing.T) { + t.Skip("TODO") +} + +func TestObject_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 + } + 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) { + o := Object{ + 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, + } + got, err := o.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 TestSource_MarshalJSON(t *testing.T) { + type fields struct { + Content NaturalLanguageValues + MediaType MimeType + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Empty", + fields: fields{}, + want: nil, + wantErr: false, + }, + { + name: "MediaType", + fields: fields{ + MediaType: MimeType("blank"), + }, + want: []byte(`{"mediaType":"blank"}`), + wantErr: false, + }, + { + name: "OneContentValue", + fields: fields{ + Content: NaturalLanguageValues{ + {Value: "test"}, + }, + }, + want: []byte(`{"content":"test"}`), + wantErr: false, + }, + { + name: "MultipleContentValues", + fields: fields{ + Content: NaturalLanguageValues{ + { + Ref: "en", + Value: "test", + }, + { + Ref: "fr", + Value: "teste", + }, + }, + }, + want: []byte(`{"contentMap":{"en":"test","fr":"teste"}}`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Source{ + Content: tt.fields.Content, + MediaType: tt.fields.MediaType, + } + got, err := s.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/ordered_collection.go b/ordered_collection.go index 615e3dc..20a4d5e 100644 --- a/ordered_collection.go +++ b/ordered_collection.go @@ -114,6 +114,53 @@ type OrderedCollection struct { OrderedItems ItemCollection `jsonld:"orderedItems,omitempty"` } +type ( + // InboxStream contains all activities received by the actor. + // The server SHOULD filter content according to the requester's permission. + // In general, the owner of an inbox is likely to be able to access all of their inbox contents. + // Depending on access control, some other content may be public, whereas other content may + // require authentication for non-owner users, if they can access the inbox at all. + InboxStream = Inbox + + // Inbox is a type alias for an Ordered Collection + Inbox = OrderedCollection + + // LikedCollection is a list of every object from all of the actor's Like activities, + // added as a side effect. The liked collection MUST be either an OrderedCollection or + // a Collection and MAY be filtered on privileges of an authenticated user or as + // appropriate when no authentication is given. + LikedCollection = Liked + + // Liked is a type alias for an Ordered Collection + Liked = OrderedCollection + + // LikesCollection is a list of all Like activities with this object as the object property, + // added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection + // and MAY be filtered on privileges of an authenticated user or as appropriate when + // no authentication is given. + LikesCollection = Likes + + // Likes is a type alias for an Ordered Collection + Likes = OrderedCollection + + // OutboxStream contains activities the user has published, + // subject to the ability of the requestor to retrieve the activity (that is, + // the contents of the outbox are filtered by the permissions of the person reading it). + OutboxStream = Outbox + + // Outbox is a type alias for an Ordered Collection + Outbox = OrderedCollection + + // SharesCollection is a list of all Announce activities with this object as the object property, + // added as a side effect. The shares collection MUST be either an OrderedCollection or a Collection + // and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication + // is given. + SharesCollection = Shares + + // Shares is a type alias for an Ordered Collection + Shares = OrderedCollection +) + // GetType returns the OrderedCollection's type func (o OrderedCollection) GetType() ActivityVocabularyType { return o.Type @@ -244,6 +291,37 @@ func (o *OrderedCollection) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (o OrderedCollection) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(o, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if o.Current != nil { + notEmpty = writeItemProp(&b, "current", o.Current) || notEmpty + } + if o.First != nil { + notEmpty = writeItemProp(&b, "first", o.First) || notEmpty + } + if o.Last != nil { + notEmpty = writeItemProp(&b, "last", o.Last) || notEmpty + } + if o.OrderedItems != nil { + notEmpty = writeItemCollectionProp(&b, "orderedItems", o.OrderedItems) || notEmpty + } + notEmpty = writeIntProp(&b, "totalItems", int64(o.TotalItems)) || notEmpty + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // OrderedCollectionPageNew initializes a new OrderedCollectionPage func OrderedCollectionPageNew(parent CollectionInterface) *OrderedCollectionPage { p := OrderedCollectionPage{ @@ -306,3 +384,68 @@ func copyOrderedCollectionToPage(c *OrderedCollection, p *OrderedCollectionPage) p.PartOf = c.GetLink() return nil } + +// InboxNew initializes a new Inbox +func InboxNew() *OrderedCollection { + id := ID("inbox") + + i := OrderedCollection{ID: id, Type: CollectionType} + i.Name = NaturalLanguageValuesNew() + i.Content = NaturalLanguageValuesNew() + + i.TotalItems = 0 + + return &i +} + +// LikedCollection initializes a new Outbox +func LikedNew() *OrderedCollection { + id := ID("liked") + + l := OrderedCollection{ID: id, Type: CollectionType} + l.Name = NaturalLanguageValuesNew() + l.Content = NaturalLanguageValuesNew() + + l.TotalItems = 0 + + return &l +} + +// LikesCollection initializes a new Outbox +func LikesNew() *Likes { + id := ID("likes") + + l := Likes{ID: id, Type: CollectionType} + l.Name = NaturalLanguageValuesNew() + l.Content = NaturalLanguageValuesNew() + + l.TotalItems = 0 + + return &l +} + +// OutboxNew initializes a new Outbox +func OutboxNew() *Outbox { + id := ID("outbox") + + i := Outbox{ID: id, Type: OrderedCollectionType} + i.Name = NaturalLanguageValuesNew() + i.Content = NaturalLanguageValuesNew() + i.TotalItems = 0 + i.OrderedItems = make(ItemCollection, 0) + + return &i +} + +// SharesNew initializes a new Shares +func SharesNew() *Shares { + id := ID("Shares") + + i := Shares{ID: id, Type: CollectionType} + i.Name = NaturalLanguageValuesNew() + i.Content = NaturalLanguageValuesNew() + + i.TotalItems = 0 + + return &i +} diff --git a/ordered_collection_page.go b/ordered_collection_page.go index df868a6..dac2b05 100644 --- a/ordered_collection_page.go +++ b/ordered_collection_page.go @@ -260,6 +260,45 @@ func (o *OrderedCollectionPage) UnmarshalJSON(data []byte) error { } return nil } +// MarshalJSON +func (c OrderedCollectionPage) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(c, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if c.Current != nil { + notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + notEmpty = writeItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty + } + if c.OrderedItems != nil { + notEmpty = writeItemCollectionProp(&b, "orderedItems", c.OrderedItems) || notEmpty + } + if c.PartOf != nil { + notEmpty = writeItemProp(&b, "partOf", c.PartOf) || notEmpty + } + if c.Next != nil { + notEmpty = writeItemProp(&b, "next", c.Next) || notEmpty + } + if c.Prev != nil { + notEmpty = writeItemProp(&b, "prev", c.Prev) || notEmpty + } + notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} // ToOrderedCollectionPage func ToOrderedCollectionPage(it Item) (*OrderedCollectionPage, error) { diff --git a/ordered_collection_test.go b/ordered_collection_test.go index bc61b27..8f3135c 100644 --- a/ordered_collection_test.go +++ b/ordered_collection_test.go @@ -215,3 +215,74 @@ func TestToOrderedCollection(t *testing.T) { func TestOrderedCollection_Contains(t *testing.T) { t.Skipf("TODO") } + +func TestInboxNew(t *testing.T) { + i := InboxNew() + + id := ID("inbox") + if i.ID != id { + t.Errorf("%T should be initialized with %q as %T", i, id, id) + } + if len(i.Name) != 0 { + t.Errorf("%T should be initialized with 0 length Name", i) + } + if len(i.Content) != 0 { + t.Errorf("%T should be initialized with 0 length Content", i) + } + if len(i.Summary) != 0 { + t.Errorf("%T should be initialized with 0 length Summary", i) + } + if i.TotalItems != 0 { + t.Errorf("%T should be initialized with 0 TotalItems", i) + } +} + +func TestLikedNew(t *testing.T) { + l := LikedNew() + + id := ID("liked") + if l.ID != id { + t.Errorf("%T should be initialized with %q as %T", l, id, id) + } + if len(l.Name) != 0 { + t.Errorf("%T should be initialized with 0 length Name", l) + } + if len(l.Content) != 0 { + t.Errorf("%T should be initialized with 0 length Content", l) + } + if len(l.Summary) != 0 { + t.Errorf("%T should be initialized with 0 length Summary", l) + } + if l.TotalItems != 0 { + t.Errorf("%T should be initialized with 0 TotalItems", l) + } +} + +func TestLikesNew(t *testing.T) { + t.Skipf("TODO") +} + +func TestOutboxNew(t *testing.T) { + o := OutboxNew() + + id := ID("outbox") + if o.ID != id { + t.Errorf("%T should be initialized with %q as %T", o, id, id) + } + if len(o.Name) != 0 { + t.Errorf("%T should be initialized with 0 length Name", o) + } + if len(o.Content) != 0 { + t.Errorf("%T should be initialized with 0 length Content", o) + } + if len(o.Summary) != 0 { + t.Errorf("%T should be initialized with 0 length Summary", o) + } + if o.TotalItems != 0 { + t.Errorf("%T should be initialized with 0 TotalItems", o) + } +} + +func TestSharesNew(t *testing.T) { + t.Skipf("TODO") +} diff --git a/outbox.go b/outbox.go deleted file mode 100644 index 4de90a3..0000000 --- a/outbox.go +++ /dev/null @@ -1,24 +0,0 @@ -package activitypub - -type ( - // OutboxStream contains activities the user has published, - // subject to the ability of the requestor to retrieve the activity (that is, - // the contents of the outbox are filtered by the permissions of the person reading it). - OutboxStream = Outbox - - // Outbox is a type alias for an Ordered Collection - Outbox = OrderedCollection -) - -// OutboxNew initializes a new Outbox -func OutboxNew() *Outbox { - id := ID("outbox") - - i := Outbox{ID: id, Type: OrderedCollectionType} - i.Name = NaturalLanguageValuesNew() - i.Content = NaturalLanguageValuesNew() - i.TotalItems = 0 - i.OrderedItems = make(ItemCollection, 0) - - return &i -} diff --git a/outbox_test.go b/outbox_test.go deleted file mode 100644 index 32a93fa..0000000 --- a/outbox_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package activitypub - -import ( - "reflect" - "testing" -) - -func TestOutboxNew(t *testing.T) { - o := OutboxNew() - - id := ID("outbox") - if o.ID != id { - t.Errorf("%T should be initialized with %q as %T", o, id, id) - } - if len(o.Name) != 0 { - t.Errorf("%T should be initialized with 0 length Name", o) - } - if len(o.Content) != 0 { - t.Errorf("%T should be initialized with 0 length Content", o) - } - if len(o.Summary) != 0 { - t.Errorf("%T should be initialized with 0 length Summary", o) - } - if o.TotalItems != 0 { - t.Errorf("%T should be initialized with 0 TotalItems", o) - } -} - -func TestOutboxStream_GetID(t *testing.T) { - o := OutboxStream{} - if o.GetID() != "" { - t.Errorf("%T should be initialized with empty %T", o, o.GetID()) - } - id := ID("test_out_stream") - o.ID = id - if o.GetID() != id { - t.Errorf("%T should have %T as %q", o, id, id) - } -} - -func TestOutboxStream_GetType(t *testing.T) { - o := OutboxStream{} - - if o.GetType() != "" { - t.Errorf("%T should be initialized with empty %T", o, o.GetType()) - } - - o.Type = OrderedCollectionType - if o.GetType() != OrderedCollectionType { - t.Errorf("%T should have %T as %q", o, o.GetType(), OrderedCollectionType) - } -} - -func TestOutboxStream_Append(t *testing.T) { - o := OutboxStream{} - - val := Object{ID: ID("grrr")} - - o.Append(val) - if !reflect.DeepEqual(o.OrderedItems[0], val) { - t.Errorf("First item in %T.%T does not match %q", o, o.OrderedItems, val.ID) - } -} - -func TestOutbox_Append(t *testing.T) { - o := OutboxNew() - - val := Object{ID: ID("grrr")} - - o.Append(val) - if !reflect.DeepEqual(o.OrderedItems[0], val) { - t.Errorf("First item in %T.%T does not match %q", o, o.OrderedItems, val.ID) - } -} - -func TestOutbox_Collection(t *testing.T) { - t.Skipf("TODO") -} - -func TestOutbox_GetID(t *testing.T) { - t.Skipf("TODO") -} - -func TestOutbox_GetLink(t *testing.T) { - t.Skipf("TODO") -} - -func TestOutbox_GetType(t *testing.T) { - t.Skipf("TODO") -} - -func TestOutbox_IsLink(t *testing.T) { - t.Skipf("TODO") -} - -func TestOutbox_IsObject(t *testing.T) { - t.Skipf("TODO") -} - -func TestOutbox_UnmarshalJSON(t *testing.T) { - t.Skipf("TODO") -} diff --git a/place.go b/place.go index dbb00ac..714ff27 100644 --- a/place.go +++ b/place.go @@ -102,22 +102,22 @@ type Place struct { Source Source `jsonld:"source,omitempty"` // Accuracy indicates the accuracy of position coordinates on a Place objects. // Expressed in properties of percentage. e.g. "94.0" means "94.0% accurate". - Accuracy float64 + Accuracy float64 `jsonld:"accuracy,omitempty"` // Altitude indicates the altitude of a place. The measurement units is indicated using the units property. // If units is not specified, the default is assumed to be "m" indicating meters. - Altitude float64 + Altitude float64 `jsonld:"altitude,omitempty"` // Latitude the latitude of a place - Latitude float64 + Latitude float64 `jsonld:"latitude,omitempty"` // Longitude the longitude of a place - Longitude float64 + Longitude float64 `jsonld:"longitude,omitempty"` // Radius the radius from the given latitude and longitude for a Place. // The units is expressed by the units property. If units is not specified, // the default is assumed to be "m" indicating "meters". - Radius int64 + Radius int64 `jsonld:"radius,omitempty"` // Specifies the measurement units for the radius and altitude properties on a Place object. // If not specified, the default is assumed to be "m" for "meters". // Values "cm" | " feet" | " inches" | " km" | " m" | " miles" | xsd:anyURI - Units string + Units string `jsonld:"units,omitempty"` } // IsLink returns false for Place objects @@ -218,6 +218,41 @@ func (p *Place) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (p Place) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(p, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if p.Accuracy > 0 { + notEmpty = writeFloatProp(&b, "accuracy", p.Accuracy) || notEmpty + } + if p.Altitude > 0 { + notEmpty = writeFloatProp(&b, "altitude", p.Altitude) || notEmpty + } + if p.Latitude > 0 { + notEmpty = writeFloatProp(&b, "latitude", p.Latitude) || notEmpty + } + if p.Longitude > 0 { + notEmpty = writeFloatProp(&b, "longitude", p.Longitude) || notEmpty + } + if p.Radius > 0 { + notEmpty = writeIntProp(&b, "radius", p.Radius) || notEmpty + } + if len(p.Units) > 0 { + notEmpty = writeStringProp(&b, "radius", p.Units) || notEmpty + } + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // Recipients performs recipient de-duplication on the Place object's To, Bto, CC and BCC properties func (p *Place) Recipients() ItemCollection { var aud ItemCollection @@ -226,7 +261,7 @@ func (p *Place) Recipients() ItemCollection { } // Clean removes Bto and BCC properties -func (p *Place) Clean(){ +func (p *Place) Clean() { p.BCC = nil p.Bto = nil } diff --git a/profile.go b/profile.go index 5032476..b6cb1a1 100644 --- a/profile.go +++ b/profile.go @@ -199,6 +199,27 @@ func (p *Profile) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (p Profile) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(p, func(o *Object) error { + return nil + }) + + if p.Describes != nil { + notEmpty = writeItemProp(&b, "describes", p.Describes) || notEmpty + } + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // Recipients performs recipient de-duplication on the Profile object's To, Bto, CC and BCC properties func (p *Profile) Recipients() ItemCollection { var aud ItemCollection diff --git a/question.go b/question.go index c151bc5..6286d33 100644 --- a/question.go +++ b/question.go @@ -229,6 +229,17 @@ func (q *Question) UnmarshalJSON(data []byte) error { return nil } +func (q Question) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + write(&b, '{') + + if !writeQuestion(&b, q) { + return nil, nil + } + write(&b, '}') + return b, nil +} + // QuestionNew initializes a Question activity func QuestionNew(id ID) *Question { q := Question{ID: id, Type: QuestionType} diff --git a/relationship.go b/relationship.go index 8c7c905..99ad231 100644 --- a/relationship.go +++ b/relationship.go @@ -108,12 +108,12 @@ type Relationship struct { Source Source `jsonld:"source,omitempty"` // Subject Subject On a Relationship object, the subject property identifies one of the connected individuals. // For instance, for a Relationship object describing "John is related to Sally", subject would refer to John. - Subject Item + Subject Item `jsonld:"subject,omitempty"` // Object - Object Item + Object Item `jsonld:"object,omitempty"` // Relationship On a Relationship object, the relationship property identifies the kind // of relationship that exists between subject and object. - Relationship Item + Relationship Item `jsonld:"relationship,omitempty"` } // IsLink returns false for Relationship objects @@ -211,6 +211,34 @@ func (r *Relationship) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (r Relationship) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(r, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + + if r.Subject != nil { + notEmpty = writeItemProp(&b, "subject", r.Subject) || notEmpty + } + if r.Object != nil { + notEmpty = writeItemProp(&b, "object", r.Object) || notEmpty + } + if r.Relationship != nil { + notEmpty = writeItemProp(&b, "relationship", r.Relationship) || notEmpty + } + + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // Recipients performs recipient de-duplication on the Relationship object's To, Bto, CC and BCC properties func (r *Relationship) Recipients() ItemCollection { var aud ItemCollection @@ -219,12 +247,11 @@ func (r *Relationship) Recipients() ItemCollection { } // Clean removes Bto and BCC properties -func (r *Relationship) Clean(){ +func (r *Relationship) Clean() { r.BCC = nil r.Bto = nil } - // ToRelationship func ToRelationship(it Item) (*Relationship, error) { switch i := it.(type) { diff --git a/shares.go b/shares.go deleted file mode 100644 index 03a7dac..0000000 --- a/shares.go +++ /dev/null @@ -1,25 +0,0 @@ -package activitypub - -type ( - // SharesCollection is a list of all Announce activities with this object as the object property, - // added as a side effect. The shares collection MUST be either an OrderedCollection or a Collection - // and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication - // is given. - SharesCollection = Shares - - // Shares is a type alias for an Ordered Collection - Shares = OrderedCollection -) - -// SharesNew initializes a new Shares -func SharesNew() *Shares { - id := ID("Shares") - - i := Shares{ID: id, Type: CollectionType} - i.Name = NaturalLanguageValuesNew() - i.Content = NaturalLanguageValuesNew() - - i.TotalItems = 0 - - return &i -} diff --git a/shares_test.go b/shares_test.go deleted file mode 100644 index 75343d4..0000000 --- a/shares_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package activitypub - -import "testing" - -func TestSharesNew(t *testing.T) { - t.Skipf("TODO") -} diff --git a/tombstone.go b/tombstone.go index a7356ca..c0c5e9a 100644 --- a/tombstone.go +++ b/tombstone.go @@ -202,6 +202,31 @@ func (t *Tombstone) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (t Tombstone) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + write(&b, '{') + + OnObject(t, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if len(t.FormerType) > 0 { + if v, err := t.FormerType.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(&b, "formerType", v) || notEmpty + } + } + if !t.Deleted.IsZero() { + notEmpty = writeTimeProp(&b, "deleted", t.Deleted) || notEmpty + } + if notEmpty { + write(&b, '}') + return b, nil + } + return nil, nil +} + // Recipients performs recipient de-duplication on the Tombstone object's To, Bto, CC and BCC properties func (t *Tombstone) Recipients() ItemCollection { var aud ItemCollection 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) + } + }) + } +} +