Adding typer function for the RepositoryType
Adding some tests
---
models/forgefed/forgefed.go | 26 --
models/forgefed/repository.go | 57 +++++
models/forgefed/repository_json_decode.go | 69 ++++++
models/forgefed/repository_json_encode.go | 283 ++++++++++++++++++++++
models/forgefed/repository_test.go | 167 +++++++++++++
5 files changed, 576 insertions(+), 26 deletions(-)
delete mode 100644 models/forgefed/forgefed.go
create mode 100644 models/forgefed/repository.go
create mode 100644 models/forgefed/repository_json_decode.go
create mode 100644 models/forgefed/repository_json_encode.go
create mode 100644 models/forgefed/repository_test.go
diff --git a/models/forgefed/forgefed.go b/models/forgefed/forgefed.go
deleted file mode 100644
index 53c0db646..000000000
--- a/models/forgefed/forgefed.go
@@ -1,26 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package forgefed
-
-import (
- ap "github.com/go-ap/activitypub"
-)
-
-const (
- RepositoryType ap.ActivityVocabularyType = "Repository"
-)
-
-type Repository struct {
- ap.Actor
- // Team specifies a Collection of actors who are working on the object
- Team ap.Item `jsonld:"team,omitempty"`
-}
-
-// RepositoryNew initializes a Repository type actor
-func RepositoryNew(id ap.ID) *Repository {
- a := ap.ActorNew(id, RepositoryType)
- o := Repository{Actor: *a}
- return &o
-}
\ No newline at end of file
diff --git a/models/forgefed/repository.go b/models/forgefed/repository.go
new file mode 100644
index 000000000..120fcedc1
--- /dev/null
+++ b/models/forgefed/repository.go
@@ -0,0 +1,57 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package forgefed
+
+import (
+ ap "github.com/go-ap/activitypub"
+ "github.com/valyala/fastjson"
+)
+
+const (
+ RepositoryType ap.ActivityVocabularyType = "Repository"
+)
+
+type Repository struct {
+ ap.Actor
+ // Team specifies a Collection of actors who are working on the object
+ Team ap.Item `jsonld:"team,omitempty"`
+}
+
+// GetItemByType instantiates a new Repository object if the type matches
+// otherwise it defaults to existing activitypub package typer function.
+func GetItemByType(typ ap.ActivityVocabularyType) (ap.Item, error) {
+ if typ == RepositoryType {
+ return RepositoryNew(""), nil
+ }
+ return ap.GetItemByType(typ)
+}
+
+// RepositoryNew initializes a Repository type actor
+func RepositoryNew(id ap.ID) *Repository {
+ a := ap.ActorNew(id, RepositoryType)
+ o := Repository{Actor: *a}
+ return &o
+}
+
+func (r Repository) MarshalJSON() ([]byte, error) {
+ b := make([]byte, 0)
+ write(&b, '{')
+
+ if notEmpty := writeRepositoryJSONValue(&b, r); notEmpty {
+ write(&b, '}')
+ return b, nil
+ }
+
+ return nil, nil
+}
+
+func (r *Repository) UnmarshalJSON(data []byte) error {
+ p := fastjson.Parser{}
+ val, err := p.ParseBytes(data)
+ if err != nil {
+ return err
+ }
+ return loadRepository(val, r)
+}
diff --git a/models/forgefed/repository_json_decode.go b/models/forgefed/repository_json_decode.go
new file mode 100644
index 000000000..1833f5dc6
--- /dev/null
+++ b/models/forgefed/repository_json_decode.go
@@ -0,0 +1,69 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package forgefed
+
+import (
+ ap "github.com/go-ap/activitypub"
+ "github.com/valyala/fastjson"
+)
+
+func loadRepository(val *fastjson.Value, r *Repository) error {
+ r.Team = ap.JSONGetItem(val, "team")
+
+ return ap.OnActor(&r.Actor, func(a *ap.Actor) error {
+ return loadActor(val, a)
+ })
+}
+
+func loadActor(val *fastjson.Value, a *ap.Actor) error {
+ a.PreferredUsername = ap.JSONGetNaturalLanguageField(val, "preferredUsername")
+ a.Followers = ap.JSONGetItem(val, "followers")
+ a.Following = ap.JSONGetItem(val, "following")
+ a.Inbox = ap.JSONGetItem(val, "inbox")
+ a.Outbox = ap.JSONGetItem(val, "outbox")
+ a.Liked = ap.JSONGetItem(val, "liked")
+ a.Endpoints = ap.JSONGetActorEndpoints(val, "endpoints")
+ a.Streams = ap.JSONGetItems(val, "streams")
+ a.PublicKey = ap.JSONGetPublicKey(val, "publicKey")
+
+ return ap.OnObject(a, func(o *ap.Object) error {
+ return loadObject(val, o)
+ })
+}
+
+func loadObject(val *fastjson.Value, o *ap.Object) error {
+ o.ID = ap.JSONGetID(val)
+ o.Type = ap.JSONGetType(val)
+ o.Name = ap.JSONGetNaturalLanguageField(val, "name")
+ o.Content = ap.JSONGetNaturalLanguageField(val, "content")
+ o.Summary = ap.JSONGetNaturalLanguageField(val, "summary")
+ o.Context = ap.JSONGetItem(val, "context")
+ o.URL = ap.JSONGetURIItem(val, "url")
+ o.MediaType = ap.JSONGetMimeType(val, "mediaType")
+ o.Generator = ap.JSONGetItem(val, "generator")
+ o.AttributedTo = ap.JSONGetItem(val, "attributedTo")
+ o.Attachment = ap.JSONGetItem(val, "attachment")
+ o.Location = ap.JSONGetItem(val, "location")
+ o.Published = ap.JSONGetTime(val, "published")
+ o.StartTime = ap.JSONGetTime(val, "startTime")
+ o.EndTime = ap.JSONGetTime(val, "endTime")
+ o.Duration = ap.JSONGetDuration(val, "duration")
+ o.Icon = ap.JSONGetItem(val, "icon")
+ o.Preview = ap.JSONGetItem(val, "preview")
+ o.Image = ap.JSONGetItem(val, "image")
+ o.Updated = ap.JSONGetTime(val, "updated")
+ o.InReplyTo = ap.JSONGetItem(val, "inReplyTo")
+ o.To = ap.JSONGetItems(val, "to")
+ o.Audience = ap.JSONGetItems(val, "audience")
+ o.Bto = ap.JSONGetItems(val, "bto")
+ o.CC = ap.JSONGetItems(val, "cc")
+ o.BCC = ap.JSONGetItems(val, "bcc")
+ o.Replies = ap.JSONGetItem(val, "replies")
+ o.Tag = ap.JSONGetItems(val, "tag")
+ o.Likes = ap.JSONGetItem(val, "likes")
+ o.Shares = ap.JSONGetItem(val, "shares")
+ o.Source = ap.GetAPSource(val)
+ return nil
+}
diff --git a/models/forgefed/repository_json_encode.go b/models/forgefed/repository_json_encode.go
new file mode 100644
index 000000000..e5aab61ca
--- /dev/null
+++ b/models/forgefed/repository_json_encode.go
@@ -0,0 +1,283 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package forgefed
+
+import (
+ "encoding/json"
+ "time"
+
+ "git.sr.ht/~mariusor/go-xsd-duration"
+ ap "github.com/go-ap/activitypub"
+)
+
+func writeRepositoryJSONValue(b *[]byte, r Repository) (notEmpty bool) {
+ ap.OnActor(r.Actor, func(a *ap.Actor) error {
+ if a == nil {
+ return nil
+ }
+ notEmpty = writeActorJSONValue(b, a) || notEmpty
+ return nil
+ })
+ if r.Team != nil {
+ notEmpty = writeItemJSONProp(b, "team", r.Team) || notEmpty
+ }
+ return notEmpty
+}
+
+func writeItemJSONProp(b *[]byte, n string, i ap.Item) (notEmpty bool) {
+ if i == nil {
+ return notEmpty
+ }
+ if im, ok := i.(json.Marshaler); ok {
+ v, err := im.MarshalJSON()
+ if err != nil {
+ return false
+ }
+ return writeJSONProp(b, n, v)
+ }
+ return notEmpty
+}
+
+func writeJSONProp(b *[]byte, name string, val []byte) (notEmpty bool) {
+ if len(val) == 0 {
+ return false
+ }
+ writeComma(b)
+ success := writePropJSONName(b, name) && writeJSONValue(b, val)
+ if !success {
+ *b = (*b)[:len(*b)-1]
+ }
+ return success
+}
+
+func writePropJSONName(b *[]byte, s string) (notEmpty bool) {
+ if len(s) == 0 {
+ return false
+ }
+ write(b, '"')
+ writeS(b, s)
+ write(b, '"', ':')
+ return true
+}
+
+func writeJSONValue(b *[]byte, s []byte) (notEmpty bool) {
+ if len(s) == 0 {
+ return false
+ }
+ write(b, s...)
+ return true
+}
+
+func writeActorJSONValue(b *[]byte, a *ap.Actor) (notEmpty bool) {
+ ap.OnObject(a, func(o *ap.Object) error {
+ notEmpty = writeObjectJSONValue(b, *o)
+ return nil
+ })
+ if a.Inbox != nil {
+ notEmpty = writeItemJSONProp(b, "inbox", a.Inbox) || notEmpty
+ }
+ if a.Outbox != nil {
+ notEmpty = writeItemJSONProp(b, "outbox", a.Outbox) || notEmpty
+ }
+ if a.Following != nil {
+ notEmpty = writeItemJSONProp(b, "following", a.Following) || notEmpty
+ }
+ if a.Followers != nil {
+ notEmpty = writeItemJSONProp(b, "followers", a.Followers) || notEmpty
+ }
+ if a.Liked != nil {
+ notEmpty = writeItemJSONProp(b, "liked", a.Liked) || notEmpty
+ }
+ if a.PreferredUsername != nil {
+ notEmpty = writeNaturalLanguageJSONProp(b, "preferredUsername", a.PreferredUsername) || notEmpty
+ }
+ if a.Endpoints != nil {
+ if v, err := a.Endpoints.MarshalJSON(); err == nil && len(v) > 0 {
+ notEmpty = writeJSONProp(b, "endpoints", v) || notEmpty
+ }
+ }
+ if len(a.Streams) > 0 {
+ notEmpty = writeItemCollectionJSONProp(b, "streams", a.Streams)
+ }
+ if len(a.PublicKey.PublicKeyPem)+len(a.PublicKey.ID) > 0 {
+ if v, err := a.PublicKey.MarshalJSON(); err == nil && len(v) > 0 {
+ notEmpty = writeJSONProp(b, "publicKey", v) || notEmpty
+ }
+ }
+ return notEmpty
+}
+
+func writeItemCollectionJSONProp(b *[]byte, n string, col ap.ItemCollection) (notEmpty bool) {
+ if len(col) == 0 {
+ return notEmpty
+ }
+ writeComma(b)
+ success := writePropJSONName(b, n) && writeItemCollectionJSONValue(b, col)
+ if !success {
+ *b = (*b)[:len(*b)-1]
+ }
+ return success
+}
+
+func writeItemCollectionJSONValue(b *[]byte, col ap.ItemCollection) (notEmpty bool) {
+ if len(col) == 0 {
+ return notEmpty
+ }
+ writeCommaIfNotEmpty := func(notEmpty bool) {
+ if notEmpty {
+ write(b, ',')
+ }
+ }
+ write(b, '[')
+ for i, it := range col {
+ if im, ok := it.(json.Marshaler); ok {
+ v, err := im.MarshalJSON()
+ if err != nil {
+ return false
+ }
+ writeCommaIfNotEmpty(i > 0)
+ write(b, v...)
+ }
+ }
+ write(b, ']')
+ return true
+}
+
+func writeObjectJSONValue(b *[]byte, o ap.Object) (notEmpty bool) {
+ if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 {
+ notEmpty = writeJSONProp(b, "id", v) || notEmpty
+ }
+ if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 {
+ notEmpty = writeJSONProp(b, "type", v) || notEmpty
+ }
+ if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 {
+ notEmpty = writeJSONProp(b, "mediaType", v) || notEmpty
+ }
+ if len(o.Name) > 0 {
+ notEmpty = writeNaturalLanguageJSONProp(b, "name", o.Name) || notEmpty
+ }
+ if len(o.Summary) > 0 {
+ notEmpty = writeNaturalLanguageJSONProp(b, "summary", o.Summary) || notEmpty
+ }
+ if len(o.Content) > 0 {
+ notEmpty = writeNaturalLanguageJSONProp(b, "content", o.Content) || notEmpty
+ }
+ if o.Attachment != nil {
+ notEmpty = writeItemJSONProp(b, "attachment", o.Attachment) || notEmpty
+ }
+ if o.AttributedTo != nil {
+ notEmpty = writeItemJSONProp(b, "attributedTo", o.AttributedTo) || notEmpty
+ }
+ if o.Audience != nil {
+ notEmpty = writeItemJSONProp(b, "audience", o.Audience) || notEmpty
+ }
+ if o.Context != nil {
+ notEmpty = writeItemJSONProp(b, "context", o.Context) || notEmpty
+ }
+ if o.Generator != nil {
+ notEmpty = writeItemJSONProp(b, "generator", o.Generator) || notEmpty
+ }
+ if o.Icon != nil {
+ notEmpty = writeItemJSONProp(b, "icon", o.Icon) || notEmpty
+ }
+ if o.Image != nil {
+ notEmpty = writeItemJSONProp(b, "image", o.Image) || notEmpty
+ }
+ if o.InReplyTo != nil {
+ notEmpty = writeItemJSONProp(b, "inReplyTo", o.InReplyTo) || notEmpty
+ }
+ if o.Location != nil {
+ notEmpty = writeItemJSONProp(b, "location", o.Location) || notEmpty
+ }
+ if o.Preview != nil {
+ notEmpty = writeItemJSONProp(b, "preview", o.Preview) || notEmpty
+ }
+ if o.Replies != nil {
+ notEmpty = writeItemJSONProp(b, "replies", o.Replies) || notEmpty
+ }
+ if o.Tag != nil {
+ notEmpty = writeItemJSONProp(b, "tag", o.Tag) || notEmpty
+ }
+ if o.URL != nil {
+ notEmpty = writeItemJSONProp(b, "url", o.URL) || notEmpty
+ }
+ if o.To != nil {
+ notEmpty = writeItemJSONProp(b, "to", o.To) || notEmpty
+ }
+ if o.Bto != nil {
+ notEmpty = writeItemJSONProp(b, "bto", o.Bto) || notEmpty
+ }
+ if o.CC != nil {
+ notEmpty = writeItemJSONProp(b, "cc", o.CC) || notEmpty
+ }
+ if o.BCC != nil {
+ notEmpty = writeItemJSONProp(b, "bcc", o.BCC) || notEmpty
+ }
+ if !o.Published.IsZero() {
+ notEmpty = writeTimeJSONProp(b, "published", o.Published) || notEmpty
+ }
+ if !o.Updated.IsZero() {
+ notEmpty = writeTimeJSONProp(b, "updated", o.Updated) || notEmpty
+ }
+ if !o.StartTime.IsZero() {
+ notEmpty = writeTimeJSONProp(b, "startTime", o.StartTime) || notEmpty
+ }
+ if !o.EndTime.IsZero() {
+ notEmpty = writeTimeJSONProp(b, "endTime", o.EndTime) || notEmpty
+ }
+ if o.Duration != 0 {
+ notEmpty = writeDurationJSONProp(b, "duration", o.Duration) || notEmpty
+ }
+ if o.Likes != nil {
+ notEmpty = writeItemJSONProp(b, "likes", o.Likes) || notEmpty
+ }
+ if o.Shares != nil {
+ notEmpty = writeItemJSONProp(b, "shares", o.Shares) || notEmpty
+ }
+ if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 {
+ notEmpty = writeJSONProp(b, "source", v) || notEmpty
+ }
+ return notEmpty
+}
+
+func writeNaturalLanguageJSONProp(b *[]byte, n string, nl ap.NaturalLanguageValues) (notEmpty bool) {
+ l := nl.Count()
+ if l > 1 {
+ n += "Map"
+ }
+ if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 {
+ return writeJSONProp(b, n, v)
+ }
+ return false
+}
+
+func writeTimeJSONProp(b *[]byte, n string, t time.Time) (notEmpty bool) {
+ var tb []byte
+ write(&tb, '"')
+ writeS(&tb, t.UTC().Format(time.RFC3339))
+ write(&tb, '"')
+ return writeJSONProp(b, n, tb)
+}
+
+func writeDurationJSONProp(b *[]byte, n string, d time.Duration) (notEmpty bool) {
+ if v, err := xsd.Marshal(d); err == nil {
+ return writeJSONProp(b, n, v)
+ }
+ return false
+}
+
+func writeComma(b *[]byte) {
+ if len(*b) > 1 && (*b)[len(*b)-1] != ',' {
+ *b = append(*b, ',')
+ }
+}
+
+func write(b *[]byte, c ...byte) {
+ *b = append(*b, c...)
+}
+
+func writeS(b *[]byte, s string) {
+ *b = append(*b, s...)
+}
diff --git a/models/forgefed/repository_test.go b/models/forgefed/repository_test.go
new file mode 100644
index 000000000..4bf36a66c
--- /dev/null
+++ b/models/forgefed/repository_test.go
@@ -0,0 +1,167 @@
+package forgefed
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "testing"
+
+ ap "github.com/go-ap/activitypub"
+)
+
+func Test_GetItemByType(t *testing.T) {
+ type testtt struct {
+ typ ap.ActivityVocabularyType
+ want ap.Item
+ wantErr error
+ }
+ tests := map[string]testtt{
+ "invalid type": {
+ typ: ap.ActivityVocabularyType("invalidtype"),
+ wantErr: fmt.Errorf("empty ActivityStreams type"), // TODO(marius): this error message needs to be improved in go-ap/activitypub
+ },
+ "Repository": {
+ typ: RepositoryType,
+ want: new(Repository),
+ },
+ "Person - fall back": {
+ typ: ap.PersonType,
+ want: new(ap.Person),
+ },
+ "Question - fall back": {
+ typ: ap.QuestionType,
+ want: new(ap.Question),
+ },
+ }
+
+ for name, tt := range tests {
+ t.Run(name, func(t *testing.T) {
+ maybeRepository, err := GetItemByType(tt.typ)
+ if !reflect.DeepEqual(tt.wantErr, err) {
+ t.Errorf("GetItemByType() error = \"%+v\", wantErr = \"%+v\" when getting Item for type %q", tt.wantErr, err, tt.typ)
+ }
+ if reflect.TypeOf(tt.want) != reflect.TypeOf(maybeRepository) {
+ t.Errorf("Invalid type received %T, expected %T", maybeRepository, tt.want)
+ }
+ })
+ }
+}
+
+func Test_RepositoryMarshalJSON(t *testing.T) {
+ type testPair struct {
+ item Repository
+ want []byte
+ wantErr error
+ }
+
+ tests := map[string]testPair{
+ "empty": {
+ item: Repository{},
+ want: nil,
+ },
+ "with ID": {
+ item: Repository{
+ Actor: ap.Actor{
+ ID: "https://example.com/1",
+ },
+ Team: nil,
+ },
+ want: []byte(`{"id":"https://example.com/1"}`),
+ },
+ "with Team as IRI": {
+ item: Repository{
+ Team: ap.IRI("https://example.com/1"),
+ },
+ want: []byte(`{"team":"https://example.com/1"}`),
+ },
+ "with Team as IRIs": {
+ item: Repository{
+ Team: ap.ItemCollection{
+ ap.IRI("https://example.com/1"),
+ ap.IRI("https://example.com/2"),
+ },
+ },
+ want: []byte(`{"team":["https://example.com/1","https://example.com/2"]}`),
+ },
+ "with Team as Object": {
+ item: Repository{
+ Team: ap.Object{ID: "https://example.com/1"},
+ },
+ want: []byte(`{"team":{"id":"https://example.com/1"}}`),
+ },
+ "with Team as slice of Objects": {
+ item: Repository{
+ Team: ap.ItemCollection{
+ ap.Object{ID: "https://example.com/1"},
+ ap.Object{ID: "https://example.com/2"},
+ },
+ },
+ want: []byte(`{"team":[{"id":"https://example.com/1"},{"id":"https://example.com/2"}]}`),
+ },
+ }
+
+ for name, tt := range tests {
+ t.Run(name, func(t *testing.T) {
+ got, err := tt.item.MarshalJSON()
+ if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
+ t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("MarshalJSON() got = %q, want %q", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_RepositoryUnmarshalJSON(t *testing.T) {
+ type testPair struct {
+ data []byte
+ want *Repository
+ wantErr error
+ }
+
+ tests := map[string]testPair{
+ "nil": {
+ data: nil,
+ wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")),
+ },
+ "empty": {
+ data: []byte{},
+ wantErr: fmt.Errorf("cannot parse JSON: %w", fmt.Errorf("cannot parse empty string; unparsed tail: %q", "")),
+ },
+ "with Type": {
+ data: []byte(`{"type":"Repository"}`),
+ want: &Repository{
+ Actor: ap.Actor{
+ Type: RepositoryType,
+ },
+ },
+ },
+ "with Type and ID": {
+ data: []byte(`{"id":"https://example.com/1","type":"Repository"}`),
+ want: &Repository{
+ Actor: ap.Actor{
+ ID: "https://example.com/1",
+ Type: RepositoryType,
+ },
+ },
+ },
+ }
+
+ for name, tt := range tests {
+ t.Run(name, func(t *testing.T) {
+ got := new(Repository)
+ err := got.UnmarshalJSON(tt.data)
+ if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
+ t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
+ return
+ }
+ if tt.want != nil && !reflect.DeepEqual(got, tt.want) {
+ jGot, _ := json.Marshal(got)
+ jWant, _ := json.Marshal(tt.want)
+ t.Errorf("UnmarshalJSON() got = %s, want %s", jGot, jWant)
+ }
+ })
+ }
+}
--
2.36.1