Merge pull request #8 from go-ap/custom-marshalers

Custom Marshalers for the vocab structs
This commit is contained in:
Marius Orcsik 2019-12-19 17:16:22 +01:00 committed by GitHub
commit ed094c68d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 2026 additions and 369 deletions

View file

@ -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
}

View file

@ -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)
}
})
}
}

100
actor.go
View file

@ -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

View file

@ -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
}

View file

@ -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{

View file

@ -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")
}

310
encoding.go Normal file
View file

@ -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
}

View file

@ -1,9 +0,0 @@
package activitypub
type (
// FollowersCollection is a collection of followers
FollowersCollection = Followers
// Followers is a Collection type
Followers = Collection
)

View file

@ -1,7 +0,0 @@
package activitypub
import "testing"
func TestFollowersNew(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -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
}

View file

@ -1,7 +0,0 @@
package activitypub
import "testing"
func TestFollowingNew(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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:

32
iri.go
View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -1,7 +0,0 @@
package activitypub
import "testing"
func TestLikesNew(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)
}
})
}
}

View file

@ -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
}

View file

@ -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) {

View file

@ -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")
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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

View file

@ -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}

View file

@ -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) {

View file

@ -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
}

View file

@ -1,7 +0,0 @@
package activitypub
import "testing"
func TestSharesNew(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -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

View file

@ -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
}

84
xsdduration.go Normal file
View file

@ -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
}

48
xsdduration_test.go Normal file
View file

@ -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)
}
})
}
}