634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # astreams
2 |
3 | [](LICENSE)
4 |
5 | > A *hand-crafted* implementation of the [Activity Streams 2.0](https://www.w3.org/TR/activitystreams-core) specification in **Go**, especially suitable for projects implementing [ActivityPub](https://activitypub.rocks).
6 |
7 | ## Usage
8 |
9 | [](https://pkg.go.dev/github.com/MatejLach/astreams#section-documentation)
10 |
11 | Example Activity Streams 2.0 object:
12 |
13 | ```go
14 | object := `
15 | {
16 | "@context": "https://www.w3.org/ns/activitystreams",
17 | "summary": "Martin's recent activities",
18 | "type": "Collection",
19 | "totalItems": 1,
20 | "items" : [
21 | {
22 | "type": "Add",
23 | "published": "2011-02-10T15:04:55Z",
24 | "generator": "http://example.org/activities-app",
25 | "nameMap": {
26 | "en": "Martin added a new image to his album.",
27 | "ga": "Martin phost le fisean nua a albam."
28 | },
29 | "actor": {
30 | "type": "Person",
31 | "id": "http://www.test.example/martin",
32 | "name": "Martin Smith",
33 | "url": "http://example.org/martin",
34 | "image": {
35 | "type": "Link",
36 | "href": "http://example.org/martin/image",
37 | "mediaType": "image/jpeg",
38 | "width": 250,
39 | "height": 250
40 | }
41 | },
42 | "object" : {
43 | "name": "My fluffy cat",
44 | "type": "Image",
45 | "id": "http://example.org/album/máiréad.jpg",
46 | "preview": {
47 | "type": "Link",
48 | "href": "http://example.org/album/máiréad.jpg",
49 | "mediaType": "image/jpeg"
50 | },
51 | "url": [
52 | {
53 | "type": "Link",
54 | "href": "http://example.org/album/máiréad.jpg",
55 | "mediaType": "image/jpeg"
56 | },
57 | {
58 | "type": "Link",
59 | "href": "http://example.org/album/máiréad.png",
60 | "mediaType": "image/png"
61 | }
62 | ]
63 | },
64 | "target": {
65 | "type": "Collection",
66 | "id": "http://example.org/album/",
67 | "nameMap": {
68 | "en": "Martin's Photo Album",
69 | "ga": "Grianghraif Mairtin"
70 | },
71 | "image": {
72 | "type": "Link",
73 | "href": "http://example.org/album/thumbnail.jpg",
74 | "mediaType": "image/jpeg"
75 | }
76 | }
77 | }
78 | ]
79 | }
80 | `
81 | ```
82 |
83 | > Unmarshaling Activity Streams 2.0 JSON objects into Go objects:
84 |
85 | ```go
86 | // Unmarshal from AS 2.0 JSON
87 | var exobj astreams.Collection
88 | err := json.Unmarshal([]byte(object), &exobj)
89 | if err != nil {
90 | log.Println(err)
91 | }
92 | ```
93 | ↑ An example of unmarshaling into an [astreams.Collection](https://godoc.org/github.com/MatejLach/astreams#Collection) object.
94 |
95 | > Marshaling Go objects into Activity Streams 2.0 JSON objects:
96 |
97 | ```go
98 | // Marshal to AS 2.0 JSON
99 | jsB, err := json.Marshal(&exobj) // note the use of a pointer here; astreams MarshalJSON methods are defined on pointer receivers
100 | if err != nil {
101 | log.Println(err)
102 | }
103 | ```
104 | ↑ Marshaling the same [astreams.Collection](https://godoc.org/github.com/MatejLach/astreams#Collection) back into the original ActivityStreams 2.0 JSON is equally as straightforward.
105 |
106 | ## Contributing
107 |
108 | Contributions are welcome. If you've found a bug, or have a feature request, don't hesitate to [open an issue](https://github.com/MatejLach/astreams/issues/new).
109 |
--------------------------------------------------------------------------------
/asvocab_base_types.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import (
4 | "sort"
5 | "time"
6 | )
7 |
8 | // ObjectOrLink is a wrapper type that represents any valid ActivityStreams 2.0 object or link
9 | // Single objects are still presented as slices, but with a single element
10 | type ObjectOrLink []ObjectLinker
11 |
12 | // ObjectOrLinkOrString is a type that can either represent simple string URL(s) or an Object/Link slice
13 | type ObjectOrLinkOrString struct {
14 | Target ObjectOrLink
15 | URL []string
16 | }
17 |
18 | // StringWithOrderedCollectionPage can store a string URL pointing to an OrderedCollectionPage, which can itself be stored in the struct
19 | type StringWithOrderedCollectionPage struct {
20 | OrdCollectionPage OrderedCollectionPage
21 | URL string
22 | }
23 |
24 | // StringWithOrderedCollection can store a string URL pointing to an OrderedCollection, which can itself be stored in the struct
25 | type StringWithOrderedCollection struct {
26 | OrdCollection OrderedCollection
27 | URL string
28 | }
29 |
30 | // StringWithCollectionPage can store a string URL pointing to a CollectionPage, which can itself be stored in the struct
31 | type StringWithCollectionPage struct {
32 | CollectionPage CollectionPage
33 | URL string
34 | }
35 |
36 | // StringWithCollection can store a string URL pointing to a Collection, which can itself be stored in the struct
37 | type StringWithCollection struct {
38 | Collection Collection
39 | URL string
40 | }
41 |
42 | // Icons is a type representing the possible ActivityPub icon values
43 | type Icons struct {
44 | Icons []Icon
45 | URL string
46 | }
47 |
48 | // https://www.w3.org/TR/activitystreams-vocabulary
49 |
50 | // Object represents the base ActivityStreams Object and all of its properties
51 | // Most of the other types extend Object
52 | type Object struct {
53 | AlsoKnownAs *ObjectOrLinkOrString `json:"alsoKnownAs,omitempty"`
54 | ASContext any `json:"@context,omitempty"`
55 | ASLanguage string `json:"@language,omitempty"`
56 | AtID string `json:"@id,omitempty"`
57 | AtType string `json:"@type,omitempty"`
58 | Attachment *ObjectOrLinkOrString `json:"attachment,omitempty"`
59 | AttributedTo *ObjectOrLinkOrString `json:"attributedTo,omitempty"`
60 | Audience *ObjectOrLinkOrString `json:"audience,omitempty"`
61 | Bcc *ObjectOrLinkOrString `json:"bcc,omitempty"`
62 | Bto *ObjectOrLinkOrString `json:"bto,omitempty"`
63 | Cc *ObjectOrLinkOrString `json:"cc,omitempty"`
64 | Content string `json:"content,omitempty"` // needs to be parsed safely ie by https://golang.org/pkg/html/template
65 | ContentMap map[string]string `json:"contentMap,omitempty"`
66 | Context *ObjectOrLinkOrString `json:"context,omitempty"`
67 | Conversation string `json:"conversation,omitempty"`
68 | Duration string `json:"duration,omitempty"` // xsd:duration
69 | EndTime *time.Time `json:"endTime,omitempty"`
70 | Generator *ObjectOrLinkOrString `json:"generator,omitempty"`
71 | Icon *ObjectOrLinkOrString `json:"icon,omitempty"`
72 | ID string `json:"id,omitempty"`
73 | Image *ObjectOrLinkOrString `json:"image,omitempty"`
74 | InReplyTo *ObjectOrLinkOrString `json:"inReplyTo,omitempty"`
75 | Likes *ObjectOrLinkOrString `json:"likes,omitempty"`
76 | Location *ObjectOrLinkOrString `json:"location,omitempty"`
77 | MediaType string `json:"mediaType,omitempty"`
78 | MovedTo *ObjectOrLinkOrString `json:"movedTo,omitempty"`
79 | Name string `json:"name,omitempty"`
80 | NameMap map[string]string `json:"nameMap,omitempty"`
81 | Preview *ObjectOrLinkOrString `json:"preview,omitempty"`
82 | Published *time.Time `json:"published,omitempty"`
83 | Replies *OrderedCollection `json:"replies,omitempty"`
84 | Schema string `json:"schema,omitempty"`
85 | Sensitive bool `json:"sensitive,omitempty"`
86 | Shares *ObjectOrLinkOrString `json:"shares,omitempty"`
87 | Source *ObjectOrLinkOrString `json:"source,omitempty"`
88 | StartTime *time.Time `json:"startTime,omitempty"`
89 | Summary string `json:"summary,omitempty"`
90 | SummaryMap map[string]string `json:"summaryMap,omitempty"`
91 | Tag *ObjectOrLinkOrString `json:"tag,omitempty"`
92 | To *ObjectOrLinkOrString `json:"to,omitempty"`
93 | Type string `json:"type"`
94 | Updated *time.Time `json:"updated,omitempty"`
95 | URL *ObjectOrLinkOrString `json:"url,omitempty"`
96 | }
97 |
98 | // Link represents the base ActivityStreams Link and all of its properties
99 | // Other Link types extend it
100 | type Link struct {
101 | ASContext any `json:"@context,omitempty"`
102 | ASLanguage string `json:"@language,omitempty"`
103 | Height int `json:"height,omitempty"`
104 | Href string `json:"href,omitempty"`
105 | Hreflang string `json:"hreflang,omitempty"`
106 | MediaType string `json:"mediaType,omitempty"`
107 | Name string `json:"name,omitempty"`
108 | NameMap map[string]string `json:"nameMap,omitempty"`
109 | Preview *ObjectOrLinkOrString `json:"preview,omitempty"`
110 | Published *time.Time `json:"published,omitempty"`
111 | Rel []string `json:"rel,omitempty"`
112 | Type string `json:"type"`
113 | Value string `json:"value,omitempty"`
114 | Width int `json:"width,omitempty"`
115 | }
116 |
117 | type Location struct {
118 | Object
119 |
120 | Altitude int `json:"altitude,omitempty"`
121 | Latitude float32 `json:"latitude,omitempty"`
122 | Longitude float32 `json:"longitude,omitempty"`
123 | Units string `json:"units,omitempty"`
124 | }
125 |
126 | type Endpoints struct {
127 | OauthAuthorizationEndpoint string `json:"oauthAuthorizationEndpoint,omitempty"`
128 | OauthTokenEndpoint string `json:"oauthTokenEndpoint,omitempty"`
129 | ProvideClientKey string `json:"provideClientKey,omitempty"`
130 | ProxyUrl string `json:"proxyUrl,omitempty"`
131 | SharedInbox *StringWithOrderedCollection `json:"sharedInbox,omitempty"`
132 | SignClientKey string `json:"signClientKey,omitempty"`
133 | }
134 |
135 | type Icon struct {
136 | Object
137 |
138 | Height int `json:"height"`
139 | Width int `json:"width"`
140 | }
141 |
142 | type Actor struct {
143 | Object
144 |
145 | Devices *StringWithCollection `json:"devices,omitempty"`
146 | Discoverable *bool `json:"discoverable,omitempty"`
147 | Endpoints *Endpoints `json:"endpoints,omitempty"`
148 | Featured *StringWithOrderedCollection `json:"featured,omitempty"`
149 | FeaturedTags *StringWithCollection `json:"featuredTags,omitempty"`
150 | Followers *StringWithOrderedCollection `json:"followers,omitempty"`
151 | Following *StringWithOrderedCollection `json:"following,omitempty"`
152 | Inbox *StringWithOrderedCollection `json:"inbox,omitempty"`
153 | Indexable *bool `json:"indexable,omitempty"`
154 | Liked *StringWithOrderedCollection `json:"liked,omitempty"`
155 | ManuallyApprovesFollowers *bool `json:"manuallyApprovesFollowers,omitempty"`
156 | Memorial *bool `json:"memorial,omitempty"`
157 | Outbox *StringWithOrderedCollection `json:"outbox,omitempty"`
158 | PreferredUsername string `json:"preferredUsername,omitempty"`
159 | PublicKey *PublicKey `json:"publicKey,omitempty"`
160 | Streams *ObjectOrLinkOrString `json:"streams,omitempty"`
161 | }
162 |
163 | type PublicKey struct {
164 | ID string `json:"id"`
165 | Owner string `json:"owner"`
166 | PublicKeyPem string `json:"publicKeyPem"`
167 | }
168 |
169 | type Activity struct {
170 | Object
171 |
172 | Actor *ObjectOrLinkOrString `json:"actor,omitempty"`
173 | ActivityObject *ObjectOrLinkOrString `json:"object,omitempty"` // the 'object' property of Activity
174 | Instrument *ObjectOrLinkOrString `json:"instrument,omitempty"`
175 | Origin *ObjectOrLinkOrString `json:"origin,omitempty"`
176 | Result *ObjectOrLinkOrString `json:"result,omitempty"`
177 | Target *ObjectOrLinkOrString `json:"target,omitempty"`
178 | }
179 |
180 | type IntransitiveActivity struct {
181 | Object
182 |
183 | Actor *ObjectOrLinkOrString `json:"actor,omitempty"`
184 | Instrument *ObjectOrLinkOrString `json:"instrument,omitempty"`
185 | Origin *ObjectOrLinkOrString `json:"origin,omitempty"`
186 | Result *ObjectOrLinkOrString `json:"result,omitempty"`
187 | Target *ObjectOrLinkOrString `json:"target,omitempty"`
188 | }
189 |
190 | type CollectionPage struct {
191 | Collection
192 |
193 | Next *ObjectOrLinkOrString `json:"next,omitempty"`
194 | PartOf *ObjectOrLinkOrString `json:"partOf,omitempty"`
195 | Prev *ObjectOrLinkOrString `json:"prev,omitempty"`
196 | }
197 |
198 | type Collection struct {
199 | Object
200 |
201 | Current *StringWithCollectionPage `json:"current,omitempty"`
202 | First *StringWithCollectionPage `json:"first,omitempty"`
203 | Items *ObjectOrLinkOrString `json:"items,omitempty"`
204 | Last *StringWithCollectionPage `json:"last,omitempty"`
205 | TotalItems int `json:"totalItems,omitempty"`
206 | }
207 |
208 | // OrderedCollectionPage implements https://golang.org/pkg/sort/#Interface
209 | type OrderedCollectionPage struct {
210 | OrderedCollection
211 |
212 | Next *ObjectOrLinkOrString `json:"next,omitempty"`
213 | PartOf *ObjectOrLinkOrString `json:"partOf,omitempty"`
214 | Prev *ObjectOrLinkOrString `json:"prev,omitempty"`
215 | StartIndex uint `json:"startIndex,omitempty"`
216 | }
217 |
218 | // OrderedCollection implements https://golang.org/pkg/sort/#Interface
219 | type OrderedCollection struct {
220 | Object
221 |
222 | Current *StringWithOrderedCollectionPage `json:"current,omitempty"`
223 | First *StringWithOrderedCollectionPage `json:"first,omitempty"`
224 | Last *StringWithOrderedCollectionPage `json:"last,omitempty"`
225 | OrderedItems *ObjectOrLinkOrString `json:"orderedItems"`
226 | TotalItems int `json:"totalItems"`
227 | }
228 |
229 | func (oc OrderedCollection) Len() int {
230 | if len(oc.OrderedItems.URL) > 0 {
231 | return len(oc.OrderedItems.URL)
232 | }
233 |
234 | return len(oc.OrderedItems.Target)
235 | }
236 |
237 | func (oc OrderedCollection) Less(i, j int) bool {
238 | if len(oc.OrderedItems.Target) > 0 {
239 | if oc.OrderedItems.Target[i].IsObject() && oc.OrderedItems.Target[j].IsObject() {
240 | return oc.OrderedItems.Target[j].GetObject().Published.Before(*oc.OrderedItems.Target[i].GetObject().Published)
241 | } else if oc.OrderedItems.Target[i].IsObject() && oc.OrderedItems.Target[j].IsLink() {
242 | return oc.OrderedItems.Target[j].GetObject().Published.Before(*oc.OrderedItems.Target[i].GetLink().Published)
243 | } else if oc.OrderedItems.Target[i].IsLink() && oc.OrderedItems.Target[j].IsLink() {
244 | return oc.OrderedItems.Target[j].GetLink().Published.Before(*oc.OrderedItems.Target[i].GetLink().Published)
245 | } else if oc.OrderedItems.Target[i].IsLink() && oc.OrderedItems.Target[j].IsObject() {
246 | return oc.OrderedItems.Target[j].GetLink().Published.Before(*oc.OrderedItems.Target[i].GetObject().Published)
247 | }
248 | }
249 |
250 | return false
251 | }
252 |
253 | func (oc OrderedCollection) Swap(i, j int) {
254 | if len(oc.OrderedItems.URL) > 0 {
255 | oc.OrderedItems.URL[i], oc.OrderedItems.URL[j] = oc.OrderedItems.URL[j], oc.OrderedItems.URL[i]
256 | } else {
257 | oc.OrderedItems.Target[i], oc.OrderedItems.Target[j] = oc.OrderedItems.Target[j], oc.OrderedItems.Target[i]
258 | }
259 | }
260 |
261 | // SortByUpdated sorts OrderedCollection objects by Updated rather than Published date
262 | func (oc OrderedCollection) SortByUpdated() {
263 | sort.Slice(oc.OrderedItems.Target, func(i, j int) bool {
264 | if len(oc.OrderedItems.Target) > 0 {
265 | if oc.OrderedItems.Target[i].IsObject() && oc.OrderedItems.Target[j].IsObject() {
266 | return oc.OrderedItems.Target[j].GetObject().Updated.Before(*oc.OrderedItems.Target[i].GetObject().Updated)
267 | } else if oc.OrderedItems.Target[i].IsObject() && oc.OrderedItems.Target[j].IsLink() {
268 | return oc.OrderedItems.Target[j].GetObject().Updated.Before(*oc.OrderedItems.Target[i].GetLink().Published)
269 | } else if oc.OrderedItems.Target[i].IsLink() && oc.OrderedItems.Target[j].IsLink() {
270 | return oc.OrderedItems.Target[j].GetLink().Published.Before(*oc.OrderedItems.Target[i].GetLink().Published)
271 | } else if oc.OrderedItems.Target[i].IsLink() && oc.OrderedItems.Target[j].IsObject() {
272 | return oc.OrderedItems.Target[j].GetLink().Published.Before(*oc.OrderedItems.Target[i].GetObject().Updated)
273 | }
274 | }
275 |
276 | return false
277 |
278 | })
279 | }
280 |
--------------------------------------------------------------------------------
/asvocab_ext_types.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import "time"
4 |
5 | // Extended 'Object' types
6 | type Article = Object
7 |
8 | type Document = Object
9 |
10 | type Audio = Document
11 |
12 | type Video = Document
13 |
14 | type Image struct {
15 | Document
16 |
17 | Height int `json:"height,omitempty"`
18 | Width int `json:"width,omitempty"`
19 | }
20 |
21 | type Event = Object
22 |
23 | type Note = Object
24 |
25 | type Page = Document
26 |
27 | // Extended 'Link' types
28 | type Mention = Link
29 |
30 | type Hashtag = Link
31 |
32 | type PropertyValue = Link
33 |
34 | type Place struct {
35 | Location
36 |
37 | Accuracy float32 `json:"accuracy,omitempty"`
38 | Radius float32 `json:"radius,omitempty"`
39 | }
40 |
41 | type Profile struct {
42 | Object
43 |
44 | Describes ObjectOrLinkOrString `json:"describes,omitempty"`
45 | }
46 |
47 | type Tombstone struct {
48 | Object
49 |
50 | Deleted *time.Time `json:"deleted,omitempty"`
51 | FormerType string `json:"formerType,omitempty"`
52 | }
53 |
54 | type Relationship struct {
55 | Object
56 |
57 | Relationship string `json:"relationship,omitempty"`
58 | RelationshipObject *ObjectOrLinkOrString `json:"object,omitempty"`
59 | Subject *ObjectOrLinkOrString `json:"subject,omitempty"`
60 | }
61 |
62 | // Extended 'Actor' types
63 | type Application = Actor
64 |
65 | type Group = Actor
66 |
67 | type Organization = Actor
68 |
69 | type Person = Actor
70 |
71 | type Service = Actor
72 |
73 | // Extended 'Activity' types
74 | type Accept = Activity
75 |
76 | type Add = Activity
77 |
78 | type Announce = Activity
79 |
80 | type Arrive = IntransitiveActivity
81 |
82 | type Ignore = Activity
83 |
84 | type Block = Ignore
85 |
86 | type Create = Activity
87 |
88 | type Delete = Activity
89 |
90 | type Dislike = Activity
91 |
92 | type Flag = Activity
93 |
94 | type Follow = Activity
95 |
96 | type Offer = Activity
97 |
98 | type Invite = Offer
99 |
100 | type Join = Activity
101 |
102 | type Leave = Activity
103 |
104 | type Like = Activity
105 |
106 | type Listen = Activity
107 |
108 | type Move = Activity
109 |
110 | type Question struct {
111 | IntransitiveActivity
112 |
113 | // only OneOf or AnyOf can be set, not both
114 | AnyOf *ObjectOrLinkOrString `json:"anyOf,omitempty"`
115 | Closed *time.Time `json:"closed,omitempty"`
116 | OneOf *ObjectOrLinkOrString `json:"oneOf,omitempty"`
117 | }
118 |
119 | type Reject = Activity
120 |
121 | type Read = Activity
122 |
123 | type Remove = Activity
124 |
125 | type TentativeReject = Reject
126 |
127 | type TentativeAccept = Accept
128 |
129 | type Travel = IntransitiveActivity
130 |
131 | type Undo = Activity
132 |
133 | type Update = Activity
134 |
135 | type View = Activity
136 |
--------------------------------------------------------------------------------
/asvocab_interfaces.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "reflect"
7 | )
8 |
9 | type JsonPayload struct {
10 | Type string `json:"type"`
11 | }
12 |
13 | // ActivityStreamer is a generic type constraint representing all valid Activity Streams 2.0 types
14 | type ActivityStreamer interface {
15 | Object | Link | Actor | Activity | IntransitiveActivity | Collection | CollectionPage |
16 | OrderedCollection | OrderedCollectionPage | Location | Icon | Image | Place | Profile |
17 | Tombstone | Relationship | Question
18 | }
19 |
20 | // DecodePayloadObjectType can be used to check the specific type of unknown JSON payload
21 | /*
22 | payloadMeta, err := DecodePayloadObjectType(payload)
23 | if err != nil {
24 | //
25 | }
26 |
27 | switch payloadMeta.Type {
28 | case "Note":
29 | var note Note
30 | err = json.Unmarshal([]byte(tc), ¬e)
31 | if err != nil {
32 | //
33 | }
34 | case "Offer":
35 | var offer Offer
36 | err = json.Unmarshal([]byte(tc), &offer)
37 | if err != nil {
38 | //
39 | }
40 | case "Person":
41 | var person Person
42 | err = json.Unmarshal([]byte(tc), &person)
43 | if err != nil {
44 | //
45 | }
46 |
47 | if person.Name == "" {
48 | //
49 | }
50 | default:
51 | var obj ObjectOrLinkOrString
52 | err = json.Unmarshal([]byte(tc), &obj)
53 | if err != nil {
54 | //
55 | }
56 | }
57 | */
58 | func DecodePayloadObjectType(payload io.Reader) (JsonPayload, error) {
59 | var payloadType JsonPayload
60 | err := json.NewDecoder(payload).Decode(&payloadType)
61 | if err != nil {
62 | return payloadType, err
63 | }
64 |
65 | return payloadType, nil
66 | }
67 |
68 | // ObjectLinker can be either a (sub)type of 'Object' or a (sub)type of 'Link'
69 | type ObjectLinker interface {
70 | IsObject() bool
71 | IsLink() bool
72 | GetObject() *Object
73 | GetLink() *Link
74 | }
75 |
76 | // ConcreteType returns both, the type name obtained using reflection,
77 | // and the Type property of the Object / Link JSON payload.
78 | // The object's own Type property is going to be more specific, so use that where useful.
79 | func ConcreteType(t ObjectLinker) (reflectType, astreamsType string) {
80 | if t.IsLink() {
81 | return reflect.TypeOf(t).Name(), t.GetLink().Type
82 | }
83 | return reflect.TypeOf(t).Name(), t.GetObject().Type
84 | }
85 |
86 | // Implements 'ObjectLinker' interface for 'Object'
87 | func (o Object) IsObject() bool {
88 | return true
89 | }
90 |
91 | func (o Object) IsLink() bool {
92 | return false
93 | }
94 |
95 | func (o Object) GetObject() *Object {
96 | return &o
97 | }
98 |
99 | func (o Object) GetLink() *Link {
100 | return nil
101 | }
102 |
103 | // Implements 'ObjectLinker' interface for 'Link'
104 | func (l Link) IsObject() bool {
105 | return false
106 | }
107 |
108 | func (l Link) IsLink() bool {
109 | return true
110 | }
111 |
112 | func (l Link) GetObject() *Object {
113 | return nil
114 | }
115 |
116 | func (l Link) GetLink() *Link {
117 | return &l
118 | }
119 |
--------------------------------------------------------------------------------
/asvocab_serialization.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "net/url"
10 | )
11 |
12 | // Implements https://golang.org/pkg/encoding/json/#Unmarshaler
13 | func (ol *ObjectOrLink) UnmarshalJSON(data []byte) error {
14 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'['}) {
15 | var rawJSONSlice []json.RawMessage
16 | if err := json.Unmarshal(data, &rawJSONSlice); err != nil {
17 | return err
18 | }
19 | for _, jsonObj := range rawJSONSlice {
20 | rawJsonMap := make(map[string]interface{})
21 | if err := json.Unmarshal(jsonObj, &rawJsonMap); err != nil {
22 | return err
23 | }
24 | if key, ok := rawJsonMap["type"]; ok {
25 | if objectType, ok := key.(string); ok {
26 | astype, err := decodeASType(jsonObj, objectType)
27 | if err != nil {
28 | return err
29 | }
30 | *ol = append(*ol, astype)
31 | }
32 | } else {
33 | // assuming a base Object if there's not a more specific type
34 | // a Link without an explicit type is considered invalid
35 | asObject := Object{}
36 | if err := json.Unmarshal(data, &asObject); err != nil {
37 | return err
38 | }
39 | *ol = append(*ol, asObject)
40 | }
41 | }
42 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
43 | rawJsonMap := make(map[string]interface{})
44 | if err := json.Unmarshal(data, &rawJsonMap); err != nil {
45 | return err
46 | }
47 | if key, ok := rawJsonMap["type"]; ok {
48 | if objectType, ok := key.(string); ok {
49 | astype, err := decodeASType(data, objectType)
50 | if err != nil {
51 | return err
52 | }
53 | *ol = append(*ol, astype)
54 | }
55 | } else {
56 | // assuming a base Object if there's not a more specific type
57 | // a Link without an explicit type is considered invalid
58 | asObject := Object{}
59 | if err := json.Unmarshal(data, &asObject); err != nil {
60 | return err
61 | }
62 | *ol = append(*ol, asObject)
63 | }
64 | }
65 | return nil
66 | }
67 |
68 | // Implements https://golang.org/pkg/encoding/json/#Unmarshaler
69 | func (ics *Icons) UnmarshalJSON(data []byte) error {
70 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'['}) {
71 | var rawJSONSlice []json.RawMessage
72 | if err := json.Unmarshal(data, &rawJSONSlice); err != nil {
73 | return err
74 | }
75 | for _, bts := range rawJSONSlice {
76 | if bytes.HasPrefix(bytes.TrimSpace(bts), []byte{'"'}) {
77 | if err := json.Unmarshal(bts, &ics.URL); err != nil {
78 | return err
79 | }
80 | if _, err := url.ParseRequestURI(ics.URL); err != nil {
81 | return err
82 | }
83 | }
84 | if bytes.HasPrefix(bytes.TrimSpace(bts), []byte{'{'}) {
85 | decoder := json.NewDecoder(bytes.NewReader(bts))
86 | icon := Icon{}
87 | if err := decoder.Decode(&icon); err != nil {
88 | return err
89 | }
90 | ics.Icons = append(ics.Icons, icon)
91 | }
92 | }
93 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
94 | decoder := json.NewDecoder(bytes.NewReader(data))
95 | icon := Icon{}
96 | if err := decoder.Decode(&icon); err != nil {
97 | return err
98 | }
99 | ics.Icons = append(ics.Icons, icon)
100 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'"'}) {
101 | if err := json.Unmarshal(data, &ics.URL); err != nil {
102 | return err
103 | }
104 | if _, err := url.ParseRequestURI(ics.URL); err != nil {
105 | return err
106 | }
107 | }
108 | return nil
109 | }
110 |
111 | // Implements https://golang.org/pkg/encoding/json/#Unmarshaler
112 | func (ols *ObjectOrLinkOrString) UnmarshalJSON(data []byte) error {
113 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'['}) {
114 | var rawJSONSlice []json.RawMessage
115 | if err := json.Unmarshal(data, &rawJSONSlice); err != nil {
116 | return err
117 | }
118 | for _, bts := range rawJSONSlice {
119 | if bytes.HasPrefix(bytes.TrimSpace(bts), []byte{'"'}) {
120 | var strlink string
121 | if err := json.Unmarshal(bts, &strlink); err != nil {
122 | return err
123 | }
124 | if _, err := url.ParseRequestURI(strlink); err != nil {
125 | return err
126 | }
127 | ols.URL = append(ols.URL, strlink)
128 | }
129 | if bytes.HasPrefix(bytes.TrimSpace(bts), []byte{'{'}) {
130 | if err := json.Unmarshal(bts, &ols.Target); err != nil {
131 | return err
132 | }
133 | }
134 | }
135 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
136 | if err := json.Unmarshal(data, &ols.Target); err != nil {
137 | return err
138 | }
139 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'"'}) {
140 | var strlink string
141 | if err := json.Unmarshal(data, &strlink); err != nil {
142 | return err
143 | }
144 | if _, err := url.ParseRequestURI(strlink); err != nil {
145 | return err
146 | }
147 | ols.URL = append(ols.URL, strlink)
148 | }
149 | return nil
150 | }
151 |
152 | // Implements https://golang.org/pkg/encoding/json/#Unmarshaler
153 | func (socp *StringWithOrderedCollectionPage) UnmarshalJSON(data []byte) error {
154 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'"'}) {
155 | if err := json.Unmarshal(data, &socp.URL); err != nil {
156 | return err
157 | }
158 | if _, err := url.ParseRequestURI(socp.URL); err != nil {
159 | return err
160 | }
161 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
162 | if err := json.Unmarshal(data, &socp.OrdCollectionPage); err != nil {
163 | return err
164 | }
165 | }
166 | return nil
167 | }
168 |
169 | // Implements https://golang.org/pkg/encoding/json/#Unmarshaler
170 | func (soc *StringWithOrderedCollection) UnmarshalJSON(data []byte) error {
171 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'"'}) {
172 | if err := json.Unmarshal(data, &soc.URL); err != nil {
173 | return err
174 | }
175 | if _, err := url.ParseRequestURI(soc.URL); err != nil {
176 | return err
177 | }
178 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
179 | if err := json.Unmarshal(data, &soc.OrdCollection); err != nil {
180 | return err
181 | }
182 | }
183 | return nil
184 | }
185 |
186 | // Implements https://golang.org/pkg/encoding/json/#Unmarshaler
187 | func (scp *StringWithCollectionPage) UnmarshalJSON(data []byte) error {
188 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'"'}) {
189 | if err := json.Unmarshal(data, &scp.URL); err != nil {
190 | return err
191 | }
192 | if _, err := url.ParseRequestURI(scp.URL); err != nil {
193 | return err
194 | }
195 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
196 | if err := json.Unmarshal(data, &scp.CollectionPage); err != nil {
197 | return err
198 | }
199 | }
200 | return nil
201 | }
202 |
203 | // Implements https://golang.org/pkg/encoding/json/#Unmarshaler
204 | func (sc *StringWithCollection) UnmarshalJSON(data []byte) error {
205 | if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'"'}) {
206 | if err := json.Unmarshal(data, &sc.URL); err != nil {
207 | return err
208 | }
209 | if _, err := url.ParseRequestURI(sc.URL); err != nil {
210 | return err
211 | }
212 | } else if bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
213 | if err := json.Unmarshal(data, &sc.Collection); err != nil {
214 | return err
215 | }
216 | }
217 | return nil
218 | }
219 |
220 | // Implements https://golang.org/pkg/encoding/json/#Marshal
221 | func (ol *ObjectOrLink) MarshalJSON() ([]byte, error) {
222 | if len(*ol) > 0 {
223 | if len(*ol) == 1 {
224 | objl := *ol
225 | target, err := json.Marshal(objl[0])
226 | if err != nil {
227 | return []byte{}, err
228 | }
229 |
230 | return target, nil
231 | }
232 |
233 | marshalB := bytes.NewBufferString("[")
234 | for idx, target := range *ol {
235 | encoded, err := encodeASType(target)
236 | if err != nil {
237 | return []byte{}, err
238 | }
239 | marshalB.Write(encoded)
240 | if idx < len(*ol)-1 {
241 | _, err := marshalB.WriteString(",")
242 | if err != nil {
243 | return []byte{}, err
244 | }
245 | }
246 | }
247 | if _, err := marshalB.WriteString("]"); err != nil {
248 | return []byte{}, err
249 | }
250 | return marshalB.Bytes(), nil
251 | }
252 | return bytes.NewBufferString("[]").Bytes(), nil
253 | }
254 |
255 | // Implements https://golang.org/pkg/encoding/json/#Marshal
256 | func (ols *ObjectOrLinkOrString) MarshalJSON() ([]byte, error) {
257 | var uri []byte
258 | var target []byte
259 | var err error
260 |
261 | if len(ols.URL) > 0 && len(ols.Target) > 0 {
262 | for _, uri := range ols.URL {
263 | if _, err = url.ParseRequestURI(uri); err != nil {
264 | return []byte{}, err
265 | }
266 | }
267 | if len(ols.URL) == 1 {
268 | uri, err = json.Marshal(ols.URL[0])
269 | if err != nil {
270 | return []byte{}, err
271 | }
272 | } else {
273 | var uris string
274 | for idx, link := range ols.URL {
275 | if idx < len(ols.URL)-1 {
276 | uris += fmt.Sprintf("\"%s\",\n", link)
277 | } else {
278 | uris += fmt.Sprintf("\"%s\"\n", link)
279 | }
280 | }
281 | uri = bytes.NewBufferString(uris).Bytes()
282 | }
283 | if len(ols.Target) == 1 {
284 | target, err = json.Marshal(ols.Target[0])
285 | if err != nil {
286 | return []byte{}, err
287 | }
288 | } else {
289 | target, err = json.Marshal(&ols.Target)
290 | if err != nil {
291 | return []byte{}, err
292 | }
293 | }
294 | return bytes.NewBufferString(fmt.Sprintf("[%s,%s]", string(uri), string(target))).Bytes(), nil
295 | } else if len(ols.URL) == 0 && len(ols.Target) > 0 {
296 | if len(ols.Target) > 1 {
297 | target, err = json.Marshal(&ols.Target)
298 | if err != nil {
299 | return []byte{}, err
300 | }
301 | return target, nil
302 | }
303 | target, err = json.Marshal(ols.Target[0])
304 | if err != nil {
305 | return []byte{}, err
306 | }
307 | return target, nil
308 | } else if len(ols.URL) > 0 && len(ols.Target) == 0 {
309 | for _, uri := range ols.URL {
310 | if _, err = url.ParseRequestURI(uri); err != nil {
311 | return []byte{}, err
312 | }
313 | }
314 | if len(ols.URL) == 1 {
315 | uri, err = json.Marshal(ols.URL[0])
316 | if err != nil {
317 | return []byte{}, err
318 | }
319 | return uri, nil
320 | }
321 | uri, err = json.Marshal(ols.URL)
322 | if err != nil {
323 | return []byte{}, err
324 | }
325 | return uri, nil
326 | }
327 |
328 | if len(ols.Target) == 0 || len(ols.URL) == 0 {
329 | return bytes.NewBufferString("[]").Bytes(), nil
330 | }
331 |
332 | return []byte{}, errors.New("unrecognised content, cannot Marshal ObjectOrLinkOrString, use nil for empty value")
333 | }
334 |
335 | // Implements https://golang.org/pkg/encoding/json/#Marshal
336 | func (colp *CollectionPage) MarshalJSON() ([]byte, error) {
337 | encodedBase, err := json.Marshal(&colp.Collection)
338 | if err != nil {
339 | return []byte{}, err
340 | }
341 | encodedBase = bytes.TrimSuffix(encodedBase, []byte("}"))
342 |
343 | encodedPage, err := json.Marshal(struct {
344 | Next *ObjectOrLinkOrString `json:"next,omitempty"`
345 | PartOf *ObjectOrLinkOrString `json:"partOf,omitempty"`
346 | Prev *ObjectOrLinkOrString `json:"prev,omitempty"`
347 | }{
348 | Next: colp.Next,
349 | PartOf: colp.PartOf,
350 | Prev: colp.Prev,
351 | })
352 | if err != nil {
353 | return []byte{}, err
354 | }
355 | encodedPage = bytes.TrimPrefix(encodedPage, []byte("{"))
356 |
357 | return bytes.NewBufferString(fmt.Sprintf("%s, %s", encodedBase, encodedPage)).Bytes(), nil
358 | }
359 |
360 | // Implements https://golang.org/pkg/encoding/json/#Marshal
361 | func (col *Collection) MarshalJSON() ([]byte, error) {
362 | encodedBase, err := json.Marshal(ObjectLinker(col).GetObject())
363 | if err != nil {
364 | return []byte{}, err
365 | }
366 | encodedBase = bytes.TrimSuffix(encodedBase, []byte("}"))
367 |
368 | encodedCollection, err := json.Marshal(struct {
369 | TotalItems int `json:"totalItems,omitempty"`
370 | Current *StringWithCollectionPage `json:"current,omitempty"`
371 | First *StringWithCollectionPage `json:"first,omitempty"`
372 | Last *StringWithCollectionPage `json:"last,omitempty"`
373 | }{
374 | TotalItems: col.TotalItems,
375 | Current: col.Current,
376 | First: col.First,
377 | Last: col.Last,
378 | })
379 | if err != nil {
380 | return []byte{}, err
381 | }
382 | encodedCollection = bytes.TrimPrefix(encodedCollection, []byte("{"))
383 |
384 | if len(col.Items.URL) > 0 {
385 | encodedCollection = bytes.TrimSuffix(encodedCollection, []byte("}"))
386 | encodedBytes, err := json.Marshal(&col.Items.URL)
387 | if err != nil {
388 | return []byte{}, err
389 | }
390 |
391 | return bytes.NewBufferString(fmt.Sprintf("%s, %s, \"items\": \n%s}", encodedBase, encodedCollection, encodedBytes)).Bytes(), nil
392 | }
393 |
394 | if len(col.Items.Target) > 0 {
395 | encodedCollection = bytes.TrimSuffix(encodedCollection, []byte("}"))
396 | encodedBytes, err := json.Marshal(&col.Items.Target)
397 | if err != nil {
398 | return []byte{}, err
399 | }
400 |
401 | return bytes.NewBufferString(fmt.Sprintf("%s, %s, \"items\": [\n%s\n]\n}", encodedBase, encodedCollection, encodedBytes)).Bytes(), nil
402 | }
403 |
404 | return []byte{}, nil
405 | }
406 |
407 | // Implements https://golang.org/pkg/encoding/json/#Marshal
408 | func (ocolp *OrderedCollectionPage) MarshalJSON() ([]byte, error) {
409 | encodedBase, err := json.Marshal(&ocolp.OrderedCollection)
410 | if err != nil {
411 | return []byte{}, err
412 | }
413 | encodedBase = bytes.TrimSuffix(encodedBase, []byte("}"))
414 |
415 | encodedPage, err := json.Marshal(struct {
416 | Next *ObjectOrLinkOrString `json:"next,omitempty"`
417 | PartOf *ObjectOrLinkOrString `json:"partOf,omitempty"`
418 | Prev *ObjectOrLinkOrString `json:"prev,omitempty"`
419 | StartIndex uint `json:"startIndex,omitempty"`
420 | }{
421 | Next: ocolp.Next,
422 | PartOf: ocolp.PartOf,
423 | Prev: ocolp.Prev,
424 | StartIndex: ocolp.StartIndex,
425 | })
426 | if err != nil {
427 | return []byte{}, err
428 | }
429 | encodedPage = bytes.TrimPrefix(encodedPage, []byte("{"))
430 |
431 | return bytes.NewBufferString(fmt.Sprintf("%s, %s", encodedBase, encodedPage)).Bytes(), nil
432 | }
433 |
434 | // Implements https://golang.org/pkg/encoding/json/#Marshal
435 | func (oc *OrderedCollection) MarshalJSON() ([]byte, error) {
436 | encodedBase, err := json.Marshal(ObjectLinker(oc).GetObject())
437 | if err != nil {
438 | return []byte{}, err
439 | }
440 | encodedBase = bytes.TrimSuffix(encodedBase, []byte("}"))
441 |
442 | encodedOrderedCollection, err := json.Marshal(struct {
443 | TotalItems int `json:"totalItems,omitempty"`
444 | Current *StringWithOrderedCollectionPage `json:"current,omitempty"`
445 | First *StringWithOrderedCollectionPage `json:"first,omitempty"`
446 | Last *StringWithOrderedCollectionPage `json:"last,omitempty"`
447 | }{
448 | TotalItems: oc.TotalItems,
449 | Current: oc.Current,
450 | First: oc.First,
451 | Last: oc.Last,
452 | })
453 | if err != nil {
454 | return []byte{}, err
455 | }
456 | encodedOrderedCollection = bytes.TrimPrefix(encodedOrderedCollection, []byte("{"))
457 |
458 | if len(oc.OrderedItems.URL) > 0 {
459 | encodedOrderedCollection = bytes.TrimSuffix(encodedOrderedCollection, []byte("}"))
460 | encodedBytes, err := json.Marshal(&oc.OrderedItems.URL)
461 | if err != nil {
462 | return []byte{}, err
463 | }
464 |
465 | return bytes.NewBufferString(fmt.Sprintf("%s, %s, \"orderedItems\": \n%s}", encodedBase, encodedOrderedCollection, encodedBytes)).Bytes(), nil
466 | }
467 |
468 | if len(oc.OrderedItems.Target) > 0 {
469 | encodedOrderedCollection = bytes.TrimSuffix(encodedOrderedCollection, []byte("}"))
470 | encodedBytes, err := json.Marshal(&oc.OrderedItems.Target)
471 | if err != nil {
472 | return []byte{}, err
473 | }
474 |
475 | return bytes.NewBufferString(fmt.Sprintf("%s, %s, \"orderedItems\": [\n%s\n]\n}", encodedBase, encodedOrderedCollection, encodedBytes)).Bytes(), nil
476 | }
477 |
478 | return []byte{}, nil
479 | }
480 |
481 | // Implements https://golang.org/pkg/encoding/json/#Marshal
482 | func (ics *Icons) MarshalJSON() ([]byte, error) {
483 | if ics.URL != "" && len(ics.Icons) > 0 {
484 | if _, err := url.ParseRequestURI(ics.URL); err != nil {
485 | return []byte{}, err
486 | }
487 | uri, err := json.Marshal(ics.URL)
488 | if err != nil {
489 | return []byte{}, err
490 | }
491 | var icn []byte
492 | if len(ics.Icons) > 1 {
493 | icn, err = json.Marshal(ics.Icons)
494 | } else {
495 | icn, err = json.Marshal(ics.Icons[0])
496 | }
497 | if err != nil {
498 | return []byte{}, err
499 | }
500 | return bytes.NewBufferString(fmt.Sprintf("[%s,%s]", string(uri), string(icn))).Bytes(), nil
501 | } else if ics.URL == "" && len(ics.Icons) > 0 {
502 | if len(ics.Icons) > 1 {
503 | icn, err := json.Marshal(ics.Icons)
504 | if err != nil {
505 | return []byte{}, err
506 | }
507 | return icn, nil
508 | }
509 | icn, err := json.Marshal(ics.Icons[0])
510 | if err != nil {
511 | return []byte{}, err
512 | }
513 | return icn, nil
514 | } else if ics.URL != "" && len(ics.Icons) == 0 {
515 | if _, err := url.ParseRequestURI(ics.URL); err != nil {
516 | return []byte{}, err
517 | }
518 | uri, err := json.Marshal(ics.URL)
519 | if err != nil {
520 | return []byte{}, err
521 | }
522 | return uri, nil
523 | }
524 | return []byte{}, errors.New("unrecognised content, cannot Marshal icon, use nil for empty value")
525 | }
526 |
527 | // Implements https://golang.org/pkg/encoding/json/#Marshal
528 | func (socp *StringWithOrderedCollectionPage) MarshalJSON() ([]byte, error) {
529 | if len(socp.URL) > 0 {
530 | jsonb, err := json.Marshal(socp.URL)
531 | if err != nil {
532 | return []byte{}, err
533 | }
534 | return jsonb, nil
535 | } else if socp.OrdCollectionPage.PartOf != nil {
536 | jsonb, err := json.Marshal(socp.OrdCollectionPage)
537 | if err != nil {
538 | return []byte{}, err
539 | }
540 | return jsonb, nil
541 | }
542 | return []byte{}, errors.New("unrecognised content, cannot Marshal StringWithOrderedCollectionPage, use nil for empty value")
543 | }
544 |
545 | // Implements https://golang.org/pkg/encoding/json/#Marshal
546 | func (soc *StringWithOrderedCollection) MarshalJSON() ([]byte, error) {
547 | if len(soc.URL) > 0 {
548 | jsonb, err := json.Marshal(soc.URL)
549 | if err != nil {
550 | return []byte{}, err
551 | }
552 | return jsonb, nil
553 | } else if soc.OrdCollection.TotalItems > 0 {
554 | jsonb, err := json.Marshal(soc.OrdCollection)
555 | if err != nil {
556 | return []byte{}, err
557 | }
558 | return jsonb, nil
559 | }
560 | return []byte{}, errors.New("unrecognised content, cannot Marshal StringWithOrderedCollection, use nil for empty value")
561 | }
562 |
563 | // Implements https://golang.org/pkg/encoding/json/#Marshal
564 | func (scp *StringWithCollectionPage) MarshalJSON() ([]byte, error) {
565 | if len(scp.URL) > 0 {
566 | jsonb, err := json.Marshal(scp.URL)
567 | if err != nil {
568 | return []byte{}, err
569 | }
570 | return jsonb, nil
571 | } else if scp.CollectionPage.PartOf != nil {
572 | jsonb, err := json.Marshal(scp.CollectionPage)
573 | if err != nil {
574 | return []byte{}, err
575 | }
576 | return jsonb, nil
577 | }
578 | return []byte{}, errors.New("unrecognised content, cannot Marshal StringWithCollectionPage, use nil for empty value")
579 | }
580 |
581 | // Implements https://golang.org/pkg/encoding/json/#Marshal
582 | func (sc *StringWithCollection) MarshalJSON() ([]byte, error) {
583 | if len(sc.URL) > 0 {
584 | jsonb, err := json.Marshal(sc.URL)
585 | if err != nil {
586 | return []byte{}, err
587 | }
588 | return jsonb, nil
589 | } else if sc.Collection.TotalItems > 0 {
590 | jsonb, err := json.Marshal(sc.Collection)
591 | if err != nil {
592 | return []byte{}, err
593 | }
594 | return jsonb, nil
595 | }
596 | return []byte{}, errors.New("unrecognised content, cannot Marshal StringWithCollection, use nil for empty value")
597 | }
598 |
599 | // DecodeJSON is the generic unmarshaller for any valid ActivityStreams 2.0 object
600 | func DecodeJSON[T ActivityStreamer](jsonPayload io.Reader) (T, error) {
601 | var result T
602 |
603 | err := json.NewDecoder(jsonPayload).Decode(&result)
604 | if err != nil {
605 | return result, err
606 | }
607 |
608 | return result, nil
609 | }
610 |
611 | // EncodeJSON is the generic marshaller for any valid ActivityStreams 2.0 object
612 | func EncodeJSON[T ActivityStreamer](toEncode T) ([]byte, error) {
613 | return json.Marshal(&toEncode)
614 | }
615 |
616 | // Implements https://golang.org/pkg/fmt/#Stringer
617 | func (ics *Icons) String() string {
618 | return fmt.Sprintf("%+v", *ics)
619 | }
620 |
621 | // Implements https://golang.org/pkg/fmt/#Stringer
622 | func (ols *ObjectOrLinkOrString) String() string {
623 | return fmt.Sprintf("%+v", *ols)
624 | }
625 |
626 | // Implements https://golang.org/pkg/fmt/#Stringer
627 | func (loc *Location) String() string {
628 | return fmt.Sprintf("%+v", *loc)
629 | }
630 |
631 | // Implements https://golang.org/pkg/fmt/#Stringer
632 | func (oc *OrderedCollection) String() string {
633 | return fmt.Sprintf("%+v", *oc)
634 | }
635 |
636 | // Implements https://golang.org/pkg/fmt/#Stringer
637 | func (soc *StringWithOrderedCollection) String() string {
638 | return fmt.Sprintf("%+v", *soc)
639 | }
640 |
641 | // Implements https://golang.org/pkg/fmt/#Stringer
642 | func (sc *StringWithCollection) String() string {
643 | return fmt.Sprintf("%+v", *sc)
644 | }
645 |
--------------------------------------------------------------------------------
/asvocab_serialization_marshal_test.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestMarshalJSON_OrderedCollectionPage(t *testing.T) {
12 | want := []string{
13 | `{
14 | "@context": "https://www.w3.org/ns/activitystreams",
15 | "id": "https://social.matej-lach.me/users/MatejLach/following?page=1",
16 | "type": "OrderedCollectionPage",
17 | "totalItems": 329,
18 | "orderedItems": [
19 | "https://suma-ev.social/users/christian",
20 | "https://mastodon.social/users/Daojoan",
21 | "https://23.social/users/pantierra",
22 | "https://famichiki.jp/users/tsturm",
23 | "https://bikeshed.vibber.net/users/brooke",
24 | "https://idlethumbs.social/users/ja2ke",
25 | "https://oisaur.com/users/renchap",
26 | "https://social.alternativebit.fr/users/picnoir",
27 | "https://swiss.social/users/lx",
28 | "https://social.lol/users/whakkee",
29 | "https://oldbytes.space/users/aperezdc",
30 | "https://tech.lgbt/users/jaycie"
31 | ],
32 | "next": "https://social.matej-lach.me/users/MatejLach/following?page=2",
33 | "partOf": "https://social.matej-lach.me/users/MatejLach/following"
34 | }`,
35 | }
36 |
37 | for idx := range want {
38 | // Prepare the object to marshal first
39 | orderedCollectionPage := OrderedCollectionPage{}
40 | err := json.Unmarshal([]byte(want[idx]), &orderedCollectionPage)
41 | require.NoError(t, err)
42 | got, err := json.MarshalIndent(&orderedCollectionPage, "", " ")
43 | require.NoError(t, err)
44 |
45 | assert.JSONEq(t, want[idx], string(got))
46 | }
47 | }
48 |
49 | func TestMarshalJSON_Collection(t *testing.T) {
50 | want := []string{
51 | `
52 | {
53 | "@context": "https://www.w3.org/ns/activitystreams",
54 | "type": "Collection",
55 | "summary": "Martin's recent activities",
56 | "totalItems": 1,
57 | "items": [
58 | {
59 | "type": "Add",
60 | "nameMap": {
61 | "en": "Martin added a new image to his album.",
62 | "ga": "Martin phost le fisean nua a albam."
63 | },
64 | "generator": "http://example.org/activities-app",
65 | "published": "2011-02-10T15:04:55Z",
66 | "actor": {
67 | "id": "http://www.test.example/martin",
68 | "type": "Person",
69 | "name": "Martin Smith",
70 | "image": {
71 | "type": "Link",
72 | "href": "http://example.org/martin/image",
73 | "mediaType": "image/jpeg",
74 | "height": 250,
75 | "width": 250
76 | },
77 | "url": "http://example.org/martin"
78 | },
79 | "object": {
80 | "id": "http://example.org/album/máiréad.jpg",
81 | "type": "Image",
82 | "name": "My fluffy cat",
83 | "preview": {
84 | "type": "Link",
85 | "href": "http://example.org/album/máiréad.jpg",
86 | "mediaType": "image/jpeg"
87 | },
88 | "url": [
89 | {
90 | "type": "Link",
91 | "href": "http://example.org/album/máiréad.jpg",
92 | "mediaType": "image/jpeg"
93 | },
94 | {
95 | "type": "Link",
96 | "href": "http://example.org/album/máiréad.png",
97 | "mediaType": "image/png"
98 | }
99 | ]
100 | },
101 | "target": {
102 | "id": "http://example.org/album/",
103 | "type": "Collection",
104 | "nameMap": {
105 | "en": "Martin's Photo Album",
106 | "ga": "Grianghraif Mairtin"
107 | },
108 | "image": {
109 | "type": "Link",
110 | "href": "http://example.org/album/thumbnail.jpg",
111 | "mediaType": "image/jpeg"
112 | }
113 | }
114 | }
115 | ]
116 | }
117 | `,
118 | }
119 |
120 | for idx := range want {
121 | // Prepare the object to marshal first
122 | collection := Collection{}
123 | err := json.Unmarshal([]byte(want[idx]), &collection)
124 | require.NoError(t, err)
125 | got, err := json.MarshalIndent(&collection, "", " ")
126 | require.NoError(t, err)
127 |
128 | assert.JSONEq(t, want[idx], string(got))
129 | }
130 | }
131 |
132 | func TestMarshalJSON_Actor(t *testing.T) {
133 | want := []string{
134 | `{
135 | "@context": [
136 | "https://www.w3.org/ns/activitystreams",
137 | "https://w3id.org/security/v1",
138 | {
139 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
140 | "toot": "http://joinmastodon.org/ns#",
141 | "featured": {
142 | "@id": "toot:featured",
143 | "@type": "@id"
144 | },
145 | "featuredTags": {
146 | "@id": "toot:featuredTags",
147 | "@type": "@id"
148 | },
149 | "alsoKnownAs": {
150 | "@id": "as:alsoKnownAs",
151 | "@type": "@id"
152 | },
153 | "movedTo": {
154 | "@id": "as:movedTo",
155 | "@type": "@id"
156 | },
157 | "schema": "http://schema.org#",
158 | "PropertyValue": "schema:PropertyValue",
159 | "value": "schema:value",
160 | "discoverable": "toot:discoverable",
161 | "Device": "toot:Device",
162 | "Ed25519Signature": "toot:Ed25519Signature",
163 | "Ed25519Key": "toot:Ed25519Key",
164 | "Curve25519Key": "toot:Curve25519Key",
165 | "EncryptedMessage": "toot:EncryptedMessage",
166 | "publicKeyBase64": "toot:publicKeyBase64",
167 | "deviceId": "toot:deviceId",
168 | "claim": {
169 | "@type": "@id",
170 | "@id": "toot:claim"
171 | },
172 | "fingerprintKey": {
173 | "@type": "@id",
174 | "@id": "toot:fingerprintKey"
175 | },
176 | "identityKey": {
177 | "@type": "@id",
178 | "@id": "toot:identityKey"
179 | },
180 | "devices": {
181 | "@type": "@id",
182 | "@id": "toot:devices"
183 | },
184 | "messageFranking": "toot:messageFranking",
185 | "messageType": "toot:messageType",
186 | "cipherText": "toot:cipherText",
187 | "suspended": "toot:suspended",
188 | "memorial": "toot:memorial",
189 | "indexable": "toot:indexable",
190 | "Hashtag": "as:Hashtag",
191 | "focalPoint": {
192 | "@container": "@list",
193 | "@id": "toot:focalPoint"
194 | }
195 | }
196 | ],
197 | "id": "https://social.matej-lach.me/users/MatejLach",
198 | "type": "Person",
199 | "following": "https://social.matej-lach.me/users/MatejLach/following",
200 | "followers": "https://social.matej-lach.me/users/MatejLach/followers",
201 | "inbox": "https://social.matej-lach.me/users/MatejLach/inbox",
202 | "outbox": "https://social.matej-lach.me/users/MatejLach/outbox",
203 | "featured": "https://social.matej-lach.me/users/MatejLach/collections/featured",
204 | "featuredTags": "https://social.matej-lach.me/users/MatejLach/collections/tags",
205 | "preferredUsername": "MatejLach",
206 | "name": "Matej Ľach ✅",
207 | "summary": "Free software enthusiast, #golang, #rustlang, #swiftlang . Working on a question/answer #ActivityPub server. #systemd aficionado :-)
",
208 | "url": "https://social.matej-lach.me/@MatejLach",
209 | "manuallyApprovesFollowers": false,
210 | "discoverable": true,
211 | "indexable": false,
212 | "published": "2017-10-26T00:00:00Z",
213 | "memorial": false,
214 | "devices": "https://social.matej-lach.me/users/MatejLach/collections/devices",
215 | "publicKey": {
216 | "id": "https://social.matej-lach.me/users/MatejLach#main-key",
217 | "owner": "https://social.matej-lach.me/users/MatejLach",
218 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm9ir9e6zclOuWFN+mrtV\nqjz+qNYdgYYA7TGLWf4heNjdQRP0wOTjSY5mXST95aKY8h30LSyPAI01/GLrPsme\nm+uSgr59VX3dyvDHYiOSSuMl8lkFMGthxWlKXcUb7vFcJnHRr4q5TUZ+J4wjEksw\nWmqK5me2Lnt+wVQnWXplfnknVJaZvCPEfRWVVu53lgSfTkF+rO4Bl6osw2TrIj3T\n8MoOpnGKXSTGuL86cAQAkxbJcqkFeM/ksojVBqVpGn+xdQuOf62j6mFzZl4B9wfo\nKah+O7zbgvJEHhSmvSZlo8b9YJre0YbAJBCcQnAyj3m2oSEwIKz20jjTapsiAJFS\nTwIDAQAB\n-----END PUBLIC KEY-----\n"
219 | },
220 | "tag": [
221 | {
222 | "type": "Hashtag",
223 | "href": "https://social.matej-lach.me/tags/golang",
224 | "name": "#golang"
225 | },
226 | {
227 | "type": "Hashtag",
228 | "href": "https://social.matej-lach.me/tags/activitypub",
229 | "name": "#activitypub"
230 | },
231 | {
232 | "type": "Hashtag",
233 | "href": "https://social.matej-lach.me/tags/rustlang",
234 | "name": "#rustlang"
235 | },
236 | {
237 | "type": "Hashtag",
238 | "href": "https://social.matej-lach.me/tags/swiftlang",
239 | "name": "#swiftlang"
240 | },
241 | {
242 | "type": "Hashtag",
243 | "href": "https://social.matej-lach.me/tags/systemd",
244 | "name": "#systemd"
245 | }
246 | ],
247 | "attachment": [],
248 | "endpoints": {
249 | "sharedInbox": "https://social.matej-lach.me/inbox"
250 | },
251 | "icon": {
252 | "type": "Image",
253 | "mediaType": "image/png",
254 | "url": "https://social.matej-lach.me/system/accounts/avatars/000/000/001/original/6e9242b03795bf80.png"
255 | },
256 | "image": {
257 | "type": "Image",
258 | "mediaType": "image/png",
259 | "url": "https://social.matej-lach.me/system/accounts/headers/000/000/001/original/f18240c45b0ac254.png"
260 | }
261 | }`,
262 | }
263 |
264 | for idx := range want {
265 | // Prepare the object to marshal first
266 | person := Person{}
267 | err := json.Unmarshal([]byte(want[idx]), &person)
268 | require.NoError(t, err)
269 | got, err := json.MarshalIndent(&person, "", " ")
270 | require.NoError(t, err)
271 |
272 | assert.JSONEq(t, want[idx], string(got))
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/asvocab_serialization_unmarshal_test.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import (
4 | "encoding/json"
5 | "sort"
6 | "strings"
7 | "testing"
8 | "time"
9 |
10 | "github.com/stretchr/testify/assert"
11 | "github.com/stretchr/testify/require"
12 | )
13 |
14 | func TestUnmarshalJSON_ObjectOrLink(t *testing.T) {
15 | var obj ObjectOrLink
16 | var err error
17 | testCases := []string{
18 | `{
19 | "@context": "https://www.w3.org/ns/activitystreams",
20 | "type": "Object",
21 | "id": "http://www.test.example/object/1",
22 | "name": "A Simple, non-specific object"
23 | }`,
24 |
25 | `{"@context": "https://www.w3.org/ns/activitystreams",
26 | "type": "Person",
27 | "id": "https://social.example/alyssa/",
28 | "name": "Alyssa P. Hacker",
29 | "preferredUsername": "alyssa",
30 | "summary": "Lisp enthusiast hailing from MIT",
31 | "inbox": "https://social.example/alyssa/inbox/",
32 | "outbox": "https://social.example/alyssa/outbox/",
33 | "followers": "https://social.example/alyssa/followers/",
34 | "following": "https://social.example/alyssa/following/",
35 | "liked": "https://social.example/alyssa/liked/"}`,
36 |
37 | `{"@context": "https://www.w3.org/ns/activitystreams",
38 | "type": "Note",
39 | "to": ["https://chatty.example/ben/"],
40 | "attributedTo": "https://social.example/alyssa/",
41 | "content": "Say, did you finish reading that book I lent you?"}`,
42 |
43 | `{
44 | "@context": ["https://www.w3.org/ns/activitystreams",
45 | {"@language": "en"}],
46 | "type": "Note",
47 | "id": "http://postparty.example/p/2415",
48 | "content": "I really like strawberries!
",
49 | "source": {
50 | "content": "I *really* like strawberries!",
51 | "mediaType": "text/markdown"}
52 | }`,
53 | }
54 |
55 | for _, tc := range testCases {
56 | err = json.Unmarshal([]byte(tc), &obj)
57 | require.NoError(t, err)
58 | }
59 | }
60 |
61 | func TestUnmarshalJSON_OrderedCollectionPage(t *testing.T) {
62 | var ordColPage OrderedCollectionPage
63 | var err error
64 | testCases := []string{
65 | `{
66 | "@context": "https://www.w3.org/ns/activitystreams",
67 | "id": "https://social.matej-lach.me/users/MatejLach/following?page=1",
68 | "type": "OrderedCollectionPage",
69 | "totalItems": 329,
70 | "next": "https://social.matej-lach.me/users/MatejLach/following?page=2",
71 | "partOf": "https://social.matej-lach.me/users/MatejLach/following",
72 | "orderedItems": [
73 | "https://suma-ev.social/users/christian",
74 | "https://mastodon.social/users/Daojoan",
75 | "https://23.social/users/pantierra",
76 | "https://famichiki.jp/users/tsturm",
77 | "https://bikeshed.vibber.net/users/brooke",
78 | "https://idlethumbs.social/users/ja2ke",
79 | "https://oisaur.com/users/renchap",
80 | "https://social.alternativebit.fr/users/picnoir",
81 | "https://swiss.social/users/lx",
82 | "https://social.lol/users/whakkee",
83 | "https://oldbytes.space/users/aperezdc",
84 | "https://tech.lgbt/users/jaycie"
85 | ]
86 | }`,
87 | }
88 |
89 | for _, tc := range testCases {
90 | err = json.Unmarshal([]byte(tc), &ordColPage)
91 | require.NoError(t, err)
92 | }
93 | }
94 |
95 | func TestUnmarshalJSON_ObjectOrLinkOrString(t *testing.T) {
96 | testCases := []string{
97 | `{
98 | "@context": [
99 | "https://www.w3.org/ns/activitystreams",
100 | {
101 | "@language": "en"
102 | }],
103 | "type": "Object",
104 | "name": "This is the title"
105 | }`,
106 |
107 | `{
108 | "@context": "https://www.w3.org/ns/activitystreams",
109 | "summary": "A note",
110 | "type": "Note",
111 | "content": "My dog has fleas."
112 | }`,
113 |
114 | `{
115 | "@context": {
116 | "@vocab": "https://www.w3.org/ns/activitystreams",
117 | "ext": "https://canine-extension.example/terms/",
118 | "@language": "en"
119 | },
120 | "summary": "A note",
121 | "type": "Note",
122 | "content": "My dog has fleas."
123 | }`,
124 |
125 | `{
126 | "@context": [
127 | "https://www.w3.org/ns/activitystreams",
128 | {
129 | "css": "http://www.w3.org/ns/oa#styledBy"
130 | }
131 | ],
132 | "summary": "A note",
133 | "type": "Note",
134 | "content": "My dog has fleas."
135 | }`,
136 |
137 | `{
138 | "@context": "https://www.w3.org/ns/activitystreams",
139 | "summary": "A simple note",
140 | "type": "Note",
141 | "content": "A simple note",
142 | "icon": [
143 | {
144 | "type": "Image",
145 | "summary": "Note (16x16)",
146 | "url": "http://example.org/note1.png",
147 | "width": 16,
148 | "height": 16
149 | },
150 | {
151 | "type": "Image",
152 | "summary": "Note (32x32)",
153 | "url": "http://example.org/note2.png",
154 | "width": 32,
155 | "height": 32
156 | }
157 | ]
158 | }`,
159 |
160 | `{
161 | "@context": "https://www.w3.org/ns/activitystreams",
162 | "summary": "A simple note",
163 | "type": "Note",
164 | "content": "This is all there is.",
165 | "inReplyTo": {
166 | "summary": "Previous note",
167 | "type": "Note",
168 | "content": "What else is there?"
169 | }
170 | }`,
171 |
172 | `{
173 | "@context": "https://www.w3.org/ns/activitystreams",
174 | "type": "Person",
175 | "name": "Sally",
176 | "location": {
177 | "name": "Over the Arabian Sea, east of Socotra Island Nature Sanctuary",
178 | "type": "Place",
179 | "longitude": 12.34,
180 | "latitude": 56.78,
181 | "altitude": 90,
182 | "units": "m"
183 | }
184 | }`,
185 |
186 | `{
187 | "@context": "https://www.w3.org/ns/activitystreams",
188 | "type": "Image",
189 | "summary": "Picture of Sally",
190 | "url": "http://example.org/sally.jpg",
191 | "tag": [
192 | {
193 | "type": "Person",
194 | "id": "http://sally.example.org",
195 | "name": "Sally"
196 | }
197 | ]
198 | }`,
199 |
200 | `{
201 | "@context": "https://www.w3.org/ns/activitystreams",
202 | "summary": "Sally offered the post to John",
203 | "type": "Offer",
204 | "actor": "http://sally.example.org",
205 | "object": "http://example.org/posts/1",
206 | "target": {
207 | "type": "Person",
208 | "name": "John"
209 | }
210 | }`,
211 |
212 | `
213 | {
214 | "@context": [
215 | "https://www.w3.org/ns/activitystreams",
216 | "https://w3id.org/security/v1",
217 | {
218 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
219 | "toot": "http://joinmastodon.org/ns#",
220 | "featured": {
221 | "@id": "toot:featured",
222 | "@type": "@id"
223 | },
224 | "alsoKnownAs": {
225 | "@id": "as:alsoKnownAs",
226 | "@type": "@id"
227 | },
228 | "movedTo": {
229 | "@id": "as:movedTo",
230 | "@type": "@id"
231 | },
232 | "schema": "http://schema.org#",
233 | "PropertyValue": "schema:PropertyValue",
234 | "value": "schema:value",
235 | "Hashtag": "as:Hashtag",
236 | "Emoji": "toot:Emoji",
237 | "IdentityProof": "toot:IdentityProof",
238 | "focalPoint": {
239 | "@container": "@list",
240 | "@id": "toot:focalPoint"
241 | }
242 | }
243 | ],
244 | "id": "https://social.matej-lach.me/users/MatejLach",
245 | "type": "Person",
246 | "following": "https://social.matej-lach.me/users/MatejLach/following",
247 | "followers": "https://social.matej-lach.me/users/MatejLach/followers",
248 | "inbox": "https://social.matej-lach.me/users/MatejLach/inbox",
249 | "outbox": "https://social.matej-lach.me/users/MatejLach/outbox",
250 | "featured": "https://social.matej-lach.me/users/MatejLach/collections/featured",
251 | "preferredUsername": "MatejLach",
252 | "name": "Matej Ľach ✅",
253 | "summary": "Free software enthusiast, #golang, #rustlang, #jvm & #swiftlang . Working on a question/answer #ActivityPub server. #systemd aficionado :-)
",
254 | "url": "https://social.matej-lach.me/@MatejLach",
255 | "manuallyApprovesFollowers": false,
256 | "publicKey": {
257 | "id": "https://social.matej-lach.me/users/MatejLach#main.go-key",
258 | "owner": "https://social.matej-lach.me/users/MatejLach",
259 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm9ir9e6zclOuWFN+mrtV\nqjz+qNYdgYYA7TGLWf4heNjdQRP0wOTjSY5mXST95aKY8h30LSyPAI01/GLrPsme\nm+uSgr59VX3dyvDHYiOSSuMl8lkFMGthxWlKXcUb7vFcJnHRr4q5TUZ+J4wjEksw\nWmqK5me2Lnt+wVQnWXplfnknVJaZvCPEfRWVVu53lgSfTkF+rO4Bl6osw2TrIj3T\n8MoOpnGKXSTGuL86cAQAkxbJcqkFeM/ksojVBqVpGn+xdQuOf62j6mFzZl4B9wfo\nKah+O7zbgvJEHhSmvSZlo8b9YJre0YbAJBCcQnAyj3m2oSEwIKz20jjTapsiAJFS\nTwIDAQAB\n-----END PUBLIC KEY-----\n"
260 | },
261 | "tag": [
262 | {
263 | "type": "Hashtag",
264 | "href": "https://social.matej-lach.me/explore/golang",
265 | "name": "#golang"
266 | },
267 | {
268 | "type": "Hashtag",
269 | "href": "https://social.matej-lach.me/explore/activitypub",
270 | "name": "#activitypub"
271 | },
272 | {
273 | "type": "Hashtag",
274 | "href": "https://social.matej-lach.me/explore/rustlang",
275 | "name": "#rustlang"
276 | },
277 | {
278 | "type": "Hashtag",
279 | "href": "https://social.matej-lach.me/explore/swiftlang",
280 | "name": "#swiftlang"
281 | },
282 | {
283 | "type": "Hashtag",
284 | "href": "https://social.matej-lach.me/explore/jvm",
285 | "name": "#jvm"
286 | },
287 | {
288 | "type": "Hashtag",
289 | "href": "https://social.matej-lach.me/explore/systemd",
290 | "name": "#systemd"
291 | }
292 | ],
293 | "attachment": [
294 | {
295 | "type": "PropertyValue",
296 | "name": "Blog",
297 | "value": "https://blog.matej-lach.me"
298 | }
299 | ],
300 | "endpoints": {
301 | "sharedInbox": "https://social.matej-lach.me/inbox"
302 | },
303 | "icon": {
304 | "type": "Image",
305 | "mediaType": "image/png",
306 | "url": "https://social.matej-lach.me/system/accounts/avatars/000/000/001/original/6e9242b03795bf80.png?1509060490"
307 | },
308 | "image": {
309 | "type": "Image",
310 | "mediaType": "image/png",
311 | "url": "https://social.matej-lach.me/system/accounts/headers/000/000/001/original/0865cab093a8367b.png?1536786967"
312 | }
313 | }`,
314 | }
315 |
316 | for _, tc := range testCases {
317 | payloadMeta, err := DecodePayloadObjectType(strings.NewReader(tc))
318 | require.NoError(t, err)
319 |
320 | switch payloadMeta.Type {
321 | case "Note":
322 | var note Note
323 | err = json.Unmarshal([]byte(tc), ¬e)
324 | require.NoError(t, err)
325 | case "Offer":
326 | var offer Offer
327 | err = json.Unmarshal([]byte(tc), &offer)
328 | require.NoError(t, err)
329 | case "Person":
330 | var person Person
331 | err = json.Unmarshal([]byte(tc), &person)
332 | require.NoError(t, err)
333 | require.NotEmpty(t, person.Name)
334 | default:
335 | var obj ObjectOrLinkOrString
336 | err = json.Unmarshal([]byte(tc), &obj)
337 | require.NoError(t, err)
338 | }
339 | }
340 | }
341 |
342 | func TestUnmarshalJSON_Link(t *testing.T) {
343 | var lnk Link
344 | var err error
345 | testCases := []string{
346 | `{
347 | "@context": "https://www.w3.org/ns/activitystreams",
348 | "type": "Link",
349 | "href": "http://example.org/abc",
350 | "hreflang": "en",
351 | "mediaType": "text/html",
352 | "name": "An example link"
353 | }`,
354 | }
355 |
356 | for _, tc := range testCases {
357 | err = json.Unmarshal([]byte(tc), &lnk)
358 | require.NoError(t, err)
359 | }
360 | }
361 |
362 | func TestOrderedCollection_Sorting(t *testing.T) {
363 | payload := `{
364 | "@context": "https://www.w3.org/ns/activitystreams",
365 | "type": "OrderedCollection",
366 | "id": "https://example.com/users/alice/outbox",
367 | "totalItems": 5,
368 | "first": "https://example.com/users/alice/outbox?page=1",
369 | "last": "https://example.com/users/alice/outbox?page=1",
370 | "orderedItems": [
371 | {
372 | "id": "https://example.com/users/alice/statuses/2",
373 | "type": "Like",
374 | "actor": "https://example.com/users/alice",
375 | "published": "2024-05-03T11:42:19Z",
376 | "object": "https://example.com/users/charlie/statuses/99"
377 | },
378 | {
379 | "id": "https://example.com/users/alice/statuses/5",
380 | "type": "Create",
381 | "actor": "https://example.com/users/alice",
382 | "published": "2024-08-15T09:23:47Z",
383 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
384 | "cc": ["https://example.com/users/alice/followers"],
385 | "object": {
386 | "id": "https://example.com/users/alice/statuses/5",
387 | "type": "Note",
388 | "content": "Just finished reading a great book on AI ethics!",
389 | "attributedTo": "https://example.com/users/alice",
390 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
391 | "cc": ["https://example.com/users/alice/followers"],
392 | "published": "2024-08-15T09:23:47Z"
393 | }
394 | },
395 | {
396 | "id": "https://example.com/users/alice/statuses/4",
397 | "type": "Announce",
398 | "actor": "https://example.com/users/alice",
399 | "published": "2024-07-22T14:05:32Z",
400 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
401 | "cc": ["https://example.com/users/alice/followers"],
402 | "object": "https://example.com/users/bob/statuses/42"
403 | },
404 | {
405 | "id": "https://example.com/users/alice/statuses/3",
406 | "type": "Create",
407 | "actor": "https://example.com/users/alice",
408 | "published": "2024-06-10T18:30:00Z",
409 | "to": ["https://example.com/users/bob"],
410 | "cc": ["https://example.com/users/alice/followers"],
411 | "object": {
412 | "id": "https://example.com/users/alice/statuses/3",
413 | "type": "Note",
414 | "content": "@bob Looking forward to our meetup next week!",
415 | "attributedTo": "https://example.com/users/alice",
416 | "to": ["https://example.com/users/bob"],
417 | "cc": ["https://example.com/users/alice/followers"],
418 | "published": "2024-06-10T18:30:00Z",
419 | "tag": [
420 | {
421 | "type": "Mention",
422 | "href": "https://example.com/users/bob",
423 | "name": "@bob"
424 | }
425 | ]
426 | }
427 | },
428 | {
429 | "id": "https://example.com/users/alice/statuses/1",
430 | "type": "Create",
431 | "actor": "https://example.com/users/alice",
432 | "published": "2024-04-01T08:00:00Z",
433 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
434 | "cc": ["https://example.com/users/alice/followers"],
435 | "object": {
436 | "id": "https://example.com/users/alice/statuses/1",
437 | "type": "Note",
438 | "content": "Hello, Mastodon! This is my first toot. #introduction",
439 | "attributedTo": "https://example.com/users/alice",
440 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
441 | "cc": ["https://example.com/users/alice/followers"],
442 | "published": "2024-04-01T08:00:00Z",
443 | "tag": [
444 | {
445 | "type": "Hashtag",
446 | "href": "https://example.com/tags/introduction",
447 | "name": "#introduction"
448 | }
449 | ]
450 | }
451 | }
452 | ]
453 | }
454 | `
455 | oc := OrderedCollection{}
456 | err := json.Unmarshal([]byte(payload), &oc)
457 | require.NoError(t, err)
458 |
459 | sort.Sort(oc)
460 | mostRecentPostPublishedAt := oc.OrderedItems.Target[0].GetObject().Published.Format(time.RFC3339)
461 | assert.Equal(t, "2024-08-15T09:23:47Z", mostRecentPostPublishedAt)
462 | }
463 |
--------------------------------------------------------------------------------
/asvocab_type_decoder.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | )
8 |
9 | // Decode the supplied JSON into the appropriate type based on its "type" property
10 | func decodeASType(data []byte, jsonType string) (ObjectLinker, error) {
11 | decoder := json.NewDecoder(bytes.NewReader(data))
12 | switch jsonType {
13 | case "Activity":
14 | activity := Activity{}
15 | if err := decoder.Decode(&activity); err != nil {
16 | return nil, err
17 | }
18 | return activity, nil
19 | case "Application":
20 | application := Application{}
21 | if err := decoder.Decode(&application); err != nil {
22 | return nil, err
23 | }
24 | return application, nil
25 | case "Article":
26 | article := Article{}
27 | if err := decoder.Decode(&article); err != nil {
28 | return nil, err
29 | }
30 | return article, nil
31 | case "Audio":
32 | audio := Audio{}
33 | if err := decoder.Decode(&audio); err != nil {
34 | return nil, err
35 | }
36 | return audio, nil
37 | case "Collection":
38 | collection := Collection{}
39 | if err := decoder.Decode(&collection); err != nil {
40 | return nil, err
41 | }
42 | return collection, nil
43 | case "CollectionPage":
44 | colPage := CollectionPage{}
45 | if err := decoder.Decode(&colPage); err != nil {
46 | return nil, err
47 | }
48 | return colPage, nil
49 | case "Relationship":
50 | relationship := Relationship{}
51 | if err := decoder.Decode(&relationship); err != nil {
52 | return nil, err
53 | }
54 | return relationship, nil
55 | case "Document":
56 | document := Document{}
57 | if err := decoder.Decode(&document); err != nil {
58 | return nil, err
59 | }
60 | return document, nil
61 | case "Event":
62 | event := Event{}
63 | if err := decoder.Decode(&event); err != nil {
64 | return nil, err
65 | }
66 | return event, nil
67 | case "Group":
68 | group := Group{}
69 | if err := decoder.Decode(&group); err != nil {
70 | return nil, err
71 | }
72 | return group, nil
73 | case "Icon":
74 | icon := Icon{}
75 | if err := decoder.Decode(&icon); err != nil {
76 | return nil, err
77 | }
78 | return icon, nil
79 | case "Image":
80 | image := Image{}
81 | if err := decoder.Decode(&image); err != nil {
82 | return nil, err
83 | }
84 | return image, nil
85 | case "IntransitiveActivity":
86 | intransitiveActivity := IntransitiveActivity{}
87 | if err := decoder.Decode(&intransitiveActivity); err != nil {
88 | return nil, err
89 | }
90 | return intransitiveActivity, nil
91 | case "Note":
92 | note := Note{}
93 | if err := decoder.Decode(¬e); err != nil {
94 | return nil, err
95 | }
96 | return note, nil
97 | case "Object":
98 | object := Object{}
99 | if err := decoder.Decode(&object); err != nil {
100 | return nil, err
101 | }
102 | return object, nil
103 | case "OrderedCollection":
104 | ordCollection := OrderedCollection{}
105 | if err := decoder.Decode(&ordCollection); err != nil {
106 | return nil, err
107 | }
108 | return ordCollection, nil
109 | case "OrderedCollectionPage":
110 | ordColPage := OrderedCollectionPage{}
111 | if err := decoder.Decode(&ordColPage); err != nil {
112 | return nil, err
113 | }
114 | return ordColPage, nil
115 | case "Organization":
116 | organization := Organization{}
117 | if err := decoder.Decode(&organization); err != nil {
118 | return nil, err
119 | }
120 | return organization, nil
121 | case "Page":
122 | page := Page{}
123 | if err := decoder.Decode(&page); err != nil {
124 | return nil, err
125 | }
126 | return page, nil
127 | case "Person":
128 | person := Person{}
129 | if err := decoder.Decode(&person); err != nil {
130 | return nil, err
131 | }
132 | return person, nil
133 | case "Place":
134 | place := Place{}
135 | if err := decoder.Decode(&place); err != nil {
136 | return nil, err
137 | }
138 | return place, nil
139 | case "Profile":
140 | profile := Profile{}
141 | if err := decoder.Decode(&profile); err != nil {
142 | return nil, err
143 | }
144 | return profile, nil
145 | case "Question":
146 | question := Question{}
147 | if err := decoder.Decode(&question); err != nil {
148 | return nil, err
149 | }
150 | return question, nil
151 | case "Service":
152 | service := Service{}
153 | if err := decoder.Decode(&service); err != nil {
154 | return nil, err
155 | }
156 | return service, nil
157 | case "Tombstone":
158 | tombStone := Tombstone{}
159 | if err := decoder.Decode(&tombStone); err != nil {
160 | return nil, err
161 | }
162 | return tombStone, nil
163 | case "Video":
164 | video := Video{}
165 | if err := decoder.Decode(&video); err != nil {
166 | return nil, err
167 | }
168 | return video, nil
169 | case "Accept":
170 | accept := Accept{}
171 | if err := decoder.Decode(&accept); err != nil {
172 | return nil, err
173 | }
174 | return accept, nil
175 | case "Add":
176 | addActivity := Add{}
177 | if err := decoder.Decode(&addActivity); err != nil {
178 | return nil, err
179 | }
180 | return addActivity, nil
181 | case "Announce":
182 | announce := Announce{}
183 | if err := decoder.Decode(&announce); err != nil {
184 | return nil, err
185 | }
186 | return announce, nil
187 | case "Arrive":
188 | arrive := Arrive{}
189 | if err := decoder.Decode(&arrive); err != nil {
190 | return nil, err
191 | }
192 | return arrive, nil
193 | case "Block":
194 | block := Block{}
195 | if err := decoder.Decode(&block); err != nil {
196 | return nil, err
197 | }
198 | return block, nil
199 | case "Create":
200 | create := Create{}
201 | if err := decoder.Decode(&create); err != nil {
202 | return nil, err
203 | }
204 | return create, nil
205 | case "Delete":
206 | deleteActivity := Delete{}
207 | if err := decoder.Decode(&deleteActivity); err != nil {
208 | return nil, err
209 | }
210 | return deleteActivity, nil
211 | case "Follow":
212 | follow := Follow{}
213 | if err := decoder.Decode(&follow); err != nil {
214 | return nil, err
215 | }
216 | return follow, nil
217 | case "Flag":
218 | flag := Flag{}
219 | if err := decoder.Decode(&flag); err != nil {
220 | return nil, err
221 | }
222 | return flag, nil
223 | case "Ignore":
224 | ignore := Ignore{}
225 | if err := decoder.Decode(&ignore); err != nil {
226 | return nil, err
227 | }
228 | return ignore, nil
229 | case "Invite":
230 | invite := Invite{}
231 | if err := decoder.Decode(&invite); err != nil {
232 | return nil, err
233 | }
234 | return invite, nil
235 | case "Join":
236 | join := Join{}
237 | if err := decoder.Decode(&join); err != nil {
238 | return nil, err
239 | }
240 | return join, nil
241 | case "Leave":
242 | leave := Leave{}
243 | if err := decoder.Decode(&leave); err != nil {
244 | return nil, err
245 | }
246 | return leave, nil
247 | case "Like":
248 | like := Like{}
249 | if err := decoder.Decode(&like); err != nil {
250 | return nil, err
251 | }
252 | return like, nil
253 | case "Listen":
254 | listen := Listen{}
255 | if err := decoder.Decode(&listen); err != nil {
256 | return nil, err
257 | }
258 | return listen, nil
259 | case "Move":
260 | move := Move{}
261 | if err := decoder.Decode(&move); err != nil {
262 | return nil, err
263 | }
264 | return move, nil
265 | case "Offer":
266 | offer := Offer{}
267 | if err := decoder.Decode(&offer); err != nil {
268 | return nil, err
269 | }
270 | return offer, nil
271 | case "Read":
272 | read := Read{}
273 | if err := decoder.Decode(&read); err != nil {
274 | return nil, err
275 | }
276 | return read, nil
277 | case "Reject":
278 | reject := Reject{}
279 | if err := decoder.Decode(&reject); err != nil {
280 | return nil, err
281 | }
282 | return reject, nil
283 | case "Remove":
284 | remove := Remove{}
285 | if err := decoder.Decode(&remove); err != nil {
286 | return nil, err
287 | }
288 | return remove, nil
289 | case "TentativeAccept":
290 | tentativeAccept := TentativeAccept{}
291 | if err := decoder.Decode(&tentativeAccept); err != nil {
292 | return nil, err
293 | }
294 | return tentativeAccept, nil
295 | case "TentativeReject":
296 | tentativeReject := TentativeReject{}
297 | if err := decoder.Decode(&tentativeReject); err != nil {
298 | return nil, err
299 | }
300 | return tentativeReject, nil
301 | case "Travel":
302 | travel := Travel{}
303 | if err := decoder.Decode(&travel); err != nil {
304 | return nil, err
305 | }
306 | return travel, nil
307 | case "Undo":
308 | undo := Undo{}
309 | if err := decoder.Decode(&undo); err != nil {
310 | return nil, err
311 | }
312 | return undo, nil
313 | case "Update":
314 | update := Update{}
315 | if err := decoder.Decode(&update); err != nil {
316 | return nil, err
317 | }
318 | return update, nil
319 | case "View":
320 | view := View{}
321 | if err := decoder.Decode(&view); err != nil {
322 | return nil, err
323 | }
324 | return view, nil
325 | case "Link":
326 | link := Link{}
327 | if err := decoder.Decode(&link); err != nil {
328 | return nil, err
329 | }
330 | return link, nil
331 | case "Mention":
332 | mention := Mention{}
333 | if err := decoder.Decode(&mention); err != nil {
334 | return nil, err
335 | }
336 | return mention, nil
337 | case "Hashtag":
338 | hashtag := Hashtag{}
339 | if err := decoder.Decode(&hashtag); err != nil {
340 | return nil, err
341 | }
342 | return hashtag, nil
343 | case "PropertyValue":
344 | propertyValue := PropertyValue{}
345 | if err := decoder.Decode(&propertyValue); err != nil {
346 | return nil, err
347 | }
348 | return propertyValue, nil
349 | }
350 | return nil, errors.New("unsupported ActivityStreams type, or invalid AS document")
351 | }
352 |
--------------------------------------------------------------------------------
/asvocab_type_encoder.go:
--------------------------------------------------------------------------------
1 | package astreams
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | )
8 |
9 | func encodeASType(t ObjectLinker) ([]byte, error) {
10 | _, dataType := ConcreteType(t)
11 | switch dataType {
12 | case "Activity":
13 | if activity, ok := t.(Activity); ok {
14 | marshalB, err := json.Marshal(&activity)
15 | if err != nil {
16 | return []byte{}, err
17 | }
18 | return marshalB, nil
19 | }
20 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
21 | case "Application":
22 | if application, ok := t.(Application); ok {
23 | marshalB, err := json.Marshal(&application)
24 | if err != nil {
25 | return []byte{}, err
26 | }
27 | return marshalB, nil
28 | }
29 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
30 | case "Article":
31 | if article, ok := t.(Article); ok {
32 | marshalB, err := json.Marshal(&article)
33 | if err != nil {
34 | return []byte{}, err
35 | }
36 | return marshalB, nil
37 | }
38 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
39 | case "Audio":
40 | if audio, ok := t.(Audio); ok {
41 | marshalB, err := json.Marshal(&audio)
42 | if err != nil {
43 | return []byte{}, err
44 | }
45 | return marshalB, nil
46 | }
47 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
48 | case "Collection":
49 | if collection, ok := t.(Collection); ok {
50 | marshalB, err := json.Marshal(&collection)
51 | if err != nil {
52 | return []byte{}, err
53 | }
54 | return marshalB, nil
55 | }
56 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
57 | case "CollectionPage":
58 | if colPage, ok := t.(CollectionPage); ok {
59 | marshalB, err := json.Marshal(&colPage)
60 | if err != nil {
61 | return []byte{}, err
62 | }
63 | return marshalB, nil
64 | }
65 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
66 | case "Relationship":
67 | if relationship, ok := t.(Relationship); ok {
68 | marshalB, err := json.Marshal(&relationship)
69 | if err != nil {
70 | return []byte{}, err
71 | }
72 | return marshalB, nil
73 | }
74 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
75 | case "Document":
76 | if document, ok := t.(Document); ok {
77 | marshalB, err := json.Marshal(&document)
78 | if err != nil {
79 | return []byte{}, err
80 | }
81 | return marshalB, nil
82 | }
83 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
84 | case "Event":
85 | if event, ok := t.(Event); ok {
86 | marshalB, err := json.Marshal(&event)
87 | if err != nil {
88 | return []byte{}, err
89 | }
90 | return marshalB, nil
91 | }
92 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
93 | case "Group":
94 | if group, ok := t.(Group); ok {
95 | marshalB, err := json.Marshal(&group)
96 | if err != nil {
97 | return []byte{}, err
98 | }
99 | return marshalB, nil
100 | }
101 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
102 | case "Icon":
103 | if icon, ok := t.(Icon); ok {
104 | marshalB, err := json.Marshal(&icon)
105 | if err != nil {
106 | return []byte{}, err
107 | }
108 | return marshalB, nil
109 | }
110 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
111 | case "Image":
112 | if image, ok := t.(Image); ok {
113 | marshalB, err := json.Marshal(&image)
114 | if err != nil {
115 | return []byte{}, err
116 | }
117 | return marshalB, nil
118 | }
119 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
120 | case "IntransitiveActivity":
121 | if intransitiveActivity, ok := t.(IntransitiveActivity); ok {
122 | marshalB, err := json.Marshal(&intransitiveActivity)
123 | if err != nil {
124 | return []byte{}, err
125 | }
126 | return marshalB, nil
127 | }
128 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
129 | case "Note":
130 | if note, ok := t.(Note); ok {
131 | marshalB, err := json.Marshal(¬e)
132 | if err != nil {
133 | return []byte{}, err
134 | }
135 | return marshalB, nil
136 | }
137 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
138 | case "Object":
139 | if object, ok := t.(Object); ok {
140 | marshalB, err := json.Marshal(&object)
141 | if err != nil {
142 | return []byte{}, err
143 | }
144 | return marshalB, nil
145 | }
146 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
147 | case "OrderedCollection":
148 | if ordCollection, ok := t.(OrderedCollection); ok {
149 | marshalB, err := json.Marshal(&ordCollection)
150 | if err != nil {
151 | return []byte{}, err
152 | }
153 | return marshalB, nil
154 | }
155 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
156 | case "OrderedCollectionPage":
157 | if ordColPage, ok := t.(OrderedCollectionPage); ok {
158 | marshalB, err := json.Marshal(&ordColPage)
159 | if err != nil {
160 | return []byte{}, err
161 | }
162 | return marshalB, nil
163 | }
164 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
165 | case "Organization":
166 | if organization, ok := t.(Organization); ok {
167 | marshalB, err := json.Marshal(&organization)
168 | if err != nil {
169 | return []byte{}, err
170 | }
171 | return marshalB, nil
172 | }
173 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
174 | case "Page":
175 | if page, ok := t.(Page); ok {
176 | marshalB, err := json.Marshal(&page)
177 | if err != nil {
178 | return []byte{}, err
179 | }
180 | return marshalB, nil
181 | }
182 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
183 | case "Person":
184 | if person, ok := t.(Person); ok {
185 | marshalB, err := json.Marshal(&person)
186 | if err != nil {
187 | return []byte{}, err
188 | }
189 | return marshalB, nil
190 | }
191 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
192 | case "Place":
193 | if place, ok := t.(Place); ok {
194 | marshalB, err := json.Marshal(&place)
195 | if err != nil {
196 | return []byte{}, err
197 | }
198 | return marshalB, nil
199 | }
200 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
201 | case "Profile":
202 | if profile, ok := t.(Profile); ok {
203 | marshalB, err := json.Marshal(&profile)
204 | if err != nil {
205 | return []byte{}, err
206 | }
207 | return marshalB, nil
208 | }
209 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
210 | case "Question":
211 | if question, ok := t.(Question); ok {
212 | marshalB, err := json.Marshal(&question)
213 | if err != nil {
214 | return []byte{}, err
215 | }
216 | return marshalB, nil
217 | }
218 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
219 | case "Service":
220 | if service, ok := t.(Service); ok {
221 | marshalB, err := json.Marshal(&service)
222 | if err != nil {
223 | return []byte{}, err
224 | }
225 | return marshalB, nil
226 | }
227 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
228 | case "Tombstone":
229 | if tombstone, ok := t.(Tombstone); ok {
230 | marshalB, err := json.Marshal(&tombstone)
231 | if err != nil {
232 | return []byte{}, err
233 | }
234 | return marshalB, nil
235 | }
236 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
237 | case "Video":
238 | if video, ok := t.(Video); ok {
239 | marshalB, err := json.Marshal(&video)
240 | if err != nil {
241 | return []byte{}, err
242 | }
243 | return marshalB, nil
244 | }
245 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
246 | case "Accept":
247 | if accept, ok := t.(Accept); ok {
248 | marshalB, err := json.Marshal(&accept)
249 | if err != nil {
250 | return []byte{}, err
251 | }
252 | return marshalB, nil
253 | }
254 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
255 | case "Add":
256 | if add, ok := t.(Add); ok {
257 | marshalB, err := json.Marshal(&add)
258 | if err != nil {
259 | return []byte{}, err
260 | }
261 | return marshalB, nil
262 | }
263 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
264 | case "Announce":
265 | if announce, ok := t.(Announce); ok {
266 | marshalB, err := json.Marshal(&announce)
267 | if err != nil {
268 | return []byte{}, err
269 | }
270 | return marshalB, nil
271 | }
272 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
273 | case "Arrive":
274 | if arrive, ok := t.(Arrive); ok {
275 | marshalB, err := json.Marshal(&arrive)
276 | if err != nil {
277 | return []byte{}, err
278 | }
279 | return marshalB, nil
280 | }
281 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
282 | case "Block":
283 | if block, ok := t.(Block); ok {
284 | marshalB, err := json.Marshal(&block)
285 | if err != nil {
286 | return []byte{}, err
287 | }
288 | return marshalB, nil
289 | }
290 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
291 | case "Create":
292 | if create, ok := t.(Create); ok {
293 | marshalB, err := json.Marshal(&create)
294 | if err != nil {
295 | return []byte{}, err
296 | }
297 | return marshalB, nil
298 | }
299 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
300 | case "Delete":
301 | if del, ok := t.(Delete); ok {
302 | marshalB, err := json.Marshal(&del)
303 | if err != nil {
304 | return []byte{}, err
305 | }
306 | return marshalB, nil
307 | }
308 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
309 | case "Dislike":
310 | if dislike, ok := t.(Dislike); ok {
311 | marshalB, err := json.Marshal(&dislike)
312 | if err != nil {
313 | return []byte{}, err
314 | }
315 | return marshalB, nil
316 | }
317 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
318 | case "Follow":
319 | if follow, ok := t.(Follow); ok {
320 | marshalB, err := json.Marshal(&follow)
321 | if err != nil {
322 | return []byte{}, err
323 | }
324 | return marshalB, nil
325 | }
326 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
327 | case "Flag":
328 | if flag, ok := t.(Flag); ok {
329 | marshalB, err := json.Marshal(&flag)
330 | if err != nil {
331 | return []byte{}, err
332 | }
333 | return marshalB, nil
334 | }
335 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
336 | case "Ignore":
337 | if ignore, ok := t.(Ignore); ok {
338 | marshalB, err := json.Marshal(&ignore)
339 | if err != nil {
340 | return []byte{}, err
341 | }
342 | return marshalB, nil
343 | }
344 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
345 | case "Invite":
346 | if invite, ok := t.(Invite); ok {
347 | marshalB, err := json.Marshal(&invite)
348 | if err != nil {
349 | return []byte{}, err
350 | }
351 | return marshalB, nil
352 | }
353 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
354 | case "Join":
355 | if join, ok := t.(Join); ok {
356 | marshalB, err := json.Marshal(&join)
357 | if err != nil {
358 | return []byte{}, err
359 | }
360 | return marshalB, nil
361 | }
362 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
363 | case "Leave":
364 | if leave, ok := t.(Leave); ok {
365 | marshalB, err := json.Marshal(&leave)
366 | if err != nil {
367 | return []byte{}, err
368 | }
369 | return marshalB, nil
370 | }
371 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
372 | case "Like":
373 | if like, ok := t.(Like); ok {
374 | marshalB, err := json.Marshal(&like)
375 | if err != nil {
376 | return []byte{}, err
377 | }
378 | return marshalB, nil
379 | }
380 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
381 | case "Listen":
382 | if listen, ok := t.(Listen); ok {
383 | marshalB, err := json.Marshal(&listen)
384 | if err != nil {
385 | return []byte{}, err
386 | }
387 | return marshalB, nil
388 | }
389 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
390 | case "Move":
391 | if move, ok := t.(Move); ok {
392 | marshalB, err := json.Marshal(&move)
393 | if err != nil {
394 | return []byte{}, err
395 | }
396 | return marshalB, nil
397 | }
398 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
399 | case "Offer":
400 | if offer, ok := t.(Offer); ok {
401 | marshalB, err := json.Marshal(&offer)
402 | if err != nil {
403 | return []byte{}, err
404 | }
405 | return marshalB, nil
406 | }
407 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
408 | case "Read":
409 | if read, ok := t.(Read); ok {
410 | marshalB, err := json.Marshal(&read)
411 | if err != nil {
412 | return []byte{}, err
413 | }
414 | return marshalB, nil
415 | }
416 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
417 | case "Reject":
418 | if reject, ok := t.(Reject); ok {
419 | marshalB, err := json.Marshal(&reject)
420 | if err != nil {
421 | return []byte{}, err
422 | }
423 | return marshalB, nil
424 | }
425 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
426 | case "Remove":
427 | if remove, ok := t.(Remove); ok {
428 | marshalB, err := json.Marshal(&remove)
429 | if err != nil {
430 | return []byte{}, err
431 | }
432 | return marshalB, nil
433 | }
434 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
435 | case "TentativeAccept":
436 | if tentativeAccept, ok := t.(TentativeAccept); ok {
437 | marshalB, err := json.Marshal(&tentativeAccept)
438 | if err != nil {
439 | return []byte{}, err
440 | }
441 | return marshalB, nil
442 | }
443 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
444 | case "TentativeReject":
445 | if tentativeReject, ok := t.(TentativeReject); ok {
446 | marshalB, err := json.Marshal(&tentativeReject)
447 | if err != nil {
448 | return []byte{}, err
449 | }
450 | return marshalB, nil
451 | }
452 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
453 | case "Travel":
454 | if travel, ok := t.(Travel); ok {
455 | marshalB, err := json.Marshal(&travel)
456 | if err != nil {
457 | return []byte{}, err
458 | }
459 | return marshalB, nil
460 | }
461 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
462 | case "Undo":
463 | if undo, ok := t.(Undo); ok {
464 | marshalB, err := json.Marshal(&undo)
465 | if err != nil {
466 | return []byte{}, err
467 | }
468 | return marshalB, nil
469 | }
470 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
471 | case "Update":
472 | if update, ok := t.(Update); ok {
473 | marshalB, err := json.Marshal(&update)
474 | if err != nil {
475 | return []byte{}, err
476 | }
477 | return marshalB, nil
478 | }
479 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
480 | case "View":
481 | if view, ok := t.(View); ok {
482 | marshalB, err := json.Marshal(&view)
483 | if err != nil {
484 | return []byte{}, err
485 | }
486 | return marshalB, nil
487 | }
488 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
489 | case "Link":
490 | if link, ok := t.(Link); ok {
491 | marshalB, err := json.Marshal(&link)
492 | if err != nil {
493 | return []byte{}, err
494 | }
495 | return marshalB, nil
496 | }
497 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
498 | case "Mention":
499 | if mention, ok := t.(Mention); ok {
500 | marshalB, err := json.Marshal(&mention)
501 | if err != nil {
502 | return []byte{}, err
503 | }
504 | return marshalB, nil
505 | }
506 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
507 | case "Hashtag":
508 | if hashtag, ok := t.(Hashtag); ok {
509 | marshalB, err := json.Marshal(&hashtag)
510 | if err != nil {
511 | return []byte{}, err
512 | }
513 | return marshalB, nil
514 | }
515 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
516 | case "PropertyValue":
517 | if propertyValue, ok := t.(PropertyValue); ok {
518 | marshalB, err := json.Marshal(&propertyValue)
519 | if err != nil {
520 | return []byte{}, err
521 | }
522 | return marshalB, nil
523 | }
524 | return []byte{}, fmt.Errorf("failed to Marshal %s", dataType)
525 | }
526 | return []byte{}, errors.New("unsupported ActivityStreams type, or invalid AS document")
527 | }
528 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package astreams implements JSON serialization/deserialization support for the ActivityStreams 2.0 vocabulary.
3 | Simply use json.Marshal/json.Unmarshal on AS 2.0 compliant objects to obtain their JSON/Go representation respectively.
4 |
5 | Indirect object references such as a link to an outbox collection within an actor profile need to be fetched explicitly and parsed as a separate object.
6 | */
7 | package astreams
8 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/MatejLach/astreams
2 |
3 | retract [v0.8.0, v0.9.0] // experiments, not fit for consumption
4 |
5 | go 1.18
6 |
7 | require github.com/stretchr/testify v1.9.0
8 |
9 | require (
10 | github.com/davecgh/go-spew v1.1.1 // indirect
11 | github.com/pmezard/go-difflib v1.0.0 // indirect
12 | gopkg.in/yaml.v3 v3.0.1 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11 |
--------------------------------------------------------------------------------