From 88cce1d2f9bf74bd98e389e79be4953d771faae6 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Sun, 15 Dec 2019 19:05:26 +0100 Subject: [PATCH 01/11] Adding some marshaler functions for Object related types --- object.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++ object_id.go | 17 +++++- object_id_test.go | 8 +++ object_test.go | 20 +++++++ 4 files changed, 175 insertions(+), 1 deletion(-) diff --git a/object.go b/object.go index 7d1478b..efa42e4 100644 --- a/object.go +++ b/object.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "fmt" "github.com/buger/jsonparser" "strings" @@ -106,6 +107,17 @@ type ( MimeType string ) +func (a ActivityVocabularyType) MarshalJSON() ([]byte, error) { + if len(a) == 0 { + return nil, nil + } + b := bytes.Buffer{} + b.Write([]byte{'"'}) + b.WriteString(string(a)) + b.Write([]byte{'"'}) + return b.Bytes(), 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 +320,92 @@ func (o *Object) UnmarshalJSON(data []byte) error { return nil } +func writeProp(b *bytes.Buffer, name string, val []byte) { + writePropName(b, name) + writeValue(b, val) +} +func writePropName(b *bytes.Buffer, s string) { + b.Write([]byte{'"'}) + b.WriteString(s) + b.Write([]byte{'"', ':'}) +} + +func writeValue(b *bytes.Buffer, s []byte) { + b.Write([]byte{'"'}) + b.Write(s) + b.Write([]byte{'"'}) +} + +func writeNaturalLanguageProp (b *bytes.Buffer, n string, nl NaturalLanguageValues) { + if v, err := nl.MarshalJSON(); err == nil { + if nl.Count() > 1 { + n += "Map" + } + writeProp(b, n, v) + } +} + +func writeItemProp (b *bytes.Buffer, n string, i Item) { + if i == nil { + b.WriteString("nil") + return + } + if i.IsObject() { + OnObject(i, func(o *Object) error { + v, err := o.MarshalJSON() + if err != nil { + return nil + } + writeProp(b, n, v) + return nil + }) + } else if i.IsCollection() { + OnCollection(i, func(c CollectionInterface) error { + writeItemCollectionProp(b, n, c.Collection()) + return nil + }) + } +} + +func writeItemCollectionProp (b *bytes.Buffer, n string, i ItemCollection) { +} + +// MarshalJSON +func (o Object) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + writeComma := func () { b.WriteString(",") } + + if v, err := o.ID.MarshalJSON(); err == nil { + writeProp(&b, "id", v) + writeComma() + } + if v, err := o.Type.MarshalJSON(); err == nil { + writeProp(&b, "type", v) + writeComma() + } + if v, err := o.MediaType.MarshalJSON(); err == nil { + writeProp(&b, "mediaType", v) + writeComma() + } + if len(o.Name) > 0 { + writeNaturalLanguageProp(&b, "name", o.Name) + writeComma() + } + if len(o.Summary) > 0 { + writeNaturalLanguageProp(&b, "summary", o.Summary) + writeComma() + } + if len(o.Content) > 0 { + writeNaturalLanguageProp(&b, "content", o.Content) + writeComma() + } + + if v, err := o.Source.MarshalJSON(); err == nil { + writeProp(&b, "source", v) + } + return b.Bytes(), 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 +444,18 @@ func (c *MimeType) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (c MimeType) MarshalJSON() ([]byte, error) { + if len(c) == 0 { + return nil, nil + } + b := bytes.Buffer{} + b.Write([]byte{'"'}) + b.WriteString(string(c)) + b.Write([]byte{'"'}) + return b.Bytes(), 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 +574,24 @@ func (s *Source) UnmarshalJSON(data []byte) error { *s = GetAPSource(data) return nil } + +// MarshalJSON +func (s Source) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.Write([]byte{'{'}) + if len(s.MediaType) > 0 { + if v, err := s.MediaType.MarshalJSON(); err == nil { + writePropName(&b, "mediaType") + b.Write(v) + b.Write([]byte{','}) + } + } + if len(s.Content) > 0 { + if v, err := s.Content.MarshalJSON(); err == nil { + b.Write([]byte(`"content":`)) + b.Write(v) + } + } + b.Write([]byte{'}'}) + return b.Bytes(), 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..1dff148 100644 --- a/object_test.go +++ b/object_test.go @@ -488,3 +488,23 @@ 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 TestObject_MarshalJSON(t *testing.T) { + t.Skip("TODO") +} + +func TestSource_MarshalJSON(t *testing.T) { + t.Skip("TODO") +} + +func TestActivityVocabularyType_MarshalJSON(t *testing.T) { + t.Skip("TODO") +} From 8420879a859cf884ed0b569d3f7bf473eda82a31 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Wed, 18 Dec 2019 10:02:23 +0100 Subject: [PATCH 02/11] Improvements to Object, NaturalLanguage and Source Json marshaling Improvements to tests --- natural_language_values.go | 47 ++++-- natural_language_values_test.go | 8 +- object.go | 168 ++++++++++++-------- object_test.go | 272 +++++++++++++++++++++++++++++++- 4 files changed, 407 insertions(+), 88 deletions(-) diff --git a/natural_language_values.go b/natural_language_values.go index 661e7d8..b952921 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 efa42e4..8f43ddb 100644 --- a/object.go +++ b/object.go @@ -320,35 +320,57 @@ func (o *Object) UnmarshalJSON(data []byte) error { return nil } -func writeProp(b *bytes.Buffer, name string, val []byte) { - writePropName(b, name) - writeValue(b, val) -} -func writePropName(b *bytes.Buffer, s string) { - b.Write([]byte{'"'}) - b.WriteString(s) - b.Write([]byte{'"', ':'}) -} - -func writeValue(b *bytes.Buffer, s []byte) { - b.Write([]byte{'"'}) - b.Write(s) - b.Write([]byte{'"'}) -} - -func writeNaturalLanguageProp (b *bytes.Buffer, n string, nl NaturalLanguageValues) { - if v, err := nl.MarshalJSON(); err == nil { - if nl.Count() > 1 { - n += "Map" - } - writeProp(b, n, v) +func writeProp(b *bytes.Buffer, name string, val []byte) (notEmpty bool) { + notEmpty = false + if len(val) == 0 { + return notEmpty } + writePropName(b, name) + return writeValue(b, val) } -func writeItemProp (b *bytes.Buffer, n string, i Item) { +func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + b.Write([]byte{'"'}) + l, err := b.WriteString(s) + b.Write([]byte{'"', ':'}) + if err != nil { + return false + } + if l == 0 { + return false + } + return true +} + +func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { + l, err := b.Write(s) + if err != nil { + return false + } + if l == 0 { + return false + } + return true +} + +func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValues) (notEmpty bool) { + l := nl.Count() + if l > 1 { + n += "Map" + } + if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(b, n, v) + } + return notEmpty +} + +func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { + notEmpty = false if i == nil { - b.WriteString("nil") - return + return notEmpty } if i.IsObject() { OnObject(i, func(o *Object) error { @@ -356,54 +378,68 @@ func writeItemProp (b *bytes.Buffer, n string, i Item) { if err != nil { return nil } - writeProp(b, n, v) + notEmpty = writeProp(b, n, v) return nil }) } else if i.IsCollection() { OnCollection(i, func(c CollectionInterface) error { - writeItemCollectionProp(b, n, c.Collection()) + notEmpty = writeItemCollectionProp(b, n, c.Collection()) return nil }) } + return notEmpty } -func writeItemCollectionProp (b *bytes.Buffer, n string, i ItemCollection) { +func writeItemCollectionProp(b *bytes.Buffer, n string, i ItemCollection) (notEmpty bool) { + return false } // MarshalJSON func (o Object) MarshalJSON() ([]byte, error) { b := bytes.Buffer{} - writeComma := func () { b.WriteString(",") } - - if v, err := o.ID.MarshalJSON(); err == nil { - writeProp(&b, "id", v) - writeComma() - } - if v, err := o.Type.MarshalJSON(); err == nil { - writeProp(&b, "type", v) - writeComma() - } - if v, err := o.MediaType.MarshalJSON(); err == nil { - writeProp(&b, "mediaType", v) - writeComma() - } - if len(o.Name) > 0 { - writeNaturalLanguageProp(&b, "name", o.Name) - writeComma() - } - if len(o.Summary) > 0 { - writeNaturalLanguageProp(&b, "summary", o.Summary) - writeComma() - } - if len(o.Content) > 0 { - writeNaturalLanguageProp(&b, "content", o.Content) - writeComma() + notEmpty := false + b.Write([]byte{'{'}) + + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } } - if v, err := o.Source.MarshalJSON(); err == nil { - writeProp(&b, "source", v) + if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(&b, "id", v) } - return b.Bytes(), nil + if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(&b, "type", v) + } + if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(&b, "mediaType", v) + } + if len(o.Name) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(&b, "name", o.Name) + } + if len(o.Summary) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(&b, "summary", o.Summary) + } + if len(o.Content) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(&b, "content", o.Content) + } + + if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(&b, "source", v) + } + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil } // Recipients performs recipient de-duplication on the Object's To, Bto, CC and BCC properties @@ -578,20 +614,22 @@ func (s *Source) UnmarshalJSON(data []byte) error { // MarshalJSON func (s Source) MarshalJSON() ([]byte, error) { b := bytes.Buffer{} + empty := true b.Write([]byte{'{'}) if len(s.MediaType) > 0 { - if v, err := s.MediaType.MarshalJSON(); err == nil { - writePropName(&b, "mediaType") - b.Write(v) - b.Write([]byte{','}) + if v, err := s.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + empty = !writeProp(&b, "mediaType", v) } } if len(s.Content) > 0 { - if v, err := s.Content.MarshalJSON(); err == nil { - b.Write([]byte(`"content":`)) - b.Write(v) + if !empty { + b.Write([]byte{','}) } + empty = !writeNaturalLanguageProp(&b, "content", s.Content) } - b.Write([]byte{'}'}) - return b.Bytes(), nil + if !empty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil } diff --git a/object_test.go b/object_test.go index 1dff148..4725411 100644 --- a/object_test.go +++ b/object_test.go @@ -3,6 +3,7 @@ package activitypub import ( "reflect" "testing" + "time" ) func TestObjectNew(t *testing.T) { @@ -497,14 +498,269 @@ func TestObject_IsCollection(t *testing.T) { t.Skip("TODO") } -func TestObject_MarshalJSON(t *testing.T) { - t.Skip("TODO") -} - -func TestSource_MarshalJSON(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: "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) + } + }) + } +} From 8af476849fc9dd0bd7b2577f41640fd11dc0f863 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Wed, 18 Dec 2019 13:09:13 +0100 Subject: [PATCH 03/11] Completed functionality of Object.MarshalJSON Added a couple more tests Added proper time.Duration to xsd:duration encoding --- natural_language_values.go | 4 +- object.go | 164 +++++++++++++++++++++++++++++++++++-- object_test.go | 39 +++++++++ unmarshal.go | 1 + xsdduration.go | 84 +++++++++++++++++++ xsdduration_test.go | 48 +++++++++++ 6 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 xsdduration.go create mode 100644 xsdduration_test.go diff --git a/natural_language_values.go b/natural_language_values.go index b952921..52b06a5 100644 --- a/natural_language_values.go +++ b/natural_language_values.go @@ -73,7 +73,7 @@ func (n *NaturalLanguageValues) Set(ref LangRef, v string) error { // MarshalJSON serializes the NaturalLanguageValues into JSON func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) { l := len(n) - if l == 0 { + if l <= 0 { return nil, nil } @@ -86,7 +86,7 @@ func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - if ll == 0 { + if ll <= 0 { return nil, nil } return b.Bytes(), nil diff --git a/object.go b/object.go index 8f43ddb..5e13017 100644 --- a/object.go +++ b/object.go @@ -339,7 +339,7 @@ func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { if err != nil { return false } - if l == 0 { + if l <= 0 { return false } return true @@ -350,7 +350,7 @@ func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { if err != nil { return false } - if l == 0 { + if l <= 0 { return false } return true @@ -367,6 +367,32 @@ func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValue return notEmpty } +func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { + v, err := t.MarshalJSON() + if err != nil { + return false + } + return writeProp(b, n, v) +} +func writeDurationProp(b *bytes.Buffer, n string, d time.Duration) (notEmpty bool) { + if v, err := marshalXSD(d); err == nil { + return writeProp(b, n, v) + } + return false +} + +func writeIRIProp(b *bytes.Buffer, n string, i LinkOrIRI) (notEmpty bool) { + url := i.GetLink() + if len(url) > 0 { + writePropName(b, n) + b.Write([]byte{'"'}) + b.Write([]byte(url)) + b.Write([]byte{'"'}) + return true + } + return false +} + func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { notEmpty = false if i == nil { @@ -390,8 +416,37 @@ func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { return notEmpty } -func writeItemCollectionProp(b *bytes.Buffer, n string, i ItemCollection) (notEmpty bool) { - return false +func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { + notEmpty = false + if len(col) == 0 { + return notEmpty + } + writePropName(b, n) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + b.Write([]byte{'['}) + for _, i := range col { + if i.IsObject() { + OnObject(i, func(o *Object) error { + v, err := o.MarshalJSON() + if err != nil { + return nil + } + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeValue(b, v) + return nil + }) + } else if i.IsLink() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeValue(b, []byte(i.GetLink())) + } + } + b.Write([]byte{']'}) + return notEmpty } // MarshalJSON @@ -406,7 +461,6 @@ func (o Object) MarshalJSON() ([]byte, error) { writeComma() } } - if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { notEmpty = writeProp(&b, "id", v) } @@ -430,6 +484,106 @@ func (o Object) MarshalJSON() ([]byte, error) { writeCommaIfNotEmpty(notEmpty) notEmpty = writeNaturalLanguageProp(&b, "content", o.Content) } + if o.Attachment != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "attachment", o.Attachment) + } + if o.AttributedTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "attributedTo", o.AttributedTo) + } + if o.Audience != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "audience", o.Audience) + } + if o.Context != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "context", o.Context) + } + if o.Generator != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "generator", o.Generator) + } + if o.Icon != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "icon", o.Icon) + } + if o.Image != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "image", o.Image) + } + if o.InReplyTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "inReplyTo", o.InReplyTo) + } + if o.Location != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "location", o.Location) + } + if o.Preview != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "preview", o.Preview) + } + if o.Replies != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "replies", o.Replies) + } + if o.Tag != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "tag", o.Tag) + } + if o.URL != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIRIProp(&b, "url", o.URL) + } + if o.To != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "to", o.To) + } + if o.Bto != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "bto", o.Bto) + } + if o.CC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "cc", o.CC) + } + if o.BCC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "bcc", o.BCC) + } + + if !o.Published.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "published", o.Published) + } + if !o.Updated.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "updated", o.Updated) + } + if !o.StartTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "startTime", o.StartTime) + } + if !o.EndTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "endTime", o.EndTime) + } + if o.Duration != 0 { + // TODO(marius): maybe don't use 0 as a nil value for Object types + // which can have a valid duration of 0 - (Video, Audio, etc) + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeDurationProp(&b, "duration", o.Duration) + } + + if o.Likes != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "likes", o.Likes) + } + if o.Shares != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "shares", o.Shares) + } if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { writeCommaIfNotEmpty(notEmpty) diff --git a/object_test.go b/object_test.go index 4725411..d846d1c 100644 --- a/object_test.go +++ b/object_test.go @@ -635,6 +635,45 @@ func TestObject_MarshalJSON(t *testing.T) { want: []byte(`{"mediaType":"text/stupid"}`), wantErr: false, }, + { + name: "Attachment", + fields: fields{ + Attachment: &Object{ + ID: "some example", + Type: VideoType, + }, + }, + want: []byte(`{"attachment":{"id":"some example","type":"Video"}}`), + wantErr: false, + }, + { + name: "AttributedTo", + fields: fields{ + AttributedTo: &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + }, + want: []byte(`{"attributedTo":{"id":"http://example.com/ana","type":"Person"}}`), + wantErr: false, + }, + { + name: "AttributedToDouble", + fields: fields{ + AttributedTo: ItemCollection{ + &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + &Actor{ + ID: "http://example.com/GGG", + Type: GroupType, + }, + }, + }, + want: []byte(`{"attributedTo":[{"id":"http://example.com/ana","type":"Person"},{"id":"http://example.com/GGG","type":"Group"}]}`), + wantErr: false, + }, { name: "Source", fields: fields{ diff --git a/unmarshal.go b/unmarshal.go index 3cc78e3..a368fc1 100644 --- a/unmarshal.go +++ b/unmarshal.go @@ -123,6 +123,7 @@ func JSONGetTime(data []byte, prop string) time.Time { func JSONGetDuration(data []byte, prop string) time.Duration { str, _ := jsonparser.GetUnsafeString(data, prop) + // TODO(marius): this needs to be replaced to be compatible with xsd:duration d, _ := time.ParseDuration(str) return d } diff --git a/xsdduration.go b/xsdduration.go new file mode 100644 index 0000000..31351d1 --- /dev/null +++ b/xsdduration.go @@ -0,0 +1,84 @@ +package activitypub + +import ( + "bytes" + "fmt" + "time" +) + +const day = time.Hour * 24 +const week = day * 7 +const month = week * 4 +const year = month * 12 + +func Days(d time.Duration) float64 { + dd := d / day + h := d % day + return float64(dd) + float64(h)/(24*60*60*1e9) +} +func Weeks(d time.Duration) float64 { + w := d / week + dd := d % week + return float64(w) + float64(dd)/(7*24*60*60*1e9) +} +func Months(d time.Duration) float64 { + m := d / month + w := d % month + return float64(m) + float64(w)/(4*7*24*60*60*1e9) +} +func Years(d time.Duration) float64 { + y := d / year + m := d % year + return float64(y) + float64(m)/(12*4*7*24*60*60*1e9) +} + +func marshalXSD(d time.Duration) ([]byte, error) { + if d == 0 { + return []byte{'P','T','0','S'}, nil + } + + neg := d < 0 + if neg { + d = -d + } + y := Years(d) + d -= time.Duration(y) * year + m := Months(d) + d -= time.Duration(m) * month + dd := Days(d) + d -= time.Duration(dd) * day + H := d.Hours() + d -= time.Duration(H) * time.Hour + M := d.Minutes() + d -= time.Duration(M) * time.Minute + s := d.Seconds() + d -= time.Duration(s) * time.Second + b := bytes.Buffer{} + if neg { + b.Write([]byte{'-'}) + } + b.Write([]byte{'P'}) + if y > 0 { + b.WriteString(fmt.Sprintf("%dY", int64(y))) + } + if m > 0 { + b.WriteString(fmt.Sprintf("%dM", int64(m))) + } + if dd > 0 { + b.WriteString(fmt.Sprintf("%dD", int64(dd))) + } + + if H + M + s > 0 { + b.Write([]byte{'T'}) + if H > 0 { + b.WriteString(fmt.Sprintf("%dH", int64(H))) + } + if M > 0 { + b.WriteString(fmt.Sprintf("%dM", int64(M))) + } + if s > 0 { + b.WriteString(fmt.Sprintf("%dS", int64(s))) + } + } + return b.Bytes(), nil +} diff --git a/xsdduration_test.go b/xsdduration_test.go new file mode 100644 index 0000000..f8e9720 --- /dev/null +++ b/xsdduration_test.go @@ -0,0 +1,48 @@ +package activitypub + +import ( + "reflect" + "testing" + "time" +) + +func Test_marshalXSD(t *testing.T) { + tests := []struct { + name string + d time.Duration + want []byte + wantErr bool + }{ + { + name: "Zero duration", + d: 0, + want: []byte("PT0S"), + wantErr: false, + }, + { + name: "One year", + d: year, + want: []byte("P1Y"), + wantErr: false, + }, + { + name: "XSD:duration example 1st", + d: 2*year+6*month+5*day+12*time.Hour+35*time.Minute+30*time.Second, + want: []byte("P2Y6M5DT12H35M30S"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := marshalXSD(tt.d) + if (err != nil) != tt.wantErr { + t.Errorf("marshalXSD() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("marshalXSD() got = %s, want %s", got, tt.want) + } + }) + } +} + From 4599863fae8bdb15a198cd66e5fa4dc99c8f151c Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Wed, 18 Dec 2019 17:34:47 +0100 Subject: [PATCH 04/11] Added Activity/IntransitiveActivity/Question Json marshaling and tests --- activity.go | 25 ++ activity_test.go | 485 +++++++++++++++++++++++++++++++++++++++ encoding.go | 348 ++++++++++++++++++++++++++++ intransitive_activity.go | 9 + object.go | 263 +-------------------- 5 files changed, 868 insertions(+), 262 deletions(-) create mode 100644 encoding.go diff --git a/activity.go b/activity.go index e88d54c..e387135 100644 --- a/activity.go +++ b/activity.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "time" "unsafe" @@ -793,3 +794,27 @@ func FlattenActivityProperties(act *Activity) *Activity { act.Instrument = Flatten(act.Instrument) return act } + +// MarshalJSON +func (a Activity) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.Write([]byte{'{'}) + + if !writeActivity(&b, a) { + return nil, nil + } + b.Write([]byte{'}'}) + return b.Bytes(), nil +} + +// MarshalJSON +func (i IntransitiveActivity) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.Write([]byte{'{'}) + + if !writeIntransitiveActivity(&b, i) { + return nil, nil + } + b.Write([]byte{'}'}) + return b.Bytes(), nil +} diff --git a/activity_test.go b/activity_test.go index 635cfa2..d860d28 100644 --- a/activity_test.go +++ b/activity_test.go @@ -2,7 +2,9 @@ package activitypub import ( "fmt" + "reflect" "testing" + "time" ) func TestActivityNew(t *testing.T) { @@ -976,4 +978,487 @@ func TestActivity_GetLink(t *testing.T) { func TestActivity_GetType(t *testing.T) { t.Skipf("TODO") + +func TestActivity_MarshalJSON(t *testing.T) { + type fields struct { + ID ID + Type ActivityVocabularyType + Name NaturalLanguageValues + Attachment Item + AttributedTo Item + Audience ItemCollection + Content NaturalLanguageValues + Context Item + MediaType MimeType + EndTime time.Time + Generator Item + Icon Item + Image Item + InReplyTo Item + Location Item + Preview Item + Published time.Time + Replies Item + StartTime time.Time + Summary NaturalLanguageValues + Tag ItemCollection + Updated time.Time + URL LinkOrIRI + To ItemCollection + Bto ItemCollection + CC ItemCollection + BCC ItemCollection + Duration time.Duration + Likes Item + Shares Item + Source Source + Actor Item + Target Item + Result Item + Origin Item + Instrument Item + Object Item + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Empty", + fields: fields{}, + want: nil, + wantErr: false, + }, + { + name: "JustID", + fields: fields{ + ID: ID("example.com"), + }, + want: []byte(`{"id":"example.com"}`), + wantErr: false, + }, + { + name: "JustType", + fields: fields{ + Type: ActivityVocabularyType("myType"), + }, + want: []byte(`{"type":"myType"}`), + wantErr: false, + }, + { + name: "JustOneName", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "ana"}, + }, + }, + want: []byte(`{"name":"ana"}`), + wantErr: false, + }, + { + name: "MoreNames", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: "en", Value: "anna"}, + {Ref: "fr", Value: "anne"}, + }, + }, + want: []byte(`{"nameMap":{"en":"anna","fr":"anne"}}`), + wantErr: false, + }, + { + name: "JustOneSummary", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test summary"}, + }, + }, + want: []byte(`{"summary":"test summary"}`), + wantErr: false, + }, + { + name: "MoreSummaryEntries", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: "en", Value: "test summary"}, + {Ref: "fr", Value: "teste summary"}, + }, + }, + want: []byte(`{"summaryMap":{"en":"test summary","fr":"teste summary"}}`), + wantErr: false, + }, + { + name: "JustOneContent", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test content"}, + }, + }, + want: []byte(`{"content":"test content"}`), + wantErr: false, + }, + { + name: "MoreContentEntries", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: "en", Value: "test content"}, + {Ref: "fr", Value: "teste content"}, + }, + }, + want: []byte(`{"contentMap":{"en":"test content","fr":"teste content"}}`), + wantErr: false, + }, + { + name: "MediaType", + fields: fields{ + MediaType: MimeType("text/stupid"), + }, + want: []byte(`{"mediaType":"text/stupid"}`), + wantErr: false, + }, + { + name: "Attachment", + fields: fields{ + Attachment: &Object{ + ID: "some example", + Type: VideoType, + }, + }, + want: []byte(`{"attachment":{"id":"some example","type":"Video"}}`), + wantErr: false, + }, + { + name: "AttributedTo", + fields: fields{ + AttributedTo: &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + }, + want: []byte(`{"attributedTo":{"id":"http://example.com/ana","type":"Person"}}`), + wantErr: false, + }, + { + name: "AttributedToDouble", + fields: fields{ + AttributedTo: ItemCollection{ + &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + &Actor{ + ID: "http://example.com/GGG", + Type: GroupType, + }, + }, + }, + want: []byte(`{"attributedTo":[{"id":"http://example.com/ana","type":"Person"},{"id":"http://example.com/GGG","type":"Group"}]}`), + wantErr: false, + }, + { + name: "Source", + fields: fields{ + Source: Source{ + MediaType: MimeType("text/plain"), + Content: NaturalLanguageValues{}, + }, + }, + want: []byte(`{"source":{"mediaType":"text/plain"}}`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := Activity{ + ID: tt.fields.ID, + Type: tt.fields.Type, + Name: tt.fields.Name, + Attachment: tt.fields.Attachment, + AttributedTo: tt.fields.AttributedTo, + Audience: tt.fields.Audience, + Content: tt.fields.Content, + Context: tt.fields.Context, + MediaType: tt.fields.MediaType, + EndTime: tt.fields.EndTime, + Generator: tt.fields.Generator, + Icon: tt.fields.Icon, + Image: tt.fields.Image, + InReplyTo: tt.fields.InReplyTo, + Location: tt.fields.Location, + Preview: tt.fields.Preview, + Published: tt.fields.Published, + Replies: tt.fields.Replies, + StartTime: tt.fields.StartTime, + Summary: tt.fields.Summary, + Tag: tt.fields.Tag, + Updated: tt.fields.Updated, + URL: tt.fields.URL, + To: tt.fields.To, + Bto: tt.fields.Bto, + CC: tt.fields.CC, + BCC: tt.fields.BCC, + Duration: tt.fields.Duration, + Likes: tt.fields.Likes, + Shares: tt.fields.Shares, + Source: tt.fields.Source, + Actor: tt.fields.Actor, + Target: tt.fields.Target, + Result: tt.fields.Result, + Origin: tt.fields.Origin, + Instrument: tt.fields.Instrument, + Object: tt.fields.Object, + } + got, err := a.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() got = %s, want %s", got, tt.want) + } + }) + } +} + +func TestIntransitiveActivity_MarshalJSON(t *testing.T) { + type fields struct { + ID ID + Type ActivityVocabularyType + Name NaturalLanguageValues + Attachment Item + AttributedTo Item + Audience ItemCollection + Content NaturalLanguageValues + Context Item + MediaType MimeType + EndTime time.Time + Generator Item + Icon Item + Image Item + InReplyTo Item + Location Item + Preview Item + Published time.Time + Replies Item + StartTime time.Time + Summary NaturalLanguageValues + Tag ItemCollection + Updated time.Time + URL LinkOrIRI + To ItemCollection + Bto ItemCollection + CC ItemCollection + BCC ItemCollection + Duration time.Duration + Likes Item + Shares Item + Source Source + Actor CanReceiveActivities + Target Item + Result Item + Origin Item + Instrument Item + } + tests := []struct { + name string + fields fields + want []byte + wantErr bool + }{ + { + name: "Empty", + fields: fields{}, + want: nil, + wantErr: false, + }, + { + name: "JustID", + fields: fields{ + ID: ID("example.com"), + }, + want: []byte(`{"id":"example.com"}`), + wantErr: false, + }, + { + name: "JustType", + fields: fields{ + Type: ActivityVocabularyType("myType"), + }, + want: []byte(`{"type":"myType"}`), + wantErr: false, + }, + { + name: "JustOneName", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "ana"}, + }, + }, + want: []byte(`{"name":"ana"}`), + wantErr: false, + }, + { + name: "MoreNames", + fields: fields{ + Name: NaturalLanguageValues{ + {Ref: "en", Value: "anna"}, + {Ref: "fr", Value: "anne"}, + }, + }, + want: []byte(`{"nameMap":{"en":"anna","fr":"anne"}}`), + wantErr: false, + }, + { + name: "JustOneSummary", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test summary"}, + }, + }, + want: []byte(`{"summary":"test summary"}`), + wantErr: false, + }, + { + name: "MoreSummaryEntries", + fields: fields{ + Summary: NaturalLanguageValues{ + {Ref: "en", Value: "test summary"}, + {Ref: "fr", Value: "teste summary"}, + }, + }, + want: []byte(`{"summaryMap":{"en":"test summary","fr":"teste summary"}}`), + wantErr: false, + }, + { + name: "JustOneContent", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: NilLangRef, Value: "test content"}, + }, + }, + want: []byte(`{"content":"test content"}`), + wantErr: false, + }, + { + name: "MoreContentEntries", + fields: fields{ + Content: NaturalLanguageValues{ + {Ref: "en", Value: "test content"}, + {Ref: "fr", Value: "teste content"}, + }, + }, + want: []byte(`{"contentMap":{"en":"test content","fr":"teste content"}}`), + wantErr: false, + }, + { + name: "MediaType", + fields: fields{ + MediaType: MimeType("text/stupid"), + }, + want: []byte(`{"mediaType":"text/stupid"}`), + wantErr: false, + }, + { + name: "Attachment", + fields: fields{ + Attachment: &Object{ + ID: "some example", + Type: VideoType, + }, + }, + want: []byte(`{"attachment":{"id":"some example","type":"Video"}}`), + wantErr: false, + }, + { + name: "AttributedTo", + fields: fields{ + AttributedTo: &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + }, + want: []byte(`{"attributedTo":{"id":"http://example.com/ana","type":"Person"}}`), + wantErr: false, + }, + { + name: "AttributedToDouble", + fields: fields{ + AttributedTo: ItemCollection{ + &Actor{ + ID: "http://example.com/ana", + Type: PersonType, + }, + &Actor{ + ID: "http://example.com/GGG", + Type: GroupType, + }, + }, + }, + want: []byte(`{"attributedTo":[{"id":"http://example.com/ana","type":"Person"},{"id":"http://example.com/GGG","type":"Group"}]}`), + wantErr: false, + }, + { + name: "Source", + fields: fields{ + Source: Source{ + MediaType: MimeType("text/plain"), + Content: NaturalLanguageValues{}, + }, + }, + want: []byte(`{"source":{"mediaType":"text/plain"}}`), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := IntransitiveActivity{ + ID: tt.fields.ID, + Type: tt.fields.Type, + Name: tt.fields.Name, + Attachment: tt.fields.Attachment, + AttributedTo: tt.fields.AttributedTo, + Audience: tt.fields.Audience, + Content: tt.fields.Content, + Context: tt.fields.Context, + MediaType: tt.fields.MediaType, + EndTime: tt.fields.EndTime, + Generator: tt.fields.Generator, + Icon: tt.fields.Icon, + Image: tt.fields.Image, + InReplyTo: tt.fields.InReplyTo, + Location: tt.fields.Location, + Preview: tt.fields.Preview, + Published: tt.fields.Published, + Replies: tt.fields.Replies, + StartTime: tt.fields.StartTime, + Summary: tt.fields.Summary, + Tag: tt.fields.Tag, + Updated: tt.fields.Updated, + URL: tt.fields.URL, + To: tt.fields.To, + Bto: tt.fields.Bto, + CC: tt.fields.CC, + BCC: tt.fields.BCC, + Duration: tt.fields.Duration, + Likes: tt.fields.Likes, + Shares: tt.fields.Shares, + Source: tt.fields.Source, + Actor: tt.fields.Actor, + Target: tt.fields.Target, + Result: tt.fields.Result, + Origin: tt.fields.Origin, + Instrument: tt.fields.Instrument, + } + got, err := i.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() got = %s, want %s", got, tt.want) + } + }) + } } diff --git a/encoding.go b/encoding.go new file mode 100644 index 0000000..5559b1b --- /dev/null +++ b/encoding.go @@ -0,0 +1,348 @@ +package activitypub + +import ( + "bytes" + "fmt" + "time" +) + +func writeProp(b *bytes.Buffer, name string, val []byte) (notEmpty bool) { + if len(val) == 0 { + return false + } + writePropName(b, name) + return writeValue(b, val) +} + +func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + b.Write([]byte{'"'}) + b.WriteString(s) + b.Write([]byte{'"', ':'}) + return true +} + +func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { + if len(s) == 0 { + return false + } + b.Write(s) + return true +} + +func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValues) (notEmpty bool) { + l := nl.Count() + if l > 1 { + n += "Map" + } + if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 { + return writeProp(b, n, v) + } + return false +} + +func writeBoolProp(b *bytes.Buffer, n string, t bool) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf("%t", t))) +} +func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { + if v, err := t.MarshalJSON(); err == nil { + return writeProp(b, n, v) + } + return false +} + +func writeDurationProp(b *bytes.Buffer, n string, d time.Duration) (notEmpty bool) { + if v, err := marshalXSD(d); err == nil { + return writeProp(b, n, v) + } + return false +} + +func writeIRIProp(b *bytes.Buffer, n string, i LinkOrIRI) (notEmpty bool) { + url := i.GetLink() + if len(url) == 0 { + return false + } + writePropName(b, n) + b.Write([]byte{'"'}) + b.Write([]byte(url)) + b.Write([]byte{'"'}) + return true +} + +func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { + if i == nil { + return notEmpty + } + if i.IsObject() { + OnObject(i, func(o *Object) error { + v, err := o.MarshalJSON() + if err != nil { + return nil + } + notEmpty = writeProp(b, n, v) + return nil + }) + } else if i.IsCollection() { + OnCollection(i, func(c CollectionInterface) error { + notEmpty = writeItemCollectionProp(b, n, c.Collection()) || notEmpty + return nil + }) + } + return notEmpty +} + +func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { + if len(col) == 0 { + return notEmpty + } + writePropName(b, n) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + b.Write([]byte{'['}) + for _, i := range col { + if i.IsObject() { + OnObject(i, func(o *Object) error { + v, err := o.MarshalJSON() + if err != nil { + return nil + } + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeValue(b, v) || notEmpty + return nil + }) + } else if i.IsLink() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeValue(b, []byte(i.GetLink())) || notEmpty + } + } + b.Write([]byte{']'}) + return notEmpty +} + +func writeObject(b *bytes.Buffer, o Object) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(b, "id", v) || notEmpty + } + if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(b, "type", v) || notEmpty + } + if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(b, "mediaType", v) || notEmpty + } + if len(o.Name) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(b, "name", o.Name) || notEmpty + } + if len(o.Summary) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(b, "summary", o.Summary) || notEmpty + } + if len(o.Content) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeNaturalLanguageProp(b, "content", o.Content) || notEmpty + } + if o.Attachment != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "attachment", o.Attachment) || notEmpty + } + if o.AttributedTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "attributedTo", o.AttributedTo) || notEmpty + } + if o.Audience != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "audience", o.Audience) || notEmpty + } + if o.Context != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "context", o.Context) || notEmpty + } + if o.Generator != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "generator", o.Generator) || notEmpty + } + if o.Icon != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "icon", o.Icon) || notEmpty + } + if o.Image != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "image", o.Image) || notEmpty + } + if o.InReplyTo != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "inReplyTo", o.InReplyTo) || notEmpty + } + if o.Location != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "location", o.Location) || notEmpty + } + if o.Preview != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "preview", o.Preview) || notEmpty + } + if o.Replies != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "replies", o.Replies) || notEmpty + } + if o.Tag != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "tag", o.Tag) || notEmpty + } + if o.URL != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIRIProp(b, "url", o.URL) || notEmpty + } + if o.To != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "to", o.To) || notEmpty + } + if o.Bto != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "bto", o.Bto) || notEmpty + } + if o.CC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "cc", o.CC) || notEmpty + } + if o.BCC != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "bcc", o.BCC) || notEmpty + } + if !o.Published.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "published", o.Published) || notEmpty + } + if !o.Updated.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "updated", o.Updated) || notEmpty + } + if !o.StartTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "startTime", o.StartTime) || notEmpty + } + if !o.EndTime.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(b, "endTime", o.EndTime) || notEmpty + } + if o.Duration != 0 { + // TODO(marius): maybe don't use 0 as a nil value for Object types + // which can have a valid duration of 0 - (Video, Audio, etc) + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeDurationProp(b, "duration", o.Duration) || notEmpty + } + if o.Likes != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "likes", o.Likes) || notEmpty + } + if o.Shares != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "shares", o.Shares) || notEmpty + } + if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeProp(b, "source", v) || notEmpty + } + return notEmpty +} + +func writeActivity(b *bytes.Buffer, a Activity) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + + OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = writeIntransitiveActivity(b, *i) || notEmpty + return nil + }) + if a.Object != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "object", a.Object) || notEmpty + } + return notEmpty +} + +func writeIntransitiveActivity(b *bytes.Buffer, i IntransitiveActivity) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + OnObject(i, func(o *Object) error { + if o == nil { + return nil + } + notEmpty = writeObject(b, *o) || notEmpty + return nil + }) + if i.Actor != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "actor", i.Actor) || notEmpty + } + if i.Target != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "target", i.Target) || notEmpty + } + if i.Result != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "result", i.Result) || notEmpty + } + if i.Origin != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "origin", i.Origin) || notEmpty + } + if i.Instrument != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "instrument", i.Instrument) || notEmpty + } + return notEmpty +} + +func writeQuestion(b *bytes.Buffer, q Question) (notEmpty bool) { + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + + OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = writeIntransitiveActivity(b, *i) || notEmpty + return nil + }) + if q.OneOf != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty + } else if q.AnyOf != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty + } + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeBoolProp(b, "closed", q.Closed) || notEmpty + return notEmpty +} diff --git a/intransitive_activity.go b/intransitive_activity.go index 9c96217..e5fc945 100644 --- a/intransitive_activity.go +++ b/intransitive_activity.go @@ -3,6 +3,7 @@ package activitypub import ( "errors" "time" + "unsafe" ) // IntransitiveActivity Instances of IntransitiveActivity are a subtype of Activity representing intransitive actions. @@ -256,6 +257,14 @@ func IntransitiveActivityNew(id ID, typ ActivityVocabularyType) *IntransitiveAct // ToIntransitiveActivity func ToIntransitiveActivity(it Item) (*IntransitiveActivity, error) { switch i := it.(type) { + case *Activity: + return (*IntransitiveActivity)(unsafe.Pointer(i)), nil + case Activity: + return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil + case *Question: + return (*IntransitiveActivity)(unsafe.Pointer(i)), nil + case Question: + return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil case *IntransitiveActivity: return i, nil case IntransitiveActivity: diff --git a/object.go b/object.go index 5e13017..e987461 100644 --- a/object.go +++ b/object.go @@ -320,275 +320,14 @@ func (o *Object) UnmarshalJSON(data []byte) error { return nil } -func writeProp(b *bytes.Buffer, name string, val []byte) (notEmpty bool) { - notEmpty = false - if len(val) == 0 { - return notEmpty - } - writePropName(b, name) - return writeValue(b, val) -} - -func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { - if len(s) == 0 { - return false - } - b.Write([]byte{'"'}) - l, err := b.WriteString(s) - b.Write([]byte{'"', ':'}) - if err != nil { - return false - } - if l <= 0 { - return false - } - return true -} - -func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { - l, err := b.Write(s) - if err != nil { - return false - } - if l <= 0 { - return false - } - return true -} - -func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValues) (notEmpty bool) { - l := nl.Count() - if l > 1 { - n += "Map" - } - if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 { - notEmpty = writeProp(b, n, v) - } - return notEmpty -} - -func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { - v, err := t.MarshalJSON() - if err != nil { - return false - } - return writeProp(b, n, v) -} -func writeDurationProp(b *bytes.Buffer, n string, d time.Duration) (notEmpty bool) { - if v, err := marshalXSD(d); err == nil { - return writeProp(b, n, v) - } - return false -} - -func writeIRIProp(b *bytes.Buffer, n string, i LinkOrIRI) (notEmpty bool) { - url := i.GetLink() - if len(url) > 0 { - writePropName(b, n) - b.Write([]byte{'"'}) - b.Write([]byte(url)) - b.Write([]byte{'"'}) - return true - } - return false -} - -func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { - notEmpty = false - if i == nil { - return notEmpty - } - if i.IsObject() { - OnObject(i, func(o *Object) error { - v, err := o.MarshalJSON() - if err != nil { - return nil - } - notEmpty = writeProp(b, n, v) - return nil - }) - } else if i.IsCollection() { - OnCollection(i, func(c CollectionInterface) error { - notEmpty = writeItemCollectionProp(b, n, c.Collection()) - return nil - }) - } - return notEmpty -} - -func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { - notEmpty = false - if len(col) == 0 { - return notEmpty - } - writePropName(b, n) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } - b.Write([]byte{'['}) - for _, i := range col { - if i.IsObject() { - OnObject(i, func(o *Object) error { - v, err := o.MarshalJSON() - if err != nil { - return nil - } - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeValue(b, v) - return nil - }) - } else if i.IsLink() { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeValue(b, []byte(i.GetLink())) - } - } - b.Write([]byte{']'}) - return notEmpty -} - // MarshalJSON func (o Object) MarshalJSON() ([]byte, error) { b := bytes.Buffer{} notEmpty := false b.Write([]byte{'{'}) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } - if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { - notEmpty = writeProp(&b, "id", v) - } - if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeProp(&b, "type", v) - } - if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeProp(&b, "mediaType", v) - } - if len(o.Name) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeNaturalLanguageProp(&b, "name", o.Name) - } - if len(o.Summary) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeNaturalLanguageProp(&b, "summary", o.Summary) - } - if len(o.Content) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeNaturalLanguageProp(&b, "content", o.Content) - } - if o.Attachment != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "attachment", o.Attachment) - } - if o.AttributedTo != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "attributedTo", o.AttributedTo) - } - if o.Audience != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "audience", o.Audience) - } - if o.Context != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "context", o.Context) - } - if o.Generator != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "generator", o.Generator) - } - if o.Icon != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "icon", o.Icon) - } - if o.Image != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "image", o.Image) - } - if o.InReplyTo != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "inReplyTo", o.InReplyTo) - } - if o.Location != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "location", o.Location) - } - if o.Preview != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "preview", o.Preview) - } - if o.Replies != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "replies", o.Replies) - } - if o.Tag != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "tag", o.Tag) - } - if o.URL != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeIRIProp(&b, "url", o.URL) - } - if o.To != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "to", o.To) - } - if o.Bto != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "bto", o.Bto) - } - if o.CC != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "cc", o.CC) - } - if o.BCC != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "bcc", o.BCC) - } + notEmpty = writeObject(&b, o) - if !o.Published.IsZero() { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeTimeProp(&b, "published", o.Published) - } - if !o.Updated.IsZero() { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeTimeProp(&b, "updated", o.Updated) - } - if !o.StartTime.IsZero() { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeTimeProp(&b, "startTime", o.StartTime) - } - if !o.EndTime.IsZero() { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeTimeProp(&b, "endTime", o.EndTime) - } - if o.Duration != 0 { - // TODO(marius): maybe don't use 0 as a nil value for Object types - // which can have a valid duration of 0 - (Video, Audio, etc) - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeDurationProp(&b, "duration", o.Duration) - } - - if o.Likes != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "likes", o.Likes) - } - if o.Shares != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "shares", o.Shares) - } - - if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeProp(&b, "source", v) - } if notEmpty { b.Write([]byte{'}'}) return b.Bytes(), nil From cf3741d6bb3fdabda03c0ba9c81d36f2a9587a96 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Thu, 19 Dec 2019 13:30:18 +0100 Subject: [PATCH 05/11] Removed the files with aliases for collections --- collection.go | 30 ++++++++++ collection_test.go | 7 +++ followers.go | 9 --- followers_test.go | 7 --- following.go | 25 --------- following_test.go | 7 --- inbox.go | 26 --------- inbox_test.go | 26 --------- liked.go | 25 --------- liked_test.go | 26 --------- likes.go | 25 --------- likes_test.go | 7 --- ordered_collection.go | 112 +++++++++++++++++++++++++++++++++++++ ordered_collection_test.go | 71 +++++++++++++++++++++++ outbox.go | 24 -------- outbox_test.go | 102 --------------------------------- shares.go | 25 --------- shares_test.go | 7 --- 18 files changed, 220 insertions(+), 341 deletions(-) delete mode 100644 followers.go delete mode 100644 followers_test.go delete mode 100644 following.go delete mode 100644 following_test.go delete mode 100644 inbox.go delete mode 100644 inbox_test.go delete mode 100644 liked.go delete mode 100644 liked_test.go delete mode 100644 likes.go delete mode 100644 likes_test.go delete mode 100644 outbox.go delete mode 100644 outbox_test.go delete mode 100644 shares.go delete mode 100644 shares_test.go diff --git a/collection.go b/collection.go index 7580e68..4b64d57 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} @@ -293,3 +309,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_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/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/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/ordered_collection.go b/ordered_collection.go index 615e3dc..9faceb1 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 @@ -306,3 +353,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_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/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") -} From c4d6f93c22010cac3a058964e2050de54fa27bd4 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Thu, 19 Dec 2019 13:34:04 +0100 Subject: [PATCH 06/11] Added actor Json marshal --- actor.go | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ encoding.go | 23 ++++++++- object.go | 12 ++--- 3 files changed, 160 insertions(+), 10 deletions(-) diff --git a/actor.go b/actor.go index 5a7e66c..0609291 100644 --- a/actor.go +++ b/actor.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "github.com/buger/jsonparser" "time" @@ -209,6 +210,36 @@ func (p *PublicKey) UnmarshalJSON(data []byte) error { } return nil } + +func (p PublicKey) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + notEmpty := true + b.Write([]byte{'{'}) + if v, err := p.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = !writeProp(&b, "id", v) + } + if p.Owner != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIRIProp(&b, "owner", p.Owner) || notEmpty + } + if len(p.PublicKeyPem) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIRIProp(&b, "publicKeyPem", p.Owner) || notEmpty + } + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil +} + type ( // Application describes a software application. Application = Actor @@ -364,6 +395,65 @@ func (a *Actor) UnmarshalJSON(data []byte) error { return nil } +func (a Actor) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(a, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + if a.Inbox != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "inbox", a.Inbox) || notEmpty + } + if a.Outbox != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "outbox", a.Outbox) || notEmpty + } + if a.Following != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "following", a.Following) || notEmpty + } + if a.Followers != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "followers", a.Followers) || notEmpty + } + if a.Liked != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "liked", a.Liked) || notEmpty + } + if a.Endpoints != nil { + writeCommaIfNotEmpty(notEmpty) + if v, err := a.Endpoints.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(&b, "endpoints", v) || notEmpty + } + } + if len(a.Streams) > 0 { + writeCommaIfNotEmpty(notEmpty) + writePropName(&b, "streams") + lNotEmpty := true + for _, ss := range a.Streams { + writeCommaIfNotEmpty(lNotEmpty) + lNotEmpty = writeItemCollection(&b, ss) || lNotEmpty + } + notEmpty = lNotEmpty || notEmpty + } + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), 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 +492,49 @@ func (e *Endpoints) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (e Endpoints) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + notEmpty := false + + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + b.Write([]byte{'{'}) + if e.OauthAuthorizationEndpoint != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "oauthAuthorizationEndpoint", e.OauthAuthorizationEndpoint) || notEmpty + } + if e.OauthTokenEndpoint != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "oauthTokenEndpoint", e.OauthTokenEndpoint) || notEmpty + } + if e.ProvideClientKey != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "provideClientKey", e.ProvideClientKey) || notEmpty + } + if e.SignClientKey != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "signClientKey", e.SignClientKey) || notEmpty + } + if e.SharedInbox != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "sharedInbox", e.SharedInbox) || notEmpty + } + if e.UploadMedia != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "uploadMedia", e.UploadMedia) || notEmpty + } + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil +} + // ToActor func ToActor(it Item) (*Actor, error) { switch i := it.(type) { @@ -410,6 +543,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/encoding.go b/encoding.go index 5559b1b..fae2717 100644 --- a/encoding.go +++ b/encoding.go @@ -46,6 +46,9 @@ func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValue func writeBoolProp(b *bytes.Buffer, n string, t bool) (notEmpty bool) { return writeProp(b, n, []byte(fmt.Sprintf("%t", t))) } +func writeIntProp(b *bytes.Buffer, n string, d int) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf("%d", d))) +} func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { if v, err := t.MarshalJSON(); err == nil { return writeProp(b, n, v) @@ -94,11 +97,20 @@ func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { return notEmpty } -func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { +func writeString(b *bytes.Buffer, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + b.Write([]byte{'"'}) + b.WriteString(s) + b.Write([]byte{'"'}) + return true +} + +func writeItemCollection(b *bytes.Buffer, col ItemCollection) (notEmpty bool) { if len(col) == 0 { return notEmpty } - writePropName(b, n) writeComma := func() { b.WriteString(",") } writeCommaIfNotEmpty := func(notEmpty bool) { if notEmpty { @@ -125,6 +137,13 @@ func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (not b.Write([]byte{']'}) return notEmpty } +func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { + if len(col) == 0 { + return notEmpty + } + writePropName(b, n) + return writeItemCollection(b, col) +} func writeObject(b *bytes.Buffer, o Object) (notEmpty bool) { writeComma := func() { b.WriteString(",") } diff --git a/object.go b/object.go index e987461..f447098 100644 --- a/object.go +++ b/object.go @@ -112,9 +112,7 @@ func (a ActivityVocabularyType) MarshalJSON() ([]byte, error) { return nil, nil } b := bytes.Buffer{} - b.Write([]byte{'"'}) - b.WriteString(string(a)) - b.Write([]byte{'"'}) + writeString(&b, string(a)) return b.Bytes(), nil } @@ -374,14 +372,12 @@ func (c *MimeType) UnmarshalJSON(data []byte) error { } // MarshalJSON -func (c MimeType) MarshalJSON() ([]byte, error) { - if len(c) == 0 { +func (m MimeType) MarshalJSON() ([]byte, error) { + if len(m) == 0 { return nil, nil } b := bytes.Buffer{} - b.Write([]byte{'"'}) - b.WriteString(string(c)) - b.Write([]byte{'"'}) + writeString(&b, string(m)) return b.Bytes(), nil } From 1659f7231e4f782b69f200aae3a11025efb572ba Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Thu, 19 Dec 2019 13:34:31 +0100 Subject: [PATCH 07/11] Added question Json marshal --- question.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/question.go b/question.go index c151bc5..9bc7778 100644 --- a/question.go +++ b/question.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "time" ) @@ -229,6 +230,17 @@ func (q *Question) UnmarshalJSON(data []byte) error { return nil } +func (q Question) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.Write([]byte{'{'}) + + if !writeQuestion(&b, q) { + return nil, nil + } + b.Write([]byte{'}'}) + return b.Bytes(), nil +} + // QuestionNew initializes a Question activity func QuestionNew(id ID) *Question { q := Question{ID: id, Type: QuestionType} From 66855adb1e343de292167241d9bad05f927300a8 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Thu, 19 Dec 2019 13:35:05 +0100 Subject: [PATCH 08/11] Added collection/ordered_collection and their pages Json marshal methods --- collection.go | 43 +++++++++++++++++++++++++++++ collection_page.go | 56 ++++++++++++++++++++++++++++++++++++++ ordered_collection.go | 43 +++++++++++++++++++++++++++++ ordered_collection_page.go | 55 +++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) diff --git a/collection.go b/collection.go index 4b64d57..027e1c0 100644 --- a/collection.go +++ b/collection.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "time" "unsafe" @@ -295,6 +296,48 @@ func (c *Collection) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (c Collection) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(c, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + if c.Current != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty + } + if c.Items != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemCollectionProp(&b, "items", c.Items) || notEmpty + } + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIntProp(&b, "totalItems", int(c.TotalItems)) || notEmpty + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil +} + // ToCollection func ToCollection(it Item) (*Collection, error) { switch i := it.(type) { diff --git a/collection_page.go b/collection_page.go index 474089a..3d0aae8 100644 --- a/collection_page.go +++ b/collection_page.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "time" ) @@ -254,6 +255,61 @@ func (c *CollectionPage) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (c CollectionPage) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(c, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + if c.Current != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty + } + if c.Items != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemCollectionProp(&b, "items", c.Items) || notEmpty + } + if c.PartOf != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "partOf", c.PartOf) || notEmpty + } + if c.Next != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "next", c.Next) || notEmpty + } + if c.Prev != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "prev", c.Prev) || notEmpty + } + + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIntProp(&b, "totalItems", int(c.TotalItems)) || notEmpty + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil +} + // CollectionNew initializes a new CollectionPage func CollectionPageNew(parent CollectionInterface) *CollectionPage { p := CollectionPage{ diff --git a/ordered_collection.go b/ordered_collection.go index 9faceb1..4f40d54 100644 --- a/ordered_collection.go +++ b/ordered_collection.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "time" "unsafe" @@ -291,6 +292,48 @@ func (o *OrderedCollection) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (o OrderedCollection) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(o, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + if o.Current != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "current", o.Current) || notEmpty + } + if o.First != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "first", o.First) || notEmpty + } + if o.Last != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "last", o.Last) || notEmpty + } + if o.OrderedItems != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemCollectionProp(&b, "orderedItems", o.OrderedItems) || notEmpty + } + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIntProp(&b, "totalItems", int(o.TotalItems)) || notEmpty + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil +} + // OrderedCollectionPageNew initializes a new OrderedCollectionPage func OrderedCollectionPageNew(parent CollectionInterface) *OrderedCollectionPage { p := OrderedCollectionPage{ diff --git a/ordered_collection_page.go b/ordered_collection_page.go index df868a6..12b25c1 100644 --- a/ordered_collection_page.go +++ b/ordered_collection_page.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "errors" "github.com/buger/jsonparser" "time" @@ -260,6 +261,60 @@ func (o *OrderedCollectionPage) UnmarshalJSON(data []byte) error { } return nil } +// MarshalJSON +func (c OrderedCollectionPage) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(c, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + if c.Current != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty + } + if c.OrderedItems != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemCollectionProp(&b, "orderedItems", c.OrderedItems) || notEmpty + } + if c.PartOf != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "partOf", c.PartOf) || notEmpty + } + if c.Next != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "next", c.Next) || notEmpty + } + if c.Prev != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "prev", c.Prev) || notEmpty + } + + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIntProp(&b, "totalItems", int(c.TotalItems)) || notEmpty + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), nil + } + return nil, nil +} // ToOrderedCollectionPage func ToOrderedCollectionPage(it Item) (*OrderedCollectionPage, error) { From c295d6d15ca164e7433af0f4a23dce930ab31145 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Thu, 19 Dec 2019 14:44:39 +0100 Subject: [PATCH 09/11] Added marshal Json method to Place, Profile, Relationship and Tombstone --- collection.go | 2 +- collection_page.go | 2 +- encoding.go | 9 ++++-- ordered_collection.go | 2 +- ordered_collection_page.go | 2 +- place.go | 62 +++++++++++++++++++++++++++++++++----- profile.go | 30 ++++++++++++++++++ relationship.go | 47 ++++++++++++++++++++++++++--- tombstone.go | 34 +++++++++++++++++++++ 9 files changed, 172 insertions(+), 18 deletions(-) diff --git a/collection.go b/collection.go index 027e1c0..a167db7 100644 --- a/collection.go +++ b/collection.go @@ -329,7 +329,7 @@ func (c Collection) MarshalJSON() ([]byte, error) { notEmpty = writeItemCollectionProp(&b, "items", c.Items) || notEmpty } writeCommaIfNotEmpty(notEmpty) - notEmpty = writeIntProp(&b, "totalItems", int(c.TotalItems)) || notEmpty + notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty if notEmpty { b.Write([]byte{'}'}) diff --git a/collection_page.go b/collection_page.go index 3d0aae8..9aa7e1f 100644 --- a/collection_page.go +++ b/collection_page.go @@ -301,7 +301,7 @@ func (c CollectionPage) MarshalJSON() ([]byte, error) { } writeCommaIfNotEmpty(notEmpty) - notEmpty = writeIntProp(&b, "totalItems", int(c.TotalItems)) || notEmpty + notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty if notEmpty { b.Write([]byte{'}'}) diff --git a/encoding.go b/encoding.go index fae2717..70c9b68 100644 --- a/encoding.go +++ b/encoding.go @@ -42,13 +42,18 @@ func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValue } return false } - +func writeStringProp(b *bytes.Buffer, n string, s string) (notEmpty bool) { + return writeProp(b, n, []byte(s)) +} func writeBoolProp(b *bytes.Buffer, n string, t bool) (notEmpty bool) { return writeProp(b, n, []byte(fmt.Sprintf("%t", t))) } -func writeIntProp(b *bytes.Buffer, n string, d int) (notEmpty bool) { +func writeIntProp(b *bytes.Buffer, n string, d int64) (notEmpty bool) { return writeProp(b, n, []byte(fmt.Sprintf("%d", d))) } +func writeFloatProp(b *bytes.Buffer, n string, f float64) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf("%f", f))) +} func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { if v, err := t.MarshalJSON(); err == nil { return writeProp(b, n, v) diff --git a/ordered_collection.go b/ordered_collection.go index 4f40d54..1b4a072 100644 --- a/ordered_collection.go +++ b/ordered_collection.go @@ -325,7 +325,7 @@ func (o OrderedCollection) MarshalJSON() ([]byte, error) { notEmpty = writeItemCollectionProp(&b, "orderedItems", o.OrderedItems) || notEmpty } writeCommaIfNotEmpty(notEmpty) - notEmpty = writeIntProp(&b, "totalItems", int(o.TotalItems)) || notEmpty + notEmpty = writeIntProp(&b, "totalItems", int64(o.TotalItems)) || notEmpty if notEmpty { b.Write([]byte{'}'}) diff --git a/ordered_collection_page.go b/ordered_collection_page.go index 12b25c1..108bac1 100644 --- a/ordered_collection_page.go +++ b/ordered_collection_page.go @@ -307,7 +307,7 @@ func (c OrderedCollectionPage) MarshalJSON() ([]byte, error) { } writeCommaIfNotEmpty(notEmpty) - notEmpty = writeIntProp(&b, "totalItems", int(c.TotalItems)) || notEmpty + notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty if notEmpty { b.Write([]byte{'}'}) diff --git a/place.go b/place.go index dbb00ac..c95abc9 100644 --- a/place.go +++ b/place.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "fmt" "time" "unsafe" @@ -102,22 +103,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 +219,53 @@ func (p *Place) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (p Place) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(p, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if p.Accuracy > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeFloatProp(&b, "accuracy", p.Accuracy) || notEmpty + } + if p.Altitude > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeFloatProp(&b, "altitude", p.Altitude) || notEmpty + } + if p.Latitude > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeFloatProp(&b, "latitude", p.Latitude) || notEmpty + } + if p.Longitude > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeFloatProp(&b, "longitude", p.Longitude) || notEmpty + } + if p.Radius > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeIntProp(&b, "radius", p.Radius) || notEmpty + } + if len(p.Units) > 0 { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeStringProp(&b, "radius", p.Units) || notEmpty + } + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), 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 +274,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..9bba7e9 100644 --- a/profile.go +++ b/profile.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "fmt" "time" "unsafe" @@ -199,6 +200,35 @@ func (p *Profile) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (p Profile) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(p, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + + if p.Describes != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "describes", p.Describes) || notEmpty + } + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), 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/relationship.go b/relationship.go index 8c7c905..74d2611 100644 --- a/relationship.go +++ b/relationship.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "fmt" "time" "unsafe" @@ -108,12 +109,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 +212,43 @@ func (r *Relationship) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (r Relationship) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(r, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + + if r.Subject != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "subject", r.Subject) || notEmpty + } + if r.Object != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "object", r.Object) || notEmpty + } + if r.Relationship != nil { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeItemProp(&b, "relationship", r.Relationship) || notEmpty + } + + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), 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 +257,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/tombstone.go b/tombstone.go index a7356ca..b1fdbdb 100644 --- a/tombstone.go +++ b/tombstone.go @@ -1,6 +1,7 @@ package activitypub import ( + "bytes" "fmt" "time" "unsafe" @@ -202,6 +203,39 @@ func (t *Tombstone) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON +func (t Tombstone) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + writeComma := func() { b.WriteString(",") } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + writeComma() + } + } + notEmpty := false + b.Write([]byte{'{'}) + + OnObject(t, func(o *Object) error { + notEmpty = writeObject(&b, *o) + return nil + }) + if len(t.FormerType) > 0 { + writeCommaIfNotEmpty(notEmpty) + if v, err := t.FormerType.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = writeProp(&b, "formerType", v) || notEmpty + } + } + if !t.Deleted.IsZero() { + writeCommaIfNotEmpty(notEmpty) + notEmpty = writeTimeProp(&b, "deleted", t.Deleted) || notEmpty + } + if notEmpty { + b.Write([]byte{'}'}) + return b.Bytes(), 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 From 22f934b796a775d446e2c3d59bf29a1758b5eb43 Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Thu, 19 Dec 2019 14:47:32 +0100 Subject: [PATCH 10/11] Fix rebase artefact --- activity_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/activity_test.go b/activity_test.go index d860d28..2fe378a 100644 --- a/activity_test.go +++ b/activity_test.go @@ -978,6 +978,7 @@ func TestActivity_GetLink(t *testing.T) { func TestActivity_GetType(t *testing.T) { t.Skipf("TODO") +} func TestActivity_MarshalJSON(t *testing.T) { type fields struct { From 6bb34603e9566cb2a665722549601d6d3dd14e2d Mon Sep 17 00:00:00 2001 From: Marius Orcsik Date: Thu, 19 Dec 2019 17:05:12 +0100 Subject: [PATCH 11/11] Refactored marshaling and removing bytes.Buffer in favor of a simple byte slice --- activity.go | 17 ++- actor.go | 95 ++++++----------- collection.go | 20 +--- collection_page.go | 23 +--- encoding.go | 210 +++++++++++++------------------------ iri.go | 32 ++++++ item_collection.go | 6 ++ object.go | 28 +++-- ordered_collection.go | 20 +--- ordered_collection_page.go | 24 +---- place.go | 21 +--- profile.go | 17 +-- question.go | 9 +- relationship.go | 18 +--- tombstone.go | 17 +-- 15 files changed, 198 insertions(+), 359 deletions(-) diff --git a/activity.go b/activity.go index e387135..531849d 100644 --- a/activity.go +++ b/activity.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "errors" "time" "unsafe" @@ -797,24 +796,24 @@ func FlattenActivityProperties(act *Activity) *Activity { // MarshalJSON func (a Activity) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - b.Write([]byte{'{'}) + b := make([]byte, 0) + write(&b, '{') if !writeActivity(&b, a) { return nil, nil } - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } // MarshalJSON func (i IntransitiveActivity) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - b.Write([]byte{'{'}) + b := make([]byte, 0) + write(&b, '{') if !writeIntransitiveActivity(&b, i) { return nil, nil } - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } diff --git a/actor.go b/actor.go index 0609291..1243a3d 100644 --- a/actor.go +++ b/actor.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "errors" "github.com/buger/jsonparser" "time" @@ -212,30 +211,22 @@ func (p *PublicKey) UnmarshalJSON(data []byte) error { } func (p PublicKey) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } + b := make([]byte, 0) notEmpty := true - b.Write([]byte{'{'}) + write(&b, '{') if v, err := p.ID.MarshalJSON(); err == nil && len(v) > 0 { - notEmpty = !writeProp(&b, "id", v) + notEmpty = !writeProp(&b, "id", v) } if p.Owner != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeIRIProp(&b, "owner", p.Owner) || notEmpty + notEmpty = writeIRIProp(&b, "owner", p.Owner) || notEmpty } if len(p.PublicKeyPem) > 0 { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeIRIProp(&b, "publicKeyPem", p.Owner) || notEmpty + notEmpty = writeIRIProp(&b, "publicKeyPem", p.Owner) || notEmpty } if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } @@ -396,60 +387,46 @@ func (a *Actor) UnmarshalJSON(data []byte) error { } func (a Actor) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(a, func(o *Object) error { - notEmpty = writeObject(&b, *o) + notEmpty = writeObject(&b, *o) return nil }) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } if a.Inbox != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "inbox", a.Inbox) || notEmpty + notEmpty = writeItemProp(&b, "inbox", a.Inbox) || notEmpty } if a.Outbox != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "outbox", a.Outbox) || notEmpty + notEmpty = writeItemProp(&b, "outbox", a.Outbox) || notEmpty } if a.Following != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "following", a.Following) || notEmpty + notEmpty = writeItemProp(&b, "following", a.Following) || notEmpty } if a.Followers != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "followers", a.Followers) || notEmpty + notEmpty = writeItemProp(&b, "followers", a.Followers) || notEmpty } if a.Liked != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "liked", a.Liked) || notEmpty + notEmpty = writeItemProp(&b, "liked", a.Liked) || notEmpty } if a.Endpoints != nil { - writeCommaIfNotEmpty(notEmpty) if v, err := a.Endpoints.MarshalJSON(); err == nil && len(v) > 0 { - notEmpty = writeProp(&b, "endpoints", v) || notEmpty + notEmpty = writeProp(&b, "endpoints", v) || notEmpty } } if len(a.Streams) > 0 { - writeCommaIfNotEmpty(notEmpty) - writePropName(&b, "streams") + writePropName(&b, "streams") lNotEmpty := true for _, ss := range a.Streams { - writeCommaIfNotEmpty(lNotEmpty) - lNotEmpty = writeItemCollection(&b, ss) || lNotEmpty + lNotEmpty = writeItemCollection(&b, ss) || lNotEmpty } notEmpty = lNotEmpty || notEmpty } if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } @@ -494,43 +471,31 @@ func (e *Endpoints) UnmarshalJSON(data []byte) error { // MarshalJSON func (e Endpoints) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) notEmpty := false - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } - b.Write([]byte{'{'}) + write(&b, '{') if e.OauthAuthorizationEndpoint != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "oauthAuthorizationEndpoint", e.OauthAuthorizationEndpoint) || notEmpty + notEmpty = writeItemProp(&b, "oauthAuthorizationEndpoint", e.OauthAuthorizationEndpoint) || notEmpty } if e.OauthTokenEndpoint != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "oauthTokenEndpoint", e.OauthTokenEndpoint) || notEmpty + notEmpty = writeItemProp(&b, "oauthTokenEndpoint", e.OauthTokenEndpoint) || notEmpty } if e.ProvideClientKey != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "provideClientKey", e.ProvideClientKey) || notEmpty + notEmpty = writeItemProp(&b, "provideClientKey", e.ProvideClientKey) || notEmpty } if e.SignClientKey != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "signClientKey", e.SignClientKey) || notEmpty + notEmpty = writeItemProp(&b, "signClientKey", e.SignClientKey) || notEmpty } if e.SharedInbox != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "sharedInbox", e.SharedInbox) || notEmpty + notEmpty = writeItemProp(&b, "sharedInbox", e.SharedInbox) || notEmpty } if e.UploadMedia != nil { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeItemProp(&b, "uploadMedia", e.UploadMedia) || notEmpty + notEmpty = writeItemProp(&b, "uploadMedia", e.UploadMedia) || notEmpty } if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/collection.go b/collection.go index a167db7..bffe714 100644 --- a/collection.go +++ b/collection.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "errors" "time" "unsafe" @@ -298,42 +297,31 @@ func (c *Collection) UnmarshalJSON(data []byte) error { // MarshalJSON func (c Collection) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(c, func(o *Object) error { notEmpty = writeObject(&b, *o) return nil }) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } if c.Current != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty } if c.First != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "first", c.First) || notEmpty } if c.Last != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty } if c.Items != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemCollectionProp(&b, "items", c.Items) || notEmpty } - writeCommaIfNotEmpty(notEmpty) notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/collection_page.go b/collection_page.go index 9aa7e1f..04615a0 100644 --- a/collection_page.go +++ b/collection_page.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "errors" "time" ) @@ -257,55 +256,41 @@ func (c *CollectionPage) UnmarshalJSON(data []byte) error { // MarshalJSON func (c CollectionPage) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(c, func(o *Object) error { notEmpty = writeObject(&b, *o) return nil }) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } if c.Current != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty } if c.First != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "first", c.First) || notEmpty } if c.Last != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty } if c.Items != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemCollectionProp(&b, "items", c.Items) || notEmpty } if c.PartOf != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "partOf", c.PartOf) || notEmpty } if c.Next != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "next", c.Next) || notEmpty } if c.Prev != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "prev", c.Prev) || notEmpty } - writeCommaIfNotEmpty(notEmpty) notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/encoding.go b/encoding.go index 70c9b68..fdfdc27 100644 --- a/encoding.go +++ b/encoding.go @@ -1,38 +1,55 @@ package activitypub import ( - "bytes" + "encoding/json" "fmt" "time" ) -func writeProp(b *bytes.Buffer, name string, val []byte) (notEmpty bool) { +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 } - writePropName(b, name) - return writeValue(b, val) + writeComma(b) + success := writePropName(b, name) && writeValue(b, val) + if !success { + *b = (*b)[:len(*b)-1] + } + return success } -func writePropName(b *bytes.Buffer, s string) (notEmpty bool) { +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 } - b.Write([]byte{'"'}) - b.WriteString(s) - b.Write([]byte{'"', ':'}) + write(b, '"') + writeS(b, s) + write(b, '"', ':') return true } -func writeValue(b *bytes.Buffer, s []byte) (notEmpty bool) { +func writeValue(b *[]byte, s []byte) (notEmpty bool) { if len(s) == 0 { return false } - b.Write(s) + write(b, s...) return true } -func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValues) (notEmpty bool) { +func writeNaturalLanguageProp(b *[]byte, n string, nl NaturalLanguageValues) (notEmpty bool) { l := nl.Count() if l > 1 { n += "Map" @@ -42,257 +59,200 @@ func writeNaturalLanguageProp(b *bytes.Buffer, n string, nl NaturalLanguageValue } return false } -func writeStringProp(b *bytes.Buffer, n string, s string) (notEmpty bool) { - return writeProp(b, n, []byte(s)) +func writeStringProp(b *[]byte, n string, s string) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf(`"%s"`, s))) } -func writeBoolProp(b *bytes.Buffer, n string, t bool) (notEmpty bool) { - return writeProp(b, n, []byte(fmt.Sprintf("%t", t))) +func writeBoolProp(b *[]byte, n string, t bool) (notEmpty bool) { + return writeProp(b, n, []byte(fmt.Sprintf(`"%t"`, t))) } -func writeIntProp(b *bytes.Buffer, n string, d int64) (notEmpty bool) { +func writeIntProp(b *[]byte, n string, d int64) (notEmpty bool) { return writeProp(b, n, []byte(fmt.Sprintf("%d", d))) } -func writeFloatProp(b *bytes.Buffer, n string, f float64) (notEmpty bool) { +func writeFloatProp(b *[]byte, n string, f float64) (notEmpty bool) { return writeProp(b, n, []byte(fmt.Sprintf("%f", f))) } -func writeTimeProp(b *bytes.Buffer, n string, t time.Time) (notEmpty bool) { +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 *bytes.Buffer, n string, d time.Duration) (notEmpty bool) { +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 *bytes.Buffer, n string, i LinkOrIRI) (notEmpty bool) { - url := i.GetLink() +func writeIRIProp(b *[]byte, n string, i LinkOrIRI) (notEmpty bool) { + url := i.GetLink().String() if len(url) == 0 { return false } - writePropName(b, n) - b.Write([]byte{'"'}) - b.Write([]byte(url)) - b.Write([]byte{'"'}) + writeStringProp(b, n, url) return true } -func writeItemProp(b *bytes.Buffer, n string, i Item) (notEmpty bool) { +func writeItemProp(b *[]byte, n string, i Item) (notEmpty bool) { if i == nil { return notEmpty } - if i.IsObject() { - OnObject(i, func(o *Object) error { - v, err := o.MarshalJSON() - if err != nil { - return nil - } - notEmpty = writeProp(b, n, v) - return nil - }) - } else if i.IsCollection() { - OnCollection(i, func(c CollectionInterface) error { - notEmpty = writeItemCollectionProp(b, n, c.Collection()) || notEmpty - return nil - }) + 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 *bytes.Buffer, s string) (notEmpty bool) { +func writeString(b *[]byte, s string) (notEmpty bool) { if len(s) == 0 { return false } - b.Write([]byte{'"'}) - b.WriteString(s) - b.Write([]byte{'"'}) + write(b, '"') + writeS(b, s) + write(b, '"') return true } -func writeItemCollection(b *bytes.Buffer, col ItemCollection) (notEmpty bool) { +func writeItemCollection(b *[]byte, col ItemCollection) (notEmpty bool) { if len(col) == 0 { return notEmpty } - writeComma := func() { b.WriteString(",") } writeCommaIfNotEmpty := func(notEmpty bool) { if notEmpty { - writeComma() + write(b, ',') } } - b.Write([]byte{'['}) - for _, i := range col { - if i.IsObject() { - OnObject(i, func(o *Object) error { - v, err := o.MarshalJSON() - if err != nil { - return nil - } - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeValue(b, v) || notEmpty - return nil - }) - } else if i.IsLink() { - writeCommaIfNotEmpty(notEmpty) - notEmpty = writeValue(b, []byte(i.GetLink())) || notEmpty + 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...) } } - b.Write([]byte{']'}) - return notEmpty + write(b, ']') + return true } -func writeItemCollectionProp(b *bytes.Buffer, n string, col ItemCollection) (notEmpty bool) { +func writeItemCollectionProp(b *[]byte, n string, col ItemCollection) (notEmpty bool) { if len(col) == 0 { return notEmpty } - writePropName(b, n) - return writeItemCollection(b, col) + writeComma(b) + success := writePropName(b, n) && writeItemCollection(b, col) + if !success { + *b = (*b)[:len(*b)-1] + } + return success } -func writeObject(b *bytes.Buffer, o Object) (notEmpty bool) { - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } +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 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeProp(b, "type", v) || notEmpty } if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeProp(b, "mediaType", v) || notEmpty } if len(o.Name) > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeNaturalLanguageProp(b, "name", o.Name) || notEmpty } if len(o.Summary) > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeNaturalLanguageProp(b, "summary", o.Summary) || notEmpty } if len(o.Content) > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeNaturalLanguageProp(b, "content", o.Content) || notEmpty } if o.Attachment != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "attachment", o.Attachment) || notEmpty } if o.AttributedTo != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "attributedTo", o.AttributedTo) || notEmpty } if o.Audience != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "audience", o.Audience) || notEmpty } if o.Context != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "context", o.Context) || notEmpty } if o.Generator != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "generator", o.Generator) || notEmpty } if o.Icon != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "icon", o.Icon) || notEmpty } if o.Image != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "image", o.Image) || notEmpty } if o.InReplyTo != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "inReplyTo", o.InReplyTo) || notEmpty } if o.Location != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "location", o.Location) || notEmpty } if o.Preview != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "preview", o.Preview) || notEmpty } if o.Replies != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "replies", o.Replies) || notEmpty } if o.Tag != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "tag", o.Tag) || notEmpty } if o.URL != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeIRIProp(b, "url", o.URL) || notEmpty } if o.To != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "to", o.To) || notEmpty } if o.Bto != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "bto", o.Bto) || notEmpty } if o.CC != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "cc", o.CC) || notEmpty } if o.BCC != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "bcc", o.BCC) || notEmpty } if !o.Published.IsZero() { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeTimeProp(b, "published", o.Published) || notEmpty } if !o.Updated.IsZero() { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeTimeProp(b, "updated", o.Updated) || notEmpty } if !o.StartTime.IsZero() { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeTimeProp(b, "startTime", o.StartTime) || notEmpty } if !o.EndTime.IsZero() { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeTimeProp(b, "endTime", o.EndTime) || notEmpty } if o.Duration != 0 { // TODO(marius): maybe don't use 0 as a nil value for Object types // which can have a valid duration of 0 - (Video, Audio, etc) - writeCommaIfNotEmpty(notEmpty) notEmpty = writeDurationProp(b, "duration", o.Duration) || notEmpty } if o.Likes != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "likes", o.Likes) || notEmpty } if o.Shares != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "shares", o.Shares) || notEmpty } if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeProp(b, "source", v) || notEmpty } return notEmpty } -func writeActivity(b *bytes.Buffer, a Activity) (notEmpty bool) { - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } - +func writeActivity(b *[]byte, a Activity) (notEmpty bool) { OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { if i == nil { return nil @@ -301,19 +261,12 @@ func writeActivity(b *bytes.Buffer, a Activity) (notEmpty bool) { return nil }) if a.Object != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "object", a.Object) || notEmpty } return notEmpty } -func writeIntransitiveActivity(b *bytes.Buffer, i IntransitiveActivity) (notEmpty bool) { - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } +func writeIntransitiveActivity(b *[]byte, i IntransitiveActivity) (notEmpty bool) { OnObject(i, func(o *Object) error { if o == nil { return nil @@ -322,36 +275,24 @@ func writeIntransitiveActivity(b *bytes.Buffer, i IntransitiveActivity) (notEmpt return nil }) if i.Actor != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "actor", i.Actor) || notEmpty } if i.Target != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "target", i.Target) || notEmpty } if i.Result != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "result", i.Result) || notEmpty } if i.Origin != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "origin", i.Origin) || notEmpty } if i.Instrument != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "instrument", i.Instrument) || notEmpty } return notEmpty } -func writeQuestion(b *bytes.Buffer, q Question) (notEmpty bool) { - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } - +func writeQuestion(b *[]byte, q Question) (notEmpty bool) { OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { if i == nil { return nil @@ -360,13 +301,10 @@ func writeQuestion(b *bytes.Buffer, q Question) (notEmpty bool) { return nil }) if q.OneOf != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty } else if q.AnyOf != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(b, "oneOf", q.OneOf) || notEmpty } - writeCommaIfNotEmpty(notEmpty) notEmpty = writeBoolProp(b, "closed", q.Closed) || notEmpty return notEmpty } diff --git a/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/object.go b/object.go index f447098..9747fa2 100644 --- a/object.go +++ b/object.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "fmt" "github.com/buger/jsonparser" "strings" @@ -111,9 +110,9 @@ func (a ActivityVocabularyType) MarshalJSON() ([]byte, error) { if len(a) == 0 { return nil, nil } - b := bytes.Buffer{} + b := make([]byte, 0) writeString(&b, string(a)) - return b.Bytes(), nil + return b, nil } // Object describes an ActivityPub object of any kind. @@ -320,15 +319,15 @@ func (o *Object) UnmarshalJSON(data []byte) error { // MarshalJSON func (o Object) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') notEmpty = writeObject(&b, o) if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } @@ -376,9 +375,9 @@ func (m MimeType) MarshalJSON() ([]byte, error) { if len(m) == 0 { return nil, nil } - b := bytes.Buffer{} + b := make([]byte, 0) writeString(&b, string(m)) - return b.Bytes(), nil + return b, nil } // ToObject returns an Object pointer to the data in the current Item @@ -502,23 +501,20 @@ func (s *Source) UnmarshalJSON(data []byte) error { // MarshalJSON func (s Source) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) empty := true - b.Write([]byte{'{'}) + 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 { - if !empty { - b.Write([]byte{','}) - } empty = !writeNaturalLanguageProp(&b, "content", s.Content) } if !empty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/ordered_collection.go b/ordered_collection.go index 1b4a072..20a4d5e 100644 --- a/ordered_collection.go +++ b/ordered_collection.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "errors" "time" "unsafe" @@ -294,42 +293,31 @@ func (o *OrderedCollection) UnmarshalJSON(data []byte) error { // MarshalJSON func (o OrderedCollection) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(o, func(o *Object) error { notEmpty = writeObject(&b, *o) return nil }) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } if o.Current != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "current", o.Current) || notEmpty } if o.First != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "first", o.First) || notEmpty } if o.Last != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "last", o.Last) || notEmpty } if o.OrderedItems != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemCollectionProp(&b, "orderedItems", o.OrderedItems) || notEmpty } - writeCommaIfNotEmpty(notEmpty) notEmpty = writeIntProp(&b, "totalItems", int64(o.TotalItems)) || notEmpty if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/ordered_collection_page.go b/ordered_collection_page.go index 108bac1..dac2b05 100644 --- a/ordered_collection_page.go +++ b/ordered_collection_page.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "errors" "github.com/buger/jsonparser" "time" @@ -263,55 +262,40 @@ func (o *OrderedCollectionPage) UnmarshalJSON(data []byte) error { } // MarshalJSON func (c OrderedCollectionPage) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(c, func(o *Object) error { notEmpty = writeObject(&b, *o) return nil }) - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } if c.Current != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "current", c.Current) || notEmpty } if c.First != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "first", c.First) || notEmpty } if c.Last != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "last", c.Last) || notEmpty } if c.OrderedItems != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemCollectionProp(&b, "orderedItems", c.OrderedItems) || notEmpty } if c.PartOf != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "partOf", c.PartOf) || notEmpty } if c.Next != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "next", c.Next) || notEmpty } if c.Prev != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "prev", c.Prev) || notEmpty } - - writeCommaIfNotEmpty(notEmpty) notEmpty = writeIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/place.go b/place.go index c95abc9..714ff27 100644 --- a/place.go +++ b/place.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "fmt" "time" "unsafe" @@ -221,47 +220,35 @@ func (p *Place) UnmarshalJSON(data []byte) error { // MarshalJSON func (p Place) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(p, func(o *Object) error { notEmpty = writeObject(&b, *o) return nil }) if p.Accuracy > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeFloatProp(&b, "accuracy", p.Accuracy) || notEmpty } if p.Altitude > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeFloatProp(&b, "altitude", p.Altitude) || notEmpty } if p.Latitude > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeFloatProp(&b, "latitude", p.Latitude) || notEmpty } if p.Longitude > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeFloatProp(&b, "longitude", p.Longitude) || notEmpty } if p.Radius > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeIntProp(&b, "radius", p.Radius) || notEmpty } if len(p.Units) > 0 { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeStringProp(&b, "radius", p.Units) || notEmpty } if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/profile.go b/profile.go index 9bba7e9..b6cb1a1 100644 --- a/profile.go +++ b/profile.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "fmt" "time" "unsafe" @@ -202,29 +201,21 @@ func (p *Profile) UnmarshalJSON(data []byte) error { // MarshalJSON func (p Profile) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(p, func(o *Object) error { - notEmpty = writeObject(&b, *o) return nil }) if p.Describes != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "describes", p.Describes) || notEmpty } if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/question.go b/question.go index 9bc7778..6286d33 100644 --- a/question.go +++ b/question.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "errors" "time" ) @@ -231,14 +230,14 @@ func (q *Question) UnmarshalJSON(data []byte) error { } func (q Question) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - b.Write([]byte{'{'}) + b := make([]byte, 0) + write(&b, '{') if !writeQuestion(&b, q) { return nil, nil } - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } // QuestionNew initializes a Question activity diff --git a/relationship.go b/relationship.go index 74d2611..99ad231 100644 --- a/relationship.go +++ b/relationship.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "fmt" "time" "unsafe" @@ -214,15 +213,9 @@ func (r *Relationship) UnmarshalJSON(data []byte) error { // MarshalJSON func (r Relationship) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(r, func(o *Object) error { notEmpty = writeObject(&b, *o) @@ -230,21 +223,18 @@ func (r Relationship) MarshalJSON() ([]byte, error) { }) if r.Subject != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "subject", r.Subject) || notEmpty } if r.Object != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "object", r.Object) || notEmpty } if r.Relationship != nil { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeItemProp(&b, "relationship", r.Relationship) || notEmpty } if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil } diff --git a/tombstone.go b/tombstone.go index b1fdbdb..c0c5e9a 100644 --- a/tombstone.go +++ b/tombstone.go @@ -1,7 +1,6 @@ package activitypub import ( - "bytes" "fmt" "time" "unsafe" @@ -205,33 +204,25 @@ func (t *Tombstone) UnmarshalJSON(data []byte) error { // MarshalJSON func (t Tombstone) MarshalJSON() ([]byte, error) { - b := bytes.Buffer{} - writeComma := func() { b.WriteString(",") } - writeCommaIfNotEmpty := func(notEmpty bool) { - if notEmpty { - writeComma() - } - } + b := make([]byte, 0) notEmpty := false - b.Write([]byte{'{'}) + write(&b, '{') OnObject(t, func(o *Object) error { notEmpty = writeObject(&b, *o) return nil }) if len(t.FormerType) > 0 { - writeCommaIfNotEmpty(notEmpty) if v, err := t.FormerType.MarshalJSON(); err == nil && len(v) > 0 { notEmpty = writeProp(&b, "formerType", v) || notEmpty } } if !t.Deleted.IsZero() { - writeCommaIfNotEmpty(notEmpty) notEmpty = writeTimeProp(&b, "deleted", t.Deleted) || notEmpty } if notEmpty { - b.Write([]byte{'}'}) - return b.Bytes(), nil + write(&b, '}') + return b, nil } return nil, nil }