Compare commits
396 Commits
Author | SHA1 | Date |
---|---|---|
Cory J Slep | b14b50eeca | |
Cory J Slep | 143897756e | |
Cory J Slep | d2deaac73f | |
Christian Muehlhaeuser | 7f61b78d08 | |
tsmethurst | a5791f8fcc | |
tsmethurst | bffb780f1b | |
tsmethurst | 511885d31b | |
tsmethurst | d35c464c58 | |
tsmethurst | 6e8450d07d | |
Cory J Slep | d866ba75dd | |
tsmethurst | 5498bae4cf | |
Cory J Slep | 4655f8f1e1 | |
David Sterry | b4d577f69b | |
Cory J Slep | e0de0863dc | |
Cory J Slep | 27010241b1 | |
Ben Lubar | 042ba2ce5e | |
komarov | 5e8c5c0bb8 | |
Cory Slep | d282a50439 | |
Cory Slep | 8d07c6c337 | |
Cory Slep | 6daa914620 | |
Cory Slep | a386d74920 | |
Cory Slep | 472d90163f | |
Cory Slep | be7f23bde7 | |
Cory Slep | 6cbfb30afa | |
Cory Slep | c7bbac61b6 | |
Cory Slep | c2285ceec2 | |
Cory Slep | 0430970bd3 | |
Cory Slep | ee53bc0369 | |
Cory Slep | 574f22607b | |
Cory J Slep | 2da4dc1d6e | |
Cory Slep | fbb513751a | |
Cory Slep | a3a3f48d19 | |
Cory Slep | 2d143b225a | |
Cory Slep | fc8e2d8c30 | |
Cory J Slep | 415c3275ef | |
Sevki | 7c20a89a61 | |
Sevki | a8fb705a22 | |
Cory Slep | acc84125fa | |
Cory Slep | e17e28bddb | |
Cory Slep | f4f19b61b3 | |
Cory Slep | 107afb6f17 | |
Cory Slep | c3b24964d4 | |
Cory Slep | 311ab07a5c | |
Cory Slep | 18fcfcf812 | |
Cory Slep | 78c96972b2 | |
Cory Slep | fa448c7559 | |
Cory Slep | 5ad09ded3e | |
Cory Slep | a8662e547a | |
Cory Slep | d003641810 | |
Cory Slep | c58f23bd17 | |
Cory Slep | 72022bfe14 | |
Cory Slep | 4c60792b09 | |
Cory Slep | 110d8bd3e5 | |
Cory Slep | 8af6ed9d4f | |
Cory Slep | a1ac83aee8 | |
Cory Slep | c994dc39f0 | |
Cory Slep | d11b96b9ed | |
Cory Slep | f2a497bdc2 | |
Cory J Slep | 95bfff952e | |
Cory J Slep | d6627c1750 | |
Cory J Slep | 91fc2338f2 | |
Cory J Slep | ceedc04ee1 | |
Cory J Slep | 28e35e3541 | |
Ben Lubar | 908aa3e9ae | |
Ben Lubar | aa98aaa1cf | |
Andreas Schärtl | 2efc3220fa | |
Andreas Schärtl | 01a23ee4d1 | |
Andreas Schärtl | 5f6df51cb8 | |
Sevki | e7c3ac2533 | |
Dan Conley | f502dfd041 | |
Cory Slep | de06e3ccdc | |
Ben Lubar | b27393e8dd | |
Ben Lubar | ffcaafbd9f | |
Cory Slep | e8a7301360 | |
Cory Slep | b977c30ce5 | |
Cory Slep | 9acafe5f97 | |
Cory Slep | 84ee361478 | |
Cory Slep | 3fc23c8c61 | |
Cory Slep | 0acf044b02 | |
Cory Slep | 82ca5994b4 | |
Cory Slep | 07df3adee5 | |
Cory J Slep | 9cdc46afd4 | |
Cory J Slep | 00c504a6f2 | |
Ben Lubar | 8d7b0196cd | |
Ben Lubar | 05dd8e7024 | |
Ben Lubar | 3ff5423014 | |
Ben Lubar | d8ecddbe23 | |
Cory Slep | 809fd1f041 | |
Cory Slep | 00d19306e4 | |
Cory Slep | 138e620834 | |
Cory J Slep | c61e64cede | |
Walfie | e0b45cb7aa | |
Cory Slep | cd97d44b99 | |
Cory Slep | e084b904b1 | |
Cory Slep | b5a6786f9e | |
Cory J Slep | bf196748f9 | |
Lukas Matt | c8e9a9ffc9 | |
Lukas Matt | 4db4275adf | |
Lukas Matt | 85d191bffb | |
Cory Slep | d87793a589 | |
Cory Slep | 9b139af408 | |
Cory Slep | 79acb4ef1c | |
Cory Slep | 224629ca54 | |
Cory Slep | 92ecc4f524 | |
Cory Slep | 351ebe64b7 | |
Cory Slep | f655c76e91 | |
Cory Slep | c365730b4e | |
Cory Slep | f339304ce4 | |
Cory Slep | 28ffa4c8ad | |
Cory Slep | 8dd3c056b3 | |
Cory Slep | 3bc578e7dd | |
Cory Slep | 78a8ee667f | |
Cory Slep | 06954abe84 | |
Cory Slep | d20d97a526 | |
Cory Slep | ca18901fcc | |
Cory Slep | a65388c27b | |
Cory Slep | d535bc95f3 | |
Cory Slep | b9a95751dc | |
Cory Slep | b5eecc692c | |
Cory Slep | 1ea8e2c7f3 | |
Cory Slep | 42b799393a | |
Cory Slep | 1ed044f7cf | |
Cory Slep | ee16417bb3 | |
Cory Slep | 305e8100f1 | |
Cory Slep | b4b18b96fa | |
Cory Slep | d58887e7b3 | |
Cory Slep | e1c2c8868a | |
Cory Slep | 48e15c99a2 | |
Cory Slep | 823ab4cc5a | |
Cory Slep | 6e4cff677a | |
Cory Slep | 5d598046c2 | |
Cory Slep | 87e56d8d96 | |
Cory Slep | 1fac7a8c8d | |
Cory Slep | 3dfe12a7a1 | |
Cory Slep | 19c351e729 | |
Cory Slep | 4a8f7314a7 | |
Cory Slep | 9417306aa3 | |
Cory Slep | 5f6b12a76d | |
Cory Slep | 36a38b74b0 | |
Cory Slep | 1e5f5f9c86 | |
Cory Slep | b41491e2c2 | |
Cory Slep | 82851801ed | |
Cory Slep | 71b853d708 | |
Cory Slep | 52bc5fb9a5 | |
Cory Slep | dc16296d74 | |
Cory Slep | 674fa0245d | |
Cory Slep | 3180eaf568 | |
Cory Slep | 4b87e634a0 | |
Cory Slep | 8a333d6192 | |
Cory Slep | 412c6737c3 | |
Cory Slep | 272d7239f3 | |
Cory Slep | 2d8651d95c | |
Cory Slep | 1227ec9a5a | |
Cory Slep | c4a7b0524e | |
Cory Slep | 2099e89851 | |
Cory Slep | b625914c1a | |
Cory Slep | 7beac61f97 | |
Cory Slep | 6ead50c643 | |
Cory Slep | cc83b751a1 | |
Cory Slep | bd8220a56c | |
Cory Slep | 8efdb62ad9 | |
Cory Slep | d3b0afef5e | |
Cory Slep | 0f7dce6839 | |
Cory Slep | 1b7cec220b | |
Cory Slep | c194883a93 | |
Cory Slep | d85ac83249 | |
Cory Slep | 16af404462 | |
Cory Slep | 293cf3e752 | |
Cory Slep | 546659ed10 | |
Cory Slep | 6d618cb630 | |
Cory Slep | f769e201b2 | |
Cory Slep | a5ea850465 | |
Cory Slep | c973eec5e0 | |
Cory Slep | b7b5c3d2c1 | |
Cory Slep | c8f384583c | |
Cory Slep | 17ac2d4df2 | |
Cory Slep | 8a7ec8c3a9 | |
Cory Slep | aca6f4e857 | |
Cory Slep | 54dccc70c9 | |
Cory Slep | 436def09b4 | |
Cory Slep | 4ca4b8235f | |
Cory Slep | 9fc75b87b8 | |
Cory Slep | a69135b238 | |
Cory Slep | 32beb6881e | |
Cory Slep | bc058086d2 | |
Cory Slep | 9f79250293 | |
Cory Slep | f90d161ed1 | |
Cory Slep | 7beafb477f | |
Cory Slep | ff885a86b3 | |
Cory Slep | ff02890ddf | |
Cory Slep | 9e1159dfd2 | |
Cory Slep | e0e809134f | |
Cory Slep | 0f6ba298b3 | |
Cory Slep | 161ebc62e3 | |
Cory Slep | 9240da5763 | |
Cory Slep | 8fa479da40 | |
Cory Slep | ba00f3f875 | |
Cory Slep | def4e9f167 | |
Cory Slep | 63d04c78b7 | |
Cory Slep | 9cf6568637 | |
Cory Slep | db98736f28 | |
Cory Slep | ed1a715571 | |
Cory Slep | 1f7f42e4cc | |
Cory Slep | 5ac1ebd79e | |
Cory Slep | 9c2c99588f | |
Cory Slep | 5d21a35f8f | |
Cory J Slep | e00b7d8d58 | |
Cory Slep | 248a464c02 | |
Cory Slep | da710a6bfd | |
Cory Slep | 929831f6e2 | |
Cory Slep | 355e2bd106 | |
Cory Slep | 694c0e6898 | |
Cory Slep | 8942794712 | |
Cory Slep | ac87264b54 | |
Cory Slep | a2a8775265 | |
Cory Slep | b349761fbc | |
Cory Slep | bb777571c8 | |
Cory Slep | c3b9686d1f | |
Cory Slep | a40bd3129f | |
Cory Slep | ceb542ffd3 | |
Cory Slep | c12e854487 | |
Cory Slep | 920144aba6 | |
Cory Slep | 4582f88848 | |
Cory Slep | e2eed869af | |
Cory Slep | d3a4507a2a | |
Cory Slep | ad06a07dfa | |
Cory Slep | 648d7fb7b3 | |
Cory Slep | d14128bc0b | |
Cory Slep | 2527b47493 | |
Cory Slep | 730135b307 | |
Cory Slep | 7b4fadd871 | |
Cory Slep | e6160858f0 | |
Cory Slep | 1f12842eeb | |
Cory Slep | ffd18e29d0 | |
Cory Slep | 3dbad22900 | |
Cory Slep | 6d12549b18 | |
Cory Slep | 884cbd8693 | |
Cory Slep | caf1e8943f | |
Cory Slep | a93c6aa678 | |
Cory Slep | c8cec42c57 | |
Cory Slep | a4f90ff8e0 | |
Cory Slep | f2c70191ab | |
Cory Slep | 1a6a1213a5 | |
Cory Slep | 9df52c8c45 | |
Cory Slep | 9369a8ad79 | |
Cory Slep | b7ec140c66 | |
Cory Slep | e8fb31437e | |
Cory Slep | 1b0ceb8344 | |
Cory Slep | 7ac133d101 | |
Cory Slep | ea8af5c968 | |
Cory Slep | 7eb1755c96 | |
Cory Slep | 1bdb66aa98 | |
Cory Slep | 26988b6cd2 | |
Cory Slep | 4bc51a9f97 | |
Cory Slep | be110cf688 | |
Cory Slep | 7e96603df9 | |
Cory Slep | 2aefaeb873 | |
Cory Slep | 73d677460b | |
Cory Slep | 99343f540a | |
Cory Slep | 8b8cc2af27 | |
Cory Slep | 5c5fcd22e8 | |
Cory Slep | 58e3d21e19 | |
Cory Slep | ec7091be51 | |
Cory Slep | 6b3d676552 | |
Cory Slep | 6ece169417 | |
Cory Slep | 7a99c1587e | |
Cory Slep | 3a49fff6bb | |
Cory Slep | 79e3cee633 | |
Cory Slep | 8b8232f1ed | |
Cory Slep | 338fe8d347 | |
Cory Slep | 4f47e7fdfa | |
Cory Slep | c36c529c5f | |
Cory Slep | 87064de883 | |
Cory Slep | 54f8549b10 | |
Cory Slep | df9ff825c2 | |
Cory Slep | 85ff299cf3 | |
Cory Slep | d087200e02 | |
Cory Slep | 2045f2602a | |
Cory Slep | 48df99f07f | |
Cory Slep | 0005e23011 | |
Cory Slep | 24265690f8 | |
Cory Slep | 133c2fd477 | |
Cory Slep | ee9aade57f | |
Cory Slep | 625e93d412 | |
Cory Slep | aeda61d2f1 | |
Cory Slep | 5db3a68a8d | |
Cory Slep | b79b381a62 | |
Cory Slep | 3da311641b | |
Cory Slep | ce699464bf | |
Cory J Slep | 3ca0b5182d | |
Cory Slep | 0a7539cdf7 | |
Cory Slep | b0937b7dec | |
Cory Slep | 43ab7d319c | |
Cory Slep | b5d927c49f | |
Cory Slep | a3c3a7b5fc | |
Cory Slep | 222074b503 | |
Cory Slep | 73f7e3cf36 | |
Cory Slep | 9a0f688c63 | |
Cory Slep | fe8b4e7261 | |
Cory Slep | 069b8de820 | |
Cory Slep | dc2ce18fd7 | |
Cory Slep | beb44b1bde | |
Cory Slep | 139dc3c5ea | |
Cory Slep | 8589b0f14f | |
Cory J Slep | 9c3005b17f | |
Cory Slep | d2182dc3b9 | |
Cory Slep | e5693c2eb9 | |
Cory Slep | dae9d884ab | |
Cory Slep | 41be1062fe | |
Cory Slep | 0f98307f07 | |
Cory Slep | 864616542c | |
Cory Slep | 0530121039 | |
Cory Slep | e508425ddb | |
Cory Slep | 676dc64dbb | |
Cory Slep | e13d837405 | |
Cory Slep | 503e95e52e | |
Cory Slep | 30f9f6a16b | |
Cory Slep | caa2e4d2fa | |
Cory Slep | 90b12d43aa | |
Cory Slep | b657bd2307 | |
Cory Slep | c61bd2d129 | |
Cory Slep | 94569ca549 | |
Cory Slep | 68f6602ebb | |
Cory Slep | 0470f8e603 | |
Cory Slep | 8b4f9fc81c | |
Cory Slep | 2709d26229 | |
Cory Slep | 9cae676dd9 | |
Cory J Slep | c2325ed7ea | |
Ben Lubar | d45efd665a | |
Ben Lubar | 468b81f61f | |
Cory Slep | ff0fcb60f3 | |
Cory Slep | 31482f867e | |
Cory Slep | 2d4695e969 | |
Cory Slep | 8795587007 | |
Cory Slep | 137c1d51fb | |
Cory Slep | 7f8b7a6723 | |
Cory Slep | 2ebd905f76 | |
Cory Slep | d08cc46275 | |
Cory Slep | e834879207 | |
Cory Slep | c4425ee50e | |
Cory Slep | 4f628f1b7e | |
Cory Slep | c91db1e4ae | |
Cory Slep | 3a9b9812b5 | |
Cory Slep | 4a35079fd0 | |
Cory Slep | eb09223588 | |
Cory Slep | c8431878da | |
Cory Slep | 1426f30be1 | |
Cory Slep | d0c1e6a3e3 | |
Cory Slep | 87032d3af9 | |
Cory Slep | c3ec4d5344 | |
Cory Slep | 1a1c50acfb | |
Cory Slep | 3076a5410a | |
Cory Slep | 806c037e3b | |
Cory Slep | 3b3c8efdd1 | |
Cory Slep | a7d2d52351 | |
Cory Slep | 9e2efad733 | |
Cory Slep | 012f23e435 | |
Cory Slep | 83277d57c9 | |
Cory Slep | 2071460bc1 | |
Cory Slep | bd6369d284 | |
Cory Slep | befab36c29 | |
Cory Slep | c430c8af1d | |
Cory Slep | 1252727ca8 | |
Cory Slep | 1c057922ca | |
Cory Slep | 797b2f79a8 | |
Cory Slep | d0812c9471 | |
Cory Slep | 387ed4a775 | |
Cory Slep | 17fce2255a | |
Cory Slep | 2f2858f92b | |
Cory Slep | 46d55732d5 | |
Cory Slep | 493b48b665 | |
Cory Slep | 3b0d31022c | |
Cory Slep | 8b0590a0cd | |
Cory Slep | 3740d4a418 | |
Cory Slep | 452d6f1af7 | |
Cory Slep | a5757b4382 | |
Cory Slep | 377455aa70 | |
Cory Slep | e372d2781e | |
Cory Slep | c655c635ff | |
Cory Slep | f8b24eef88 | |
Cory Slep | dfffcd7eb2 | |
Cory Slep | be4644fbca | |
Cory Slep | f82653f709 | |
Cory Slep | 8fa0f2395c | |
Cory Slep | 107468d8e9 | |
Cory Slep | 10e099bd8a | |
Cory Slep | b0c125a7ba | |
Cory Slep | 1ed478582e | |
Cory Slep | cfdfddfda4 | |
Cory J Slep | 50f3ddbfc5 | |
Cory J Slep | 4870298a37 | |
Cory J Slep | c6b55cc0f2 | |
21stio | 6bea627342 | |
21stio | 3c9368c902 | |
21stio | 7b788cf596 | |
Cory Slep | af5ed41bad |
|
@ -0,0 +1,29 @@
|
|||
name: build
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [~1.15, ^1]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test ./...
|
127
CHANGELOG
127
CHANGELOG
|
@ -1,3 +1,130 @@
|
|||
v1.0.0 2020-07-09
|
||||
|
||||
* Rename Callbacks to FederatingCallbacks in FederatingProtocol.
|
||||
* Rename Callbacks to SocialCallbacks in SocialProtocol.
|
||||
* Rename NewID to NewID on the Database interface.
|
||||
* Added approx. 200 unit tests at ~70% LOC coverage.
|
||||
* More fixes where nil pointers would be dereferenced.
|
||||
* Modified programmatic delivery to be lenient instead of strict.
|
||||
* Even more 'astool' bugfixes to handle multiple vocabulary code generation.
|
||||
* Added request lifecycle hooks to allow implementations greater control when
|
||||
using the 'context' standard library package and its 'Context'.
|
||||
* Fixed bugs in programmatic delivery, handling rare forms of HTTP successes,
|
||||
handling of JSON-LD's @context, race conditions.
|
||||
* Provide a Mastodon-compliant http-signatures Transport implementation, when
|
||||
using the github.com/go-fed/httpsig library.
|
||||
* Support programmatic sending of Activities
|
||||
* Added support for 3 vocabularies in addition to ActivityStreams: toot
|
||||
(Mastodon), security v1, and ForgeFed.
|
||||
* Modify 'astool' to address quirky logic and underspecification in various
|
||||
vocabulary definitions.
|
||||
* Fixed bugs in the 'astool' logic.
|
||||
* Added more helpful functions to the code generated by 'astool'.
|
||||
* Inbox forwarding bug fixes.
|
||||
* Migrate to go modules.
|
||||
|
||||
2019-02-24
|
||||
|
||||
* Removed 'tools'.
|
||||
* Removed 'vocab'.
|
||||
* Removed 'deliver'.
|
||||
* Created the new 'astool' for code-generating any ActivityStreams vocabulary.
|
||||
* The 'streams' package has entirely been redesigned and regenerated.
|
||||
* The 'pub' package has been redesigned to be extensible, and has had concepts
|
||||
previously abstracted, such as security, removed in favor of opaque
|
||||
methods that are up to the application to implement.
|
||||
* This succinct summary betrays the size, scope, and effort into rethinking
|
||||
this ActivityPub library.
|
||||
|
||||
v0.4.0 2018-11-17
|
||||
|
||||
* The 'streams' package now has constructors for each of its generated types.
|
||||
* The README now lists WriteFreely instead of Write.as as a user of this
|
||||
library, though Write.as does also use go-fed. WriteFreely is the open-
|
||||
source fork of the proprietary Write.as.
|
||||
* The 'tools/exp' subdirectory contains a half-built work-in-progress tool that
|
||||
will be used to generate the v1.X versions of this library. It does not
|
||||
materially affect the v0.X versions' functionality nor its code
|
||||
generation. Its presence is merely a consequence of the go-fed/activity
|
||||
maintainer [continuing to] fail at using git branches.
|
||||
|
||||
v0.3.0 2018-08-21
|
||||
|
||||
* Interfaces in 'vocab' now properly contain all of the unknown methods
|
||||
available for all properties. These methods were available on the structs
|
||||
themselves but only partially listed in the interfaces. Specifically,
|
||||
multiply-typed properties (regardless of being functional or not) were
|
||||
omitted.
|
||||
* 'pub' now supports Announce activities' default behavior. Upon receiving an
|
||||
Announce activity via S2S, it is added to the 'shares' property of
|
||||
object(s) owned in the application.
|
||||
* Add the missing 'shares' property to 'vocab.Object' and its child types as
|
||||
well as 'streams.Object' and its child types. It is an ActivityPub
|
||||
specific property not a part of the ActivityStreams specification.
|
||||
* The Callbackers for the SocialAPI and FederateAPI can have additional
|
||||
methods of the form 'X(c context.Context, s *streams.X) error' where X is
|
||||
a Core or Extended type not already a part of the Callbacker interface.
|
||||
This lets client applications decide which Activity types need further
|
||||
handling, without being burdened of implementing unused stub methods. The
|
||||
new Activities supported have no default behavior supported by 'pub',
|
||||
except for the Announce activity as desribed above. An exhaustive list of
|
||||
Activities that can be X (as mentioned above) are:
|
||||
- Announce
|
||||
- Arrive
|
||||
- Dislike
|
||||
- Flag
|
||||
- Ignore
|
||||
- Invite
|
||||
- Join
|
||||
- Leave
|
||||
- Listen
|
||||
- Move
|
||||
- Offer
|
||||
- Question
|
||||
- Read
|
||||
- TentativeAccept
|
||||
- TentativeReject
|
||||
- Travel
|
||||
- View
|
||||
* The 'vocab' package now properly generates Intransitive Activity subtype
|
||||
structs such that they are now convertible to the
|
||||
vocab.IntransitiveActivityType interface.
|
||||
* Types in the 'vocab' package now support setting, getting, removing, and
|
||||
testing existence for unknown properties (extended properties).
|
||||
|
||||
v0.2.1 2018-08-19
|
||||
|
||||
* Request body is now correctly copied when sending federation messages.
|
||||
* Change RWType and FollowResponse to bool and uint8, respectively.
|
||||
* Update README with applications using the go-fed/activity library.
|
||||
* Update README with links to official implementation reports for go-fed.
|
||||
* Add unit test to document behavior when maxDeliveryDepth is set to zero.
|
||||
|
||||
v0.2.0 2018-08-04
|
||||
|
||||
* Begin FederateAPI unofficial implementation report.
|
||||
* All 'vocab.Object' types and types extending from 'vocab.Object' now have an
|
||||
'IsPublic' method that will return true if the 'to', 'bto', 'cc', or 'bcc'
|
||||
properties have the ActivityPub special Public collection IRI. The
|
||||
'streams' types also have a corresponding 'IsPublic' method.
|
||||
* Use 'OrderedCollection' as the default type for 'likes', 'liked',
|
||||
'following', and 'followers' properties if the actor or object does not
|
||||
have an IRI, 'Collection', or 'OrderedCollection' set for these
|
||||
properties.
|
||||
* Examine the IRI of 'objects' when applying the Origin Check policy for Update
|
||||
and Delete activities.
|
||||
* If a federated Activity was already received, do not execute its side effects
|
||||
a second time.
|
||||
* Add 'Like' activities to the 'likes' collection, instead of adding the
|
||||
actors. This was a specification-violating behavior.
|
||||
* No longer try to fetch IRIs when deduping by IRI.
|
||||
* Remove unused methods from fed_test.go.
|
||||
* Fix Media Type header detection for ActivityPub messages.
|
||||
* Improve code generation to remove 230,000 lines of code from the vocab
|
||||
package.
|
||||
* Add list of contributors to CONTRIBUTING.md.
|
||||
* README examples are tagged with golang syntax highlighting.
|
||||
|
||||
v0.1.1 2018-06-13
|
||||
|
||||
* Begin SocialAPI unofficial implementation report.
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
# Contributor Covenant Code Of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and
|
||||
expression, level of experience, education, socio-economic status, nationality,
|
||||
personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others’ private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, or to ban temporarily or permanently any
|
||||
contributor for other behaviors that they deem inappropriate, threatening,
|
||||
offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at cjslep@gmail.com. All complaints will
|
||||
be reviewed and investigated and will result in a response that is deemed
|
||||
necessary and appropriate to the circumstances. The project team is obligated to
|
||||
maintain confidentiality with regard to the reporter of an incident. Further
|
||||
details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project’s leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
|
||||
available at
|
||||
[https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html)
|
|
@ -10,6 +10,7 @@ with the software engineering, `go-fed` welcomes you!
|
|||
3. Whoa, I have a great idea!
|
||||
4. Beep boop, I want to contribute code!
|
||||
5. FAQ
|
||||
6. Contributors
|
||||
|
||||
## I have a question!
|
||||
|
||||
|
@ -68,3 +69,10 @@ Additionally, see [#42](https://github.com/go-fed/activity/issues/42).
|
|||
Donations are strictly viewed as tips and not work-for-hire:
|
||||
|
||||
* [cjslep](https://liberapay.com/cj/)
|
||||
|
||||
## Contributors To This Repository
|
||||
|
||||
In order of first commit contribution.
|
||||
|
||||
* cjslep
|
||||
* 21stio
|
||||
|
|
96
README.md
96
README.md
|
@ -1,67 +1,85 @@
|
|||
# activity
|
||||
|
||||
> Complete ActivityStreams-based ontologies plus middleware handlers implementing ActivityPub
|
||||
|
||||
[![Build Status][Build-Status-Image]][Build-Status-Url] [![Go Reference][Go-Reference-Image]][Go-Reference-Url]
|
||||
[![Go Report Card][Go-Report-Card-Image]][Go-Report-Card-Url] [![License][License-Image]][License-Url]
|
||||
[![Chat][Chat-Image]][Chat-Url] [![OpenCollective][OpenCollective-Image]][OpenCollective-Url]
|
||||
|
||||
`go get github.com/go-fed/activity`
|
||||
|
||||
This repository supports `vgo` and is remotely verifiable.
|
||||
This repository contains two libraries and a tool:
|
||||
|
||||
This repository contains three libraries for use in your golang applications:
|
||||
* `astool`: A linked-data aware tool to generate golang native types for any
|
||||
ActivityStreams vocabulary.
|
||||
* `streams`: The ActivityStreams native types generated with the `astool`.
|
||||
* `pub`: ActivityPub Social Protocol (Client-to-Server or C2S) and Federating
|
||||
Protocol (Server-to-Server or S2S)
|
||||
|
||||
* `vocab`: An ActivityStreams Vocabulary library
|
||||
* `streams`: A convenience library for the ActivityStreams Vocabulary
|
||||
* `pub`: ActivityPub SocialAPI (Client-to-Server) and FederateAPI
|
||||
(Server-to-Server)
|
||||
|
||||
This library is biased. It forgoes understanding JSON-LD in exchange for static
|
||||
typing. It provides a large amount of default behavior to let Social,
|
||||
Federated, or both kinds of ActivityPub applications just work.
|
||||
Check out [go-fed.org](https://go-fed.org/) for tutorials and documentation.
|
||||
|
||||
## Status
|
||||
|
||||
**0.1.1** ([Semantic Versioning](https://semver.org/))
|
||||
**1.0.0** ([Semantic Versioning](https://semver.org/))
|
||||
|
||||
There is no official implementation report available... yet!
|
||||
This library has been successfully used to
|
||||
[federate since May 17, 2019](https://cjslep.com/c/blog/this-blog-is-federated).
|
||||
|
||||
[Unofficial implementation reports are available in issue #46](https://github.com/go-fed/activity/issues/46).
|
||||
An [official implementation report](https://activitypub.rocks/implementation-report/)
|
||||
was last submitted for version **0.2.0** [here](https://github.com/w3c/activitypub/issues/318).
|
||||
Unfortunately, the official implementation report tool is no longer maintained.
|
||||
Previous unofficial implementation reports are available in [issue #46](https://github.com/go-fed/activity/issues/46).
|
||||
|
||||
Please see CHANGELOG for changes between versions.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See each subdirectory for its own README for further elaboration. The
|
||||
recommended reading order is `vocab`, `streams`, and then `pub`. Others are
|
||||
optional.
|
||||
Check out [go-fed.org](https://go-fed.org/) for tutorials and documentation.
|
||||
|
||||
## How can I get help, file issues, or contribute?
|
||||
Also, see `astool`, `streams`, or `pub` for their own README.
|
||||
|
||||
Please see the CONTRIBUTING.md file!
|
||||
## FAQ
|
||||
|
||||
## How well tested are these libraries?
|
||||
### What vocabularies are supported?
|
||||
|
||||
* [ActivityStreams](https://www.w3.org/TR/activitystreams-vocabulary).
|
||||
* A subset of the [toot](https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/adapter.rb) vocabulary.
|
||||
* A subset of the [security](https://w3c-ccg.github.io/security-vocab/) vocabulary.
|
||||
* [ForgeFed](https://forgefed.peers.community/vocabulary.html).
|
||||
|
||||
### How well tested are these libraries?
|
||||
|
||||
I took great care to add numerous tests using examples directly from
|
||||
specifications, official test repositories, and my own end-to-end tests.
|
||||
|
||||
## Who is using this library currently?
|
||||
**v1.0.0** has around 200 unit tests. The **federation** or **S2S** portion of
|
||||
the library is very well tested. The **social** or **C2S** portion could use
|
||||
additional unit tests, but is far less popular than federation. About 70% of the
|
||||
lines are covered by unit tests.
|
||||
|
||||
No one. Please let me know if you are using it!
|
||||
### Who is using this library currently?
|
||||
|
||||
## How do I use these libraries?
|
||||
Note: This list only includes those who have reached out to me to explicitly be
|
||||
included.
|
||||
|
||||
Please see each subdirectory for its own README for further elaboration. The
|
||||
recommended reading order is `vocab`, `streams`, and then `pub`. Others are
|
||||
optional.
|
||||
| Application | Description | Repository | Point Of Contact | Homepage |
|
||||
|:-----------:|:-------------------------------------------------:|:--------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------:|:------------------------------------:|
|
||||
| Anancus | Self-hosted and federated social link aggregation | [https://gitlab.com/tuxether/anancus](https://gitlab.com/tuxether/anancus) | [@tuxether@floss.social](https://floss.social/@tuxether) or [tuxether@protonmail.ch](mailto:tuxether@protonmail.ch) | N/A |
|
||||
| WriteFreely | Simple, open-source, privacy-focused blogging platform | [https://github.com/writeas/writefreely](https://github.com/writeas/writefreely) | [@write_as@writing.exchange](https://writing.exchange/@write_as) or [hello@write.as](mailto:hello@write.as) | [https://writefreely.org](https://writefreely.org) |
|
||||
| Read.as | Long-form reader built on open protocols | [https://github.com/writeas/Read.as](https://github.com/writeas/Read.as) | [@write_as@writing.exchange](https://writing.exchange/@write_as) or [hello@write.as](mailto:hello@write.as) | [https://read.as](https://read.as) |
|
||||
| go-fed/apcore | Generic ActivityPub server framework in Go | [https://github.com/go-fed/apcore](https://github.com/go-fed/apcore) | [@cj@mastodon.technology](https://mastodon.technology/@cj) or [cjslep@gmail.com](mailto:cjslep@gmail.com) | [https://go-fed.org](https://go-fed.org) |
|
||||
|
||||
Passing familiarity with ActivityStreams and ActivityPub is recommended.
|
||||
### How do I use these libraries?
|
||||
|
||||
## Other Libraries
|
||||
Check out [go-fed.org](https://go-fed.org/) for tutorials and documentation.
|
||||
|
||||
* `tools` - Code generation wizardry and ActivityPub-spec-as-data.
|
||||
* `deliverer` - Provides an asynchronous `Deliverer` for use with the `pub` lib
|
||||
Please see each subdirectory for its own README for further elaboration.
|
||||
|
||||
## FAQ
|
||||
### How can I get help, file issues, or contribute?
|
||||
|
||||
Please see the CONTRIBUTING.md file!
|
||||
|
||||
## Useful References
|
||||
### Useful References
|
||||
|
||||
* [ActivityPub Specification](https://www.w3.org/TR/activitypub)
|
||||
* [ActivityPub GitHub Repo](https://github.com/w3c/activitypub)
|
||||
|
@ -74,3 +92,19 @@ Please see the CONTRIBUTING.md file!
|
|||
I would like to thank those that have worked hard to create the technologies
|
||||
and standards that created the opportunity to implement this suite of
|
||||
libraries.
|
||||
|
||||
Thanks to those who have been early adopters with v0 and/or provided early
|
||||
feedback.
|
||||
|
||||
[Build-Status-Image]: https://github.com/go-fed/activity/workflows/build/badge.svg
|
||||
[Build-Status-Url]: https://github.com/go-fed/activity/actions
|
||||
[Go-Reference-Image]: https://pkg.go.dev/badge/github.com/go-fed/activity
|
||||
[Go-Reference-Url]: https://pkg.go.dev/github.com/go-fed/activity
|
||||
[Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/go-fed/activity
|
||||
[Go-Report-Card-Url]: https://goreportcard.com/report/github.com/go-fed/activity
|
||||
[License-Image]: https://img.shields.io/github/license/go-fed/activity?color=blue
|
||||
[License-Url]: https://opensource.org/licenses/BSD-3-Clause
|
||||
[Chat-Image]: https://img.shields.io/matrix/go-fed:feneas.org?server_fqdn=matrix.org
|
||||
[Chat-Url]: https://matrix.to/#/!BLOSvIyKTDLIVjRKSc:feneas.org?via=feneas.org&via=matrix.org
|
||||
[OpenCollective-Image]: https://img.shields.io/opencollective/backers/go-fed-activitypub-labs
|
||||
[OpenCollective-Url]: https://opencollective.com/go-fed-activitypub-labs
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
# ActivityStreams Tool
|
||||
|
||||
```
|
||||
go get github.com/go-fed/activity
|
||||
cd $GOPATH/github.com/go-fed/activity/astool
|
||||
go build
|
||||
./astool -h
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The code-generation tool for ActivityStreams and extensions.
|
||||
|
||||
This tool is simple: It accepts an RDF definition in OWL2 syntax of an
|
||||
ActivityStreams vocabulary, and generates the Go code required to:
|
||||
|
||||
- Create native types and properties of this vocabulary.
|
||||
- Handle the serialization and deserialization of JSON correctly, including
|
||||
the instances where non-functional properties could be an object (`{}`),
|
||||
an array of objects and/or IRIs (`[]`), or an IRI (`https://exmaple.com/id`).
|
||||
- Manages the ActivityStreams inheritance properly of `extends` and `disjoint`,
|
||||
which is in the RDF-sense. It is not the same kind of inheritance as the
|
||||
Object Oriented sense of inheritance.
|
||||
- Provides Resolvers and PredicatedResolvers to (conditionally) take arbitrary
|
||||
objects or data and resolve them into concrete types.
|
||||
|
||||
All of the above code is autogenerated, allowing:
|
||||
|
||||
- Application developers to rapidly use the needed ActivityStreams in their
|
||||
domain.
|
||||
- Extension writers a quick way to iteratively prototype a new ActivityStreams
|
||||
extension, skipping boilerplate code writing in the process.
|
||||
- Go-fed alternatives to fork the tool and generate their own implementations,
|
||||
or hook their own implementations into the dependency-injected Manager so they
|
||||
are used in existing applications seamlessly.
|
||||
|
||||
All code is generated in the current working directory that the tool is executed
|
||||
in.
|
||||
|
||||
## Generating the ActivityStreams Vocabulary
|
||||
|
||||
Comprehensive help is available at:
|
||||
|
||||
```
|
||||
astool -h
|
||||
```
|
||||
|
||||
The ActivityStreams tool accepts one or more specifications for the
|
||||
[Core And Extended ActivityStreams](https://www.w3.org/TR/activitystreams-vocabulary)
|
||||
vocabulary as well as any derived vocabularies. For example, bundled with this
|
||||
tool is `activitystreams.jsonld` which contains the OWL2 definition of the
|
||||
ActivityStreams specification. To generate the code, in your `$GOPATH` do:
|
||||
|
||||
```
|
||||
cd $GOPATH/github.com/go-fed/activity/astool
|
||||
go generate
|
||||
```
|
||||
|
||||
This will automatically generate a number of files containing the functions,
|
||||
structs, and interfaces for use in your program. Alternatively, the
|
||||
`go-fed/activity` library has all of these pregenerated for you.
|
||||
|
||||
## Generating An Extension
|
||||
|
||||
If you want to create an ActivityStreams Extensions, see the provided file
|
||||
`example_custom_spec.jsonld` which contains a custom type and property which
|
||||
leverage the original ActivityStreams specification.
|
||||
|
||||
Any new derived extension must be passed into the tool, as well as any
|
||||
dependencies, in order of derivation:
|
||||
|
||||
```
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
astool -spec activitystreams.jsonld -spec example_custom_spec.jsonld
|
||||
```
|
||||
|
||||
This automatically generates a number of files containing the functions,
|
||||
structs, and interfaces for both of these vocabularies.
|
||||
|
||||
## Generating As A Module
|
||||
|
||||
The tool has untested, experimental support for generating code with a specific
|
||||
prefix path to all package names:
|
||||
|
||||
```
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
astool -spec activitystreams.jsonld -path mymodule
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
This tool relies on built-in knowledge of several ontologies:
|
||||
|
||||
- OWL2
|
||||
- RDF
|
||||
- RDF Schema
|
||||
- Schema.org
|
||||
- XML Schema
|
||||
- RFCs
|
||||
|
||||
It does not have complete knowledge of these schemas, so if an error is
|
||||
encountered during the code generation process with a new extension, please
|
||||
[file an issue](https://github.com/go-fed/activity/issues).
|
||||
|
||||
## Non-Standard Behaviors
|
||||
|
||||
ActivityPub has a requirement where properties in a base type are not inherited
|
||||
by deriving types. This concept is one reason why the ActivityStreams vocabulary
|
||||
cannot be mapped into a traditional object-oriented programming language. It
|
||||
also means that we need an RDF/OWL/JSON-LD concept to encapsulate this detail.
|
||||
Unfortunately, no standards body provides an RDF definition of this meaning, so
|
||||
the `astool` uses the JSON-LD reserved notation `@wtf_without_property` to
|
||||
trigger code generation that results in this behavior.
|
||||
|
||||
It is the hope of this author that the prefix `@wtf` would never be used by the
|
||||
JSON-LD authors and would never have a chance to become a de-facto standard due
|
||||
to the implied profanity. Furthermore, its use should be extremely limited.
|
||||
Therefore, the use of this non-standard keyworld only for the `astool` and in
|
||||
limited applications should never collide with anything else.
|
||||
|
||||
This notation is used in two places:
|
||||
|
||||
* [Intransitive Activity](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-intransitiveactivity)
|
||||
does not have the `object` property, so this is a spec-compliant use.
|
||||
* `OrderedCollection` so that it does not have both `items` and `orderedItems`
|
||||
properties. This is an opinion: `items` for `Collection` and `orderedItems` for
|
||||
`OrderedCollection`.
|
||||
|
||||
## References
|
||||
|
||||
* [JSON-LD Specification](https://json-ld.org/spec/latest/json-ld/)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,85 @@
|
|||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/dave/jennifer/jen"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// sortedFunctionSignature sorts FunctionSignatures by name.
|
||||
type sortedFunctionSignature []FunctionSignature
|
||||
|
||||
// Less compares Names.
|
||||
func (s sortedFunctionSignature) Less(i, j int) bool {
|
||||
return s[i].Name < s[j].Name
|
||||
}
|
||||
|
||||
// Swap values.
|
||||
func (s sortedFunctionSignature) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Len is the length of this slice.
|
||||
func (s sortedFunctionSignature) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// FunctionSignature is an interface's function definition without
|
||||
// an implementation.
|
||||
type FunctionSignature struct {
|
||||
Name string
|
||||
Params []jen.Code
|
||||
Ret []jen.Code
|
||||
Comment string
|
||||
}
|
||||
|
||||
// Signature returns the uncommented raw signature.
|
||||
func (f FunctionSignature) Signature() jen.Code {
|
||||
sig := jen.Func().Params(f.Params...)
|
||||
if len(f.Ret) > 0 {
|
||||
sig.Params(f.Ret...)
|
||||
}
|
||||
return sig
|
||||
}
|
||||
|
||||
// Interface manages and generates a Golang interface definition.
|
||||
type Interface struct {
|
||||
qual *jen.Statement
|
||||
name string
|
||||
functions []FunctionSignature
|
||||
comment string
|
||||
}
|
||||
|
||||
// NewInterface creates an Interface.
|
||||
func NewInterface(pkg, name string,
|
||||
funcs []FunctionSignature,
|
||||
comment string) *Interface {
|
||||
i := &Interface{
|
||||
qual: jen.Qual(pkg, name),
|
||||
name: name,
|
||||
functions: funcs,
|
||||
comment: comment,
|
||||
}
|
||||
sort.Sort(sortedFunctionSignature(i.functions))
|
||||
return i
|
||||
}
|
||||
|
||||
// Definition produces the Golang code.
|
||||
func (i Interface) Definition() jen.Code {
|
||||
stmts := jen.Empty()
|
||||
if len(i.comment) > 0 {
|
||||
stmts = jen.Comment(insertNewlines(i.comment)).Line()
|
||||
}
|
||||
defs := make([]jen.Code, 0, len(i.functions))
|
||||
for _, fn := range i.functions {
|
||||
def := jen.Empty()
|
||||
if len(fn.Comment) > 0 {
|
||||
def.Comment(insertNewlinesIndented(fn.Comment)).Line()
|
||||
}
|
||||
def.Id(fn.Name).Params(fn.Params...)
|
||||
if len(fn.Ret) > 0 {
|
||||
def.Params(fn.Ret...)
|
||||
}
|
||||
defs = append(defs, def)
|
||||
}
|
||||
return stmts.Type().Id(i.name).Interface(defs...)
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/dave/jennifer/jen"
|
||||
)
|
||||
|
||||
// memberType defines the way a method belongs to its struct.
|
||||
type memberType int
|
||||
|
||||
const (
|
||||
this = "this"
|
||||
)
|
||||
|
||||
const (
|
||||
// A method is by value.
|
||||
valueMember memberType = iota
|
||||
// A method is by pointer.
|
||||
pointerMember
|
||||
)
|
||||
|
||||
// This returns the string variable used by members to refer to themselves.
|
||||
func This() string {
|
||||
return this
|
||||
}
|
||||
|
||||
// Function represents a free function, not a method, for Go code to be
|
||||
// generated.
|
||||
type Function struct {
|
||||
qual *jen.Statement
|
||||
name string
|
||||
params []jen.Code
|
||||
ret []jen.Code
|
||||
block []jen.Code
|
||||
comment string
|
||||
}
|
||||
|
||||
// NewCommentedFunction creates a new function with a comment.
|
||||
func NewCommentedFunction(pkg, name string,
|
||||
params, ret, block []jen.Code,
|
||||
comment string) *Function {
|
||||
return &Function{
|
||||
qual: jen.Qual(pkg, name),
|
||||
name: name,
|
||||
params: params,
|
||||
ret: ret,
|
||||
block: block,
|
||||
comment: comment,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFunction creates a new function without any comments.
|
||||
func NewFunction(pkg, name string,
|
||||
params, ret, block []jen.Code) *Function {
|
||||
return &Function{
|
||||
qual: jen.Qual(pkg, name),
|
||||
name: name,
|
||||
params: params,
|
||||
ret: ret,
|
||||
block: block,
|
||||
}
|
||||
}
|
||||
|
||||
// CloneToPackage copies this Function into a new one defined in the provided
|
||||
// package
|
||||
func (m Function) CloneToPackage(pkg string) *Function {
|
||||
f := m
|
||||
f.qual = jen.Qual(pkg, m.name)
|
||||
return &f
|
||||
}
|
||||
|
||||
// Definition generates the Go code required to define and implement this
|
||||
// function.
|
||||
func (m Function) Definition() jen.Code {
|
||||
stmts := jen.Empty()
|
||||
if len(m.comment) > 0 {
|
||||
stmts = jen.Commentf(insertNewlines(m.comment)).Line()
|
||||
}
|
||||
return stmts.Add(jen.Func().Id(m.name).Params(
|
||||
m.params...,
|
||||
).Params(
|
||||
m.ret...,
|
||||
).Block(
|
||||
m.block...,
|
||||
))
|
||||
}
|
||||
|
||||
// Call generates the Go code required to call this function, with qualifier if
|
||||
// required.
|
||||
func (m Function) Call(params ...jen.Code) jen.Code {
|
||||
return m.qual.Clone().Call(params...)
|
||||
}
|
||||
|
||||
// Name returns the identifier of this function.
|
||||
func (m Function) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
// QualifiedName returns the qualified identifier for this function.
|
||||
func (m Function) QualifiedName() *jen.Statement {
|
||||
return m.qual.Clone()
|
||||
}
|
||||
|
||||
// ToFunctionSignature obtains this function's FunctionSignature.
|
||||
func (m Function) ToFunctionSignature() FunctionSignature {
|
||||
return FunctionSignature{
|
||||
Name: m.Name(),
|
||||
Params: m.params,
|
||||
Ret: m.ret,
|
||||
Comment: m.comment,
|
||||
}
|
||||
}
|
||||
|
||||
// Method represents a method on a type, not a free function, for Go code to be
|
||||
// generated.
|
||||
type Method struct {
|
||||
member memberType
|
||||
structName string
|
||||
function *Function
|
||||
}
|
||||
|
||||
// NewCommentedValueMethod defines a commented method for the value of a type.
|
||||
func NewCommentedValueMethod(pkg, name, structName string,
|
||||
params, ret, block []jen.Code,
|
||||
comment string) *Method {
|
||||
return &Method{
|
||||
member: valueMember,
|
||||
structName: structName,
|
||||
function: &Function{
|
||||
qual: jen.Qual(pkg, name),
|
||||
name: name,
|
||||
params: params,
|
||||
ret: ret,
|
||||
block: block,
|
||||
comment: comment,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewValueMethod defines a method for the value of a type. It is not commented.
|
||||
func NewValueMethod(pkg, name, structName string,
|
||||
params, ret, block []jen.Code) *Method {
|
||||
return &Method{
|
||||
member: valueMember,
|
||||
structName: structName,
|
||||
function: &Function{
|
||||
qual: jen.Qual(pkg, name),
|
||||
name: name,
|
||||
params: params,
|
||||
ret: ret,
|
||||
block: block,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewCommentedPointerMethod defines a commented method for the pointer to a
|
||||
// type.
|
||||
func NewCommentedPointerMethod(pkg, name, structName string,
|
||||
params, ret, block []jen.Code,
|
||||
comment string) *Method {
|
||||
return &Method{
|
||||
member: pointerMember,
|
||||
structName: structName,
|
||||
function: &Function{
|
||||
qual: jen.Qual(pkg, name),
|
||||
name: name,
|
||||
params: params,
|
||||
ret: ret,
|
||||
block: block,
|
||||
comment: comment,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewPointerMethod defines a method for the pointer to a type. It is not
|
||||
// commented.
|
||||
func NewPointerMethod(pkg, name, structName string,
|
||||
params, ret, block []jen.Code) *Method {
|
||||
return &Method{
|
||||
member: pointerMember,
|
||||
structName: structName,
|
||||
function: &Function{
|
||||
qual: jen.Qual(pkg, name),
|
||||
name: name,
|
||||
params: params,
|
||||
ret: ret,
|
||||
block: block,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Definition generates the Go code required to define and implement this
|
||||
// method.
|
||||
func (m Method) Definition() jen.Code {
|
||||
comment := jen.Empty()
|
||||
if len(m.function.comment) > 0 {
|
||||
comment = jen.Commentf(insertNewlines(m.function.comment)).Line()
|
||||
}
|
||||
var funcDef *jen.Statement
|
||||
switch m.member {
|
||||
case pointerMember:
|
||||
funcDef = jen.Func().Params(
|
||||
jen.Id(This()).Op("*").Id(m.structName),
|
||||
)
|
||||
case valueMember:
|
||||
funcDef = jen.Func().Params(
|
||||
jen.Id(This()).Id(m.structName),
|
||||
)
|
||||
default:
|
||||
panic("unhandled method memberType")
|
||||
}
|
||||
return comment.Add(funcDef.Id(
|
||||
m.function.name,
|
||||
).Params(
|
||||
m.function.params...,
|
||||
).Params(
|
||||
m.function.ret...,
|
||||
).Block(
|
||||
m.function.block...,
|
||||
))
|
||||
}
|
||||
|
||||
// Call generates the Go code required to call this method, with qualifier if
|
||||
// required.
|
||||
func (m Method) Call(on string, params ...jen.Code) jen.Code {
|
||||
return jen.Id(on).Dot(m.function.name).Call(params...)
|
||||
}
|
||||
|
||||
// On generates the Go code that determines the qualified method name on a
|
||||
// specific variable.
|
||||
func (m Method) On(on string) *jen.Statement {
|
||||
return jen.Id(on).Dot(m.function.name)
|
||||
}
|
||||
|
||||
// Name returns the identifier of this function.
|
||||
func (m Method) Name() string {
|
||||
return m.function.name
|
||||
}
|
||||
|
||||
// ToFunctionSignature obtains this method's FunctionSignature.
|
||||
func (m Method) ToFunctionSignature() FunctionSignature {
|
||||
return FunctionSignature{
|
||||
Name: m.Name(),
|
||||
Params: m.function.params,
|
||||
Ret: m.function.ret,
|
||||
Comment: m.function.comment,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/dave/jennifer/jen"
|
||||
"sort"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// join appends a bunch of Go Code together, each on their own line.
|
||||
func join(s []jen.Code) *jen.Statement {
|
||||
r := jen.Empty()
|
||||
for i, stmt := range s {
|
||||
if i > 0 {
|
||||
r.Line()
|
||||
}
|
||||
r.Add(stmt)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Struct defines a struct-based type, its functions, and its methods for Go
|
||||
// code generation.
|
||||
type Struct struct {
|
||||
comment string
|
||||
name string
|
||||
methods map[string]*Method
|
||||
constructors map[string]*Function
|
||||
members []jen.Code
|
||||
}
|
||||
|
||||
// NewStruct creates a new commented Struct type.
|
||||
func NewStruct(comment string,
|
||||
name string,
|
||||
methods []*Method,
|
||||
constructors []*Function,
|
||||
members []jen.Code) *Struct {
|
||||
s := &Struct{
|
||||
comment: comment,
|
||||
name: name,
|
||||
methods: make(map[string]*Method, len(methods)),
|
||||
constructors: make(map[string]*Function, len(constructors)),
|
||||
members: members,
|
||||
}
|
||||
for _, m := range methods {
|
||||
s.methods[m.Name()] = m
|
||||
}
|
||||
for _, c := range constructors {
|
||||
s.constructors[c.Name()] = c
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Definition generates the Go code required to define and implement this
|
||||
// struct, its methods, and its functions.
|
||||
func (s *Struct) Definition() jen.Code {
|
||||
comment := jen.Empty()
|
||||
if len(s.comment) > 0 {
|
||||
comment = jen.Commentf(insertNewlines(s.comment)).Line()
|
||||
}
|
||||
def := comment.Type().Id(s.name).Struct(
|
||||
join(s.members),
|
||||
)
|
||||
// Sort the functions and methods.
|
||||
fs := make([]string, 0, len(s.constructors))
|
||||
for _, c := range s.constructors {
|
||||
fs = append(fs, c.Name())
|
||||
}
|
||||
ms := make([]string, 0, len(s.methods))
|
||||
for _, m := range s.methods {
|
||||
ms = append(ms, m.Name())
|
||||
}
|
||||
sort.Strings(fs)
|
||||
sort.Strings(ms)
|
||||
// Add the functions and methods in order.
|
||||
for _, c := range fs {
|
||||
def = def.Line().Line().Add(s.constructors[c].Definition())
|
||||
}
|
||||
for _, m := range ms {
|
||||
def = def.Line().Line().Add(s.methods[m].Definition())
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// Method obtains the Go code to be generated for the method with a specific
|
||||
// name. Panics if no such method exists.
|
||||
func (s *Struct) Method(name string) *Method {
|
||||
return s.methods[name]
|
||||
}
|
||||
|
||||
// Constructors obtains the Go code to be generated for the function with a
|
||||
// specific name. Panics if no such function exists.
|
||||
func (s *Struct) Constructors(name string) *Function {
|
||||
return s.constructors[name]
|
||||
}
|
||||
|
||||
// ToInterface creates an interface version of this struct.
|
||||
func (s *Struct) ToInterface(pkg, name, comment string) *Interface {
|
||||
fns := make([]FunctionSignature, 0, len(s.methods))
|
||||
for _, m := range s.methods {
|
||||
if unicode.IsUpper([]rune(m.Name())[0]) {
|
||||
fns = append(fns, m.ToFunctionSignature())
|
||||
}
|
||||
}
|
||||
return NewInterface(pkg, name, fns, comment)
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/dave/jennifer/jen"
|
||||
"sort"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Typedef defines a non-struct-based type, its functions, and its methods for
|
||||
// Go code generation.
|
||||
type Typedef struct {
|
||||
comment string
|
||||
name string
|
||||
concreteType jen.Code
|
||||
methods map[string]*Method
|
||||
constructors map[string]*Function
|
||||
}
|
||||
|
||||
// NewTypedef creates a new commented Typedef.
|
||||
func NewTypedef(comment string,
|
||||
name string,
|
||||
concreteType jen.Code,
|
||||
methods []*Method,
|
||||
constructors []*Function) *Typedef {
|
||||
t := &Typedef{
|
||||
comment: comment,
|
||||
name: name,
|
||||
concreteType: concreteType,
|
||||
methods: make(map[string]*Method, len(methods)),
|
||||
constructors: make(map[string]*Function, len(constructors)),
|
||||
}
|
||||
for _, m := range methods {
|
||||
t.methods[m.Name()] = m
|
||||
}
|
||||
for _, c := range constructors {
|
||||
t.constructors[c.Name()] = c
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Definition generates the Go code required to define and implement this type,
|
||||
// its methods, and its functions.
|
||||
func (t *Typedef) Definition() jen.Code {
|
||||
def := jen.Empty()
|
||||
if len(t.comment) > 0 {
|
||||
def = jen.Commentf(insertNewlines(t.comment)).Line()
|
||||
}
|
||||
def = def.Type().Id(
|
||||
t.name,
|
||||
).Add(
|
||||
t.concreteType,
|
||||
)
|
||||
// Sort the functions and methods
|
||||
fs := make([]string, 0, len(t.constructors))
|
||||
for _, c := range t.constructors {
|
||||
fs = append(fs, c.Name())
|
||||
}
|
||||
ms := make([]string, 0, len(t.methods))
|
||||
for _, m := range t.methods {
|
||||
ms = append(ms, m.Name())
|
||||
}
|
||||
sort.Strings(fs)
|
||||
sort.Strings(ms)
|
||||
// Add the functions and methods in order
|
||||
for _, c := range fs {
|
||||
def = def.Line().Line().Add(t.constructors[c].Definition())
|
||||
}
|
||||
for _, m := range ms {
|
||||
def = def.Line().Line().Add(t.methods[m].Definition())
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// Method obtains the Go code to be generated for the method with a specific
|
||||
// name. Panics if no such method exists.
|
||||
func (t *Typedef) Method(name string) *Method {
|
||||
return t.methods[name]
|
||||
}
|
||||
|
||||
// Constructors obtains the Go code to be generated for the function with a
|
||||
// specific name. Panics if no such function exists.
|
||||
func (t *Typedef) Constructors(name string) *Function {
|
||||
return t.constructors[name]
|
||||
}
|
||||
|
||||
// ToInterface creates an interface version of this typedef.
|
||||
func (t *Typedef) ToInterface(pkg, name, comment string) *Interface {
|
||||
fns := make([]FunctionSignature, 0, len(t.methods))
|
||||
for _, m := range t.methods {
|
||||
if unicode.IsUpper([]rune(m.Name())[0]) {
|
||||
fns = append(fns, m.ToFunctionSignature())
|
||||
}
|
||||
}
|
||||
return NewInterface(pkg, name, fns, comment)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package codegen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
max_width = 80
|
||||
tab_assumed_width = 8
|
||||
replacement = "\n// "
|
||||
httpsScheme = "https://"
|
||||
httpScheme = "http://"
|
||||
)
|
||||
|
||||
// FormatPackageDocumentation is used to format package-level comments.
|
||||
func FormatPackageDocumentation(s string) string {
|
||||
return insertNewlines(s)
|
||||
}
|
||||
|
||||
// insertNewlines is used to trade a space character for a newline character
|
||||
// in order to keep a string's visual width under a certain amount.
|
||||
func insertNewlines(s string) string {
|
||||
s = strings.Replace(s, "\n", replacement, -1)
|
||||
return insertNewlinesEvery(s, max_width)
|
||||
}
|
||||
|
||||
// insertNewlinesIndented is used to trade a space character for a newline
|
||||
// character in order to keep a string's visual width under a certain amount. It
|
||||
// assumes that the string will be indented once, and accounts for it in the
|
||||
// final result.
|
||||
func insertNewlinesIndented(s string) string {
|
||||
return insertNewlinesEvery(s, max_width-tab_assumed_width)
|
||||
}
|
||||
|
||||
// insertNewlinesEvery inserts a newline every n characters maximum, unless
|
||||
// there is a very long run-on word.
|
||||
func insertNewlinesEvery(s string, n int) string {
|
||||
since := 0
|
||||
found := -1
|
||||
diff := len(replacement) - 1
|
||||
i := 0
|
||||
for i < len(s) {
|
||||
if s[i] == ' ' && (since < n || found < 0) {
|
||||
found = i
|
||||
} else if s[i] == '\n' {
|
||||
// Reset, found a newline
|
||||
since = 0
|
||||
found = -1
|
||||
} else if i > len(httpScheme) && s[i-len(httpScheme)+1:i+1] == httpScheme {
|
||||
// Reset, let the link just extend annoyingly.
|
||||
found = -1
|
||||
} else if i > len(httpsScheme) && s[i-len(httpsScheme)+1:i+1] == httpsScheme {
|
||||
// Reset, let the link just extend annoyingly.
|
||||
found = -1
|
||||
}
|
||||
if since >= n && found >= 0 {
|
||||
// Replace character
|
||||
s = s[:found] + replacement + s[found+1:]
|
||||
i += diff
|
||||
since = i - found
|
||||
found = -1
|
||||
} else {
|
||||
i++
|
||||
since++
|
||||
}
|
||||
}
|
||||
return "// " + s
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,84 @@
|
|||
package convert
|
||||
|
||||
import (
|
||||
"github.com/go-fed/activity/astool/gen"
|
||||
)
|
||||
|
||||
// sortableTypeGenerator is a TypeGenerator slice sorted by TypeName.
|
||||
type sortableTypeGenerator []*gen.TypeGenerator
|
||||
|
||||
// Len is the length of this slice.
|
||||
func (s sortableTypeGenerator) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Less returns true if the TypeName at one index is less than one at another
|
||||
// index.
|
||||
func (s sortableTypeGenerator) Less(i, j int) bool {
|
||||
return s[i].TypeName() < s[j].TypeName()
|
||||
}
|
||||
|
||||
// Swap elements at indicated indices.
|
||||
func (s sortableTypeGenerator) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// sortableFuncPropertyGenerator is a FunctionalPropertyGenerator slice sorted
|
||||
// by PropertyName.
|
||||
type sortableFuncPropertyGenerator []*gen.FunctionalPropertyGenerator
|
||||
|
||||
// Len is the length of this slice.
|
||||
func (s sortableFuncPropertyGenerator) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Less returns true if the PropertyName at one index is less than one at
|
||||
// another index.
|
||||
func (s sortableFuncPropertyGenerator) Less(i, j int) bool {
|
||||
return s[i].PropertyName() < s[j].PropertyName()
|
||||
}
|
||||
|
||||
// Swap elements at indicated indices.
|
||||
func (s sortableFuncPropertyGenerator) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// sortableNonFuncPropertyGenerator is a NonFunctionalPropertyGenerator slice
|
||||
// sorted by PropertyName.
|
||||
type sortableNonFuncPropertyGenerator []*gen.NonFunctionalPropertyGenerator
|
||||
|
||||
// Len is the length of this slice.
|
||||
func (s sortableNonFuncPropertyGenerator) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Less returns true if the PropertyName at one index is less than one at
|
||||
// another index.
|
||||
func (s sortableNonFuncPropertyGenerator) Less(i, j int) bool {
|
||||
return s[i].PropertyName() < s[j].PropertyName()
|
||||
}
|
||||
|
||||
// Swap elements at indicated indices.
|
||||
func (s sortableNonFuncPropertyGenerator) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// sortablePropertyGenerator is a PropertyGenerator slice sorted by
|
||||
// PropertyName.
|
||||
type sortablePropertyGenerator []*gen.PropertyGenerator
|
||||
|
||||
// Len is the length of this slice.
|
||||
func (s sortablePropertyGenerator) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Less returns true if the PropertyName at one index is less than one at
|
||||
// another index.
|
||||
func (s sortablePropertyGenerator) Less(i, j int) bool {
|
||||
return s[i].PropertyName() < s[j].PropertyName()
|
||||
}
|
||||
|
||||
// Swap elements at indicated indices.
|
||||
func (s sortablePropertyGenerator) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
{
|
||||
"@context": [
|
||||
{
|
||||
"as": "https://www.w3.org/ns/activitystreams",
|
||||
"owl": "http://www.w3.org/2002/07/owl#",
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||
"rfc": "https://tools.ietf.org/html/",
|
||||
"schema": "http://schema.org/",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#"
|
||||
},
|
||||
{
|
||||
"domain": "rdfs:domain",
|
||||
"example": "schema:workExample",
|
||||
"isDefinedBy": "rdfs:isDefinedBy",
|
||||
"mainEntity": "schema:mainEntity",
|
||||
"members": "owl:members",
|
||||
"name": "schema:name",
|
||||
"notes": "rdfs:comment",
|
||||
"range": "rdfs:range",
|
||||
"subClassOf": "rdfs:subClassOf",
|
||||
"disjointWith": "owl:disjointWith",
|
||||
"subPropertyOf": "rdfs:subPropertyOf",
|
||||
"unionOf": "owl:unionOf",
|
||||
"url": "schema:URL"
|
||||
}
|
||||
],
|
||||
"id": "https://example.com/fake-vocabulary",
|
||||
"type": "owl:Ontology",
|
||||
"name": "FakeVocabulary",
|
||||
"members": [
|
||||
{
|
||||
"id": "https://example.com/fake-vocabulary#CustomType",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"id": "https://example.com/fake-vocabulary#ex1-jsonld",
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"type": "CustomType",
|
||||
"actor": {
|
||||
"type": "Person",
|
||||
"name": "Nemo"
|
||||
},
|
||||
"object": "http://example.org/foo",
|
||||
"summary": "A short summary"
|
||||
},
|
||||
"name": "Example 1"
|
||||
}
|
||||
],
|
||||
"notes": "Custom note for this custom type.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-activity",
|
||||
"name": "as:Activity"
|
||||
},
|
||||
"disjointWith": [],
|
||||
"name": "CustomType",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-customtype"
|
||||
},
|
||||
{
|
||||
"id": "https://example.com/fake-vocabulary#customproperty",
|
||||
"type": "rdf:Property",
|
||||
"example": [
|
||||
{
|
||||
"id": "https://example.com/fake-vocabulary#ex2-jsonld",
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"type": "CustomType",
|
||||
"actor": "http://sally.example.org",
|
||||
"object": "http://example.org/foo",
|
||||
"summary": "Sally and the Foo object",
|
||||
"customproperty": "http://exmaple.org/customtype"
|
||||
},
|
||||
"name": "Example 2"
|
||||
}
|
||||
],
|
||||
"notes": "This is a description of a custom property.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-activity",
|
||||
"name": "as:Activity"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://example.com/fake-vocabulary#dfn-customproperty",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link",
|
||||
"name": "as:Link"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-customtype",
|
||||
"name": "CustomType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subPropertyOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-target",
|
||||
"name": "as:target"
|
||||
},
|
||||
"name": "customproperty",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-customproperty"
|
||||
},
|
||||
{
|
||||
"id": "https://example.com/fake-vocabulary#Update",
|
||||
"type": "owl:Class",
|
||||
"notes": "Collides with the ActivityStreams Update type.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-activity",
|
||||
"name": "as:Activity"
|
||||
},
|
||||
"disjointWith": [],
|
||||
"name": "Update",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-update"
|
||||
},
|
||||
{
|
||||
"id": "https://example.com/fake-vocabulary#accuracy",
|
||||
"type": "rdf:Property",
|
||||
"notes": "Collides with the ActivityStreams accuracy property",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update",
|
||||
"name": "as:Update"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-update",
|
||||
"name": "Update"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isDefinedBy": "https://example.com/fake-vocabulary#dfn-accuracy",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update",
|
||||
"name": "as:Update"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-update",
|
||||
"name": "Update"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "accuracy",
|
||||
"url": "https://example.com/fake-vocabulary#dfn-accuracy"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,881 @@
|
|||
{
|
||||
"@context": [
|
||||
{
|
||||
"as": "https://www.w3.org/ns/activitystreams",
|
||||
"owl": "http://www.w3.org/2002/07/owl#",
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||
"rfc": "https://tools.ietf.org/html/",
|
||||
"schema": "http://schema.org/",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#"
|
||||
},
|
||||
{
|
||||
"domain": "rdfs:domain",
|
||||
"example": "schema:workExample",
|
||||
"isDefinedBy": "rdfs:isDefinedBy",
|
||||
"mainEntity": "schema:mainEntity",
|
||||
"members": "owl:members",
|
||||
"name": "schema:name",
|
||||
"notes": "rdfs:comment",
|
||||
"range": "rdfs:range",
|
||||
"subClassOf": "rdfs:subClassOf",
|
||||
"disjointWith": "owl:disjointWith",
|
||||
"subPropertyOf": "rdfs:subPropertyOf",
|
||||
"unionOf": "owl:unionOf",
|
||||
"url": "schema:URL"
|
||||
}
|
||||
],
|
||||
"id": "https://forgefed.peers.community/ns",
|
||||
"type": "owl:Ontology",
|
||||
"name": "ForgeFed",
|
||||
"members": [
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#Push",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://example.org/aviva/outbox/reBGo",
|
||||
"type": "Push",
|
||||
"actor": "https://example.org/aviva",
|
||||
"to": [
|
||||
"https://example.org/aviva/followers",
|
||||
"https://example.org/aviva/myproject",
|
||||
"https://example.org/aviva/myproject/team",
|
||||
"https://example.org/aviva/myproject/followers"
|
||||
],
|
||||
"summary": "<p>Aviva pushed a commit to myproject</p>",
|
||||
"object": {
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 1,
|
||||
"items": [
|
||||
{
|
||||
"id": "https://example.org/aviva/myproject/commits/d96596230322716bd6f87a232a648ca9822a1c20",
|
||||
"type": "Commit",
|
||||
"attributedTo": "https://example.org/aviva",
|
||||
"context": "https://example.org/aviva/myproject",
|
||||
"hash": "d96596230322716bd6f87a232a648ca9822a1c20",
|
||||
"created": "2019-11-03T13:43:59Z",
|
||||
"summary": "Provide hints in sign-up form fields"
|
||||
}
|
||||
]
|
||||
},
|
||||
"target": "https://example.org/aviva/myproject/branches/master",
|
||||
"context": "https://example.org/aviva/myproject"
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Indicates that new content has been pushed to the Repository.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-activity",
|
||||
"name": "as:Activity"
|
||||
},
|
||||
"name": "Push",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#act-push"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#Repository",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://dev.example/aviva/treesim",
|
||||
"type": "Repository",
|
||||
"publicKey": {
|
||||
"id": "https://dev.example/aviva/treesim#main-key",
|
||||
"owner": "https://dev.example/aviva/treesim",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
|
||||
},
|
||||
"inbox": "https://dev.example/aviva/treesim/inbox",
|
||||
"outbox": "https://dev.example/aviva/treesim/outbox",
|
||||
"followers": "https://dev.example/aviva/treesim/followers",
|
||||
"team": "https://dev.example/aviva/treesim/team",
|
||||
"name": "Tree Growth 3D Simulation",
|
||||
"summary": "<p>Tree growth 3D simulator for my nature exploration game</p>"
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Represents a version control system repository.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
"name": "Repository",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-repository"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#Branch",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://example.org/luke/myrepo/branches/master",
|
||||
"type": "Branch",
|
||||
"name": "master",
|
||||
"context": "https://example.org/luke/myrepo",
|
||||
"ref": "refs/heads/master"
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Represents a named variable reference to a version of the Repository, typically used for committing changes in parallel to other development, and usually eventually merging the changes into the main history line.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
"name": "Branch",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-branch"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#Commit",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://example.org/alice/myrepo/commits/109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
|
||||
"type": "Commit",
|
||||
"context": "https://example.org/alice/myrepo",
|
||||
"attributedTo": "https://example.org/bob",
|
||||
"committedBy": "https://example.org/alice",
|
||||
"hash": "109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
|
||||
"summary": "Add an installation script, fixes issue #89",
|
||||
"description": {
|
||||
"mediaType": "text/plain",
|
||||
"content": "It's about time people can install on their computers!"
|
||||
},
|
||||
"created": "2019-07-11T12:34:56Z",
|
||||
"committed": "2019-07-26T23:45:01Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Represents a named set of changes in the history of a Repository. This is called \"commit\" in Git, Mercurial and Monotone; \"patch\" in Darcs; sometimes called \"change set\". Note that Commit is a set of changes that already exists in a repo’s history, while a Patch is a separate proposed change set, that could be applied and pushed to a repo, resulting with a Commit.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
"name": "Commit",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#TicketDependency",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"type": [
|
||||
"Relationship",
|
||||
"TicketDependency"
|
||||
],
|
||||
"id": "https://example.org/ticket-deps/2342593",
|
||||
"attributedTo": "https://example.org/alice",
|
||||
"summary": "Alice's ticket depends on Bob's ticket",
|
||||
"published": "2019-07-11T12:34:56Z",
|
||||
"subject": "https://example.org/alice/myproj/issues/42",
|
||||
"relationship": "dependsOn",
|
||||
"object": "https://example.com/bob/coolproj/issues/85"
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Represents a relationship between 2 Tickets, in which the resolution of one ticket requires the other ticket to be resolved too. It MUST specify the subject, object and relationship properties, and the relationship property MUST be dependsOn.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship",
|
||||
"name": "as:Relationship"
|
||||
},
|
||||
"name": "TicketDependency",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticketdependency"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#Ticket",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"type": "Ticket",
|
||||
"id": "https://example.org/alice/myrepo/issues/42",
|
||||
"context": "https://example.org/alice/myrepo",
|
||||
"attributedTo": "https://example.com/bob",
|
||||
"summary": "Nothing works!",
|
||||
"content": "<p>Please fix. <i>Everything</i> is broken!</p>",
|
||||
"mediaType": "text/html",
|
||||
"source": {
|
||||
"content": "Please fix. *Everything* is broken!",
|
||||
"mediaType": "text/markdown; variant=CommonMark"
|
||||
},
|
||||
"assignedTo": "https://example.org/alice",
|
||||
"isResolved": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Represents an item that requires work or attention. Tickets exist in the context of a project (which may or may not be a version-control repository), and are used to track ideas, proposals, tasks, bugs and more.",
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
"name": "Ticket",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#earlyItems",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://dev.example/aviva/outbox",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 712,
|
||||
"orderedItems": [
|
||||
"https://dev.example/aviva/outbox/712",
|
||||
"https://dev.example/aviva/outbox/711",
|
||||
"https://dev.example/aviva/outbox/710"
|
||||
],
|
||||
"earlyItems": [
|
||||
"https://dev.example/aviva/outbox/3",
|
||||
"https://dev.example/aviva/outbox/2",
|
||||
"https://dev.example/aviva/outbox/1"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "In an ordered collection (or an ordered collection page) in which items (or orderedItems) contains a continuous subset of the collection’s items from one end, earlyItems identifiers a continuous subset from the other end. For example, if items lists the chronologically latest items, earlyItems would list the chrologically earliest items. The ordering rule for items in earlyItems MUST be the same as in items. For examle, if items lists items in reverse chronogical order, then so does earlyItems.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection",
|
||||
"name": "as:OrderedCollection"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-earlyitems",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link",
|
||||
"name": "as:Link"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "earlyItems",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-earlyitems"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#assignedTo",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Identifies the Person assigned to work on this Ticket.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-assignedto",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person",
|
||||
"name": "as:Person"
|
||||
}
|
||||
},
|
||||
"name": "assignedTo",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-assignedto"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#isResolved",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Specifies whether the Ticket is closed, i.e. the work on it is done and it doesn’t need to attract attention anymore.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-isresolved",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:boolean"
|
||||
},
|
||||
"name": "isResolved",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-isresolved"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#dependsOn",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"notes": "Identifies one or more tickets on which this Ticket depends, i.e. it can’t be resolved without those tickets being resolved too.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-dependson",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"name": "dependsOn",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-dependson"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#dependedBy",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"notes": "Identifies one or more tickets which depend on this Ticket, i.e. they can’t be resolved without this tickets being resolved too.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-dependedby",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"name": "dependedBy",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-dependedby"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#dependencies",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Identifies a Collection of TicketDependency which specify tickets that this Ticket depends on, i.e. this ticket is the subject of the dependsOn relationship.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-dependencies",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#OrderedCollection",
|
||||
"name": "as:OrderedCollection"
|
||||
}
|
||||
},
|
||||
"name": "dependencies",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-dependencies"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#dependants",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Identifies a Collection of TicketDependency which specify tickets that depends on this Ticket, i.e. this ticket is the object of the dependsOn relationship. Often called \"reverse dependencies\".",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-ticket",
|
||||
"name": "Ticket"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-dependants",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#OrderedCollection",
|
||||
"name": "as:OrderedCollection"
|
||||
}
|
||||
},
|
||||
"name": "dependants",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-dependants"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#description",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://example.org/alice/myrepo/commits/109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
|
||||
"type": "Commit",
|
||||
"context": "https://example.org/alice/myrepo",
|
||||
"attributedTo": "https://example.org/bob",
|
||||
"hash": "109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
|
||||
"created": "2019-07-11T12:34:56Z",
|
||||
"summary": "Add an installation script, fixes issue #89",
|
||||
"description": {
|
||||
"mediaType": "text/plain",
|
||||
"content": "It's about time people can install on their computers!"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Specifies the description text of a Commit, which is an optional possibly multi-line text provided in addition to the one-line commit title. The range of the description property works the same way the range of the ActivityPub source property works.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit",
|
||||
"name": "Commit"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-description",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
}
|
||||
},
|
||||
"name": "description",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-description"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#committedBy",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Identifies the actor (usually a person, but could be something else, e.g. a bot) that added a set of changes to the version-control Repository. Sometimes the author of the changes and the committer of those changes aren’t the same actor, in which case the committedBy property can be used to specify who added the changes to the repository. For example, when applying a patch to a repository, e.g. a Git repository, the author would be the person who made the patch, and the committer would be the person who applied the patch to their copy of the repository.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit",
|
||||
"name": "Commit"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-committedby",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
}
|
||||
},
|
||||
"name": "committedBy",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-committedby"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#hash",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Specifies the hash associated with a Commit, which is a unique identifier of the commit within the Repository, usually generated as a cryptographic hash function of some (or all) of the commit’s data or metadata. For example, in Git it would be the SHA1 hash of the commit; in Darcs it would be the SHA1 hash of the patch info.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit",
|
||||
"name": "Commit"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-hash",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "hash",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-hash"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#committed",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Specifies the time that a set of changes was committed into the Repository and became a Commit in it. This can be different from the time the set of changes was produced, e.g. if one person creates a patch and sends to another, and the other person then applies the patch to their copy of the repository. We call the former event \"created\" and the latter event \"committed\", and this latter event is specified by the committed property.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit",
|
||||
"name": "Commit"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-committed",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:dateTime"
|
||||
},
|
||||
"name": "committed",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-committed"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#filesAdded",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"notes": "Specifies a filename, as a relative path, relative to the top of the tree of files in the Repository, of a file that got added in this Commit, and didn’t exist in the previous version of the tree.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit",
|
||||
"name": "Commit"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-filesadded",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "filesAdded",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-filesadded"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#filesModified",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"notes": "Specifies a filename, as a relative path, relative to the top of the tree of files in the Repository, of a file that existed in the previous version of the tree, and its contents got modified in this Commit.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit",
|
||||
"name": "Commit"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-filesmodified",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "filesModified",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-filesmodified"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#filesRemoved",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"notes": "Specifies a filename, as a relative path, relative to the top of the tree of files in the Repository, of a file that existed in the previous version of the tree, and got removed from the tree in this Commit.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-commit",
|
||||
"name": "Commit"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-filesremoved",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "filesRemoved",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-filesremoved"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#ref",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://example.org/luke/myrepo/branches/master",
|
||||
"type": "Branch",
|
||||
"name": "master",
|
||||
"context": "https://example.org/luke/myrepo",
|
||||
"ref": "refs/heads/master"
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Specifies an identifier for a Branch, that is used in the Repository to uniquely refer to it. For example, in Git, \"refs/heads/master\" would be the ref of the master branch.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-branch",
|
||||
"name": "Branch"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-ref",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "ref",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-ref"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#team",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://dev.example/aviva/treesim",
|
||||
"type": "Repository",
|
||||
"publicKey": {
|
||||
"id": "https://dev.example/aviva/treesim#main-key",
|
||||
"owner": "https://dev.example/aviva/treesim",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
|
||||
},
|
||||
"inbox": "https://dev.example/aviva/treesim/inbox",
|
||||
"outbox": "https://dev.example/aviva/treesim/outbox",
|
||||
"followers": "https://dev.example/aviva/treesim/followers",
|
||||
"name": "Tree Growth 3D Simulation",
|
||||
"summary": "<p>Tree growth 3D simulator for my nature exploration game</p>",
|
||||
"team": "https://dev.example/aviva/treesim/team"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://dev.example/aviva/treesim/team",
|
||||
"type": "Collection",
|
||||
"totalItems": 3,
|
||||
"items": [
|
||||
"https://dev.example/aviva",
|
||||
"https://dev.example/luke",
|
||||
"https://code.community/users/lorax"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Specifies a Collection of actors who are working on the object, or responsible for it, or managing or administrating it, or having edit access to it. For example, for a Repository, it could be the people who have push/edit access, the \"collaborators\" of the repository.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-team",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection",
|
||||
"name": "as:Collection"
|
||||
}
|
||||
},
|
||||
"name": "team",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-team"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#ticketsTrackedBy",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://dev.example/aviva/treesim",
|
||||
"type": "Repository",
|
||||
"name": "Tree Growth 3D Simulation",
|
||||
"summary": "<p>Tree growth 3D simulator for my nature exploration game</p>",
|
||||
"ticketsTrackedBy": "https://bugs.example/projects/treesim"
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Identifies the actor which tracks tickets related to the given object. This is the actor to whom you send tickets you’d like to open against the object.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-ticketstrackedby",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
}
|
||||
},
|
||||
"name": "ticketsTrackedBy",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-ticketstrackedby"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#tracksTicketsFor",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://forgefed.peers.community/ns"
|
||||
],
|
||||
"id": "https://bugs.example/treesim",
|
||||
"type": "Project",
|
||||
"tracksTicketsFor": [
|
||||
"https://dev.example/aviva/liblsystem",
|
||||
"https://dev.example/aviva/3d-tree-models",
|
||||
"https://dev.example/aviva/treesim"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"notes": "Identifies objects for which which this ticket tracker tracks tickets. When you’d like to open a ticket against those objects, you can send them to this tracker.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-tracksticketsfor",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object",
|
||||
"name": "as:Object"
|
||||
}
|
||||
},
|
||||
"name": "tracksTicketsFor",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-tracksticketsfor"
|
||||
},
|
||||
{
|
||||
"id": "https://forgefed.peers.community/ns#forks",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "Identifies an OrderedCollection of Repositorys which were created as forks of this Repository, i.e. by cloning it. The order of the collection items is by reverse chronological order of the forking events.",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#type-repository",
|
||||
"name": "Repository"
|
||||
}
|
||||
},
|
||||
"isDefinedBy": "https://forgefed.peers.community/vocabulary.html#prop-forks",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection",
|
||||
"name": "as:OrderedCollection"
|
||||
}
|
||||
},
|
||||
"name": "forks",
|
||||
"url": "https://forgefed.peers.community/vocabulary.html#prop-forks"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateConstants generates string constants for the type and property
|
||||
// names. Note that the properties that could be maps have an additional
|
||||
// constant generatred.
|
||||
func GenerateConstants(types []*TypeGenerator, props []*PropertyGenerator) (c []jen.Code) {
|
||||
for _, t := range types {
|
||||
c = append(c,
|
||||
jen.Commentf(
|
||||
"%s%sName is the string literal of the name for the %s type in the %s vocabulary.", t.VocabName(), t.TypeName(), t.TypeName(), t.VocabName(),
|
||||
).Line().Var().Id(
|
||||
fmt.Sprintf("%s%sName", t.VocabName(), t.TypeName()),
|
||||
).String().Op("=").Lit(t.TypeName()))
|
||||
}
|
||||
for _, p := range props {
|
||||
c = append(c,
|
||||
jen.Commentf(
|
||||
"%s%sPropertyName is the string literal of the name for the %s property in the %s vocabulary.", p.VocabName(), strings.Title(p.PropertyName()), p.PropertyName(), p.VocabName(),
|
||||
).Line().Var().Id(
|
||||
fmt.Sprintf("%s%sPropertyName", p.VocabName(), strings.Title(p.PropertyName())),
|
||||
).String().Op("=").Lit(p.PropertyName()))
|
||||
if p.HasNaturalLanguageMap() {
|
||||
c = append(c,
|
||||
jen.Commentf(
|
||||
"%s%sPropertyMapName is the string literal of the name for the %s property in the %s vocabulary when it is a natural language map.", p.VocabName(), strings.Title(p.PropertyName()), p.PropertyName(), p.VocabName(),
|
||||
).Line().Var().Id(
|
||||
fmt.Sprintf("%s%sPropertyMapName", p.VocabName(), strings.Title(p.PropertyName())),
|
||||
).String().Op("=").Lit(p.PropertyName()+"Map"))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
)
|
||||
|
||||
func GenRootPackageComment(pkgName string) string {
|
||||
return codegen.FormatPackageDocumentation(fmt.Sprintf("Package %s "+
|
||||
"contains constructors and functions necessary for "+
|
||||
"applications to serialize, deserialize, and use "+
|
||||
"ActivityStreams types in Go. This package is code-generated "+
|
||||
"and subject to the same license as the go-fed tool used to "+
|
||||
"generate it.\n\n"+
|
||||
"This package is useful to three classes of developers: "+
|
||||
"end-user-application developers, specification writers "+
|
||||
"creating an ActivityStream Extension, and ActivityPub "+
|
||||
"implementors wanting to create an alternate ActivityStreams "+
|
||||
"implementation that still satisfies the interfaces generated "+
|
||||
"by the go-fed tool.\n\n"+
|
||||
"Application developers should limit their use to the "+
|
||||
"Resolver type, the constructors beginning with \"New\", the "+
|
||||
"\"Extends\" functions, the \"DisjointWith\" functions, the "+
|
||||
"\"ExtendedBy\" functions, and any interfaces returned in "+
|
||||
"those functions in this package. This lets applications use "+
|
||||
"Resolvers to Deserialize or Dispatch specific types. The "+
|
||||
"types themselves can Serialize as needed. The \"Extends\", "+
|
||||
"\"DisjointWith\", and \"ExtendedBy\" functions help navigate "+
|
||||
"the ActivityStreams hierarchy since it is not equivalent to "+
|
||||
"object-oriented inheritance.\n\n"+
|
||||
"When creating an ActivityStreams extension, developers will "+
|
||||
"want to ensure that the generated code builds correctly and "+
|
||||
"check that the properties, types, extensions, and "+
|
||||
"disjointedness is set up correctly. Writing unit tests with "+
|
||||
"concrete types is then the next step. If the tool has an "+
|
||||
"error generating this code, a fix is needed in the tool as "+
|
||||
"it is likely there is a new RDF type being used in the "+
|
||||
"extension that the tool does not know how to resolve. Thus, "+
|
||||
"most development will focus on the go-fed tool itself."+
|
||||
"\n\n"+
|
||||
"Finally, ActivityStreams implementors that want drop-in "+
|
||||
"replacement while still using the generated interfaces are "+
|
||||
"highly encouraged to examine the Manager type in this "+
|
||||
"package (in addition to the constructors) as these are the "+
|
||||
"locations where concrete types are instantiated. When "+
|
||||
"supplying a different type in these two locations, the "+
|
||||
"other generated code will propagate it throughout the "+
|
||||
"rest of an application. The Manager is instantiated as a "+
|
||||
"singleton at init time in this library. It is then injected "+
|
||||
"into each implementation library so they can deserialize "+
|
||||
"their needed types without relying on the underlying "+
|
||||
"concrete type.\n\n"+
|
||||
"Subdirectories of this package include implementation "+
|
||||
"files and functions that are not intended to be directly "+
|
||||
"linked to applications, but are used by this particular "+
|
||||
"package. It is strongly recommended to only use the "+
|
||||
"property interfaces and type interfaces in subdirectories "+
|
||||
"and limiting concrete types to those in this package. The "+
|
||||
"go-fed tool is likely to contain a pruning feature in the "+
|
||||
"future which will analyze an application and eliminate "+
|
||||
"code that would be dead if it were to be generated which "+
|
||||
"reduces the compilation time, compilation resources, and "+
|
||||
"binary size of an application. Such a feature will not be "+
|
||||
"compatible with applications that use the concrete "+
|
||||
"implementation types.",
|
||||
pkgName))
|
||||
}
|
||||
|
||||
func VocabPackageComment(pkgName, vocabName string) string {
|
||||
return codegen.FormatPackageDocumentation(fmt.Sprintf("Package %s "+
|
||||
"contains the interfaces for the %s vocabulary. All "+
|
||||
"applications are strongly encouraged to use these interface "+
|
||||
"types instead of the concrete definitions contained in the "+
|
||||
"implementation subpackage. These interfaces allow "+
|
||||
"applications to consume only the types and properties "+
|
||||
"needed and be independent of the go-fed implementation if "+
|
||||
"another alternative implementation is created. This package "+
|
||||
"is code-generated and subject to the same license as the "+
|
||||
"go-fed tool used to generate it.\n\n"+
|
||||
"Type interfaces contain \"Get\" and \"Set\" methods for "+
|
||||
"its properties. Types also have a \"Serialize\" method to "+
|
||||
"convert the type into an interface map for use with the json "+
|
||||
"package. There is a convenience \"IsExtending\" method on "+
|
||||
"each types which helps with the ActivityStreams hierarchy, "+
|
||||
"which is not the same as object oriented inheritance. While "+
|
||||
"types also have a \"LessThan\" method, it is an arbitrary "+
|
||||
"sort. Do not use it if needing to sort on specific "+
|
||||
"properties, such as publish time. It is best used for "+
|
||||
"normalizing the type. Lastly, do not use the "+
|
||||
"\"GetUnknownProperties\" method in an application. Instead, "+
|
||||
"use the go-fed tool to code generate the property needed. "+
|
||||
"\n\n"+
|
||||
"Properties come in two flavors: functional and "+
|
||||
"non-functional. Functional means that a property can have at "+
|
||||
"most one value, while non-functional means a property could "+
|
||||
"have zero, one, or more values. Any property value may also "+
|
||||
"be an IRI, in which case the application will need to make a "+
|
||||
"HTTP request to fetch the property value.\n\n"+
|
||||
"Functional properties have \"Get\", \"Is\", and \"Set\" "+
|
||||
"methods for determining what kind of value the property is, "+
|
||||
"fetching that value, or setting that value. There is also "+
|
||||
"a \"Serialize\" method which converts the property into an "+
|
||||
"interface type, but applications should not typically use "+
|
||||
"a property's \"Serialize\" and instead should use a type's "+
|
||||
"\"Serialize\" instead. Like types, properties have an "+
|
||||
"arbitrary \"LessThan\" comparison function that should not "+
|
||||
"be used if needing to sort on specific values. Finally, "+
|
||||
"applications should not use the \"KindIndex\" method as it "+
|
||||
"is a comparison mechanism only for those looking to write an "+
|
||||
"alternate implementation.\n\n"+
|
||||
"Non-functional properties can have more than one value, so "+
|
||||
"it has \"Len\" for getting its length, \"At\" for getting "+
|
||||
"an iterator pointing to an element, \"Append\" and "+
|
||||
"\"Prepend\" for adding values, \"Remove\" for removing a "+
|
||||
"value, \"Set\" for overwriting a value, and \"Swap\" for "+
|
||||
"swapping two values' indices. Note that a non-functional "+
|
||||
"property satisfies the sort interface, but it results in an "+
|
||||
"arbitrary but stable ordering best used as a normalized "+
|
||||
"form. A non-functional property's iterator looks like a "+
|
||||
"functional property with \"Next\" and \"Previous\" methods. "+
|
||||
"Applications should not use the \"KindIndex\" methods as it "+
|
||||
"is a comparison mechanism only for those looking to write an "+
|
||||
"alternate implementation of this library.\n\n"+
|
||||
"Types and properties have a \"JSONLDContext\" method that "+
|
||||
"returns a mapping of vocabulary URIs to aliases that are "+
|
||||
"required in the JSON-LD @context when serializing this "+
|
||||
"value. The aliases used by this library when serializing "+
|
||||
"objects is done at code-generation time, unless a different "+
|
||||
"alias was used to deserialize the type or property.\n\n"+
|
||||
"Types, functional properties, and non-functional properties "+
|
||||
"are not designed for concurrent usage by two or more "+
|
||||
"goroutines. Also, certain methods on a non-functional "+
|
||||
"property will invalidate iterators and possibly cause "+
|
||||
"unexpected behaviors. To avoid this, re-obtain an iterator "+
|
||||
"after modifying a non-functional property.",
|
||||
pkgName, vocabName))
|
||||
}
|
||||
|
||||
func PrivateFlatPackageComment(pkgName, vocabName string) string {
|
||||
return codegen.FormatPackageDocumentation(fmt.Sprintf("Package %s "+
|
||||
"contains the implementations for the %s vocabulary. All "+
|
||||
"applications are strongly encouraged to use the interface "+
|
||||
"types instead of these concrete definitions. The interfaces "+
|
||||
"allow applications to consume only the types and properties "+
|
||||
"needed and be independent of the go-fed implementation if "+
|
||||
"another alternative implementation is created. This package "+
|
||||
"is code-generated and subject to the same license as the "+
|
||||
"go-fed tool used to generate it.\n\n"+
|
||||
"This package is independent of other vocabulary "+
|
||||
"implementations by having a Manager injected into it to act "+
|
||||
"as a factory for the concrete implementations of other "+
|
||||
"types. The implementations have been generated together into "+
|
||||
"a single implementation library.\n\n"+
|
||||
"Strongly consider using the interfaces instead of this "+
|
||||
"package.",
|
||||
pkgName, vocabName))
|
||||
}
|
||||
|
||||
func PrivateIndividualTypePackageComment(pkgName, typeName string) string {
|
||||
return codegen.FormatPackageDocumentation(fmt.Sprintf("Package %s "+
|
||||
"contains the implementation for the %s type. All "+
|
||||
"applications are strongly encouraged to use the interface "+
|
||||
"instead of this concrete definition. The interfaces "+
|
||||
"allow applications to consume only the types and properties "+
|
||||
"needed and be independent of the go-fed implementation if "+
|
||||
"another alternative implementation is created. This package "+
|
||||
"is code-generated and subject to the same license as the "+
|
||||
"go-fed tool used to generate it.\n\n"+
|
||||
"This package is independent of other types' and properties' "+
|
||||
"implementations by having a Manager injected into it to act "+
|
||||
"as a factory for the concrete implementations. The "+
|
||||
"implementations have been generated into their own separate "+
|
||||
"subpackages for each vocabulary.\n\n"+
|
||||
"Strongly consider using the interfaces instead of this "+
|
||||
"package.",
|
||||
pkgName, typeName))
|
||||
}
|
||||
|
||||
func PrivateIndividualPropertyPackageComment(pkgName, propertyName string) string {
|
||||
return codegen.FormatPackageDocumentation(fmt.Sprintf("Package %s "+
|
||||
"contains the implementation for the %s property. All "+
|
||||
"applications are strongly encouraged to use the interface "+
|
||||
"instead of this concrete definition. The interfaces "+
|
||||
"allow applications to consume only the types and properties "+
|
||||
"needed and be independent of the go-fed implementation if "+
|
||||
"another alternative implementation is created. This package "+
|
||||
"is code-generated and subject to the same license as the "+
|
||||
"go-fed tool used to generate it.\n\n"+
|
||||
"This package is independent of other types' and properties' "+
|
||||
"implementations by having a Manager injected into it to act "+
|
||||
"as a factory for the concrete implementations. The "+
|
||||
"implementations have been generated into their own separate "+
|
||||
"subpackages for each vocabulary.\n\n"+
|
||||
"Strongly consider using the interfaces instead of this "+
|
||||
"package.",
|
||||
pkgName, propertyName))
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,46 @@
|
|||
package gen
|
||||
|
||||
import ()
|
||||
|
||||
const (
|
||||
JSONLDVocabName = "JSONLD"
|
||||
JSONLDIdName = "id"
|
||||
JSONLDTypeName = "type"
|
||||
jsonLDIdCamelName = "Id"
|
||||
jsonLDTypeCamelName = "Type"
|
||||
jsonLDIdComment = `Provides the globally unique identifier for JSON-LD entities.`
|
||||
jsonLDTypeComment = `Identifies the schema type(s) of the JSON-LD entity.`
|
||||
)
|
||||
|
||||
// NewIdPropety returns the functional property for the JSON-LD "@id" property.
|
||||
func NewIdProperty(pm *PackageManager, xsdAnyUri Kind) (*FunctionalPropertyGenerator, error) {
|
||||
return NewFunctionalPropertyGenerator(
|
||||
JSONLDVocabName,
|
||||
nil,
|
||||
"",
|
||||
pm,
|
||||
Identifier{
|
||||
LowerName: JSONLDIdName,
|
||||
CamelName: jsonLDIdCamelName,
|
||||
},
|
||||
jsonLDIdComment,
|
||||
[]Kind{xsdAnyUri},
|
||||
false)
|
||||
}
|
||||
|
||||
// NewTypeProperty returns the non-functional property for the JSON-LD "@type"
|
||||
// property.
|
||||
func NewTypeProperty(pm *PackageManager, xsdAnyUri, xsdString Kind) (*NonFunctionalPropertyGenerator, error) {
|
||||
return NewNonFunctionalPropertyGenerator(
|
||||
JSONLDVocabName,
|
||||
nil,
|
||||
"",
|
||||
pm,
|
||||
Identifier{
|
||||
LowerName: JSONLDTypeName,
|
||||
CamelName: jsonLDTypeCamelName,
|
||||
},
|
||||
jsonLDTypeComment,
|
||||
[]Kind{xsdAnyUri, xsdString},
|
||||
false)
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
)
|
||||
|
||||
const (
|
||||
managerName = "Manager"
|
||||
managerInitVarName = "mgr"
|
||||
)
|
||||
|
||||
// managerInitName returns the package variable name for the manager.
|
||||
func managerInitName() string {
|
||||
return managerInitVarName
|
||||
}
|
||||
|
||||
// Generates the ActivityStreamManager that handles the creation of
|
||||
// ActivityStream Core, Extended, and any extension types.
|
||||
//
|
||||
// This is implicitly used by Application code, but Application code usually
|
||||
// won't need to manually use this Manager.
|
||||
//
|
||||
// This also provides interfaces to break the recursive/cyclic dependencies
|
||||
// between properties and types. The previous version of this tool did not
|
||||
// attempt to solve this problem, and instead just created one big and bloated
|
||||
// library in order to avoid having to break the dependence. This version of
|
||||
// the tool instead will generate interfaces for all of the required types.
|
||||
//
|
||||
// This means that developers will only ever need to interact with these
|
||||
// interfaces, and could switch out using this implementation for another one of
|
||||
// their own choosing.
|
||||
//
|
||||
// Also note that the manager links against all the implementations to generate
|
||||
// a comprehensive registry. So while individual properties and types are able
|
||||
// to be compiled separately, this generated output will link against all of
|
||||
// these libraries.
|
||||
//
|
||||
// TODO: Improve the code generation to examine specific Golang code to
|
||||
// determine which types to actually generate, and prune the unneeded types.
|
||||
// This would cut down on the bloat on a per-program basis.
|
||||
type ManagerGenerator struct {
|
||||
pkg Package
|
||||
tg []*TypeGenerator
|
||||
fp []*FunctionalPropertyGenerator
|
||||
nfp []*NonFunctionalPropertyGenerator
|
||||
// Constructed at creation time. These rely on pointer stability,
|
||||
// which should happen as none of these generators are treated as
|
||||
// values.
|
||||
tgManagedMethods map[*TypeGenerator]*managedMethods
|
||||
fpManagedMethods map[*FunctionalPropertyGenerator]*managedMethods
|
||||
nfpManagedMethods map[*NonFunctionalPropertyGenerator]*managedMethods
|
||||
}
|
||||
|
||||
// managedMethods caches the specific methods and interfaces mapped to specific
|
||||
// properties and types.
|
||||
type managedMethods struct {
|
||||
deserializor *codegen.Method
|
||||
}
|
||||
|
||||
// NewManagerGenerator creates a new manager system.
|
||||
//
|
||||
// This generator should be constructed in the third pass, after types and
|
||||
// property generators are all constructed.
|
||||
func NewManagerGenerator(pkg Package,
|
||||
tg []*TypeGenerator,
|
||||
fp []*FunctionalPropertyGenerator,
|
||||
nfp []*NonFunctionalPropertyGenerator) (*ManagerGenerator, error) {
|
||||
mg := &ManagerGenerator{
|
||||
pkg: pkg,
|
||||
tg: tg,
|
||||
fp: fp,
|
||||
nfp: nfp,
|
||||
tgManagedMethods: make(map[*TypeGenerator]*managedMethods, len(tg)),
|
||||
fpManagedMethods: make(map[*FunctionalPropertyGenerator]*managedMethods, len(fp)),
|
||||
nfpManagedMethods: make(map[*NonFunctionalPropertyGenerator]*managedMethods, len(nfp)),
|
||||
}
|
||||
// Pass 1: Get all deserializor-like methods created. Further passes may
|
||||
// rely on already having this data available in the manager.
|
||||
for _, t := range tg {
|
||||
mg.tgManagedMethods[t] = &managedMethods{
|
||||
deserializor: mg.createDeserializationMethodForType(t),
|
||||
}
|
||||
}
|
||||
for _, p := range fp {
|
||||
mg.fpManagedMethods[p] = &managedMethods{
|
||||
deserializor: mg.createDeserializationMethodForFuncProperty(p),
|
||||
}
|
||||
}
|
||||
for _, p := range nfp {
|
||||
mg.nfpManagedMethods[p] = &managedMethods{
|
||||
deserializor: mg.createDeserializationMethodForNonFuncProperty(p),
|
||||
}
|
||||
}
|
||||
// Pass 2: Inform the type of this ManagerGenerator so that it can keep
|
||||
// all of its bookkeeping straight.
|
||||
for _, t := range tg {
|
||||
if e := t.apply(mg); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
return mg, nil
|
||||
}
|
||||
|
||||
// getDeserializationMethodForType obtains the deserialization method for a
|
||||
// type.
|
||||
func (m *ManagerGenerator) getDeserializationMethodForType(t *TypeGenerator) *codegen.Method {
|
||||
return m.tgManagedMethods[t].deserializor
|
||||
}
|
||||
|
||||
// getDeserializationMethodForProperty obtains the deserialization method for a
|
||||
// property regardless whether it is functional or non-functional.
|
||||
func (m *ManagerGenerator) getDeserializationMethodForProperty(p Property) *codegen.Method {
|
||||
switch v := p.(type) {
|
||||
case *FunctionalPropertyGenerator:
|
||||
return m.fpManagedMethods[v].deserializor
|
||||
case *NonFunctionalPropertyGenerator:
|
||||
return m.nfpManagedMethods[v].deserializor
|
||||
default:
|
||||
panic("unknown property type")
|
||||
}
|
||||
}
|
||||
|
||||
// Definition creates a manager implementation that works with the interface
|
||||
// types required by the other PropertyGenerators and TypeGenerators for
|
||||
// serializing and deserializing.
|
||||
//
|
||||
// Applications will implicitly use this manager and be isolated from the
|
||||
// underlying specific go-fed implementation. If another alternative to go-fed
|
||||
// were to be created, it could target those interfaces and be a drop-in
|
||||
// replacement for an application.
|
||||
//
|
||||
// It is necessary to have this to acheive isolation without cyclic
|
||||
// dependencies: types and properties can each belong in their own package (if
|
||||
// desired) to minimize binary bloat.
|
||||
func (m *ManagerGenerator) Definition() *codegen.Struct {
|
||||
var methods []*codegen.Method
|
||||
for _, tg := range m.tgManagedMethods {
|
||||
methods = append(methods, tg.deserializor)
|
||||
}
|
||||
for _, fp := range m.fpManagedMethods {
|
||||
methods = append(methods, fp.deserializor)
|
||||
}
|
||||
for _, nfp := range m.nfpManagedMethods {
|
||||
methods = append(methods, nfp.deserializor)
|
||||
}
|
||||
s := codegen.NewStruct(
|
||||
fmt.Sprintf("%s manages interface types and deserializations for use by generated code. Application code implicitly uses this manager at run-time to create concrete implementations of the interfaces.", managerName),
|
||||
managerName,
|
||||
methods,
|
||||
/*functions=*/ nil,
|
||||
/*members=*/ nil)
|
||||
return s
|
||||
}
|
||||
|
||||
// createDeserializationMethodForType creates a new deserialization method for
|
||||
// a type.
|
||||
func (m *ManagerGenerator) createDeserializationMethodForType(tg *TypeGenerator) *codegen.Method {
|
||||
return m.createDeserializationMethod(
|
||||
tg.deserializationFnName(),
|
||||
tg.PublicPackage(),
|
||||
tg.PrivatePackage(),
|
||||
tg.InterfaceName(),
|
||||
tg.VocabName())
|
||||
}
|
||||
|
||||
// createDeserializationMethodForFuncProperty creates a new deserialization
|
||||
// method for a functional property.
|
||||
func (m *ManagerGenerator) createDeserializationMethodForFuncProperty(fp *FunctionalPropertyGenerator) *codegen.Method {
|
||||
return m.createDeserializationMethod(
|
||||
fp.DeserializeFnName(),
|
||||
fp.GetPublicPackage(),
|
||||
fp.GetPrivatePackage(),
|
||||
fp.InterfaceName(),
|
||||
fp.VocabName())
|
||||
}
|
||||
|
||||
// createDeserializationMethodForNonFuncProperty creates a new deserialization
|
||||
// method for a non-functional property.
|
||||
func (m *ManagerGenerator) createDeserializationMethodForNonFuncProperty(nfp *NonFunctionalPropertyGenerator) *codegen.Method {
|
||||
return m.createDeserializationMethod(
|
||||
nfp.DeserializeFnName(),
|
||||
nfp.GetPublicPackage(),
|
||||
nfp.GetPrivatePackage(),
|
||||
nfp.InterfaceName(),
|
||||
nfp.VocabName())
|
||||
}
|
||||
|
||||
// createDeserializationMethod returns a function
|
||||
func (m *ManagerGenerator) createDeserializationMethod(deserName string, pubPkg, privPkg Package, interfaceName, vocabName string) *codegen.Method {
|
||||
name := fmt.Sprintf("%s%s", deserName, vocabName)
|
||||
return codegen.NewCommentedValueMethod(
|
||||
m.pkg.Path(),
|
||||
name,
|
||||
managerName,
|
||||
/*param=*/ nil,
|
||||
[]jen.Code{
|
||||
jen.Func().Params(
|
||||
jen.Map(jen.String()).Interface(),
|
||||
jen.Map(jen.String()).String(),
|
||||
).Params(
|
||||
jen.Qual(pubPkg.Path(), interfaceName),
|
||||
jen.Error(),
|
||||
),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Func().Params(
|
||||
jen.Id("m").Map(jen.String()).Interface(),
|
||||
jen.Id("aliasMap").Map(jen.String()).String(),
|
||||
).Params(
|
||||
jen.Qual(pubPkg.Path(), interfaceName),
|
||||
jen.Error(),
|
||||
).Block(
|
||||
jen.List(
|
||||
jen.Id("i"),
|
||||
jen.Err(),
|
||||
).Op(":=").Qual(privPkg.Path(), deserName).Call(jen.Id("m"), jen.Id("aliasMap")),
|
||||
jen.If(
|
||||
jen.Id("i").Op("==").Nil(),
|
||||
).Block(
|
||||
jen.Return(jen.Nil(), jen.Err()),
|
||||
),
|
||||
jen.Return(jen.List(
|
||||
jen.Id("i"),
|
||||
jen.Err(),
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s returns the deserialization method for the %q non-functional property in the vocabulary %q", name, interfaceName, vocabName))
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,497 @@
|
|||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PackageManager manages the path and names of a package consisting of a public
|
||||
// and a private portion.
|
||||
type PackageManager struct {
|
||||
prefix string
|
||||
root string
|
||||
public string
|
||||
private string
|
||||
}
|
||||
|
||||
// NewPackageManager creates a package manager whose private implementation is
|
||||
// in an "impl" subdirectory.
|
||||
func NewPackageManager(prefix, root string) *PackageManager {
|
||||
pathPrefix := strings.Replace(prefix, string(os.PathSeparator), "/", -1)
|
||||
pathRoot := strings.Replace(root, string(os.PathSeparator), "/", -1)
|
||||
return &PackageManager{
|
||||
prefix: pathPrefix,
|
||||
root: pathRoot,
|
||||
public: "",
|
||||
private: "impl",
|
||||
}
|
||||
}
|
||||
|
||||
// PublicPackage returns the public package.
|
||||
func (p *PackageManager) PublicPackage() Package {
|
||||
return p.toPackage(p.public, true)
|
||||
}
|
||||
|
||||
// PrivatePackage returns the private package.
|
||||
func (p *PackageManager) PrivatePackage() Package {
|
||||
return p.toPackage(p.private, false)
|
||||
}
|
||||
|
||||
// Sub creates a PackageManager clone that manages a subdirectory.
|
||||
func (p *PackageManager) Sub(name string) *PackageManager {
|
||||
s := name
|
||||
if len(p.root) > 0 {
|
||||
s = fmt.Sprintf("%s/%s", p.root, name)
|
||||
}
|
||||
return &PackageManager{
|
||||
prefix: p.prefix,
|
||||
root: s,
|
||||
public: p.public,
|
||||
private: p.private,
|
||||
}
|
||||
}
|
||||
|
||||
// SubPrivate creates a PackageManager clone where the private package is one
|
||||
// subdirectory further.
|
||||
func (p *PackageManager) SubPrivate(name string) *PackageManager {
|
||||
s := name
|
||||
if len(p.private) > 0 {
|
||||
s = fmt.Sprintf("%s/%s", p.private, name)
|
||||
}
|
||||
return &PackageManager{
|
||||
prefix: p.prefix,
|
||||
root: p.root,
|
||||
public: p.public,
|
||||
private: s,
|
||||
}
|
||||
}
|
||||
|
||||
// SubPublic creates a PackageManager clone where the public package is one
|
||||
// subdirectory further.
|
||||
func (p *PackageManager) SubPublic(name string) *PackageManager {
|
||||
s := name
|
||||
if len(p.public) > 0 {
|
||||
s = fmt.Sprintf("%s/%s", p.public, name)
|
||||
}
|
||||
return &PackageManager{
|
||||
prefix: p.prefix,
|
||||
root: p.root,
|
||||
public: s,
|
||||
private: p.private,
|
||||
}
|
||||
}
|
||||
|
||||
// toPackage returns the public or private Package managed by this
|
||||
// PackageManager.
|
||||
func (p *PackageManager) toPackage(suffix string, public bool) Package {
|
||||
var path string
|
||||
if len(p.root) > 0 && len(suffix) > 0 {
|
||||
path = strings.Join([]string{p.root, suffix}, "/")
|
||||
} else if len(suffix) > 0 {
|
||||
path = suffix
|
||||
} else if len(p.root) > 0 {
|
||||
path = p.root
|
||||
}
|
||||
s := strings.Split(path, "/")
|
||||
name := s[len(s)-1]
|
||||
return Package{
|
||||
prefix: p.prefix,
|
||||
path: path,
|
||||
name: name,
|
||||
isPublic: public,
|
||||
parent: p,
|
||||
}
|
||||
}
|
||||
|
||||
// Package represents a Golang package.
|
||||
type Package struct {
|
||||
prefix string
|
||||
path string
|
||||
name string
|
||||
isPublic bool
|
||||
parent *PackageManager
|
||||
}
|
||||
|
||||
// Path is the GOPATH or module path to this package.
|
||||
func (p Package) Path() string {
|
||||
path := p.prefix
|
||||
if len(p.path) > 0 {
|
||||
path += "/" + p.path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// WriteDir obtains the relative directory this package should be written to,
|
||||
// which may not be the same as Path. The calling code may not be running at the
|
||||
// root of GOPATH.
|
||||
func (p Package) WriteDir() string {
|
||||
return p.path
|
||||
}
|
||||
|
||||
// Name returns the name of this package.
|
||||
func (p Package) Name() string {
|
||||
return strings.Replace(p.name, "_", "", -1)
|
||||
}
|
||||
|
||||
// IsPublic returns whether this package is intended to house public files for
|
||||
// application developer use.
|
||||
func (p Package) IsPublic() bool {
|
||||
return p.isPublic
|
||||
}
|
||||
|
||||
// Parent returns the PackageManager managing this Package.
|
||||
func (p Package) Parent() *PackageManager {
|
||||
return p.parent
|
||||
}
|
||||
|
||||
const (
|
||||
managerInterfaceName = "privateManager"
|
||||
setManagerFunctionName = "SetManager"
|
||||
setTypePropertyConstructorName = "SetTypePropertyConstructor"
|
||||
)
|
||||
|
||||
// TypePackageGenerator manages generating one-time files needed for types.
|
||||
type TypePackageGenerator struct {
|
||||
typeVocabName string
|
||||
m *ManagerGenerator
|
||||
typeProperty *PropertyGenerator
|
||||
}
|
||||
|
||||
// NewTypePackageGenerator creates a new TypePackageGenerator.
|
||||
func NewTypePackageGenerator(
|
||||
typeVocabName string,
|
||||
m *ManagerGenerator,
|
||||
typeProperty *NonFunctionalPropertyGenerator) *TypePackageGenerator {
|
||||
return &TypePackageGenerator{
|
||||
typeVocabName: typeVocabName,
|
||||
m: m,
|
||||
typeProperty: &typeProperty.PropertyGenerator,
|
||||
}
|
||||
}
|
||||
|
||||
// PublicDefinitions creates the public-facing code generated definitions needed
|
||||
// once per package.
|
||||
//
|
||||
// Precondition: The passed-in generators are the complete set of type
|
||||
// generators within a package. Must satisfy: len(tgs) > 0.
|
||||
func (t *TypePackageGenerator) PublicDefinitions(tgs []*TypeGenerator) (typeI *codegen.Interface) {
|
||||
return publicTypeDefinitions(tgs)
|
||||
}
|
||||
|
||||
// PrivateDefinitions creates the private code generated definitions needed once
|
||||
// per package.
|
||||
//
|
||||
// Precondition: The passed-in generators are the complete set of type
|
||||
// generators within a package. len(tgs) > 0
|
||||
func (t *TypePackageGenerator) PrivateDefinitions(tgs []*TypeGenerator) ([]*jen.Statement, []*codegen.Interface, []*codegen.Function) {
|
||||
pkg := tgs[0].PrivatePackage()
|
||||
s, i, f := privateManagerHookDefinitions(pkg, tgs, nil)
|
||||
interfaces := []*codegen.Interface{i, ContextInterface(pkg)}
|
||||
cv, setCv := privateTypePropertyConstructor(pkg, toPublicConstructor(t.typeVocabName, t.m, t.typeProperty))
|
||||
return []*jen.Statement{s, cv}, interfaces, []*codegen.Function{f, setCv}
|
||||
}
|
||||
|
||||
// PropertyPackageGenerator manages generating one-time files needed for
|
||||
// properties.
|
||||
type PropertyPackageGenerator struct{}
|
||||
|
||||
// NewPropertyPackageGenerator creates a new PropertyPackageGenerator.
|
||||
func NewPropertyPackageGenerator() *PropertyPackageGenerator {
|
||||
return &PropertyPackageGenerator{}
|
||||
}
|
||||
|
||||
// PrivateDefinitions creates the private code generated definitions needed once
|
||||
// per package.
|
||||
//
|
||||
// Precondition: The passed-in generators are the complete set of type
|
||||
// generators within a package. len(pgs) > 0
|
||||
func (p *PropertyPackageGenerator) PrivateDefinitions(pgs []*PropertyGenerator) (*jen.Statement, *codegen.Interface, *codegen.Function) {
|
||||
return privateManagerHookDefinitions(pgs[0].GetPrivatePackage(), nil, pgs)
|
||||
}
|
||||
|
||||
// PackageGenerator maanges generating one-time files needed for both type and
|
||||
// property implementations.
|
||||
type PackageGenerator struct {
|
||||
typeVocabName string
|
||||
m *ManagerGenerator
|
||||
typeProperty *PropertyGenerator
|
||||
}
|
||||
|
||||
// NewPackageGenerator creates a new PackageGenerator.
|
||||
func NewPackageGenerator(typeVocabName string, m *ManagerGenerator, typeProperty *NonFunctionalPropertyGenerator) *PackageGenerator {
|
||||
return &PackageGenerator{
|
||||
typeVocabName: typeVocabName,
|
||||
m: m,
|
||||
typeProperty: &typeProperty.PropertyGenerator,
|
||||
}
|
||||
}
|
||||
|
||||
// InitDefinitions returns the root init function needed to inject proper global
|
||||
// package-private variables needed at runtime. This is the dependency injection
|
||||
// into the implementation.
|
||||
func (t *PackageGenerator) InitDefinitions(pkg Package, tgs []*TypeGenerator, pgs []*PropertyGenerator) (globalManager *jen.Statement, init *codegen.Function) {
|
||||
return genInit(pkg, tgs, pgs, toPublicConstructor(t.typeVocabName, t.m, t.typeProperty))
|
||||
}
|
||||
|
||||
// RootDefinitions creates functions needed at the root level of the package declarations.
|
||||
func (t *PackageGenerator) RootDefinitions(vocabName string, tgs []*TypeGenerator, pgs []*PropertyGenerator) (typeCtors, propCtors, ext, disj, extBy, isA []*codegen.Function) {
|
||||
return rootDefinitions(vocabName, t.m, tgs, pgs)
|
||||
}
|
||||
|
||||
// PublicDefinitions creates the public-facing code generated definitions needed
|
||||
// once per package.
|
||||
//
|
||||
// Precondition: The passed-in generators are the complete set of type
|
||||
// generators within a package.
|
||||
func (t *PackageGenerator) PublicDefinitions(tgs []*TypeGenerator) *codegen.Interface {
|
||||
return publicTypeDefinitions(tgs)
|
||||
}
|
||||
|
||||
// PrivateDefinitions creates the private code generated definitions needed once
|
||||
// per package.
|
||||
//
|
||||
// Precondition: The passed-in generators are the complete set of type
|
||||
// generators within a package. One of tgs or pgs has at least one value.
|
||||
func (t *PackageGenerator) PrivateDefinitions(tgs []*TypeGenerator, pgs []*PropertyGenerator) ([]*jen.Statement, []*codegen.Interface, []*codegen.Function) {
|
||||
var pkg Package
|
||||
if len(tgs) > 0 {
|
||||
pkg = tgs[0].PrivatePackage()
|
||||
} else {
|
||||
pkg = pgs[0].GetPrivatePackage()
|
||||
}
|
||||
s, i, f := privateManagerHookDefinitions(pkg, tgs, pgs)
|
||||
interfaces := []*codegen.Interface{i, ContextInterface(pkg)}
|
||||
cv, setCv := privateTypePropertyConstructor(pkg, toPublicConstructor(t.typeVocabName, t.m, t.typeProperty))
|
||||
return []*jen.Statement{s, cv}, interfaces, []*codegen.Function{f, setCv}
|
||||
}
|
||||
|
||||
// privateTypePropertyConstructor creates common code needed by types to hook
|
||||
// the type property constructor into this package at init time without
|
||||
// statically linking to a specific implementation.
|
||||
func privateTypePropertyConstructor(pkg Package, typePropertyConstructor *codegen.Function) (ctrVar *jen.Statement, setCtrVar *codegen.Function) {
|
||||
sig := typePropertyConstructor.ToFunctionSignature().Signature()
|
||||
ctrVar = jen.Var().Id(typePropertyConstructorName()).Add(sig)
|
||||
setCtrVar = codegen.NewCommentedFunction(
|
||||
pkg.Path(),
|
||||
setTypePropertyConstructorName,
|
||||
[]jen.Code{
|
||||
jen.Id("f").Add(sig),
|
||||
},
|
||||
/*ret=*/ nil,
|
||||
[]jen.Code{
|
||||
jen.Id(typePropertyConstructorName()).Op("=").Id("f"),
|
||||
},
|
||||
fmt.Sprintf("%s sets the \"type\" property's constructor in the package-global variable. For internal use only, do not use as part of Application behavior. Must be called at golang init time. Permits ActivityStreams types to correctly set their \"type\" property at construction time, so users don't have to remember to do so each time. It is dependency injected so other go-fed compatible implementations could inject their own type.", setTypePropertyConstructorName))
|
||||
return
|
||||
}
|
||||
|
||||
// privateManagerHookDefinitions creates common code needed by types and
|
||||
// properties to properly hook in the manager at initialization time.
|
||||
func privateManagerHookDefinitions(pkg Package, tgs []*TypeGenerator, pgs []*PropertyGenerator) (mgrVar *jen.Statement, mgrI *codegen.Interface, setMgrFn *codegen.Function) {
|
||||
fnsMap := make(map[string]codegen.FunctionSignature)
|
||||
for _, tg := range tgs {
|
||||
for _, m := range tg.getAllManagerMethods() {
|
||||
v := m.ToFunctionSignature()
|
||||
fnsMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
for _, pg := range pgs {
|
||||
for _, m := range pg.getAllManagerMethods() {
|
||||
v := m.ToFunctionSignature()
|
||||
fnsMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
var fns []codegen.FunctionSignature
|
||||
for _, v := range fnsMap {
|
||||
fns = append(fns, v)
|
||||
}
|
||||
path := pkg.Path()
|
||||
return jen.Var().Id(managerInitName()).Id(managerInterfaceName),
|
||||
codegen.NewInterface(path,
|
||||
managerInterfaceName,
|
||||
fns,
|
||||
fmt.Sprintf("%s abstracts the code-generated manager that provides access to concrete implementations.", managerInterfaceName)),
|
||||
codegen.NewCommentedFunction(path,
|
||||
setManagerFunctionName,
|
||||
[]jen.Code{
|
||||
jen.Id("m").Id(managerInterfaceName),
|
||||
},
|
||||
/*ret=*/ nil,
|
||||
[]jen.Code{
|
||||
jen.Id(managerInitName()).Op("=").Id("m"),
|
||||
},
|
||||
fmt.Sprintf("%s sets the manager package-global variable. For internal use only, do not use as part of Application behavior. Must be called at golang init time.", setManagerFunctionName))
|
||||
}
|
||||
|
||||
// publicTypeDefinitions creates common types needed by types for their public
|
||||
// package.
|
||||
//
|
||||
// Requires tgs to not be empty.
|
||||
func publicTypeDefinitions(tgs []*TypeGenerator) (typeI *codegen.Interface) {
|
||||
return TypeInterface(tgs[0].PublicPackage())
|
||||
}
|
||||
|
||||
// rootDefinitions creates common functions needed at the root level of the
|
||||
// package declarations.
|
||||
func rootDefinitions(vocabName string, m *ManagerGenerator, tgs []*TypeGenerator, pgs []*PropertyGenerator) (typeCtors, propCtors, ext, disj, extBy, isA []*codegen.Function) {
|
||||
// Type constructors
|
||||
for _, tg := range tgs {
|
||||
typeCtors = append(typeCtors, codegen.NewCommentedFunction(
|
||||
m.pkg.Path(),
|
||||
fmt.Sprintf("New%s%s", vocabName, tg.TypeName()),
|
||||
/*params=*/ nil,
|
||||
[]jen.Code{jen.Qual(tg.PublicPackage().Path(), tg.InterfaceName())},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
tg.constructorFn().Call(),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("New%s%s creates a new %s", vocabName, tg.TypeName(), tg.InterfaceName())))
|
||||
}
|
||||
// Property Constructors
|
||||
for _, pg := range pgs {
|
||||
propCtors = append(propCtors, toPublicConstructor(vocabName, m, pg))
|
||||
}
|
||||
// Is
|
||||
for _, tg := range tgs {
|
||||
f := tg.isATypeDefinition()
|
||||
name := fmt.Sprintf("%s%s%s", isAMethod, vocabName, tg.TypeName())
|
||||
isA = append(isA, codegen.NewCommentedFunction(
|
||||
m.pkg.Path(),
|
||||
name,
|
||||
[]jen.Code{jen.Id("other").Qual(tg.PublicPackage().Path(), typeInterfaceName)},
|
||||
[]jen.Code{jen.Bool()},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
f.Call(jen.Id("other")),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s returns true if the other provided type is the %s type or extends from the %s type.", name, tg.TypeName(), tg.TypeName())))
|
||||
}
|
||||
// Extends
|
||||
for _, tg := range tgs {
|
||||
f, _ := tg.extendsDefinition()
|
||||
name := fmt.Sprintf("%s%s", vocabName, f.Name())
|
||||
ext = append(ext, codegen.NewCommentedFunction(
|
||||
m.pkg.Path(),
|
||||
name,
|
||||
[]jen.Code{jen.Id("other").Qual(tg.PublicPackage().Path(), typeInterfaceName)},
|
||||
[]jen.Code{jen.Bool()},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
f.Call(jen.Id("other")),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s returns true if %s extends from the other's type.", name, tg.TypeName())))
|
||||
}
|
||||
// DisjointWith
|
||||
for _, tg := range tgs {
|
||||
f := tg.disjointWithDefinition()
|
||||
name := fmt.Sprintf("%s%s", vocabName, f.Name())
|
||||
disj = append(disj, codegen.NewCommentedFunction(
|
||||
m.pkg.Path(),
|
||||
name,
|
||||
[]jen.Code{jen.Id("other").Qual(tg.PublicPackage().Path(), typeInterfaceName)},
|
||||
[]jen.Code{jen.Bool()},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
f.Call(jen.Id("other")),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s returns true if %s is disjoint with the other's type.", name, tg.TypeName())))
|
||||
}
|
||||
// ExtendedBy
|
||||
for _, tg := range tgs {
|
||||
f := tg.extendedByDefinition()
|
||||
name := fmt.Sprintf("%s%s", vocabName, f.Name())
|
||||
extBy = append(extBy, codegen.NewCommentedFunction(
|
||||
m.pkg.Path(),
|
||||
name,
|
||||
[]jen.Code{jen.Id("other").Qual(tg.PublicPackage().Path(), typeInterfaceName)},
|
||||
[]jen.Code{jen.Bool()},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
f.Call(jen.Id("other")),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s returns true if the other's type extends from %s. Note that it returns false if the types are the same; see the %q variant instead.", name, tg.TypeName(), isAMethod)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// init generates the code that implements the init calls per-type and
|
||||
// per-property package, so that the Manager is injected at runtime.
|
||||
func genInit(pkg Package,
|
||||
tgs []*TypeGenerator,
|
||||
pgs []*PropertyGenerator,
|
||||
typePropertyConstructor *codegen.Function) (globalManager *jen.Statement, init *codegen.Function) {
|
||||
// manager dependency injection inits
|
||||
globalManager = jen.Var().Id(managerInitName()).Op("*").Qual(pkg.Path(), managerName)
|
||||
callInitsMap := make(map[string]jen.Code, len(tgs)+len(pgs))
|
||||
callInitsSlice := make([]string, 0, len(tgs)+len(pgs))
|
||||
for _, tg := range tgs {
|
||||
key := tg.PrivatePackage().Path()
|
||||
callInitsMap[key] = jen.Qual(tg.PrivatePackage().Path(), setManagerFunctionName).Call(
|
||||
jen.Qual(pkg.Path(), managerInitName()),
|
||||
)
|
||||
callInitsSlice = append(callInitsSlice, key)
|
||||
}
|
||||
for _, pg := range pgs {
|
||||
key := pg.GetPrivatePackage().Path()
|
||||
callInitsMap[key] = jen.Qual(pg.GetPrivatePackage().Path(), setManagerFunctionName).Call(
|
||||
jen.Qual(pkg.Path(), managerInitName()),
|
||||
)
|
||||
callInitsSlice = append(callInitsSlice, key)
|
||||
}
|
||||
sort.Strings(callInitsSlice)
|
||||
callInits := make([]jen.Code, 0, len(callInitsSlice))
|
||||
for _, c := range callInitsSlice {
|
||||
callInits = append(callInits, callInitsMap[c])
|
||||
}
|
||||
// type property constructor injection inits.
|
||||
// Resets the inits map and slice from above, to
|
||||
// keep appending to the callInits result.
|
||||
callInitsMap = make(map[string]jen.Code, len(tgs))
|
||||
callInitsSlice = make([]string, 0, len(tgs))
|
||||
for _, tg := range tgs {
|
||||
key := tg.PrivatePackage().Path()
|
||||
callInitsMap[key] = jen.Qual(tg.PrivatePackage().Path(), setTypePropertyConstructorName).Call(
|
||||
typePropertyConstructor.QualifiedName(),
|
||||
)
|
||||
callInitsSlice = append(callInitsSlice, key)
|
||||
}
|
||||
sort.Strings(callInitsSlice)
|
||||
for _, c := range callInitsSlice {
|
||||
callInits = append(callInits, callInitsMap[c])
|
||||
}
|
||||
init = codegen.NewCommentedFunction(
|
||||
pkg.Path(),
|
||||
"init",
|
||||
/*params=*/ nil,
|
||||
/*ret=*/ nil,
|
||||
append([]jen.Code{
|
||||
jen.Qual(pkg.Path(), managerInitName()).Op("=").Op("&").Qual(pkg.Path(), managerName).Values(),
|
||||
}, callInits...),
|
||||
fmt.Sprintf("init handles the 'magic' of creating a %s and dependency-injecting it into every other code-generated package. This gives the implementations access to create any type needed to deserialize, without relying on the other specific concrete implementations. In order to replace a go-fed created type with your own, be sure to have the manager call your own implementation's deserialize functions instead of the built-in type. Finally, each implementation views the %s as an interface with only a subset of funcitons available. This means this %s implements the union of those interfaces.", managerName, managerName, managerName))
|
||||
return
|
||||
}
|
||||
|
||||
// toPublicConstructor creates a public constructor function for the given
|
||||
// property, vocab name, and manager.
|
||||
func toPublicConstructor(vocabName string, m *ManagerGenerator, pg *PropertyGenerator) *codegen.Function {
|
||||
return codegen.NewCommentedFunction(
|
||||
m.pkg.Path(),
|
||||
fmt.Sprintf("New%s%sProperty", vocabName, strings.Title(pg.PropertyName())),
|
||||
/*params=*/ nil,
|
||||
[]jen.Code{jen.Qual(pg.GetPublicPackage().Path(), pg.InterfaceName())},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
pg.ConstructorFn().Call(),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("New%s%s creates a new %s", vocabName, pg.StructName(), pg.InterfaceName()))
|
||||
}
|
|
@ -0,0 +1,476 @@
|
|||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Method names for generated code
|
||||
getMethod = "Get"
|
||||
setMethod = "Set"
|
||||
hasAnyMethod = "HasAny"
|
||||
clearMethod = "Clear"
|
||||
iteratorClearMethod = "clear"
|
||||
isMethod = "Is"
|
||||
atMethodName = "At"
|
||||
isIRIMethod = "IsIRI"
|
||||
getIRIMethod = "GetIRI"
|
||||
setIRIMethod = "SetIRI"
|
||||
appendMethod = "Append"
|
||||
prependMethod = "Prepend"
|
||||
insertMethod = "Insert"
|
||||
removeMethod = "Remove"
|
||||
lenMethod = "Len"
|
||||
swapMethod = "Swap"
|
||||
lessMethod = "Less"
|
||||
kindIndexMethod = "KindIndex"
|
||||
serializeMethod = "Serialize"
|
||||
deserializeMethod = "Deserialize"
|
||||
nameMethod = "Name"
|
||||
serializeIteratorMethod = "serialize"
|
||||
deserializeIteratorMethod = "deserialize"
|
||||
hasLanguageMethod = "HasLanguage"
|
||||
getLanguageMethod = "GetLanguage"
|
||||
setLanguageMethod = "SetLanguage"
|
||||
nextMethod = "Next"
|
||||
prevMethod = "Prev"
|
||||
beginMethod = "Begin"
|
||||
endMethod = "End"
|
||||
emptyMethod = "Empty"
|
||||
// Context string management
|
||||
contextMethod = "JSONLDContext"
|
||||
// Member names for generated code
|
||||
unknownMemberName = "unknown"
|
||||
// Reference to the rdf:langString member! Kludge: both of these must be
|
||||
// kept in sync with the generated code.
|
||||
langMapMember = "rdfLangStringMember"
|
||||
isLanguageMapMethod = "IsRDFLangString"
|
||||
// Kind Index constants
|
||||
iriKindIndex = -2
|
||||
noneOrUnknownKindIndex = -1
|
||||
// iterator specific
|
||||
myIndexMemberName = "myIdx"
|
||||
parentMemberName = "parent"
|
||||
)
|
||||
|
||||
// join appends a bunch of Go Code together, each on their own line.
|
||||
func join(s []jen.Code) *jen.Statement {
|
||||
r := jen.Empty()
|
||||
for i, stmt := range s {
|
||||
if i > 0 {
|
||||
r.Line()
|
||||
}
|
||||
r.Add(stmt)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Identifier determines how a name will appear in documentation and Go code.
|
||||
type Identifier struct {
|
||||
// LowerName is the typical name used in documentation.
|
||||
LowerName string
|
||||
// CamelName is the typical name used in identifiers in code.
|
||||
CamelName string
|
||||
}
|
||||
|
||||
// Kind is data that describes a concrete Go type, how to serialize and
|
||||
// deserialize such types, compare the types, and other meta-information to use
|
||||
// during Go code generation.
|
||||
//
|
||||
// Only represents values and other types.
|
||||
type Kind struct {
|
||||
Name Identifier
|
||||
Vocab string
|
||||
// ConcreteKind is expected to be properly qualified.
|
||||
ConcreteKind *jen.Statement
|
||||
Nilable bool
|
||||
IsURI bool
|
||||
|
||||
// TODO: Untangle the package management mess so that the below do not
|
||||
// need to be duplicated.
|
||||
|
||||
// These <FuncName>Fn types are for qualified names of the functions.
|
||||
// Expected to always be non-nil: a function is needed to deserialize.
|
||||
DeserializeFn *jen.Statement
|
||||
// If any of these are nil at generation time, assume to call the method
|
||||
// on the object directly (instead of a qualified function).
|
||||
SerializeFn *jen.Statement
|
||||
LessFn *jen.Statement
|
||||
|
||||
// The following are only used for values, not types, as actual implementations
|
||||
SerializeDef *codegen.Function
|
||||
DeserializeDef *codegen.Function
|
||||
LessDef *codegen.Function
|
||||
}
|
||||
|
||||
// NewKindForValue creates a Kind for a value type.
|
||||
func NewKindForValue(docName, idName, vocab string,
|
||||
defType *jen.Statement,
|
||||
isNilable, isURI bool,
|
||||
serializeFn, deserializeFn, lessFn *codegen.Function) *Kind {
|
||||
return &Kind{
|
||||
Name: Identifier{
|
||||
LowerName: docName,
|
||||
CamelName: idName,
|
||||
},
|
||||
Vocab: vocab,
|
||||
ConcreteKind: defType,
|
||||
Nilable: isNilable,
|
||||
IsURI: isURI,
|
||||
SerializeFn: serializeFn.QualifiedName(),
|
||||
DeserializeFn: deserializeFn.QualifiedName(),
|
||||
LessFn: lessFn.QualifiedName(),
|
||||
SerializeDef: serializeFn,
|
||||
DeserializeDef: deserializeFn,
|
||||
LessDef: lessFn,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKindForType creates a Kind for an ActivitySteams type.
|
||||
func NewKindForType(docName, idName, vocab string) *Kind {
|
||||
return &Kind{
|
||||
// Name must use toIdentifier for vocabValuePackage and
|
||||
// valuePackage to be the same.
|
||||
Name: Identifier{
|
||||
LowerName: docName,
|
||||
CamelName: idName,
|
||||
},
|
||||
Vocab: vocab,
|
||||
Nilable: true,
|
||||
IsURI: false,
|
||||
// Instead of populating:
|
||||
// - ConcreteKind
|
||||
// - DeserializeFn
|
||||
// - SerializeFn (Not populated for types)
|
||||
// - LessFn (Not populated for types)
|
||||
//
|
||||
// The TypeGenerator is responsible for calling SetKindFns on
|
||||
// the properties, to property wire a Property's Kind back to
|
||||
// the Type's implementation.
|
||||
}
|
||||
}
|
||||
|
||||
// lessFnCode creates the correct code calling this Kind's less function
|
||||
// depending on whether the Kind is a value or a type.
|
||||
func (k Kind) lessFnCode(this, other *jen.Statement) *jen.Statement {
|
||||
// LessFn is nil case -- call comparison Less method directly on the LHS
|
||||
lessCall := this.Clone().Dot(compareLessMethod).Call(other.Clone())
|
||||
if k.isValue() {
|
||||
// LessFn is indeed a function -- call this function
|
||||
lessCall = k.LessFn.Clone().Call(
|
||||
this.Clone(),
|
||||
other.Clone(),
|
||||
)
|
||||
}
|
||||
return lessCall
|
||||
}
|
||||
|
||||
// lessFnCode creates the correct code calling this Kind's deserialize function
|
||||
// depending on whether the Kind is a value or a type.
|
||||
func (k Kind) deserializeFnCode(m, ctx *jen.Statement) *jen.Statement {
|
||||
if k.isValue() {
|
||||
return k.DeserializeFn.Clone().Call(m)
|
||||
} else {
|
||||
// If LessFn is nil, this means it is a type. Which requires an
|
||||
// additional Call and the context.
|
||||
return k.DeserializeFn.Clone().Call().Call(m, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// isValue returns true if this Kind is a value, or false if it is a type.
|
||||
func (k Kind) isValue() bool {
|
||||
// LessFn is not nil, this means it is a value.
|
||||
// If LessFn is nil, this means it is a type. Types will have their
|
||||
// LessThan method called directly on the type.
|
||||
return k.LessFn != nil
|
||||
}
|
||||
|
||||
// PropertyGenerator is a common base struct used in both Functional and
|
||||
// NonFunctional ActivityStreams properties. It provides common naming patterns,
|
||||
// logic, and common Go code to be generated.
|
||||
//
|
||||
// It also properly handles the concept of generating Go code for property
|
||||
// iterators, which are needed for NonFunctional properties.
|
||||
type PropertyGenerator struct {
|
||||
vocabName string
|
||||
vocabURI *url.URL
|
||||
vocabAlias string
|
||||
managerMethods []*codegen.Method
|
||||
packageManager *PackageManager
|
||||
name Identifier
|
||||
comment string
|
||||
kinds []Kind
|
||||
hasNaturalLanguageMap bool
|
||||
asIterator bool
|
||||
}
|
||||
|
||||
// HasNaturalLanguageMap returns whether this property has a natural language
|
||||
// map.
|
||||
func (p *PropertyGenerator) HasNaturalLanguageMap() bool {
|
||||
return p.hasNaturalLanguageMap
|
||||
}
|
||||
|
||||
// VocabName returns this property's vocabulary name.
|
||||
func (p *PropertyGenerator) VocabName() string {
|
||||
return p.vocabName
|
||||
}
|
||||
|
||||
// GetKinds gets this property's kinds.
|
||||
func (p *PropertyGenerator) GetKinds() []Kind {
|
||||
return p.kinds
|
||||
}
|
||||
|
||||
// GetPrivatePackage gets this property's private Package.
|
||||
func (p *PropertyGenerator) GetPrivatePackage() Package {
|
||||
return p.packageManager.PrivatePackage()
|
||||
}
|
||||
|
||||
// GetPublicPackage gets this property's public Package.
|
||||
func (p *PropertyGenerator) GetPublicPackage() Package {
|
||||
return p.packageManager.PublicPackage()
|
||||
}
|
||||
|
||||
// SetKindFns allows TypeGenerators to later notify this Property what functions
|
||||
// to use when generating the serialization code.
|
||||
//
|
||||
// The name parameter must match the LowerName of an Identifier.
|
||||
//
|
||||
// This feels very hacky.
|
||||
func (p *PropertyGenerator) SetKindFns(docName, idName, vocab string, qualKind *jen.Statement, deser *codegen.Method) error {
|
||||
for i, kind := range p.kinds {
|
||||
if kind.Name.LowerName == docName && kind.Vocab == vocab {
|
||||
if kind.SerializeFn != nil || kind.DeserializeFn != nil || kind.LessFn != nil {
|
||||
return fmt.Errorf("property kind already has serialization functions set for %q: %s", docName, p.PropertyName())
|
||||
}
|
||||
kind.ConcreteKind = qualKind
|
||||
kind.DeserializeFn = deser.On(managerInitName())
|
||||
p.managerMethods = append(p.managerMethods, deser)
|
||||
p.kinds[i] = kind
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// In the case of extended types applying themselves to their parents'
|
||||
// range, they will be missing from the property's kinds list. Append a
|
||||
// new kind to handle this use case.
|
||||
k := NewKindForType(docName, idName, vocab)
|
||||
k.ConcreteKind = qualKind
|
||||
k.DeserializeFn = deser.On(managerInitName())
|
||||
p.managerMethods = append(p.managerMethods, deser)
|
||||
p.kinds = append(p.kinds, *k)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAllManagerMethods returns the list of manager methods used by this
|
||||
// property.
|
||||
func (p *PropertyGenerator) getAllManagerMethods() []*codegen.Method {
|
||||
return p.managerMethods
|
||||
}
|
||||
|
||||
// StructName returns the name of the type, which may or may not be a struct,
|
||||
// to generate.
|
||||
func (p *PropertyGenerator) StructName() string {
|
||||
if p.asIterator {
|
||||
return p.name.CamelName
|
||||
}
|
||||
return fmt.Sprintf("%s%sProperty", p.VocabName(), p.name.CamelName)
|
||||
}
|
||||
|
||||
// iteratorTypeName determines the identifier to use for the iterator type.
|
||||
func (p *PropertyGenerator) iteratorTypeName() Identifier {
|
||||
s := fmt.Sprintf("%s%s", p.VocabName(), p.name.CamelName)
|
||||
return Identifier{
|
||||
LowerName: s,
|
||||
CamelName: fmt.Sprintf("%sPropertyIterator", s),
|
||||
}
|
||||
}
|
||||
|
||||
// InterfaceName returns the interface name of the property type.
|
||||
func (p *PropertyGenerator) InterfaceName() string {
|
||||
return p.StructName()
|
||||
}
|
||||
|
||||
// parentTypeInterfaceName is useful for iterators that need the base property
|
||||
// type's interface name.
|
||||
func (p *PropertyGenerator) parentTypeInterfaceName() string {
|
||||
return strings.TrimSuffix(p.StructName(), "Iterator")
|
||||
}
|
||||
|
||||
// PropertyName returns the name of this property, as defined in
|
||||
// specifications. It is not suitable for use in generated code function
|
||||
// identifiers.
|
||||
func (p *PropertyGenerator) PropertyName() string {
|
||||
return p.name.LowerName
|
||||
}
|
||||
|
||||
// Comments returns the comment for this property.
|
||||
func (p *PropertyGenerator) Comments() string {
|
||||
return p.comment
|
||||
}
|
||||
|
||||
// DeserializeFnName returns the identifier of the function that deserializes
|
||||
// raw JSON into the generated Go type.
|
||||
func (p *PropertyGenerator) DeserializeFnName() string {
|
||||
if p.asIterator {
|
||||
return fmt.Sprintf("%s%s", deserializeIteratorMethod, p.name.CamelName)
|
||||
}
|
||||
return fmt.Sprintf("%s%sProperty", deserializeMethod, p.name.CamelName)
|
||||
}
|
||||
|
||||
// getFnName returns the identifier of the function that fetches concrete types
|
||||
// of the property.
|
||||
func (p *PropertyGenerator) getFnName(i int) string {
|
||||
if len(p.kinds) == 1 {
|
||||
return getMethod
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s", getMethod, p.kinds[i].Vocab, p.kindCamelName(i))
|
||||
}
|
||||
|
||||
// setFnName returns the identifier of the function that sets concrete types
|
||||
// of the property.
|
||||
func (p *PropertyGenerator) setFnName(i int) string {
|
||||
if len(p.kinds) == 1 {
|
||||
return setMethod
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s", setMethod, p.kinds[i].Vocab, p.kindCamelName(i))
|
||||
}
|
||||
|
||||
// serializeFnName returns the identifier of the function that serializes the
|
||||
// generated Go type into raw JSON.
|
||||
func (p *PropertyGenerator) serializeFnName() string {
|
||||
if p.asIterator {
|
||||
return serializeIteratorMethod
|
||||
}
|
||||
return serializeMethod
|
||||
}
|
||||
|
||||
// kindCamelName returns an identifier-friendly name for the kind at the
|
||||
// specified index.
|
||||
//
|
||||
// It will panic if 'i' is out of range.
|
||||
func (p *PropertyGenerator) kindCamelName(i int) string {
|
||||
return p.kinds[i].Name.CamelName
|
||||
}
|
||||
|
||||
// memberName returns the identifier to use for the kind at the specified index.
|
||||
//
|
||||
// It will panic if 'i' is out of range.
|
||||
func (p *PropertyGenerator) memberName(i int) string {
|
||||
k := p.kinds[i]
|
||||
v := strings.ToLower(k.Vocab)
|
||||
return fmt.Sprintf("%s%sMember", v, k.Name.CamelName)
|
||||
}
|
||||
|
||||
// hasMemberName returns the identifier to use for struct members that determine
|
||||
// whether non-nilable types have been set. Panics if called for a Kind that is
|
||||
// nilable.
|
||||
func (p *PropertyGenerator) hasMemberName(i int) string {
|
||||
if len(p.kinds) == 1 && p.kinds[0].Nilable {
|
||||
panic("PropertyGenerator.hasMemberName called for nilable single value")
|
||||
}
|
||||
return fmt.Sprintf("has%sMember", p.kinds[i].Name.CamelName)
|
||||
}
|
||||
|
||||
// clearMethodName returns the identifier to use for methods that clear all
|
||||
// values from the property.
|
||||
func (p *PropertyGenerator) clearMethodName() string {
|
||||
if p.asIterator {
|
||||
return iteratorClearMethod
|
||||
}
|
||||
return clearMethod
|
||||
}
|
||||
|
||||
// commonMethods returns methods common to every property.
|
||||
func (p *PropertyGenerator) commonMethods() (m []*codegen.Method) {
|
||||
if p.asIterator {
|
||||
// Next & Prev methods
|
||||
m = append(m, codegen.NewCommentedValueMethod(
|
||||
p.GetPrivatePackage().Path(),
|
||||
nextMethod,
|
||||
p.StructName(),
|
||||
/*params=*/ nil,
|
||||
[]jen.Code{jen.Qual(p.GetPublicPackage().Path(), p.InterfaceName())},
|
||||
[]jen.Code{
|
||||
jen.If(
|
||||
jen.Id(codegen.This()).Dot(myIndexMemberName).Op("+").Lit(1).Op(">=").Id(codegen.This()).Dot(parentMemberName).Dot(lenMethod).Call(),
|
||||
).Block(
|
||||
jen.Return(jen.Nil()),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Id(codegen.This()).Dot(parentMemberName).Dot(atMethodName).Call(jen.Id(codegen.This()).Dot(myIndexMemberName).Op("+").Lit(1)),
|
||||
),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s returns the next iterator, or nil if there is no next iterator.", nextMethod)))
|
||||
m = append(m, codegen.NewCommentedValueMethod(
|
||||
p.GetPrivatePackage().Path(),
|
||||
prevMethod,
|
||||
p.StructName(),
|
||||
/*params=*/ nil,
|
||||
[]jen.Code{jen.Qual(p.GetPublicPackage().Path(), p.InterfaceName())},
|
||||
[]jen.Code{
|
||||
jen.If(
|
||||
jen.Id(codegen.This()).Dot(myIndexMemberName).Op("-").Lit(1).Op("<").Lit(0),
|
||||
).Block(
|
||||
jen.Return(jen.Nil()),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Id(codegen.This()).Dot(parentMemberName).Dot(atMethodName).Call(jen.Id(codegen.This()).Dot(myIndexMemberName).Op("-").Lit(1)),
|
||||
),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s returns the previous iterator, or nil if there is no previous iterator.", prevMethod)))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// isMethodName returns the identifier to use for methods that determine if a
|
||||
// property holds a specific Kind of value.
|
||||
func (p *PropertyGenerator) isMethodName(i int) string {
|
||||
return fmt.Sprintf("%s%s%s", isMethod, p.kinds[i].Vocab, p.kindCamelName(i))
|
||||
}
|
||||
|
||||
// ConstructorFn creates a constructor function with a default vocabulary
|
||||
// alias.
|
||||
func (p *PropertyGenerator) ConstructorFn() *codegen.Function {
|
||||
return codegen.NewCommentedFunction(
|
||||
p.GetPrivatePackage().Path(),
|
||||
fmt.Sprintf("%s%s", constructorName, p.StructName()),
|
||||
/*params=*/ nil,
|
||||
[]jen.Code{
|
||||
jen.Op("*").Qual(p.GetPrivatePackage().Path(), p.StructName()),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Op("&").Qual(p.GetPrivatePackage().Path(), p.StructName()).Values(
|
||||
jen.Dict{
|
||||
jen.Id(aliasMember): jen.Lit(p.vocabAlias),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s%s creates a new %s property.", constructorName, p.StructName(), p.PropertyName()))
|
||||
}
|
||||
|
||||
// hasURIKind returns true if this property already has a Kind that is a URI.
|
||||
func (p *PropertyGenerator) hasURIKind() bool {
|
||||
for _, k := range p.kinds {
|
||||
if k.IsURI {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasTypeKind returns true if this property has a Kind that is a type.
|
||||
func (p *PropertyGenerator) hasTypeKind() bool {
|
||||
for _, k := range p.kinds {
|
||||
if !k.isValue() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,959 @@
|
|||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
contextJSONLDName = "@context"
|
||||
typePropertyName = "type"
|
||||
jsonResolverStructName = "JSONResolver"
|
||||
typeResolverStructName = "TypeResolver"
|
||||
typePredicatedResolverStructName = "TypePredicatedResolver"
|
||||
resolveMethod = "Resolve"
|
||||
applyMethod = "Apply"
|
||||
activityStreamInterface = "ActivityStreamsInterface"
|
||||
resolverInterface = "Resolver"
|
||||
callbackMember = "callbacks"
|
||||
predicateMember = "predicate"
|
||||
delegateMember = "delegate"
|
||||
errorNoMatch = "ErrNoCallbackMatch"
|
||||
errorUnhandled = "ErrUnhandledType"
|
||||
errorPredicateUnmatched = "ErrPredicateUnmatched"
|
||||
errorCannotTypeAssert = "errCannotTypeAssertType"
|
||||
isUnFnName = "IsUnmatchedErr"
|
||||
toAliasMapFnName = "toAliasMap"
|
||||
)
|
||||
|
||||
// ResolverGenerator generates the code required for the TypeResolver and the
|
||||
// PredicateTypeResolver.
|
||||
type ResolverGenerator struct {
|
||||
pkg Package
|
||||
types []*TypeGenerator
|
||||
manGen *ManagerGenerator
|
||||
cacheOnce sync.Once
|
||||
cachedJSON *codegen.Struct
|
||||
cachedTypePredicate *codegen.Struct
|
||||
cachedType *codegen.Struct
|
||||
cachedErrNoMatch jen.Code
|
||||
cachedErrUnhandled jen.Code
|
||||
cachedErrPredicateUnmatched jen.Code
|
||||
cachedErrCannotTypeAssert jen.Code
|
||||
cachedFns []*codegen.Function
|
||||
cachedASInterface *codegen.Interface
|
||||
cachedResolverInterface *codegen.Interface
|
||||
}
|
||||
|
||||
// Creates a new ResolverGenerator for generating all the methods, functions,
|
||||
// errors, interface, and struct types needed for them.
|
||||
//
|
||||
// Must be constructed after all TypeGenerators.
|
||||
func NewResolverGenerator(
|
||||
tgs []*TypeGenerator,
|
||||
m *ManagerGenerator,
|
||||
pkg Package) *ResolverGenerator {
|
||||
return &ResolverGenerator{
|
||||
pkg: pkg,
|
||||
types: tgs,
|
||||
manGen: m,
|
||||
}
|
||||
}
|
||||
|
||||
// Definition returns the TypeResolver and PredicateTypeResolver.
|
||||
//
|
||||
// This function signature is pure garbage and yet I keep heaping it on.
|
||||
func (r *ResolverGenerator) Definition() (jsonRes, typeRes, typePredRes *codegen.Struct, errs []jen.Code, fns []*codegen.Function, iFaces []*codegen.Interface) {
|
||||
r.cacheOnce.Do(func() {
|
||||
r.cachedJSON = codegen.NewStruct(
|
||||
fmt.Sprintf("%s resolves a JSON-deserialized map into "+
|
||||
"its concrete ActivityStreams type", jsonResolverStructName),
|
||||
jsonResolverStructName,
|
||||
r.jsonResolverMethods(),
|
||||
append(r.resolverFunctions(jsonResolverStructName,
|
||||
"creates a new Resolver that takes a "+
|
||||
"JSON-deserialized generic map and determines "+
|
||||
"the correct concrete Go type. The callback "+
|
||||
"function is guaranteed to receive a value "+
|
||||
"whose underlying ActivityStreams type "+
|
||||
"matches the concrete interface name in its "+
|
||||
"signature. The callback functions must be of "+
|
||||
"the form:\n\n"+
|
||||
" func(context.Context, <TypeInterface>) error\n\n"+
|
||||
"where TypeInterface is the code-generated "+
|
||||
"interface for an ActivityStream type. An "+
|
||||
"error is returned if a callback function "+
|
||||
"does not match this signature."),
|
||||
r.toAliasFunction()),
|
||||
r.resolverMembers())
|
||||
r.cachedType = codegen.NewStruct(
|
||||
fmt.Sprintf("%s resolves ActivityStreams values based "+
|
||||
"on their type name.", typeResolverStructName),
|
||||
typeResolverStructName,
|
||||
r.typeResolverMethods(),
|
||||
r.resolverFunctions(typeResolverStructName,
|
||||
"creates a new Resolver that examines the "+
|
||||
"type of an ActivityStream value to determine "+
|
||||
"what callback function to pass the concretely "+
|
||||
"typed value. The callback is guaranteed to "+
|
||||
"receive a value whose underlying "+
|
||||
"ActivityStreams type matches the concrete "+
|
||||
"interface name in its signature. The "+
|
||||
"callback functions must be "+
|
||||
"of the form:\n\n"+
|
||||
" func(context.Context, <TypeInterface>) error\n\n"+
|
||||
"where TypeInterface is the code-generated "+
|
||||
"interface for an ActivityStream type. An "+
|
||||
"error is returned if a callback function "+
|
||||
"does not match this signature."),
|
||||
r.resolverMembers())
|
||||
r.cachedTypePredicate = codegen.NewStruct(
|
||||
fmt.Sprintf("%s resolves ActivityStreams values if "+
|
||||
"the value satisfies a predicate condition "+
|
||||
"based on its type.", typePredicatedResolverStructName),
|
||||
typePredicatedResolverStructName,
|
||||
r.typePredicatedResolverMethods(),
|
||||
r.predicateResolverFunctions(typePredicatedResolverStructName,
|
||||
"creates a new Resolver that applies a "+
|
||||
"predicate to an ActivityStreams value to "+
|
||||
"determine whether to Resolve or not. The "+
|
||||
"ActivityStreams value's type is examined "+
|
||||
"to determine if the predicate can apply "+
|
||||
"itself to the value. This guarantees the "+
|
||||
"predicate will receive a concrete value "+
|
||||
"whose underlying ActivityStreams type "+
|
||||
"matches the concrete interface name. "+
|
||||
"The predicate function must be of the form: \n\n"+
|
||||
" func(context.Context, <TypeInterface>) (bool, error)\n\n"+
|
||||
"where TypeInterface is the code-generated "+
|
||||
"interface for an ActivityStreams type. An "+
|
||||
"error is returned if the predicate does "+
|
||||
"not match this signature."),
|
||||
r.predicateResolverMembers())
|
||||
r.cachedErrNoMatch = r.errorNoMatch()
|
||||
r.cachedErrUnhandled = r.errorUnhandled()
|
||||
r.cachedErrPredicateUnmatched = r.errorPredicateUnmatched()
|
||||
r.cachedErrCannotTypeAssert = r.errorCannotTypeAssert()
|
||||
r.cachedFns = r.fns()
|
||||
r.cachedASInterface = r.asInterface()
|
||||
r.cachedResolverInterface = r.resolverInterface()
|
||||
})
|
||||
return r.cachedJSON, r.cachedType, r.cachedTypePredicate, []jen.Code{
|
||||
r.cachedErrNoMatch,
|
||||
r.cachedErrUnhandled,
|
||||
r.cachedErrPredicateUnmatched,
|
||||
r.cachedErrCannotTypeAssert,
|
||||
}, r.cachedFns, []*codegen.Interface{
|
||||
r.cachedASInterface,
|
||||
r.cachedResolverInterface,
|
||||
}
|
||||
}
|
||||
|
||||
// errorNoMatch returns the declaration for the ErrNoMatch global value.
|
||||
func (r *ResolverGenerator) errorNoMatch() jen.Code {
|
||||
return jen.Commentf(
|
||||
"%s indicates a Resolver could not match the ActivityStreams value to a "+
|
||||
"callback function.",
|
||||
errorNoMatch,
|
||||
).Line().Var().Id(errorNoMatch).Error().Op("=").Qual("errors", "New").Call(jen.Lit("activity stream did not match the callback function"))
|
||||
}
|
||||
|
||||
// errorUnhandled returns the declaration for the ErrUnhandled global value.
|
||||
func (r *ResolverGenerator) errorUnhandled() jen.Code {
|
||||
return jen.Commentf(
|
||||
"%s indicates that an ActivityStreams value has a type that is "+
|
||||
"not handled by the code that has been generated.",
|
||||
errorUnhandled,
|
||||
).Line().Var().Id(errorUnhandled).Error().Op("=").Qual("errors", "New").Call(jen.Lit("activity stream did not match any known types"))
|
||||
}
|
||||
|
||||
// errorCannotTypeAssert returns the declaration for the errCannotTypeAssert
|
||||
// global value.
|
||||
func (r *ResolverGenerator) errorCannotTypeAssert() jen.Code {
|
||||
return jen.Commentf(
|
||||
"%s indicates that the 'type' property returned by the "+
|
||||
"ActivityStreams value cannot be type-asserted to its "+
|
||||
"interface form.",
|
||||
errorCannotTypeAssert,
|
||||
).Line().Var().Id(errorCannotTypeAssert).Error().Op("=").Qual("errors", "New").Call(jen.Lit("activity stream type cannot be asserted to its interface"))
|
||||
}
|
||||
|
||||
// errorPredicateUnmatched returns the declaration for the ErrPredicateUnmatched
|
||||
// global value.
|
||||
func (r *ResolverGenerator) errorPredicateUnmatched() jen.Code {
|
||||
return jen.Commentf(
|
||||
"%s indicates that a predicate is accepting a type or "+
|
||||
"interface that does not match an ActivityStreams value's "+
|
||||
"type or interface.",
|
||||
errorPredicateUnmatched,
|
||||
).Line().Var().Id(errorPredicateUnmatched).Error().Op("=").Qual("errors", "New").Call(jen.Lit("activity stream did not match type demanded by predicate"))
|
||||
}
|
||||
|
||||
// fns returns all utility functions.
|
||||
func (r *ResolverGenerator) fns() []*codegen.Function {
|
||||
allTypeFns := make([]jen.Code, 0)
|
||||
for _, t := range r.types {
|
||||
allTypeFns = append(allTypeFns, jen.Func().Params(
|
||||
jen.Id("ctx").Qual("context", "Context"),
|
||||
jen.Id("i").Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
).Error().Block(
|
||||
jen.Id("t").Op("=").Id("i"),
|
||||
jen.Return(jen.Nil()),
|
||||
))
|
||||
}
|
||||
return []*codegen.Function{
|
||||
codegen.NewCommentedFunction(
|
||||
r.pkg.Path(),
|
||||
isUnFnName,
|
||||
[]jen.Code{
|
||||
jen.Err().Error(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Bool(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Err().Op("==").Id(errorPredicateUnmatched).Op(
|
||||
"||",
|
||||
).Err().Op("==").Id(errorUnhandled).Op(
|
||||
"||",
|
||||
).Err().Op("==").Id(errorNoMatch),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s is true when the error indicates that a Resolver was unsuccessful due to the ActivityStreams value not matching its callbacks or predicates.", isUnFnName)),
|
||||
codegen.NewCommentedFunction(
|
||||
r.types[0].PublicPackage().Path(),
|
||||
fmt.Sprintf("To%s", typeInterfaceName),
|
||||
[]jen.Code{
|
||||
jen.Id("c").Qual("context", "Context"),
|
||||
jen.Id("m").Map(jen.String()).Interface(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Id("t").Qual(r.types[0].PublicPackage().Path(), typeInterfaceName),
|
||||
jen.Err().Error(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Var().Id("r").Op("*").Qual(r.pkg.Path(), jsonResolverStructName),
|
||||
jen.List(
|
||||
jen.Id("r"),
|
||||
jen.Err(),
|
||||
).Op("=").Qual(
|
||||
r.pkg.Path(),
|
||||
fmt.Sprintf("%s%s", constructorName, jsonResolverStructName),
|
||||
).Call(
|
||||
jen.List(
|
||||
allTypeFns...,
|
||||
),
|
||||
),
|
||||
jen.If(
|
||||
jen.Err().Op("!=").Nil(),
|
||||
).Block(
|
||||
jen.Return(),
|
||||
),
|
||||
jen.Err().Op("=").Id("r").Dot(resolveMethod).Call(
|
||||
jen.Id("c"),
|
||||
jen.Id("m"),
|
||||
),
|
||||
jen.Return(),
|
||||
},
|
||||
fmt.Sprintf("To%s attempts to resolve the generic JSON map into a Type.", typeInterfaceName)),
|
||||
}
|
||||
}
|
||||
|
||||
// jsonResolverMethods returns the methods for the TypeResolver.
|
||||
func (r *ResolverGenerator) jsonResolverMethods() (m []*codegen.Method) {
|
||||
aliasToId := make(map[string]string)
|
||||
aliasFetching := jen.Empty()
|
||||
impl := jen.Empty()
|
||||
for i, t := range r.types {
|
||||
if i > 0 {
|
||||
impl = impl.Else()
|
||||
}
|
||||
// Get the vocab URI in http and https forms
|
||||
vocabHttps := *t.vocabURI
|
||||
vocabHttps.Scheme = "https"
|
||||
vocabHttp := vocabHttps
|
||||
vocabHttp.Scheme = "http"
|
||||
// Determine if we've already generated the code for fetching
|
||||
// the alias for this vocabulary.
|
||||
if _, ok := aliasToId[vocabHttps.String()]; !ok {
|
||||
// If not, generate the code.
|
||||
vocabId := t.vocabName + "Alias"
|
||||
aliasToId[vocabHttps.String()] = vocabId
|
||||
aliasFetching = aliasFetching.Add(
|
||||
jen.List(
|
||||
jen.Id(vocabId),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("aliasMap").Index(
|
||||
jen.Lit(vocabHttps.String()),
|
||||
),
|
||||
).Line().Add(
|
||||
jen.If(
|
||||
jen.Op("!").Id("ok"),
|
||||
).Block(
|
||||
jen.Id(vocabId).Op("=").Id("aliasMap").Index(
|
||||
jen.Lit(vocabHttp.String()),
|
||||
),
|
||||
),
|
||||
).Line().Add(
|
||||
// If it is not empty post-pend with a ":".
|
||||
jen.If(
|
||||
jen.Len(jen.Id(vocabId)).Op(">").Lit(0),
|
||||
).Block(
|
||||
jen.Id(vocabId).Op("+=").Lit(":"),
|
||||
),
|
||||
).Line()
|
||||
}
|
||||
// Fetch the identifier holding the alias for this vocabulary,
|
||||
aliasId := aliasToId[vocabHttps.String()]
|
||||
impl = impl.If(
|
||||
jen.Id("typeString").Op("==").Id(aliasId).Op("+").Lit(t.TypeName()),
|
||||
).Block(
|
||||
jen.List(
|
||||
jen.Id("v"),
|
||||
jen.Err(),
|
||||
).Op(":=").Add(r.manGen.getDeserializationMethodForType(t).On(managerInitVarName).Call().Call(
|
||||
jen.Id("m"),
|
||||
jen.Id("aliasMap"),
|
||||
)),
|
||||
jen.If(
|
||||
jen.Err().Op("!=").Nil(),
|
||||
).Block(
|
||||
jen.Return(jen.Err()),
|
||||
),
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("_"),
|
||||
jen.Id("i"),
|
||||
).Op(":=").Range().Id(codegen.This()).Dot(callbackMember),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("fn"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("i").Assert(
|
||||
jen.Func().Parens(
|
||||
jen.List(
|
||||
jen.Qual("context", "Context"),
|
||||
jen.Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
),
|
||||
).Error(),
|
||||
),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Id("fn").Call(jen.Id("ctx"), jen.Id("v")),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Return(
|
||||
jen.Id(errorNoMatch),
|
||||
),
|
||||
)
|
||||
}
|
||||
m = append(m, codegen.NewCommentedValueMethod(
|
||||
r.pkg.Path(),
|
||||
resolveMethod,
|
||||
jsonResolverStructName,
|
||||
[]jen.Code{
|
||||
jen.Id("ctx").Qual("context", "Context"),
|
||||
jen.Id("m").Map(jen.String()).Interface(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Error(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.List(
|
||||
jen.Id("typeValue"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("m").Index(jen.Lit(typePropertyName)),
|
||||
jen.If(
|
||||
jen.Op("!").Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Qual("fmt", "Errorf").Call(
|
||||
jen.Lit("cannot determine ActivityStreams type: 'type' property is missing"),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.List(
|
||||
jen.Id("rawContext"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("m").Index(jen.Lit(contextJSONLDName)),
|
||||
jen.If(
|
||||
jen.Op("!").Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Qual("fmt", "Errorf").Call(
|
||||
jen.Lit("cannot determine ActivityStreams type: '@context' is missing"),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Id("aliasMap").Op(":=").Id(toAliasMapFnName).Call(jen.Id("rawContext")),
|
||||
jen.Commentf("Begin: Private lambda to handle a single string %q value. Makes code generation easier.", typePropertyName),
|
||||
jen.Id("handleFn").Op(":=").Func().Parens(
|
||||
jen.Id("typeString").String(),
|
||||
).Error().Block(
|
||||
aliasFetching,
|
||||
impl.Else().Block(
|
||||
jen.Return(
|
||||
jen.Id(errorUnhandled),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Commentf("End: Private lambda"),
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("typeStr"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("typeValue").Assert(jen.String()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Id("handleFn").Call(jen.Id("typeStr")),
|
||||
),
|
||||
).Else().If(
|
||||
jen.List(
|
||||
jen.Id("typeIArr"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("typeValue").Assert(jen.Index().Interface()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("_"),
|
||||
jen.Id("typeI"),
|
||||
).Op(":=").Range().Id("typeIArr"),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("typeStr"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("typeI").Assert(jen.String()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Err(),
|
||||
).Op(":=").Id("handleFn").Call(jen.Id("typeStr")),
|
||||
jen.Err().Op("==").Nil(),
|
||||
).Block(
|
||||
jen.Return(jen.Nil()),
|
||||
).Else().If(
|
||||
jen.Err().Op("==").Id(errorUnhandled),
|
||||
).Block(
|
||||
jen.Commentf("Keep trying other types: only if all fail do we return this error."),
|
||||
jen.Continue(),
|
||||
).Else().Block(
|
||||
jen.Return(jen.Err()),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Return(
|
||||
jen.Id(errorUnhandled),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Id(errorUnhandled),
|
||||
),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s determines the ActivityStreams type of the payload, then applies the first callback function whose signature accepts the ActivityStreams value's type. This strictly assures that the callback function will only be passed ActivityStream objects whose type matches its interface. Returns an error if the ActivityStreams type does not match callbackers or is not a type handled by the generated code. If multiple types are present, it will check each one in order and apply only the first one. It returns an unhandled error for a multi-typed object if none of the types were able to be handled.", resolveMethod)))
|
||||
return
|
||||
}
|
||||
|
||||
// typeResolverMethods returns the methods for the TypeResolver.
|
||||
func (r *ResolverGenerator) typeResolverMethods() (m []*codegen.Method) {
|
||||
impl := jen.Empty()
|
||||
for i, t := range r.types {
|
||||
if i > 0 {
|
||||
impl = impl.Else()
|
||||
}
|
||||
impl = impl.If(
|
||||
jen.Id("o").Dot(vocabURIMethod).Call().Op("==").Lit(t.vocabURI.String()).Op(
|
||||
"&&",
|
||||
).Id("o").Dot(typeNameMethod).Call().Op("==").Lit(t.TypeName()),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("fn"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("i").Assert(
|
||||
jen.Func().Parens(
|
||||
jen.List(
|
||||
jen.Qual("context", "Context"),
|
||||
jen.Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
),
|
||||
).Error(),
|
||||
),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("v"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("o").Assert(
|
||||
jen.Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Id("fn").Call(jen.Id("ctx"), jen.Id("v")),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Commentf("This occurs when the value is either not a go-fed type and is improperly satisfying various interfaces, or there is a bug in the go-fed generated code."),
|
||||
jen.Return(
|
||||
jen.Id(errorCannotTypeAssert),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
m = append(m, codegen.NewCommentedValueMethod(
|
||||
r.pkg.Path(),
|
||||
resolveMethod,
|
||||
typeResolverStructName,
|
||||
[]jen.Code{
|
||||
jen.Id("ctx").Qual("context", "Context"),
|
||||
jen.Id("o").Id(activityStreamInterface),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Error(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("_"),
|
||||
jen.Id("i"),
|
||||
).Op(":=").Range().Id(codegen.This()).Dot(callbackMember),
|
||||
).Block(
|
||||
impl.Else().Block(
|
||||
jen.Return(
|
||||
jen.Id(errorUnhandled),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Return(
|
||||
jen.Id(errorNoMatch),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s applies the first callback function whose signature accepts the ActivityStreams value's type. This strictly assures that the callback function will only be passed ActivityStream objects whose type matches its interface. Returns an error if the ActivityStreams type does not match callbackers, is not a type handled by the generated code, or the value passed in is not go-fed compatible.", resolveMethod)))
|
||||
return
|
||||
}
|
||||
|
||||
// typePredicatedResolverMethods returns the methods for the TypePredicatedResolver.
|
||||
func (r *ResolverGenerator) typePredicatedResolverMethods() (m []*codegen.Method) {
|
||||
impl := jen.Empty()
|
||||
for i, t := range r.types {
|
||||
if i > 0 {
|
||||
impl = impl.Else()
|
||||
}
|
||||
impl = impl.If(
|
||||
jen.Id("o").Dot(vocabURIMethod).Call().Op("==").Lit(t.vocabURI.String()).Op(
|
||||
"&&",
|
||||
).Id("o").Dot(typeNameMethod).Call().Op("==").Lit(t.TypeName()),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("fn"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id(codegen.This()).Dot(predicateMember).Assert(
|
||||
jen.Func().Parens(
|
||||
jen.List(
|
||||
jen.Qual("context", "Context"),
|
||||
jen.Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
),
|
||||
).Parens(
|
||||
jen.List(
|
||||
jen.Bool(),
|
||||
jen.Error(),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("v"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("o").Assert(
|
||||
jen.Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.List(
|
||||
jen.Id("predicatePasses"),
|
||||
jen.Err(),
|
||||
).Op("=").Id("fn").Call(jen.Id("ctx"), jen.Id("v")),
|
||||
).Else().Block(
|
||||
jen.Commentf("This occurs when the value is either not a go-fed type and is improperly satisfying various interfaces, or there is a bug in the go-fed generated code."),
|
||||
jen.Return(
|
||||
jen.False(),
|
||||
jen.Id(errorCannotTypeAssert),
|
||||
),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.False(),
|
||||
jen.Id(errorPredicateUnmatched),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
m = append(m, codegen.NewCommentedValueMethod(
|
||||
r.pkg.Path(),
|
||||
applyMethod,
|
||||
typePredicatedResolverStructName,
|
||||
[]jen.Code{
|
||||
jen.Id("ctx").Qual("context", "Context"),
|
||||
jen.Id("o").Id(activityStreamInterface),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Bool(),
|
||||
jen.Error(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Var().Id("predicatePasses").Bool(),
|
||||
jen.Var().Err().Error(),
|
||||
impl.Else().Block(
|
||||
jen.Return(
|
||||
jen.False(),
|
||||
jen.Id(errorUnhandled),
|
||||
),
|
||||
),
|
||||
jen.If(
|
||||
jen.Err().Op("!=").Nil(),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Id("predicatePasses"),
|
||||
jen.Err(),
|
||||
),
|
||||
),
|
||||
jen.If(
|
||||
jen.Id("predicatePasses"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.True(),
|
||||
jen.Id(codegen.This()).Dot(delegateMember).Dot(resolveMethod).Call(
|
||||
jen.Id("ctx"),
|
||||
jen.Id("o"),
|
||||
),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.False(),
|
||||
jen.Nil(),
|
||||
),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s uses a predicate to determine whether to resolve the ActivityStreams value. The predicate's signature is matched with the ActivityStreams value's type. This strictly assures that the predicate will only be passed ActivityStream objects whose type matches its interface. Returns an error if the ActivityStreams type does not match the predicate, is not a type handled by the generated code, or the resolver returns an error. Returns true if the predicate returned true.", applyMethod)))
|
||||
return
|
||||
}
|
||||
|
||||
// resolverFunctions returns the functions for the TypeResolver.
|
||||
func (r *ResolverGenerator) resolverFunctions(name, comment string) (f []*codegen.Function) {
|
||||
f = append(f, codegen.NewCommentedFunction(
|
||||
r.pkg.Path(),
|
||||
fmt.Sprintf("%s%s", constructorName, name),
|
||||
[]jen.Code{
|
||||
jen.Id("callbacks").Op("...").Interface(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Op("*").Id(name),
|
||||
jen.Error(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("_"),
|
||||
jen.Id("cb"),
|
||||
).Op(":=").Range().Id("callbacks"),
|
||||
).Block(
|
||||
jen.Commentf("Each callback function must satisfy one known function signature, or else we will generate a runtime error instead of silently fail."),
|
||||
jen.Switch(
|
||||
jen.Id("cb").Assert(jen.Type()),
|
||||
).Block(
|
||||
r.mustAssertToKnownTypes("cb"),
|
||||
),
|
||||
),
|
||||
jen.Return(
|
||||
jen.Op("&").Id(name).Values(
|
||||
jen.Dict{
|
||||
jen.Id(callbackMember): jen.Id("callbacks"),
|
||||
},
|
||||
),
|
||||
jen.Nil(),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s%s %s", constructorName, name, comment)))
|
||||
return
|
||||
}
|
||||
|
||||
// predicateResolverFunctions returns the functions for the PredicateTypeResolver.
|
||||
func (r *ResolverGenerator) predicateResolverFunctions(name, comment string) (f []*codegen.Function) {
|
||||
f = append(f, codegen.NewCommentedFunction(
|
||||
r.pkg.Path(),
|
||||
fmt.Sprintf("%s%s", constructorName, name),
|
||||
[]jen.Code{
|
||||
jen.Id("delegate").Id(resolverInterface),
|
||||
jen.Id("predicate").Interface(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Op("*").Id(name),
|
||||
jen.Error(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Commentf("The predicate must satisfy one known predicate function signature, or else we will generate a runtime error instead of silently fail."),
|
||||
jen.Switch(
|
||||
jen.Id("predicate").Assert(jen.Type()),
|
||||
).Block(
|
||||
r.mustAssertToKnownPredicate("predicate"),
|
||||
),
|
||||
jen.Return(
|
||||
jen.Op("&").Id(name).Values(
|
||||
jen.Dict{
|
||||
jen.Id(delegateMember): jen.Id("delegate"),
|
||||
jen.Id(predicateMember): jen.Id("predicate"),
|
||||
},
|
||||
),
|
||||
jen.Nil(),
|
||||
),
|
||||
},
|
||||
fmt.Sprintf("%s%s %s", constructorName, name, comment)))
|
||||
return
|
||||
}
|
||||
|
||||
// resolverMembers returns the members for the TypeResolver.
|
||||
func (r *ResolverGenerator) resolverMembers() (m []jen.Code) {
|
||||
m = append(m, jen.Id(callbackMember).Index().Interface())
|
||||
return
|
||||
}
|
||||
|
||||
// predicateResolverMembers returns the members for the PredicateTypResolver.
|
||||
func (r *ResolverGenerator) predicateResolverMembers() (m []jen.Code) {
|
||||
m = append(m, jen.Id(delegateMember).Id(resolverInterface))
|
||||
m = append(m, jen.Id(predicateMember).Interface())
|
||||
return
|
||||
}
|
||||
|
||||
// mustAssertToKnownTypes creates the type assertion switch statement that will
|
||||
// return an error if the parameter named does not match any of the expected
|
||||
// function signatures.
|
||||
func (r *ResolverGenerator) mustAssertToKnownTypes(paramName string) jen.Code {
|
||||
c := jen.Empty()
|
||||
for _, t := range r.types {
|
||||
c = c.Case(
|
||||
jen.Func().Parens(
|
||||
jen.List(
|
||||
jen.Qual("context", "Context"),
|
||||
jen.Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
),
|
||||
).Error(),
|
||||
).Block(
|
||||
jen.Commentf("Do nothing, this callback has a correct signature."),
|
||||
).Line()
|
||||
}
|
||||
c = c.Default().Block(
|
||||
jen.Return(
|
||||
jen.Nil(),
|
||||
jen.Qual("errors", "New").Call(jen.Lit("a callback function is of the wrong signature and would never be called")),
|
||||
),
|
||||
)
|
||||
return c
|
||||
}
|
||||
|
||||
// mustAssertToKnownPredicate ensures the parameter name types-asserts to a
|
||||
// known signature, or returns an error.
|
||||
func (r *ResolverGenerator) mustAssertToKnownPredicate(paramName string) jen.Code {
|
||||
c := jen.Empty()
|
||||
for _, t := range r.types {
|
||||
c = c.Case(
|
||||
jen.Func().Parens(
|
||||
jen.List(
|
||||
jen.Qual("context", "Context"),
|
||||
jen.Qual(t.PublicPackage().Path(), t.InterfaceName()),
|
||||
),
|
||||
).Parens(
|
||||
jen.List(
|
||||
jen.Bool(),
|
||||
jen.Error(),
|
||||
),
|
||||
),
|
||||
).Block(
|
||||
jen.Commentf("Do nothing, this predicate has a correct signature."),
|
||||
).Line()
|
||||
}
|
||||
c = c.Default().Block(
|
||||
jen.Return(
|
||||
jen.Nil(),
|
||||
jen.Qual("errors", "New").Call(jen.Lit("the predicate function is of the wrong signature and would never be called")),
|
||||
),
|
||||
)
|
||||
return c
|
||||
}
|
||||
|
||||
// asInterface returns the ActivityStreamsInterface.
|
||||
func (r *ResolverGenerator) asInterface() *codegen.Interface {
|
||||
return codegen.NewInterface(
|
||||
r.pkg.Path(),
|
||||
activityStreamInterface,
|
||||
[]codegen.FunctionSignature{
|
||||
{
|
||||
Name: typeNameMethod,
|
||||
Params: nil,
|
||||
Ret: []jen.Code{jen.String()},
|
||||
Comment: fmt.Sprintf("%s returns the ActiivtyStreams value's type.", typeNameMethod),
|
||||
},
|
||||
{
|
||||
Name: vocabURIMethod,
|
||||
Params: nil,
|
||||
Ret: []jen.Code{jen.String()},
|
||||
Comment: fmt.Sprintf("%s returns the vocabulary's URI as a string.", vocabURIMethod),
|
||||
},
|
||||
},
|
||||
fmt.Sprintf("%s represents any ActivityStream value code-generated by go-fed or compatible with the generated interfaces.", activityStreamInterface))
|
||||
}
|
||||
|
||||
// resolverInterface returns the Resolver interface.
|
||||
func (r *ResolverGenerator) resolverInterface() *codegen.Interface {
|
||||
return codegen.NewInterface(
|
||||
r.pkg.Path(),
|
||||
resolverInterface,
|
||||
[]codegen.FunctionSignature{
|
||||
{
|
||||
Name: resolveMethod,
|
||||
Params: []jen.Code{
|
||||
jen.Id("ctx").Qual("context", "Context"),
|
||||
jen.Id("o").Id(activityStreamInterface),
|
||||
},
|
||||
Ret: []jen.Code{
|
||||
jen.Error(),
|
||||
},
|
||||
Comment: fmt.Sprintf("%s will attempt to resolve an untyped ActivityStreams value into a Go concrete type.", resolveMethod),
|
||||
},
|
||||
},
|
||||
fmt.Sprintf("%s represents any %s.", resolverInterface, typeResolverStructName))
|
||||
}
|
||||
|
||||
// toAliasFunction returns the toAliasMap function
|
||||
func (r *ResolverGenerator) toAliasFunction() *codegen.Function {
|
||||
return codegen.NewCommentedFunction(
|
||||
r.pkg.Path(),
|
||||
toAliasMapFnName,
|
||||
[]jen.Code{
|
||||
jen.Id("i").Interface(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Id("m").Map(jen.String()).String(),
|
||||
},
|
||||
[]jen.Code{
|
||||
jen.Id("m").Op("=").Make(
|
||||
jen.Map(jen.String()).String(),
|
||||
),
|
||||
jen.Id("toHttpHttpsFn").Op(":=").Func().Parens(
|
||||
jen.Id("s").String(),
|
||||
).Parens(
|
||||
jen.List(
|
||||
jen.Id("ok").Bool(),
|
||||
jen.Id("http"),
|
||||
jen.Id("https").String(),
|
||||
),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.Qual("strings", "HasPrefix").Call(
|
||||
jen.Id("s"),
|
||||
jen.Lit("http://"),
|
||||
),
|
||||
).Block(
|
||||
jen.Id("ok").Op("=").True(),
|
||||
jen.Id("http").Op("=").Id("s"),
|
||||
jen.Id("https").Op("=").Lit("https").Op("+").Qual("strings", "TrimPrefix").Call(
|
||||
jen.Id("s"),
|
||||
jen.Lit("http"),
|
||||
),
|
||||
).Else().If(
|
||||
jen.Qual("strings", "HasPrefix").Call(
|
||||
jen.Id("s"),
|
||||
jen.Lit("https://"),
|
||||
),
|
||||
).Block(
|
||||
jen.Id("ok").Op("=").True(),
|
||||
jen.Id("https").Op("=").Id("s"),
|
||||
jen.Id("http").Op("=").Lit("http").Op("+").Qual("strings", "TrimPrefix").Call(
|
||||
jen.Id("s"),
|
||||
jen.Lit("https"),
|
||||
),
|
||||
),
|
||||
jen.Return(),
|
||||
),
|
||||
jen.Switch(jen.Id("v").Op(":=").Id("i").Assert(jen.Type())).Block(
|
||||
jen.Case(jen.String()).Block(
|
||||
jen.Commentf("Single entry, no alias."),
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("ok"),
|
||||
jen.Id("http"),
|
||||
jen.Id("https"),
|
||||
).Op(":=").Id("toHttpHttpsFn").Call(jen.Id("v")),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Id("m").Index(
|
||||
jen.Id("http"),
|
||||
).Op("=").Lit(""),
|
||||
jen.Id("m").Index(
|
||||
jen.Id("https"),
|
||||
).Op("=").Lit(""),
|
||||
).Else().Block(
|
||||
jen.Id("m").Index(
|
||||
jen.Id("v"),
|
||||
).Op("=").Lit(""),
|
||||
),
|
||||
),
|
||||
jen.Case(jen.Index().Interface()).Block(
|
||||
jen.Commentf("Recursively apply."),
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("_"),
|
||||
jen.Id("elem"),
|
||||
).Op(":=").Range().Id("v"),
|
||||
).Block(
|
||||
jen.Id("r").Op(":=").Id(toAliasMapFnName).Call(
|
||||
jen.Id("elem"),
|
||||
),
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("k"),
|
||||
jen.Id("val"),
|
||||
).Op(":=").Range().Id("r"),
|
||||
).Block(
|
||||
jen.Id("m").Index(
|
||||
jen.Id("k"),
|
||||
).Op("=").Id("val"),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Case(jen.Map(jen.String()).Interface()).Block(
|
||||
jen.Commentf("Map any aliases."),
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("k"),
|
||||
jen.Id("val"),
|
||||
).Op(":=").Range().Id("v"),
|
||||
).Block(
|
||||
jen.Commentf("Only handle string aliases."),
|
||||
jen.Switch(jen.Id("conc").Op(":=").Id("val").Assert(jen.Type())).Block(
|
||||
jen.Case(jen.String()).Block(
|
||||
jen.Id("m").Index(
|
||||
jen.Id("k"),
|
||||
).Op("=").Id("conc"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Return(),
|
||||
},
|
||||
fmt.Sprintf("%s converts a JSONLD context into a map of vocabulary name to alias.", toAliasMapFnName))
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,423 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/astool/convert"
|
||||
"github.com/go-fed/activity/astool/gen"
|
||||
"github.com/go-fed/activity/astool/rdf"
|
||||
"github.com/go-fed/activity/astool/rdf/owl"
|
||||
"github.com/go-fed/activity/astool/rdf/rdfs"
|
||||
"github.com/go-fed/activity/astool/rdf/rfc"
|
||||
"github.com/go-fed/activity/astool/rdf/schema"
|
||||
"github.com/go-fed/activity/astool/rdf/xsd"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
pathFlag = "path"
|
||||
specFlag = "spec"
|
||||
helpText = `
|
||||
Usage: astool [-spec=<file>] [-path=<gopath prefix>] <directory>
|
||||
|
||||
The ActivityStreams tool (astool) is used to generate ActivityStreams types,
|
||||
properties, and values from an OWL2 RDF specification. The tool generates the
|
||||
code necessary to create interfaces and functions that solve the problems of
|
||||
serialization & deserialization of functional and nonfunctional properties,
|
||||
serialization & deserialization of types, navigating the extends/disjoint
|
||||
hierarchy, and resolving an arbitrary ActivityStreams into a concrete Go type.
|
||||
|
||||
The tool generates files in the current working directory, and creates
|
||||
subpackages as needed. To generate the code for a specification, pass the OWL
|
||||
ontology defined as JSON-LD to the tool:
|
||||
|
||||
astool -spec specification.jsonld ./gen/to/subdir
|
||||
|
||||
The @context provided in the ActivityStreams specification may be insufficient
|
||||
for this tool to use to generate code. However, if this tool is able to use the
|
||||
JSON-LD specification to generate the code, then it should also be compatible
|
||||
with the @context.
|
||||
|
||||
This tool will automatically detect the correct Go prefix path to use if used
|
||||
in a subdirectory under GOPATH. If used outside of GOPATH, the prefix to the
|
||||
current working directory must be provided:
|
||||
|
||||
astool -spec specification.jsonld -path path/to/my/module/cwd .
|
||||
|
||||
If a specification builds off of a previous specification, they must be provided
|
||||
in the order of root to dependency, with the ActivityStreams Core & Extended
|
||||
Types specification as the root:
|
||||
|
||||
astool -spec activitystreams.jsonld -spec derived_extension.jsonld .
|
||||
|
||||
The following directories are generated in the current working directory (cwd)
|
||||
given a particular specification for a <vocabulary>:
|
||||
|
||||
cwd/
|
||||
gen_doc.go
|
||||
- Package level documentation.
|
||||
gen_init.go
|
||||
- Init function definitions.
|
||||
gen_manager.go
|
||||
- Definition of Manager, which is responsible for dependency
|
||||
injection of concrete values at runtime for deserialization.
|
||||
gen_pkg_<vocabulary>_disjoint.go
|
||||
- Functions determining the "disjointedness" of ActivityStreams
|
||||
types in the specified vocabulary.
|
||||
gen_pkg_<vocabulary>_extendedby.go
|
||||
- Functions determining the parent-to-child "extends" of
|
||||
ActivityStreams types in the specified vocabulary.
|
||||
gen_pkg_<vocabulary>_extends.go
|
||||
- Functions determining the child-to-parent "extends" of
|
||||
ActivityStreams types in the specified vocabulary.
|
||||
gen_pkg_<vocabulary>_property_constructors.go
|
||||
- Constructors of properties in the specified vocabulary.
|
||||
gen_pkg_<vocabulary>_type_constructors.go
|
||||
- Constructors of types in the specified vocabulary.
|
||||
|
||||
resolver/
|
||||
gen_type_resolver.go
|
||||
- Resolves arbitrary ActivityStream objects by type.
|
||||
gen_interface_resolver.go
|
||||
- Resolves arbitrary ActivityStream objects by their assertable
|
||||
interfaces.
|
||||
gen_type_predicated_resolver.go
|
||||
- Conditionally resolves based on the ActivityStream object's
|
||||
type.
|
||||
gen_interface_predicated_resolver.go
|
||||
- Conditionally resolves based on the ACtivityStream's
|
||||
assertable interfaces.
|
||||
gen_resolver_utils.go
|
||||
- Functions aiding in handling resolver errors.
|
||||
|
||||
vocab/
|
||||
gen_doc.go
|
||||
- Package level documentation.
|
||||
gen_pkg.go
|
||||
- Generic interface definition.
|
||||
gen_property_<property>_interface.go
|
||||
- Interface definition of a property.
|
||||
- NOTE: Application developers should prefer using these
|
||||
interfaces over the concrete types defined in "impl".
|
||||
gen_type_<type>_interface.go
|
||||
- Interface definition of a type.
|
||||
- NOTE: Application developers should prefer using these
|
||||
interfaces over the concrete types defined in "impl".
|
||||
|
||||
values/
|
||||
<value>/
|
||||
- Contains RDF values and their serialization, deserialization,
|
||||
and comparison methods.
|
||||
|
||||
impl/
|
||||
<vocabulary>/
|
||||
- Implementation of the vocabulary.
|
||||
- NOTE: Application developers should strongly prefer using the
|
||||
interfaces in "vocab" over these.
|
||||
|
||||
This tool is geared for three kinds of developers:
|
||||
|
||||
1) Application developers can use the tool to generate the native Go types
|
||||
needed to build an application.
|
||||
2) Developers wishing to extend ActivityStreams may use the tool to evaluate
|
||||
their OWL definition of their new ActivityStreams types and properties to
|
||||
rapidly prototype in Go code.
|
||||
3) Finally, developers wishing to provide an alternate implementation to go-fed
|
||||
can target the same interfaces generated by this tool, and create a fork that
|
||||
allows the generated Manager and constructors to inject their concrete type
|
||||
into any existing application using go-fed.
|
||||
|
||||
The tool relies on built-in knowledge of several ontologies: RDF, RDFS, OWL,
|
||||
Schema.org, XML, and a few RFCs. However, this tool doesn't have complete
|
||||
knowledge of all of these ontologies. It may error out because a provided
|
||||
specification uses a definition that the tool doesn't currently know. In such a
|
||||
case, please file an issue at https://github.com/go-fed/activity in order to
|
||||
include the missing definition.
|
||||
|
||||
Experimental support for generating the code as a module is provided by settting
|
||||
the 'path' flag, which will prefix all generated code with the 'path':
|
||||
|
||||
astool -spec specification.jsonld -path mymodule ./subdir
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
// Global registry of "known" RDF ontologies. This manages the built-in
|
||||
// knowledge of how to parse specific linked data documents. It may be cloned
|
||||
// in the course of processing a JSON-LD document, due to "@context" dictating
|
||||
// certain ontologies being aliased in some specifications and not others.
|
||||
var registry *rdf.RDFRegistry
|
||||
|
||||
// mustAddOntology ensures that the registry global variable is not nil, and
|
||||
// then adds the specific ontology or panics if it cannot.
|
||||
func mustAddOntology(o rdf.Ontology) {
|
||||
if registry == nil {
|
||||
registry = rdf.NewRDFRegistry()
|
||||
}
|
||||
if err := registry.AddOntology(o); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// At init time, get our built-in knowledge of OWL and other RDF ontologies
|
||||
// into the registry, before main executes.
|
||||
func init() {
|
||||
flag.Usage = func() {
|
||||
_, _ = io.WriteString(flag.CommandLine.Output(), helpText)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
mustAddOntology(&xsd.XMLOntology{Package: "xml"})
|
||||
mustAddOntology(&owl.OWLOntology{})
|
||||
mustAddOntology(&rdf.RDFOntology{Package: "rdf"})
|
||||
mustAddOntology(&rdfs.RDFSchemaOntology{})
|
||||
mustAddOntology(&schema.SchemaOntology{})
|
||||
mustAddOntology(&rfc.RFCOntology{Package: "rfc"})
|
||||
}
|
||||
|
||||
// list is a flag-friendly comma-separated list of strings. Also allows multiple
|
||||
// definitions of the flag to not overwrite each other and instead result in a
|
||||
// list of strings.
|
||||
//
|
||||
// The values of the flag cannot contain commas within them because the value
|
||||
// will be split into two.
|
||||
type list []string
|
||||
|
||||
// String turns this list into a single comma-separated string.
|
||||
func (l *list) String() string {
|
||||
return strings.Join(*l, ",")
|
||||
}
|
||||
|
||||
// Set adds a string value to the list, after splitting on the comma separator.
|
||||
func (l *list) Set(v string) error {
|
||||
vals := strings.Split(v, ",")
|
||||
*l = append(*l, vals...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// settableString is a flag-friendly string that distinguishes an empty string
|
||||
// due to not being set and explicitly being set as empty at the command line.
|
||||
type settableString struct {
|
||||
set bool
|
||||
str string
|
||||
}
|
||||
|
||||
// String simply returns the string value of this settableString.
|
||||
func (s *settableString) String() string {
|
||||
return s.str
|
||||
}
|
||||
|
||||
// Set will mark this settableString's set as true and store the value.
|
||||
func (s *settableString) Set(v string) error {
|
||||
s.set = true
|
||||
s.str = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSet returns true if this value was explicitly set as a flag value.
|
||||
func (s settableString) IsSet() bool {
|
||||
return s.set
|
||||
}
|
||||
|
||||
// CommandLineFlags manages the flags defined by this tool.
|
||||
type CommandLineFlags struct {
|
||||
// Flags
|
||||
specs list
|
||||
path settableString
|
||||
// Additional data
|
||||
pathAutoDetected bool
|
||||
// Destination on the file system for the code generation
|
||||
destination string
|
||||
}
|
||||
|
||||
// NewCommandLineFlags defines the flags expected to be used by this tool. Calls
|
||||
// flag.Parse on behalf of the main program, and validates the flags. Returns an
|
||||
// error if validation fails.
|
||||
func NewCommandLineFlags() (*CommandLineFlags, error) {
|
||||
c := &CommandLineFlags{}
|
||||
flag.Var(
|
||||
&c.path,
|
||||
pathFlag,
|
||||
"Package path to use for all generated package paths. If using GOPATH, this is automatically detected as $GOPATH/<path>/ when generating in a subdirectory. Cannot be explicitly set to be empty.")
|
||||
flag.Var(&(c.specs), specFlag, "Input JSON-LD specification used to generate Go code.")
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
if len(args) != 1 {
|
||||
return nil, fmt.Errorf("astool requires a destination directory")
|
||||
}
|
||||
c.destination = args[0]
|
||||
return c, c.Validate()
|
||||
}
|
||||
|
||||
// detectPath attempts to detect the path to use when generating the code. The
|
||||
// path is only detected if the tool is running in a subdirectory of GOPATH,
|
||||
// and will be set to $GOPATH/<path>/. After this method runs without errors,
|
||||
// c.path.IsSet will always return true.
|
||||
//
|
||||
// When auto-detecting, if GOPATH is not set then will return an error.
|
||||
//
|
||||
// If the path has already been set at the command line, does nothing.
|
||||
func (c *CommandLineFlags) detectPath() error {
|
||||
if c.path.IsSet() {
|
||||
return nil
|
||||
}
|
||||
gopath, isSet := os.LookupEnv("GOPATH")
|
||||
if !isSet {
|
||||
return fmt.Errorf("cannot detect %q because GOPATH environmental variable is not set and %q flag was not explicitly set", pathFlag, pathFlag)
|
||||
}
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(pwd, gopath) {
|
||||
return fmt.Errorf("cannot detect %q because current working directory is not under GOPATH and %q flag was not explicitly set", pathFlag, pathFlag)
|
||||
}
|
||||
c.pathAutoDetected = true
|
||||
gopath = strings.Join([]string{gopath, "src", ""}, "/")
|
||||
return c.path.Set(strings.TrimPrefix(pwd, gopath))
|
||||
}
|
||||
|
||||
// Validate applies custom validation logic to flags and returns an error if any
|
||||
// flags violate these rules.
|
||||
func (c *CommandLineFlags) Validate() error {
|
||||
if len(c.specs) == 0 {
|
||||
return fmt.Errorf("%q flag must not be empty", specFlag)
|
||||
}
|
||||
if err := c.detectPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(c.path.String()) == 0 {
|
||||
return fmt.Errorf("%q flag must not be empty", pathFlag)
|
||||
}
|
||||
if strings.Contains(c.destination, "..") {
|
||||
return fmt.Errorf("destination with '..' in path is not supported")
|
||||
}
|
||||
if !strings.HasPrefix(c.destination, "."+string(os.PathSeparator)) && c.destination != "." {
|
||||
return fmt.Errorf("destination directory must be a relative path")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSpecs returns the JSONLD contents of files specified in the 'spec' flag.
|
||||
func (c *CommandLineFlags) ReadSpecs() (j []rdf.JSONLD, err error) {
|
||||
j = make([]rdf.JSONLD, 0, len(c.specs))
|
||||
for _, spec := range c.specs {
|
||||
var b []byte
|
||||
b, err = ioutil.ReadFile(spec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var inputJSON map[string]interface{}
|
||||
err = json.Unmarshal(b, &inputJSON)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
j = append(j, inputJSON)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateDestination creates the destination path
|
||||
func (c *CommandLineFlags) CreateDestination() error {
|
||||
return os.MkdirAll(c.destination, 0777)
|
||||
}
|
||||
|
||||
// AutoDetectedPath returns true if the path flag was auto-detected.
|
||||
func (c *CommandLineFlags) AutoDetectedPath() bool {
|
||||
return c.pathAutoDetected
|
||||
}
|
||||
|
||||
// Path returns the path flag.
|
||||
func (c *CommandLineFlags) Path() string {
|
||||
return c.path.String()
|
||||
}
|
||||
|
||||
// NewPackageManager creates the correct package manager for the flag inputs.
|
||||
func (c *CommandLineFlags) NewPackageManager() *gen.PackageManager {
|
||||
g := gen.NewPackageManager(c.Path(), "")
|
||||
subdirs := strings.Split(
|
||||
// Trim "./" prefix as well as "trim" (aka remove) the sole "."
|
||||
// path.
|
||||
strings.TrimPrefix(
|
||||
// Trim "." first
|
||||
strings.TrimPrefix(c.destination, "."),
|
||||
// Then trim "/"
|
||||
string(os.PathSeparator)),
|
||||
string(os.PathSeparator))
|
||||
for _, subdir := range subdirs {
|
||||
g = g.Sub(subdir)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Read, Parse, and Validate command line flags
|
||||
cmd, err := NewCommandLineFlags()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print auto-determined values
|
||||
if cmd.AutoDetectedPath() {
|
||||
fmt.Printf("Auto-detected path: %s\n", cmd.Path())
|
||||
}
|
||||
|
||||
// Create the destination directory
|
||||
if err := cmd.CreateDestination(); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Read input specification files
|
||||
fmt.Printf("Reading input specifications...\n")
|
||||
inputJSONs, err := cmd.ReadSpecs()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse specifications
|
||||
fmt.Printf("Parsing %d vocabularies...\n", len(inputJSONs))
|
||||
p, err := rdf.ParseVocabularies(registry, inputJSONs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Convert to generated code
|
||||
fmt.Printf("Converting %d types, properties, and values...\n", p.Size())
|
||||
c := &convert.Converter{
|
||||
GenRoot: cmd.NewPackageManager(),
|
||||
PackagePolicy: convert.IndividualUnderRoot,
|
||||
}
|
||||
f, err := c.Convert(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Write generated code
|
||||
fmt.Printf("Writing %d files...\n", len(f))
|
||||
for _, file := range f {
|
||||
dir := file.Directory
|
||||
// If the cwd ("." or "./") are specified as the
|
||||
// destination, then the directory may be empty. The cwd does
|
||||
// not need to have MkdirAll called on it.
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
} else if e := os.MkdirAll(dir, 0777); e != nil {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
// Standard generated Go code header.
|
||||
// https://github.com/golang/go/issues/13560#issuecomment-288457920
|
||||
file.F.HeaderComment("// Code generated by astool. DO NOT EDIT.\n")
|
||||
|
||||
if e := file.F.Save(dir + string(os.PathSeparator) + file.FileName); e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
fmt.Printf("Done!\n")
|
||||
}
|
|
@ -0,0 +1,428 @@
|
|||
package rdf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ParsedVocabulary is the internal data structure produced after parsing the
|
||||
// definition of an ActivityStream vocabulary. It is the intermediate
|
||||
// understanding of the specification in the context of certain ontologies.
|
||||
//
|
||||
// At the end of parsing, the ParsedVocabulary is not guaranteed to be
|
||||
// semantically valid, just that the parser resolved all important ontological
|
||||
// details.
|
||||
//
|
||||
// Note that the Order field contains the order in which parsed specs were
|
||||
// understood and resolved. Kinds added as references (such as XML, Schema.org,
|
||||
// or rdfs types) are not included in Order. It is expected that the last
|
||||
// element of Order must be the vocabulary in Vocab.
|
||||
type ParsedVocabulary struct {
|
||||
Vocab Vocabulary
|
||||
References map[string]*Vocabulary
|
||||
Order []string
|
||||
}
|
||||
|
||||
// Size returns the number of types, properties, and values in the parsed
|
||||
// vocabulary.
|
||||
func (p ParsedVocabulary) Size() int {
|
||||
s := p.Vocab.Size()
|
||||
for _, v := range p.References {
|
||||
s += v.Size()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Clone creates a copy of this ParsedVocabulary. Note that the cloned
|
||||
// vocabulary does not copy References, so the original and clone both have
|
||||
// pointers to the same referenced vocabularies.
|
||||
func (p ParsedVocabulary) Clone() *ParsedVocabulary {
|
||||
clone := &ParsedVocabulary{
|
||||
Vocab: p.Vocab,
|
||||
References: make(map[string]*Vocabulary, len(p.References)),
|
||||
Order: make([]string, len(p.Order)),
|
||||
}
|
||||
for k, v := range p.References {
|
||||
clone.References[k] = v
|
||||
}
|
||||
copy(clone.Order, p.Order)
|
||||
return clone
|
||||
}
|
||||
|
||||
// GetReference looks up a reference based on its URI.
|
||||
func (p *ParsedVocabulary) GetReference(uri string) (*Vocabulary, error) {
|
||||
httpSpec, httpsSpec, err := ToHttpAndHttps(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.References == nil {
|
||||
p.References = make(map[string]*Vocabulary)
|
||||
}
|
||||
if v, ok := p.References[httpSpec]; ok {
|
||||
return v, nil
|
||||
} else if v, ok := p.References[httpsSpec]; ok {
|
||||
return v, nil
|
||||
} else {
|
||||
p.References[uri] = &Vocabulary{}
|
||||
}
|
||||
return p.References[uri], nil
|
||||
}
|
||||
|
||||
// String returns a printable version of this ParsedVocabulary for debugging.
|
||||
func (p ParsedVocabulary) String() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(fmt.Sprintf("Vocab:\n%s", p.Vocab))
|
||||
for k, v := range p.References {
|
||||
b.WriteString(fmt.Sprintf("Reference %s:\n\t%s\n", k, v))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Vocabulary contains the type, property, and value definitions for a single
|
||||
// ActivityStreams or extension vocabulary.
|
||||
type Vocabulary struct {
|
||||
Name string
|
||||
WellKnownAlias string // Hack.
|
||||
URI *url.URL
|
||||
Types map[string]VocabularyType
|
||||
Properties map[string]VocabularyProperty
|
||||
Values map[string]VocabularyValue
|
||||
Registry *RDFRegistry
|
||||
}
|
||||
|
||||
// Size returns the number of types, properties, and values in this vocabulary.
|
||||
func (v Vocabulary) Size() int {
|
||||
return len(v.Types) + len(v.Properties) + len(v.Values)
|
||||
}
|
||||
|
||||
// GetName returns the vocabulary's name.
|
||||
func (v Vocabulary) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// GetWellKnownAlias returns the vocabulary's name.
|
||||
func (v Vocabulary) GetWellKnownAlias() string {
|
||||
return v.WellKnownAlias
|
||||
}
|
||||
|
||||
// SetName sets the vocabulary's name.
|
||||
func (v *Vocabulary) SetName(s string) {
|
||||
v.Name = s
|
||||
}
|
||||
|
||||
// SetURI sets the value's URI.
|
||||
func (v *Vocabulary) SetURI(s string) error {
|
||||
var e error
|
||||
v.URI, e = url.Parse(s)
|
||||
return e
|
||||
}
|
||||
|
||||
// String returns a printable version of this Vocabulary for debugging.
|
||||
func (v Vocabulary) String() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(fmt.Sprintf("Vocabulary %q\n", v.Name))
|
||||
for k, v := range v.Types {
|
||||
b.WriteString(fmt.Sprintf("Type %s:\n\t%s\n", k, v))
|
||||
}
|
||||
for k, v := range v.Properties {
|
||||
b.WriteString(fmt.Sprintf("Property %s:\n\t%s\n", k, v))
|
||||
}
|
||||
for k, v := range v.Values {
|
||||
b.WriteString(fmt.Sprintf("Value %s:\n\t%s\n", k, v))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// SetType sets a type keyed by its name. Returns an error if a type is already
|
||||
// set for that name.
|
||||
func (v *Vocabulary) SetType(name string, a *VocabularyType) error {
|
||||
if v.Types == nil {
|
||||
v.Types = make(map[string]VocabularyType, 1)
|
||||
}
|
||||
if _, has := v.Types[name]; has {
|
||||
return fmt.Errorf("name %q already exists for vocabulary Types", name)
|
||||
}
|
||||
v.Types[name] = *a
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetProperty sets a property keyed by its name. Returns an error if a property
|
||||
// is already set for that name.
|
||||
func (v *Vocabulary) SetProperty(name string, a *VocabularyProperty) error {
|
||||
if v.Properties == nil {
|
||||
v.Properties = make(map[string]VocabularyProperty, 1)
|
||||
}
|
||||
if _, has := v.Properties[name]; has {
|
||||
return fmt.Errorf("name already exists for vocabulary Properties")
|
||||
}
|
||||
v.Properties[name] = *a
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetValue sets a value keyed by its name. Returns an error if the value is
|
||||
// already set for that name.
|
||||
func (v *Vocabulary) SetValue(name string, a *VocabularyValue) error {
|
||||
if v.Values == nil {
|
||||
v.Values = make(map[string]VocabularyValue, 1)
|
||||
}
|
||||
if _, has := v.Values[name]; has {
|
||||
return fmt.Errorf("name already exists for vocabulary Values")
|
||||
}
|
||||
v.Values[name] = *a
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ NameSetter = &Vocabulary{}
|
||||
_ NameGetter = &Vocabulary{}
|
||||
_ URISetter = &Vocabulary{}
|
||||
)
|
||||
|
||||
// VocabularyValue represents a value type that properties can take on.
|
||||
type VocabularyValue struct {
|
||||
Name string
|
||||
URI *url.URL
|
||||
DefinitionType *jen.Statement
|
||||
Zero string
|
||||
IsNilable bool
|
||||
IsURI bool
|
||||
SerializeFn *codegen.Function
|
||||
DeserializeFn *codegen.Function
|
||||
LessFn *codegen.Function
|
||||
}
|
||||
|
||||
// String returns a printable version of this value for debugging.
|
||||
func (v VocabularyValue) String() string {
|
||||
return fmt.Sprintf("Value=%s,%s,%s,%s", v.Name, v.URI, v.DefinitionType, v.Zero)
|
||||
}
|
||||
|
||||
// SetName sets the value's name.
|
||||
func (v *VocabularyValue) SetName(s string) {
|
||||
v.Name = s
|
||||
}
|
||||
|
||||
// GetName returns the value's name.
|
||||
func (v VocabularyValue) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// SetURI sets the value's URI.
|
||||
func (v *VocabularyValue) SetURI(s string) error {
|
||||
var e error
|
||||
v.URI, e = url.Parse(s)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
_ NameSetter = &VocabularyValue{}
|
||||
_ NameGetter = &VocabularyValue{}
|
||||
_ URISetter = &VocabularyValue{}
|
||||
)
|
||||
|
||||
// VocabularyType represents a single ActivityStream type in a vocabulary.
|
||||
type VocabularyType struct {
|
||||
Name string
|
||||
Typeless bool // Hack
|
||||
URI *url.URL
|
||||
Notes string
|
||||
DisjointWith []VocabularyReference
|
||||
Extends []VocabularyReference
|
||||
Examples []VocabularyExample
|
||||
Properties []VocabularyReference
|
||||
WithoutProperties []VocabularyReference
|
||||
}
|
||||
|
||||
// String returns a printable version of this type, for debugging.
|
||||
func (v VocabularyType) String() string {
|
||||
return fmt.Sprintf("Type=%s,%s,%s\n\tDJW=%s\n\tExt=%s\n\tEx=%s", v.Name, v.URI, v.Notes, v.DisjointWith, v.Extends, v.Examples)
|
||||
}
|
||||
|
||||
// SetName sets the name of this type.
|
||||
func (v *VocabularyType) SetName(s string) {
|
||||
v.Name = s
|
||||
}
|
||||
|
||||
// SetName returns the name of this type.
|
||||
func (v VocabularyType) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// TypeName returns the name of this type.
|
||||
//
|
||||
// Used to satisfy an interface.
|
||||
func (v VocabularyType) TypeName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// SetURI sets the URI of this type, returning an error if it cannot parse the
|
||||
// URI.
|
||||
func (v *VocabularyType) SetURI(s string) error {
|
||||
var e error
|
||||
v.URI, e = url.Parse(s)
|
||||
return e
|
||||
}
|
||||
|
||||
// SetNotes sets the notes on this type.
|
||||
func (v *VocabularyType) SetNotes(s string) {
|
||||
v.Notes = s
|
||||
}
|
||||
|
||||
// AddExample adds an example on this type.
|
||||
func (v *VocabularyType) AddExample(e *VocabularyExample) {
|
||||
v.Examples = append(v.Examples, *e)
|
||||
}
|
||||
|
||||
// IsTypeless determines if this type is, in fact, typeless
|
||||
func (v *VocabularyType) IsTypeless() bool {
|
||||
return v.Typeless
|
||||
}
|
||||
|
||||
var (
|
||||
_ NameSetter = &VocabularyType{}
|
||||
_ NameGetter = &VocabularyType{}
|
||||
_ URISetter = &VocabularyType{}
|
||||
_ NotesSetter = &VocabularyType{}
|
||||
_ ExampleAdder = &VocabularyType{}
|
||||
)
|
||||
|
||||
// VocabularyProperty represents a single ActivityStream property type in a
|
||||
// vocabulary.
|
||||
type VocabularyProperty struct {
|
||||
Name string
|
||||
URI *url.URL
|
||||
Notes string
|
||||
Domain []VocabularyReference
|
||||
Range []VocabularyReference
|
||||
DoesNotApplyTo []VocabularyReference
|
||||
Examples []VocabularyExample
|
||||
// SubpropertyOf is ignorable as long as data is set up correctly
|
||||
SubpropertyOf VocabularyReference // Must be a VocabularyProperty
|
||||
Functional bool
|
||||
NaturalLanguageMap bool
|
||||
}
|
||||
|
||||
// String returns a printable version of this property for debugging.
|
||||
func (v VocabularyProperty) String() string {
|
||||
return fmt.Sprintf("Property=%s,%s,%s\n\tD=%s\n\tR=%s\n\tEx=%s\n\tSub=%s\n\tDNApply=%s\n\tfunc=%t,natLangMap=%t", v.Name, v.URI, v.Notes, v.Domain, v.Range, v.Examples, v.SubpropertyOf, v.DoesNotApplyTo, v.Functional, v.NaturalLanguageMap)
|
||||
}
|
||||
|
||||
// SetName sets the name on this property.
|
||||
func (v *VocabularyProperty) SetName(s string) {
|
||||
v.Name = s
|
||||
}
|
||||
|
||||
// GetName returns the name of this property.
|
||||
func (v VocabularyProperty) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// PropertyName returns the name of this property.
|
||||
//
|
||||
// Used to satisfy an interface.
|
||||
func (v VocabularyProperty) PropertyName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// SetURI sets the URI for this property, returning an error if it cannot be
|
||||
// parsed.
|
||||
func (v *VocabularyProperty) SetURI(s string) error {
|
||||
var e error
|
||||
v.URI, e = url.Parse(s)
|
||||
return e
|
||||
}
|
||||
|
||||
// SetNotes sets notes on this Property.
|
||||
func (v *VocabularyProperty) SetNotes(s string) {
|
||||
v.Notes = s
|
||||
}
|
||||
|
||||
// AddExample adds an example for this property.
|
||||
func (v *VocabularyProperty) AddExample(e *VocabularyExample) {
|
||||
v.Examples = append(v.Examples, *e)
|
||||
}
|
||||
|
||||
var (
|
||||
_ NameSetter = &VocabularyProperty{}
|
||||
_ NameGetter = &VocabularyProperty{}
|
||||
_ URISetter = &VocabularyProperty{}
|
||||
_ NotesSetter = &VocabularyProperty{}
|
||||
_ ExampleAdder = &VocabularyProperty{}
|
||||
)
|
||||
|
||||
// VocabularyExample documents an Example for an ActivityStream type or property
|
||||
// in the vocabulary.
|
||||
type VocabularyExample struct {
|
||||
Name string
|
||||
URI *url.URL
|
||||
Example interface{}
|
||||
}
|
||||
|
||||
// String returns a printable string used for debugging.
|
||||
func (v VocabularyExample) String() string {
|
||||
return fmt.Sprintf("VocabularyExample: %s,%s,%s", v.Name, v.URI, v.Example)
|
||||
}
|
||||
|
||||
// SetName sets the name on this example.
|
||||
func (v *VocabularyExample) SetName(s string) {
|
||||
v.Name = s
|
||||
}
|
||||
|
||||
// GetName returns the name of this example.
|
||||
func (v VocabularyExample) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// SetURI sets the URI of this example, returning an error if it cannot be
|
||||
// parsed.
|
||||
func (v *VocabularyExample) SetURI(s string) error {
|
||||
var e error
|
||||
v.URI, e = url.Parse(s)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
_ NameSetter = &VocabularyExample{}
|
||||
_ NameGetter = &VocabularyExample{}
|
||||
_ URISetter = &VocabularyExample{}
|
||||
)
|
||||
|
||||
// VocabularyReference refers to another Vocabulary reference, either a
|
||||
// VocabularyType, VocabularyValue, or a VocabularyProperty. It may refer to
|
||||
// another Vocabulary's type or property entirely.
|
||||
type VocabularyReference struct {
|
||||
Name string
|
||||
URI *url.URL
|
||||
Vocab string // If present, must match key in ParsedVocabulary.References
|
||||
}
|
||||
|
||||
// String returns a printable string for this reference, used for debugging.
|
||||
func (v VocabularyReference) String() string {
|
||||
return fmt.Sprintf("VocabularyReference: %s,%s,%s", v.Name, v.URI, v.Vocab)
|
||||
}
|
||||
|
||||
// SetName sets the name of this reference.
|
||||
func (v *VocabularyReference) SetName(s string) {
|
||||
v.Name = s
|
||||
}
|
||||
|
||||
// GetName returns the name of this reference.
|
||||
func (v VocabularyReference) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
// SetURI sets the URI for this reference. Returns an error if the URI cannot
|
||||
// be parsed.
|
||||
func (v *VocabularyReference) SetURI(s string) error {
|
||||
var e error
|
||||
v.URI, e = url.Parse(s)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
_ NameSetter = &VocabularyReference{}
|
||||
_ NameGetter = &VocabularyReference{}
|
||||
_ URISetter = &VocabularyReference{}
|
||||
)
|
|
@ -0,0 +1,264 @@
|
|||
package rdf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
typeSpec = "@type"
|
||||
typeActivityStreamsSpec = "type"
|
||||
IdSpec = "@id"
|
||||
IdActivityStreamsSpec = "id"
|
||||
ContainerSpec = "@container"
|
||||
IndexSpec = "@index"
|
||||
// ActivityStreams specifically disallows the 'object' property on
|
||||
// certain IntransitiveActivity and subtypes. There is no RDF mechanism
|
||||
// to describe this. So this is a stupid hack, based on the assumption
|
||||
// that no one -- W3C or otherwise -- will name a reserved word with a
|
||||
// "@wtf_" prefix due to the reserved '@', the use of the unprofessional
|
||||
// 'wtf', and a style-breaking underscore.
|
||||
withoutPropertySpec = "@wtf_without_property"
|
||||
typelessSpec = "@wtf_typeless"
|
||||
// TODO: Support WellKnownAlias
|
||||
)
|
||||
|
||||
// jsonLDNodes contains the well-known set of nodes as defined by the JSON-LD
|
||||
// specification.
|
||||
func jsonLDNodes(r *RDFRegistry) []RDFNode {
|
||||
// Order matters -- we want to be able to distinguish the types of
|
||||
// things without other nodes hijacking the flow.
|
||||
return []RDFNode{
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: typeSpec,
|
||||
Delegate: &typeLD{r: r},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: typeActivityStreamsSpec,
|
||||
Delegate: &typeLD{r: r},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: IdSpec,
|
||||
Delegate: &idLD{},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: IdActivityStreamsSpec,
|
||||
Delegate: &idLD{},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: ContainerSpec,
|
||||
Delegate: &ContainerLD{},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: IndexSpec,
|
||||
Delegate: &IndexLD{},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: withoutPropertySpec,
|
||||
Delegate: &withoutProperty{},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: typelessSpec,
|
||||
Delegate: &typeless{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ RDFNode = &idLD{}
|
||||
|
||||
// idLD is an RDFNode for the 'id' property.
|
||||
type idLD struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (i *idLD) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("id cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (i *idLD) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("id cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the URI for the context's Current item.
|
||||
func (i *idLD) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
if ctx.Current == nil {
|
||||
return true, nil
|
||||
} else if ider, ok := ctx.Current.(URISetter); !ok {
|
||||
return true, fmt.Errorf("id apply called with non-URISetter")
|
||||
} else if str, ok := value.(string); !ok {
|
||||
return true, fmt.Errorf("id apply called with non-string value")
|
||||
} else {
|
||||
return true, ider.SetURI(str)
|
||||
}
|
||||
}
|
||||
|
||||
var _ RDFNode = &typeLD{}
|
||||
|
||||
// typeLD is an RDFNode for the 'type' property.
|
||||
type typeLD struct {
|
||||
r *RDFRegistry
|
||||
}
|
||||
|
||||
// Enter does nothing.
|
||||
func (t *typeLD) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (t *typeLD) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply attempts to get the RDFNode for the type and apply it.
|
||||
func (t *typeLD) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
vs, ok := value.(string)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("@type is not string")
|
||||
}
|
||||
n, e := t.r.getNode(vs)
|
||||
if e != nil {
|
||||
return true, e
|
||||
}
|
||||
return n.Apply(vs, nil, ctx)
|
||||
}
|
||||
|
||||
var _ RDFNode = &ContainerLD{}
|
||||
|
||||
// ContainerLD is an RDFNode that delegates to an RDFNode but only at this
|
||||
// next level.
|
||||
type ContainerLD struct {
|
||||
ContainsNode RDFNode
|
||||
}
|
||||
|
||||
// Enter sets OnlyApplyThisNodeNextLevel on the ParsingContext.
|
||||
//
|
||||
// Returns an error if this is the second time Enter is called in a row.
|
||||
func (c *ContainerLD) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
if ctx.OnlyApplyThisNodeNextLevel != nil {
|
||||
return true, fmt.Errorf("@container parsing context exit already has non-nil node")
|
||||
}
|
||||
ctx.SetOnlyApplyThisNodeNextLevel(c.ContainsNode)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit clears OnlyApplyThisNodeNextLevel on the ParsingContext.
|
||||
//
|
||||
// Returns an error if this is the second time Exit is called in a row.
|
||||
func (c *ContainerLD) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
if ctx.OnlyApplyThisNodeNextLevel == nil {
|
||||
return true, fmt.Errorf("@container parsing context exit already has nil node")
|
||||
}
|
||||
ctx.ResetOnlyAppliedThisNodeNextLevel()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply does nothing.
|
||||
func (c *ContainerLD) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ RDFNode = &IndexLD{}
|
||||
|
||||
// IndexLD does nothing.
|
||||
//
|
||||
// It could try to manage human-defined indices, but the machine doesn't care.
|
||||
type IndexLD struct{}
|
||||
|
||||
// Enter does nothing.
|
||||
func (i *IndexLD) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (i *IndexLD) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply does nothing.
|
||||
func (i *IndexLD) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ RDFNode = &withoutProperty{}
|
||||
|
||||
// withoutProperty is a hacky-as-hell way to manage ActivityStream's concept of
|
||||
// "WithoutProperty". It isn't a defined RDF relationship, so this is
|
||||
// non-standard but required of the ActivityStreams Core or Extended Types spec.
|
||||
type withoutProperty struct{}
|
||||
|
||||
// Enter pushes a VocabularyReference. It is expected further nodes will
|
||||
// populate it with information before dxiting this node.
|
||||
func (w *withoutProperty) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
ctx.Push()
|
||||
ctx.Current = &VocabularyReference{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit pops a VocabularyReferences and sets DoesNotApplyTo on the parent
|
||||
// VocabularyProperty on the stack.
|
||||
func (w *withoutProperty) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
i := ctx.Current
|
||||
ctx.Pop()
|
||||
vr, ok := i.(*VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("hacky withoutProperty exit did not get *rdf.VocabularyReference")
|
||||
}
|
||||
vp, ok := ctx.Current.(*VocabularyProperty)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("hacky withoutProperty exit Current is not *rdf.VocabularyProperty")
|
||||
}
|
||||
vp.DoesNotApplyTo = append(vp.DoesNotApplyTo, *vr)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (w *withoutProperty) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("hacky withoutProperty cannot be applied")
|
||||
}
|
||||
|
||||
var _ RDFNode = &typeless{}
|
||||
|
||||
// typeless is a hacky-as-hell way to rectify the fact that certain ontologies
|
||||
// have classes that do not correspond to the JSON-LD idea of an @type.
|
||||
// I didn't even bother looking for an existing RDF concept and instead would
|
||||
// rather force myself to suffer in order to prove how awful this is. Waah.
|
||||
type typeless struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (t *typeless) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("hacky typeless cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (t *typeless) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("hacky typeless cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets whether this type is actually typeless.
|
||||
func (t *typeless) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
val, ok := value.(bool)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("hacky typeless value is not a bool")
|
||||
}
|
||||
vt, ok := ctx.Current.(*VocabularyType)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("hacky typeless Current is not *rdf.VocabularyType")
|
||||
}
|
||||
vt.Typeless = val
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package rdf
|
||||
|
||||
var _ RDFNode = &AliasedDelegate{}
|
||||
|
||||
// AliasedDelegate will call the delegated RDFNode if the passed in keys
|
||||
// conform to either the spec or alias.
|
||||
type AliasedDelegate struct {
|
||||
Spec string
|
||||
Alias string
|
||||
Name string
|
||||
Delegate RDFNode
|
||||
}
|
||||
|
||||
// Enter calls the Delegate's Enter if the key conforms to the Spec or Alias.
|
||||
func (a *AliasedDelegate) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
if IsKeyApplicable(key, a.Spec, a.Alias, a.Name) {
|
||||
return a.Delegate.Enter(key, ctx)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Exit calls the Delegate's Exit if the key conforms to the Spec or Alias.
|
||||
func (a *AliasedDelegate) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
if IsKeyApplicable(key, a.Spec, a.Alias, a.Name) {
|
||||
return a.Delegate.Exit(key, ctx)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Apply calls the Delegate's Apply if the key conforms to the Spec or Alias.
|
||||
func (a *AliasedDelegate) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
if IsKeyApplicable(key, a.Spec, a.Alias, a.Name) {
|
||||
return a.Delegate.Apply(key, value, ctx)
|
||||
}
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
package rdf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
rdfName = "RDF"
|
||||
rdfSpec = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
langstringSpec = "langString"
|
||||
propertySpec = "Property"
|
||||
)
|
||||
|
||||
// SerializeValueFunction is a helper for creating a value's Serialize function.
|
||||
func SerializeValueFunction(pkg, valueName string,
|
||||
concreteType jen.Code,
|
||||
impl []jen.Code) *codegen.Function {
|
||||
name := fmt.Sprintf("Serialize%s", strings.Title(valueName))
|
||||
return codegen.NewCommentedFunction(
|
||||
pkg,
|
||||
name,
|
||||
[]jen.Code{jen.Id(codegen.This()).Add(concreteType)},
|
||||
[]jen.Code{jen.Interface(), jen.Error()},
|
||||
impl,
|
||||
fmt.Sprintf("%s converts a %s value to an interface representation suitable for marshalling into a text or binary format.", name, valueName))
|
||||
}
|
||||
|
||||
// DeserializeValueFunction is a helper for creating a value's Deserialize
|
||||
// function.
|
||||
func DeserializeValueFunction(pkg, valueName string,
|
||||
concreteType jen.Code,
|
||||
impl []jen.Code) *codegen.Function {
|
||||
name := fmt.Sprintf("Deserialize%s", strings.Title(valueName))
|
||||
return codegen.NewCommentedFunction(
|
||||
pkg,
|
||||
name,
|
||||
[]jen.Code{jen.Id(codegen.This()).Interface()},
|
||||
[]jen.Code{concreteType, jen.Error()},
|
||||
impl,
|
||||
fmt.Sprintf("%s creates %s value from an interface representation that has been unmarshalled from a text or binary format.", name, valueName))
|
||||
}
|
||||
|
||||
// LessFunction is a helper for creating a value's Less function.
|
||||
func LessFunction(pkg, valueName string,
|
||||
concreteType jen.Code,
|
||||
impl []jen.Code) *codegen.Function {
|
||||
name := fmt.Sprintf("Less%s", strings.Title(valueName))
|
||||
return codegen.NewCommentedFunction(
|
||||
pkg,
|
||||
name,
|
||||
[]jen.Code{jen.List(jen.Id("lhs"), jen.Id("rhs")).Add(concreteType)},
|
||||
[]jen.Code{jen.Bool()},
|
||||
impl,
|
||||
fmt.Sprintf("%s returns true if the left %s value is less than the right value.", name, valueName))
|
||||
}
|
||||
|
||||
var _ Ontology = &RDFOntology{}
|
||||
|
||||
// RDFOntology is an Ontology for the RDF namespace.
|
||||
type RDFOntology struct {
|
||||
Package string
|
||||
alias string
|
||||
}
|
||||
|
||||
// SpecURI returns the RDF URI spec.
|
||||
func (o *RDFOntology) SpecURI() string {
|
||||
return rdfSpec
|
||||
}
|
||||
|
||||
// Load loads the ontology with no alias set.
|
||||
func (o *RDFOntology) Load() ([]RDFNode, error) {
|
||||
return o.LoadAsAlias("")
|
||||
}
|
||||
|
||||
// LoadAsAlias loads the ontology with an alias.
|
||||
func (o *RDFOntology) LoadAsAlias(s string) ([]RDFNode, error) {
|
||||
o.alias = s
|
||||
return []RDFNode{
|
||||
&AliasedDelegate{
|
||||
Spec: rdfSpec,
|
||||
Alias: s,
|
||||
Name: langstringSpec,
|
||||
Delegate: &langstring{pkg: o.Package, alias: o.alias},
|
||||
},
|
||||
&AliasedDelegate{
|
||||
Spec: rdfSpec,
|
||||
Alias: s,
|
||||
Name: propertySpec,
|
||||
Delegate: &property{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadSpecificAsAlias loads a specific RDFNode with the given alias.
|
||||
func (o *RDFOntology) LoadSpecificAsAlias(alias, name string) ([]RDFNode, error) {
|
||||
switch name {
|
||||
case langstringSpec:
|
||||
return []RDFNode{
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &langstring{pkg: o.Package, alias: o.alias},
|
||||
},
|
||||
}, nil
|
||||
case propertySpec:
|
||||
return []RDFNode{
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &property{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("rdf ontology cannot find %q to make alias %q", name, alias)
|
||||
}
|
||||
|
||||
// LoadElement does nothing.
|
||||
func (o *RDFOntology) LoadElement(name string, payload map[string]interface{}) ([]RDFNode, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetByName returns a raw, unguarded node by name.
|
||||
func (o *RDFOntology) GetByName(name string) (RDFNode, error) {
|
||||
name = strings.TrimPrefix(name, o.SpecURI())
|
||||
switch name {
|
||||
case langstringSpec:
|
||||
return &langstring{pkg: o.Package, alias: o.alias}, nil
|
||||
case propertySpec:
|
||||
return &property{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("rdf ontology could not find node for name %s", name)
|
||||
}
|
||||
|
||||
var _ RDFNode = &langstring{}
|
||||
|
||||
// langstring is an RDF node representing the langstring value.
|
||||
type langstring struct {
|
||||
alias string
|
||||
pkg string
|
||||
}
|
||||
|
||||
// Enter returns an error.
|
||||
func (l *langstring) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdf langstring cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (l *langstring) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdf langstring cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the langstring value in the context as a referenced spec.
|
||||
func (l *langstring) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
for k, p := range ctx.Result.Vocab.Properties {
|
||||
for i, ref := range p.Range {
|
||||
if ref.Name == langstringSpec && ref.Vocab == l.alias {
|
||||
p.NaturalLanguageMap = true
|
||||
ctx.Result.Vocab.Properties[k] = p
|
||||
p.Range = append(p.Range[:i], p.Range[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
u, e := url.Parse(rdfSpec + langstringSpec)
|
||||
if e != nil {
|
||||
return true, e
|
||||
}
|
||||
var vocab *Vocabulary
|
||||
vocab, e = ctx.GetResultReferenceWithDefaults(rdfSpec, rdfName)
|
||||
if e != nil {
|
||||
return true, e
|
||||
}
|
||||
e = vocab.SetValue(langstringSpec, &VocabularyValue{
|
||||
Name: langstringSpec,
|
||||
URI: u,
|
||||
DefinitionType: jen.Map(jen.String()).String(),
|
||||
Zero: "make(map[string]string)",
|
||||
IsNilable: true,
|
||||
SerializeFn: SerializeValueFunction(
|
||||
l.pkg,
|
||||
langstringSpec,
|
||||
jen.Map(jen.String()).String(),
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Id(codegen.This()),
|
||||
jen.Nil(),
|
||||
),
|
||||
}),
|
||||
DeserializeFn: DeserializeValueFunction(
|
||||
l.pkg,
|
||||
langstringSpec,
|
||||
jen.Map(jen.String()).String(),
|
||||
[]jen.Code{
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("m"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id(codegen.This()).Assert(jen.Map(jen.String()).Interface()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Id("r").Op(":=").Make(jen.Map(jen.String()).String()),
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("k"),
|
||||
jen.Id("v"),
|
||||
).Op(":=").Range().Id("m"),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("s"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id("v").Assert(jen.String()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Id("r").Index(jen.Id("k")).Op("=").Id("s"),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Nil(),
|
||||
jen.Qual("fmt", "Errorf").Call(
|
||||
jen.Lit("value %v cannot be interpreted as a string for rdf:langString"),
|
||||
jen.Id("v"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
jen.Return(
|
||||
jen.Id("r"),
|
||||
jen.Nil(),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Nil(),
|
||||
jen.Qual("fmt", "Errorf").Call(
|
||||
jen.Lit("%v cannot be interpreted as a map[string]interface{} for rdf:langString"),
|
||||
jen.Id(codegen.This()),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
LessFn: LessFunction(
|
||||
l.pkg,
|
||||
langstringSpec,
|
||||
jen.Map(jen.String()).String(),
|
||||
[]jen.Code{
|
||||
jen.Var().Id("lk").Index().String(),
|
||||
jen.Var().Id("rk").Index().String(),
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("k"),
|
||||
).Op(":=").Range().Id("lhs"),
|
||||
).Block(
|
||||
jen.Id("lk").Op("=").Append(
|
||||
jen.Id("lk"),
|
||||
jen.Id("k"),
|
||||
),
|
||||
),
|
||||
jen.For(
|
||||
jen.List(
|
||||
jen.Id("k"),
|
||||
).Op(":=").Range().Id("rhs"),
|
||||
).Block(
|
||||
jen.Id("rk").Op("=").Append(
|
||||
jen.Id("rk"),
|
||||
jen.Id("k"),
|
||||
),
|
||||
),
|
||||
jen.Qual("sort", "Strings").Call(jen.Id("lk")),
|
||||
jen.Qual("sort", "Strings").Call(jen.Id("rk")),
|
||||
jen.For(
|
||||
jen.Id("i").Op(":=").Lit(0),
|
||||
jen.Id("i").Op("<").Len(jen.Id("lk")).Op("&&").Id("i").Op("<").Len(jen.Id("rk")),
|
||||
jen.Id("i").Op("++"),
|
||||
).Block(
|
||||
jen.If(
|
||||
jen.Id("lk").Index(jen.Id("i")).Op("<").Id("rk").Index(jen.Id("i")),
|
||||
).Block(
|
||||
jen.Return(jen.True()),
|
||||
).Else().If(
|
||||
jen.Id("rk").Index(jen.Id("i")).Op("<").Id("lk").Index(jen.Id("i")),
|
||||
).Block(
|
||||
jen.Return(jen.False()),
|
||||
).Else().If(
|
||||
jen.Id("lhs").Index(jen.Id("lk").Index(jen.Id("i"))).Op("<").Id("rhs").Index(jen.Id("rk").Index(jen.Id("i"))),
|
||||
).Block(
|
||||
jen.Return(jen.True()),
|
||||
).Else().If(
|
||||
jen.Id("rhs").Index(jen.Id("rk").Index(jen.Id("i"))).Op("<").Id("lhs").Index(jen.Id("lk").Index(jen.Id("i"))),
|
||||
).Block(
|
||||
jen.Return(jen.False()),
|
||||
),
|
||||
),
|
||||
jen.If(
|
||||
jen.Len(jen.Id("lk")).Op("<").Len(jen.Id("rk")),
|
||||
).Block(
|
||||
jen.Return(jen.True()),
|
||||
).Else().Block(
|
||||
jen.Return(jen.False()),
|
||||
),
|
||||
}),
|
||||
})
|
||||
return true, e
|
||||
}
|
||||
|
||||
var _ RDFNode = &property{}
|
||||
|
||||
// property is an RDFNode that sets a VocabularyProperty as the current.
|
||||
type property struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (p *property) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdf property cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (p *property) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdf property cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the current context to be a VocabularyProperty, if it is not
|
||||
// already. If the context isn't reset, an error is returned due to another node
|
||||
// not having cleaned up properly.
|
||||
func (p *property) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
// Prepare a new VocabularyProperty in the context. If one already
|
||||
// exists, skip.
|
||||
if _, ok := ctx.Current.(*VocabularyProperty); ok {
|
||||
return true, nil
|
||||
} else if !ctx.IsReset() {
|
||||
return true, fmt.Errorf("rdf property applied with non-reset ParsingContext")
|
||||
}
|
||||
ctx.Current = &VocabularyProperty{}
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,539 @@
|
|||
package owl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/astool/rdf"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
owlSpec = "http://www.w3.org/2002/07/owl#"
|
||||
membersSpec = "members"
|
||||
disjointWithSpec = "disjointWith"
|
||||
unionOfSpec = "unionOf"
|
||||
importsSpec = "imports"
|
||||
ontologySpec = "Ontology"
|
||||
classSpec = "Class"
|
||||
objectPropertySpec = "ObjectProperty"
|
||||
functionalPropertySpec = "FunctionalProperty"
|
||||
)
|
||||
|
||||
// OWLOntology is an Ontology for OWL2.
|
||||
type OWLOntology struct {
|
||||
alias string
|
||||
}
|
||||
|
||||
// SpecURI returns the URI of the OWL specification.
|
||||
func (o *OWLOntology) SpecURI() string {
|
||||
return owlSpec
|
||||
}
|
||||
|
||||
// Load this ontology without an alias.
|
||||
func (o *OWLOntology) Load() ([]rdf.RDFNode, error) {
|
||||
return o.LoadAsAlias("")
|
||||
}
|
||||
|
||||
// LoadAsAlias loads the ontology with the alias.
|
||||
func (o *OWLOntology) LoadAsAlias(s string) ([]rdf.RDFNode, error) {
|
||||
o.alias = s
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: membersSpec,
|
||||
Delegate: &members{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: disjointWithSpec,
|
||||
Delegate: &disjointWith{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: unionOfSpec,
|
||||
Delegate: &unionOf{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: importsSpec,
|
||||
Delegate: &imports{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: ontologySpec,
|
||||
Delegate: &ontology{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: classSpec,
|
||||
Delegate: &class{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: objectPropertySpec,
|
||||
Delegate: &objectProperty{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: owlSpec,
|
||||
Alias: s,
|
||||
Name: functionalPropertySpec,
|
||||
Delegate: &functionalProperty{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadSpecificAsAlias loads a specific ontology definition as an alias.
|
||||
func (o *OWLOntology) LoadSpecificAsAlias(alias, name string) ([]rdf.RDFNode, error) {
|
||||
switch name {
|
||||
case membersSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &members{},
|
||||
},
|
||||
}, nil
|
||||
case disjointWithSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &disjointWith{},
|
||||
},
|
||||
}, nil
|
||||
case unionOfSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &unionOf{},
|
||||
},
|
||||
}, nil
|
||||
case importsSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &imports{},
|
||||
},
|
||||
}, nil
|
||||
case ontologySpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &ontology{},
|
||||
},
|
||||
}, nil
|
||||
case classSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &class{},
|
||||
},
|
||||
}, nil
|
||||
case objectPropertySpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &objectProperty{},
|
||||
},
|
||||
}, nil
|
||||
case functionalPropertySpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &functionalProperty{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("owl ontology cannot find %q to alias to %q", name, alias)
|
||||
}
|
||||
|
||||
// LoadElement allows loading nodes to enable contexts containing a container
|
||||
// with an index.
|
||||
func (o *OWLOntology) LoadElement(name string, payload map[string]interface{}) ([]rdf.RDFNode, error) {
|
||||
// First, detect if an idValue exists
|
||||
var idValue interface{}
|
||||
var ok bool
|
||||
idValue, ok = payload[rdf.IdSpec]
|
||||
if !ok {
|
||||
idValue, ok = payload[rdf.IdActivityStreamsSpec]
|
||||
}
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
vStr, ok := idValue.(string)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
// Now that we have a string idValue, handle the import use case
|
||||
if !rdf.IsKeyApplicable(vStr, owlSpec, o.alias, importsSpec) {
|
||||
return nil, nil
|
||||
}
|
||||
node := &rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: name,
|
||||
// Need to set Delegate, based on below logic
|
||||
}
|
||||
for k, v := range payload {
|
||||
if k == rdf.IdSpec || k == rdf.IdActivityStreamsSpec {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case rdf.ContainerSpec:
|
||||
container := &rdf.ContainerLD{}
|
||||
node.Delegate = container
|
||||
// Ugly, maybe move out to its own function when needed
|
||||
if cValStr, ok := v.(string); !ok {
|
||||
return nil, fmt.Errorf("unhandled owl import @container to non-string type: %T", v)
|
||||
} else {
|
||||
switch cValStr {
|
||||
case rdf.IndexSpec:
|
||||
container.ContainsNode = &rdf.IndexLD{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled owl import @container to string type %s", cValStr)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled owl import use case: %s", k)
|
||||
}
|
||||
}
|
||||
return []rdf.RDFNode{node}, nil
|
||||
}
|
||||
|
||||
// GetByName returns a bare node.
|
||||
func (o *OWLOntology) GetByName(name string) (rdf.RDFNode, error) {
|
||||
name = strings.TrimPrefix(name, o.SpecURI())
|
||||
switch name {
|
||||
case membersSpec:
|
||||
return &members{}, nil
|
||||
case disjointWithSpec:
|
||||
return &disjointWith{}, nil
|
||||
case unionOfSpec:
|
||||
return &unionOf{}, nil
|
||||
case importsSpec:
|
||||
return &imports{}, nil
|
||||
case ontologySpec:
|
||||
return &ontology{}, nil
|
||||
case classSpec:
|
||||
return &class{}, nil
|
||||
case objectPropertySpec:
|
||||
return &objectProperty{}, nil
|
||||
case functionalPropertySpec:
|
||||
return &functionalProperty{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("owl ontology could not find node for name %s", name)
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &members{}
|
||||
|
||||
// members represents owl:members.
|
||||
type members struct {
|
||||
pushed bool
|
||||
}
|
||||
|
||||
// Enter does nothing but returns an error if the context is not reset.
|
||||
func (m *members) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
if !ctx.IsReset() {
|
||||
ctx.Push()
|
||||
m.pushed = true
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit adds a Vocabulary Type, Property or Value to Result.
|
||||
func (m *members) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
// Finish adding the Current item to the resulting vocabulary
|
||||
if ctx.Current == nil {
|
||||
return true, fmt.Errorf("owl members exiting with nil Current")
|
||||
}
|
||||
switch v := ctx.Current.(type) {
|
||||
case *rdf.VocabularyType:
|
||||
if err := ctx.Result.Vocab.SetType(ctx.Name, v); err != nil {
|
||||
return true, err
|
||||
}
|
||||
case *rdf.VocabularyProperty:
|
||||
if err := ctx.Result.Vocab.SetProperty(ctx.Name, v); err != nil {
|
||||
return true, err
|
||||
}
|
||||
case *rdf.VocabularyValue:
|
||||
if err := ctx.Result.Vocab.SetValue(ctx.Name, v); err != nil {
|
||||
return true, err
|
||||
}
|
||||
default:
|
||||
return true, fmt.Errorf("owl members exiting with unhandled type: %T", ctx.Current)
|
||||
}
|
||||
if m.pushed {
|
||||
ctx.Pop()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (m *members) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl members cannot be applied")
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &disjointWith{}
|
||||
|
||||
// disjointWith represents owl:disjointWith.
|
||||
type disjointWith struct{}
|
||||
|
||||
// Enter ensures the Current is a Type, then pushes a Reference.
|
||||
func (d *disjointWith) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
// Push the Current type aside, to build a Reference.
|
||||
if ctx.Current == nil {
|
||||
return true, fmt.Errorf("owl disjointWith enter given a nil Current")
|
||||
} else if _, ok := ctx.Current.(*rdf.VocabularyType); !ok {
|
||||
return true, fmt.Errorf("owl disjointWith enter not given a *rdf.VocabularyType")
|
||||
}
|
||||
ctx.Push()
|
||||
ctx.Current = &rdf.VocabularyReference{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit pops the Reference and adds it to the Type's DisjointWith.
|
||||
func (d *disjointWith) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
// Pop the Reference, put into the type.
|
||||
ref, ok := ctx.Current.(*rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl disjointWith exit not given a *rdf.VocabularyReference")
|
||||
}
|
||||
ctx.Pop()
|
||||
vType, ok := ctx.Current.(*rdf.VocabularyType)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl disjointWith exit not given a *rdf.VocabularyType")
|
||||
}
|
||||
vType.DisjointWith = append(vType.DisjointWith, *ref)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (d *disjointWith) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl disjointWith cannot be applied")
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &unionOf{}
|
||||
|
||||
// unionOf represents owl:unionOf.
|
||||
type unionOf struct {
|
||||
entered bool
|
||||
}
|
||||
|
||||
// Enter pushes a single Reference.
|
||||
func (u *unionOf) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
u.entered = true
|
||||
ctx.Push()
|
||||
ctx.Current = &rdf.VocabularyReference{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit pops a Reference and appends it to Current, which is a slice of
|
||||
// References.
|
||||
func (u *unionOf) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
u.entered = false
|
||||
if ctx.Current == nil {
|
||||
return true, fmt.Errorf("owl unionOf exit given nil Current")
|
||||
}
|
||||
i := ctx.Current
|
||||
ctx.Pop()
|
||||
ref, ok := i.(*rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl unionOf exit not given *rdf.VocabularyReference")
|
||||
}
|
||||
arr, ok := ctx.Current.([]rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl unionOf exit's previous Current not given []rdf.VocabularyReference")
|
||||
}
|
||||
ctx.Current = append(arr, *ref)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply will either apply a value onto a current Reference (if it was entered
|
||||
// due to being a JSON array), or will append a new reference to Current (which
|
||||
// is a slice of references).
|
||||
func (u *unionOf) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl unionOf apply given non-string value")
|
||||
}
|
||||
strs := rdf.SplitAlias(s)
|
||||
ref := &rdf.VocabularyReference{}
|
||||
if len(strs) == 1 {
|
||||
ref.Name = strs[0]
|
||||
} else if len(strs) == 2 {
|
||||
ref.Name = strs[1]
|
||||
ref.Vocab = strs[0]
|
||||
} else {
|
||||
return true, fmt.Errorf("owl unionOf apply bad SplitAlias")
|
||||
}
|
||||
if u.entered {
|
||||
in, ok := ctx.Current.(*rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl unionOf apply's Current not given *rdf.VocabularyReference: %T", ctx.Current)
|
||||
}
|
||||
in.Name = ref.Name
|
||||
in.Vocab = ref.Vocab
|
||||
} else {
|
||||
arr, ok := ctx.Current.([]rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl unionOf apply's Current not given []rdf.VocabularyReference: %T", ctx.Current)
|
||||
}
|
||||
ctx.Current = append(arr, *ref)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &imports{}
|
||||
|
||||
// imports does nothing but returns errors. It should instead be handled by
|
||||
// special cases in an Ontology's LoadElement.
|
||||
//
|
||||
// Overall, this is a pain to implement. If these errors are seen, then I am
|
||||
// about to have a really not-fun day.
|
||||
type imports struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (i *imports) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl imports cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (i *imports) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl imports cannot be entered")
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (i *imports) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl imports cannot be entered")
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &ontology{}
|
||||
|
||||
// ontology does nothing.
|
||||
type ontology struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (o *ontology) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl ontology cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (o *ontology) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl ontology cannot be exited")
|
||||
}
|
||||
|
||||
// Apply does nothing.
|
||||
func (o *ontology) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Current = &ctx.Result.Vocab
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &class{}
|
||||
|
||||
// class prepares a new Type on Current, unless Reference has already been
|
||||
// prepared.
|
||||
type class struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (c *class) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl class cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (c *class) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl class cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets a Type on Current, unless a Reference is already set.
|
||||
func (c *class) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
// Prepare a new VocabularyType in the context, unless it is a
|
||||
// reference already prepared.
|
||||
if ctx.IsReset() {
|
||||
ctx.Current = &rdf.VocabularyType{}
|
||||
} else if _, ok := ctx.Current.(*rdf.VocabularyReference); ok {
|
||||
return true, nil
|
||||
} else if _, ok := ctx.Current.([]rdf.VocabularyReference); ok {
|
||||
return true, nil
|
||||
} else {
|
||||
return true, fmt.Errorf("owl class applied with non-reset ctx and not a vocab reference: %T", ctx.Current)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &objectProperty{}
|
||||
|
||||
// objectProperty is owl:objectProperty
|
||||
type objectProperty struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (o *objectProperty) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl objectProperty cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (o *objectProperty) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl objectProperty cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets Current to be a Property, unless it is already a Property.
|
||||
func (o *objectProperty) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
// Prepare a new VocabularyProperty in the context. If one already
|
||||
// exists, skip.
|
||||
if _, ok := ctx.Current.(*rdf.VocabularyProperty); ok {
|
||||
return true, nil
|
||||
} else if !ctx.IsReset() {
|
||||
return true, fmt.Errorf("owl objectProperty applied with non-reset ParsingContext")
|
||||
}
|
||||
ctx.Current = &rdf.VocabularyProperty{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &functionalProperty{}
|
||||
|
||||
// functionalProperty represents owl:functionalProperty
|
||||
type functionalProperty struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (f *functionalProperty) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl functionalProperty cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (f *functionalProperty) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("owl functionalProperty cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the Current Property's Functional to true.
|
||||
//
|
||||
// Returns an error if Current is not a Property.
|
||||
func (f *functionalProperty) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
if ctx.Current == nil {
|
||||
return true, fmt.Errorf("owl functionalProperty given nil Current in context")
|
||||
}
|
||||
prop, ok := ctx.Current.(*rdf.VocabularyProperty)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("owl functionalProperty given Current that is not *rdf.VocabularyProperty")
|
||||
}
|
||||
prop.Functional = true
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,619 @@
|
|||
package rdf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
JSON_LD_CONTEXT = "@context"
|
||||
JSON_LD_TYPE = "@type"
|
||||
JSON_LD_TYPE_AS = "type"
|
||||
)
|
||||
|
||||
// JSONLD is an alias for the generic map of keys to interfaces, presumably
|
||||
// parsed from a JSON-encoded context definition file.
|
||||
type JSONLD map[string]interface{}
|
||||
|
||||
// ParsingContext contains the results of the parsing as well as scratch space
|
||||
// required for RDFNodes to be able to statefully apply changes.
|
||||
type ParsingContext struct {
|
||||
// Result contains the final ParsedVocabulary from a file.
|
||||
Result *ParsedVocabulary
|
||||
// Current item to operate upon. A call to Push or Pop will overwrite
|
||||
// this field.
|
||||
Current interface{}
|
||||
// Name of the Current item. A call to Push or Pop will modify this
|
||||
// field.
|
||||
Name string
|
||||
// The Stack of Types, Properties, References, Examples, and other
|
||||
// items being analyzed. A call to Push or Pop will modify this field.
|
||||
//
|
||||
// Do not use directly, instead use Push and Pop.
|
||||
Stack []interface{}
|
||||
// Applies the node only for the next level of processing.
|
||||
//
|
||||
// Do not touch, instead use the accessor methods.
|
||||
OnlyApplyThisNodeNextLevel RDFNode
|
||||
// OnlyApplied keeps track if OnlyApplyThisNodeNextLevel has applied
|
||||
// once.
|
||||
OnlyApplied bool
|
||||
// Applies the node once, for the rest of the data. This skips the
|
||||
// recursive parsing, and the node's Apply is given an empty string
|
||||
// for a key.
|
||||
//
|
||||
// Do not touch, instead use the accessor methods.
|
||||
OnlyApplyThisNode RDFNode
|
||||
}
|
||||
|
||||
// GetResultReferenceWithDefaults will fetch the spec and set the Vocabulary
|
||||
// Name and URI values as well. Helper function when getting a reference in
|
||||
// order to populate known value types.
|
||||
func (p *ParsingContext) GetResultReferenceWithDefaults(spec, name string) (*Vocabulary, error) {
|
||||
v, err := p.Result.GetReference(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Name = name
|
||||
v.URI = u
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// SetOnlyApplyThisNode sets the provided node to be the only one applied until
|
||||
// ResetOnlyApplyThisNode is called.
|
||||
func (p *ParsingContext) SetOnlyApplyThisNode(n RDFNode) {
|
||||
p.OnlyApplyThisNode = n
|
||||
}
|
||||
|
||||
// ResetOnlyApplyThisNode clears the only node to apply, if set.
|
||||
func (p *ParsingContext) ResetOnlyApplyThisNode() {
|
||||
p.OnlyApplyThisNode = nil
|
||||
}
|
||||
|
||||
// SetOnlyApplyThisNodeNExtLevel will apply the next node only for the next
|
||||
// level.
|
||||
func (p *ParsingContext) SetOnlyApplyThisNodeNextLevel(n RDFNode) {
|
||||
p.OnlyApplyThisNodeNextLevel = n
|
||||
p.OnlyApplied = false
|
||||
}
|
||||
|
||||
// GetNextNodes is given the list of nodes a parent process believes should be
|
||||
// applied, and returns the list of nodes that actually should be used.
|
||||
//
|
||||
// If there is node that should only apply or should only apply at the next
|
||||
// level (and hasn't yet), then the passed in list will not match the resulting
|
||||
// list.
|
||||
func (p *ParsingContext) GetNextNodes(n []RDFNode) (r []RDFNode, clearFn func()) {
|
||||
if p.OnlyApplyThisNodeNextLevel == nil {
|
||||
return n, func() {}
|
||||
} else if p.OnlyApplied {
|
||||
return n, func() {}
|
||||
} else {
|
||||
p.OnlyApplied = true
|
||||
return []RDFNode{p.OnlyApplyThisNodeNextLevel}, func() {
|
||||
p.OnlyApplied = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResetOnlyAppliedThisNodeNextLevel clears the node that should have been
|
||||
// applied for the next level of depth only.
|
||||
func (p *ParsingContext) ResetOnlyAppliedThisNodeNextLevel() {
|
||||
p.OnlyApplyThisNodeNextLevel = nil
|
||||
p.OnlyApplied = false
|
||||
}
|
||||
|
||||
// Push puts the Current onto the Stack.
|
||||
func (p *ParsingContext) Push() {
|
||||
p.Stack = append([]interface{}{p.Current}, p.Stack...)
|
||||
p.Reset()
|
||||
}
|
||||
|
||||
// Pop puts the top item on the Stack into Current, and sets Name as
|
||||
// appropriate.
|
||||
func (p *ParsingContext) Pop() {
|
||||
p.Current = p.Stack[0]
|
||||
p.Stack = p.Stack[1:]
|
||||
if ng, ok := p.Current.(NameGetter); ok {
|
||||
p.Name = ng.GetName()
|
||||
}
|
||||
}
|
||||
|
||||
// IsReset determines if the Context's Current is nil and Name is empty. Note
|
||||
func (p *ParsingContext) IsReset() bool {
|
||||
return p.Current == nil &&
|
||||
p.Name == ""
|
||||
}
|
||||
|
||||
// Reset sets Current to nil and Name to empty string.
|
||||
func (p *ParsingContext) Reset() {
|
||||
p.Current = nil
|
||||
p.Name = ""
|
||||
}
|
||||
|
||||
// NameSetter is a utility interface for the rdf Vocabulary types.
|
||||
type NameSetter interface {
|
||||
SetName(string)
|
||||
}
|
||||
|
||||
// NameGetter is a utility interface for the rdf Vocabulary types.
|
||||
type NameGetter interface {
|
||||
GetName() string
|
||||
}
|
||||
|
||||
// URISetter is a utility interface for the rdf Vocabulary types.
|
||||
type URISetter interface {
|
||||
SetURI(string) error
|
||||
}
|
||||
|
||||
// NotesSetter is a utility interface for the rdf Vocabulary types.
|
||||
type NotesSetter interface {
|
||||
SetNotes(string)
|
||||
}
|
||||
|
||||
// ExampleAdder is a utility interface for the rdf Vocabulary types.
|
||||
type ExampleAdder interface {
|
||||
AddExample(*VocabularyExample)
|
||||
}
|
||||
|
||||
// RDFNode is able to operate on a specific key if it applies towards its
|
||||
// ontology (determined at creation time). It applies the value in its own
|
||||
// specific implementation on the context.
|
||||
type RDFNode interface {
|
||||
// Enter is called when the RDFNode is a label for an array of values or
|
||||
// a key within a JSON object, and the parser is about to examine its
|
||||
// value(s). Exit is guaranteed to be called afterwards.
|
||||
Enter(key string, ctx *ParsingContext) (bool, error)
|
||||
// Exit is called after the parser examines the node's value(s).
|
||||
Exit(key string, ctx *ParsingContext) (bool, error)
|
||||
// Apply is called by the parser on nodes when they appear as values.
|
||||
Apply(key string, value interface{}, ctx *ParsingContext) (bool, error)
|
||||
}
|
||||
|
||||
// ParseVocabularies parses the provided inputs in order as an ActivityStreams
|
||||
// context that specifies one or more extension vocabularies.
|
||||
func ParseVocabularies(registry *RDFRegistry, inputs []JSONLD) (vocabulary *ParsedVocabulary, err error) {
|
||||
vocabulary = &ParsedVocabulary{
|
||||
References: make(map[string]*Vocabulary, len(inputs)-1),
|
||||
}
|
||||
currentRegistry := registry.clone()
|
||||
for i, input := range inputs {
|
||||
var v *ParsedVocabulary
|
||||
v, err = parseVocabulary(currentRegistry, input, vocabulary.References)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for k, ref := range v.References {
|
||||
if ref.Registry != nil {
|
||||
err = ref.Registry.AddOntology(&ReferenceOntology{v.Vocab})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
vocabulary.References[k] = ref
|
||||
}
|
||||
if i < len(inputs)-1 {
|
||||
currentRegistry = v.Vocab.Registry.clone()
|
||||
err = currentRegistry.AddOntology(&ReferenceOntology{v.Vocab})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
vocabulary.References[v.Vocab.URI.String()] = &v.Vocab
|
||||
} else {
|
||||
vocabulary.Vocab = v.Vocab
|
||||
}
|
||||
vocabulary.Order = append(vocabulary.Order, v.Vocab.URI.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseVocabulary parses the specified input as an ActivityStreams context that
|
||||
// specifies a Core, Extended, or Extension vocabulary.
|
||||
func parseVocabulary(registry *RDFRegistry, input JSONLD, references map[string]*Vocabulary) (vocabulary *ParsedVocabulary, err error) {
|
||||
var nodes []RDFNode
|
||||
nodes, err = parseJSONLDContext(registry, input)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
vocabulary = &ParsedVocabulary{References: make(map[string]*Vocabulary, len(references))}
|
||||
for k, v := range references {
|
||||
vocabulary.References[k] = v
|
||||
}
|
||||
ctx := &ParsingContext{
|
||||
Result: vocabulary,
|
||||
}
|
||||
// Prepend well-known JSON LD parsing nodes. Order matters, so that the
|
||||
// parser can understand things like types so that other nodes do not
|
||||
// hijack processing.
|
||||
nodes = append(jsonLDNodes(registry), nodes...)
|
||||
// Step 1: Parse all core data, excluding:
|
||||
// - Value types
|
||||
// - Referenced types
|
||||
// - VocabularyType's 'Properties' and 'WithoutProperties' fields
|
||||
//
|
||||
// This is all horrible code but it works, so....
|
||||
err = apply(nodes, input, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx.Reset()
|
||||
// Step 2: Populate value and referenced types.
|
||||
err = resolveReferences(registry, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Step 3: Populate VocabularyType's 'Properties' and
|
||||
// 'WithoutProperties' fields
|
||||
err = populatePropertiesOnTypes(registry, ctx)
|
||||
vocabulary.Vocab.Registry = registry
|
||||
return
|
||||
}
|
||||
|
||||
// populatePropertiesOnTypes populates the 'Properties' and 'WithoutProperties'
|
||||
// entries on a VocabularyType.
|
||||
func populatePropertiesOnTypes(registry *RDFRegistry, ctx *ParsingContext) error {
|
||||
for _, p := range ctx.Result.Vocab.Properties {
|
||||
if err := populatePropertyOnTypes(registry, p, ctx.Result.Vocab.URI.String(), ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// populatePropertyOnTypes populates the VocabularyType's 'Properties' and
|
||||
// 'WithoutProperties' fields based on the 'Domain' and 'DoesNotApplyTo'.
|
||||
func populatePropertyOnTypes(registry *RDFRegistry, p VocabularyProperty, vocabName string, ctx *ParsingContext) error {
|
||||
ref := VocabularyReference{
|
||||
Name: p.Name,
|
||||
URI: p.URI,
|
||||
// Vocab will only be populated on types outside of its own
|
||||
// vocabulary.
|
||||
}
|
||||
for _, d := range p.Domain {
|
||||
if len(d.Vocab) == 0 {
|
||||
t, ok := ctx.Result.Vocab.Types[d.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot populate property on type %q for desired vocab", d.Name)
|
||||
}
|
||||
t.Properties = append(t.Properties, ref)
|
||||
ctx.Result.Vocab.Types[d.Name] = t
|
||||
} else {
|
||||
vocab := d.Vocab
|
||||
if u, err := registry.ResolveAlias(d.Vocab); err == nil {
|
||||
vocab = u
|
||||
}
|
||||
v, err := ctx.Result.GetReference(vocab)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t, ok := v.Types[d.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot populate property on type %q for vocab %q", d.Name, vocab)
|
||||
}
|
||||
// Since the type is outside this property's vocabulary,
|
||||
// populate the Vocab field.
|
||||
refCopy := ref
|
||||
refCopy.Vocab = vocabName
|
||||
t.Properties = append(t.Properties, refCopy)
|
||||
v.Types[d.Name] = t
|
||||
}
|
||||
}
|
||||
for _, dna := range p.DoesNotApplyTo {
|
||||
if len(dna.Vocab) == 0 {
|
||||
t, ok := ctx.Result.Vocab.Types[dna.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot populate withoutproperty on type %q for desired vocab", dna.Name)
|
||||
}
|
||||
t.WithoutProperties = append(t.WithoutProperties, ref)
|
||||
ctx.Result.Vocab.Types[dna.Name] = t
|
||||
} else {
|
||||
vocab := dna.Vocab
|
||||
if u, err := registry.ResolveAlias(dna.Vocab); err == nil {
|
||||
vocab = u
|
||||
}
|
||||
v, err := ctx.Result.GetReference(vocab)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t, ok := v.Types[dna.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot populate withoutproperty on type %q for vocab %q", dna.Name, vocab)
|
||||
}
|
||||
// Since the type is outside this property's vocabulary,
|
||||
// populate the Vocab field.
|
||||
refCopy := ref
|
||||
refCopy.Vocab = vocabName
|
||||
t.WithoutProperties = append(t.WithoutProperties, refCopy)
|
||||
v.Types[dna.Name] = t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveReferences ensures that all references mentioned have been
|
||||
// successfully parsed, and if not attempts to search the ontologies for any
|
||||
// values, types, and properties that need to be referenced.
|
||||
//
|
||||
// Currently, this is the only way that values are added to the
|
||||
// ParsedVocabulary.
|
||||
func resolveReferences(registry *RDFRegistry, ctx *ParsingContext) error {
|
||||
vocabulary := ctx.Result
|
||||
for _, t := range vocabulary.Vocab.Types {
|
||||
for _, ref := range t.DisjointWith {
|
||||
if err := resolveReference(ref, registry, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ref := range t.Extends {
|
||||
if err := resolveReference(ref, registry, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range vocabulary.Vocab.Properties {
|
||||
for _, ref := range p.Domain {
|
||||
if err := resolveReference(ref, registry, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ref := range p.Range {
|
||||
if err := resolveReference(ref, registry, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, ref := range p.DoesNotApplyTo {
|
||||
if err := resolveReference(ref, registry, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(p.SubpropertyOf.Name) > 0 {
|
||||
if err := resolveReference(p.SubpropertyOf, registry, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveReference will attempt to resolve the reference by either finding it
|
||||
// in the known References of the vocabulary, or load it from the registry. Will
|
||||
// fail if a reference is not found.
|
||||
func resolveReference(reference VocabularyReference, registry *RDFRegistry, ctx *ParsingContext) error {
|
||||
name := reference.Name
|
||||
vocab := &ctx.Result.Vocab
|
||||
if len(reference.Vocab) > 0 {
|
||||
name = joinAlias(reference.Vocab, reference.Name)
|
||||
url, e := registry.ResolveAlias(reference.Vocab)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
vocab, e = ctx.Result.GetReference(url)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
if _, ok := vocab.Types[reference.Name]; ok {
|
||||
return nil
|
||||
} else if _, ok := vocab.Properties[reference.Name]; ok {
|
||||
return nil
|
||||
} else if _, ok := vocab.Values[reference.Name]; ok {
|
||||
return nil
|
||||
} else if n, e := registry.getNode(name); e != nil {
|
||||
return e
|
||||
} else {
|
||||
applicable, e := n.Apply("", nil, ctx)
|
||||
if !applicable {
|
||||
return fmt.Errorf("cannot resolve reference with unapplicable node for %s", reference)
|
||||
} else if e != nil {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// apply takes a specification input to populate the ParsingContext, based on
|
||||
// the capabilities of the RDFNodes created from ontologies.
|
||||
//
|
||||
// This function will populate all non-value data in the Vocabulary. It does not
|
||||
// populate the 'Properties' nor the 'WithoutProperties' fields on any
|
||||
// VocabularyType.
|
||||
func apply(nodes []RDFNode, input JSONLD, ctx *ParsingContext) error {
|
||||
// Hijacked processing: Process the rest of the data in this single
|
||||
// node.
|
||||
if ctx.OnlyApplyThisNode != nil {
|
||||
if applied, err := ctx.OnlyApplyThisNode.Apply("", input, ctx); !applied {
|
||||
return fmt.Errorf("applying requested node failed")
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Special processing: '@type' or 'type' if they are present
|
||||
if v, ok := input[JSON_LD_TYPE]; ok {
|
||||
if err := doApply(nodes, JSON_LD_TYPE, v, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if v, ok := input[JSON_LD_TYPE_AS]; ok {
|
||||
if err := doApply(nodes, JSON_LD_TYPE_AS, v, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Normal recursive processing
|
||||
for k, v := range input {
|
||||
// Skip things we have already processed: context and type
|
||||
if k == JSON_LD_CONTEXT {
|
||||
continue
|
||||
} else if k == JSON_LD_TYPE {
|
||||
continue
|
||||
} else if k == JSON_LD_TYPE_AS {
|
||||
continue
|
||||
}
|
||||
if err := doApply(nodes, k, v, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// doApply actually does the application logic for the apply function.
|
||||
func doApply(nodes []RDFNode,
|
||||
k string, v interface{},
|
||||
ctx *ParsingContext) error {
|
||||
// Hijacked processing: Only use the ParsingContext's node to
|
||||
// handle all elements.
|
||||
recurNodes := nodes
|
||||
enterApplyExitNodes, clearFn := ctx.GetNextNodes(nodes)
|
||||
defer clearFn()
|
||||
// Normal recursive processing
|
||||
if mapValue, ok := v.(map[string]interface{}); ok {
|
||||
if err := enterFirstNode(enterApplyExitNodes, k, ctx); err != nil {
|
||||
return err
|
||||
} else if err = apply(recurNodes, mapValue, ctx); err != nil {
|
||||
return err
|
||||
} else if err = exitFirstNode(enterApplyExitNodes, k, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if arrValue, ok := v.([]interface{}); ok {
|
||||
for _, val := range arrValue {
|
||||
// First, enter for this key
|
||||
if err := enterFirstNode(enterApplyExitNodes, k, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Recur or handle the value as necessary.
|
||||
if mapValue, ok := val.(map[string]interface{}); ok {
|
||||
if err := apply(recurNodes, mapValue, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := applyFirstNode(enterApplyExitNodes, k, val, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Finally, exit for this key
|
||||
if err := exitFirstNode(enterApplyExitNodes, k, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if err := applyFirstNode(enterApplyExitNodes, k, v, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// enterFirstNode will Enter the first RDFNode that returns true or an error.
|
||||
func enterFirstNode(nodes []RDFNode, key string, ctx *ParsingContext) error {
|
||||
for _, node := range nodes {
|
||||
if applied, err := node.Enter(key, ctx); applied {
|
||||
return err
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("no RDFNode applicable for entering %q", key)
|
||||
}
|
||||
|
||||
// exitFirstNode will Exit the first RDFNode that returns true or an error.
|
||||
func exitFirstNode(nodes []RDFNode, key string, ctx *ParsingContext) error {
|
||||
for _, node := range nodes {
|
||||
if applied, err := node.Exit(key, ctx); applied {
|
||||
return err
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("no RDFNode applicable for exiting %q", key)
|
||||
}
|
||||
|
||||
// applyFirstNode will Apply the first RDFNode that returns true or an error.
|
||||
func applyFirstNode(nodes []RDFNode, key string, value interface{}, ctx *ParsingContext) error {
|
||||
for _, node := range nodes {
|
||||
if applied, err := node.Apply(key, value, ctx); applied {
|
||||
return err
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("no RDFNode applicable for applying %q with value %v", key, value)
|
||||
}
|
||||
|
||||
// parseJSONLDContext implements a super basic JSON-LD @context parsing
|
||||
// algorithm in order to build a set of nodes which will be able to parse the
|
||||
// rest of the document.
|
||||
func parseJSONLDContext(registry *RDFRegistry, input JSONLD) (nodes []RDFNode, err error) {
|
||||
i, ok := input[JSON_LD_CONTEXT]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no @context in input")
|
||||
return
|
||||
}
|
||||
if inArray, ok := i.([]interface{}); ok {
|
||||
// @context is an array
|
||||
for _, iVal := range inArray {
|
||||
if valMap, ok := iVal.(map[string]interface{}); ok {
|
||||
// Element is a JSON Object (dictionary)
|
||||
for alias, val := range valMap {
|
||||
if s, ok := val.(string); ok {
|
||||
var n []RDFNode
|
||||
n, err = registry.getAliased(alias, s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nodes = append(nodes, n...)
|
||||
} else if aliasedMap, ok := val.(map[string]interface{}); ok {
|
||||
var n []RDFNode
|
||||
n, err = registry.getAliasedObject(alias, aliasedMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nodes = append(nodes, n...)
|
||||
} else {
|
||||
err = fmt.Errorf("@context value in dict in array is neither a dict nor a string")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if s, ok := iVal.(string); ok {
|
||||
// Element is a single value
|
||||
var n []RDFNode
|
||||
n, err = registry.getFor(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nodes = append(nodes, n...)
|
||||
} else {
|
||||
err = fmt.Errorf("@context value in array is neither a dict nor a string")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if inMap, ok := i.(map[string]interface{}); ok {
|
||||
// @context is a JSON object (dictionary)
|
||||
for alias, iVal := range inMap {
|
||||
if s, ok := iVal.(string); ok {
|
||||
var n []RDFNode
|
||||
n, err = registry.getAliased(alias, s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nodes = append(nodes, n...)
|
||||
} else if aliasedMap, ok := iVal.(map[string]interface{}); ok {
|
||||
var n []RDFNode
|
||||
n, err = registry.getAliasedObject(alias, aliasedMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nodes = append(nodes, n...)
|
||||
} else {
|
||||
err = fmt.Errorf("@context value in dict is neither a dict nor a string")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// @context is a single value
|
||||
s, ok := i.(string)
|
||||
if !ok {
|
||||
err = fmt.Errorf("single @context value is not a string")
|
||||
return
|
||||
}
|
||||
return registry.getFor(s)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,325 @@
|
|||
package rdf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ALIAS_DELIMITER = ":"
|
||||
HTTP = "http"
|
||||
HTTPS = "https"
|
||||
ID = "@id"
|
||||
)
|
||||
|
||||
// IsKeyApplicable returns true if the key has a spec or alias prefix and the
|
||||
// property is equal to the desired name.
|
||||
//
|
||||
// If 'alias' is an empty string, it is ignored.
|
||||
func IsKeyApplicable(key, spec, alias, name string) bool {
|
||||
if key == spec+name {
|
||||
return true
|
||||
} else if len(alias) > 0 {
|
||||
strs := strings.Split(key, ALIAS_DELIMITER)
|
||||
if len(strs) > 1 && strs[0] != HTTP && strs[0] != HTTPS {
|
||||
return strs[0] == alias && strs[1] == name
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SplitAlias splits a possibly-aliased string, without splitting on the colon
|
||||
// if it is part of the http or https spec.
|
||||
func SplitAlias(s string) []string {
|
||||
strs := strings.Split(s, ALIAS_DELIMITER)
|
||||
if len(strs) == 1 {
|
||||
return strs
|
||||
} else if strs[0] == HTTP || strs[0] == HTTPS {
|
||||
return []string{s}
|
||||
} else {
|
||||
return strs
|
||||
}
|
||||
}
|
||||
|
||||
// ToHttpAndHttps converts a URI to both its http and https versions.
|
||||
func ToHttpAndHttps(s string) (http, https string, err error) {
|
||||
// Trailing fragments are not preserved by url.Parse, so we
|
||||
// need to do proper bookkeeping and preserve it if present.
|
||||
hasFragment := s[len(s)-1] == '#'
|
||||
var specUri *url.URL
|
||||
specUri, err = url.Parse(s)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// HTTP
|
||||
httpScheme := *specUri
|
||||
httpScheme.Scheme = HTTP
|
||||
http = httpScheme.String()
|
||||
// HTTPS
|
||||
httpsScheme := *specUri
|
||||
httpsScheme.Scheme = HTTPS
|
||||
https = httpsScheme.String()
|
||||
if hasFragment {
|
||||
http += "#"
|
||||
https += "#"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// joinAlias combines a string and prepends an RDF alias to it.
|
||||
func joinAlias(alias, s string) string {
|
||||
return fmt.Sprintf("%s%s%s", alias, ALIAS_DELIMITER, s)
|
||||
}
|
||||
|
||||
// Ontology returns different RDF "actions" or "handlers" that are able to
|
||||
// interpret the schema definitions as actions upon a set of data, specific
|
||||
// for this ontology.
|
||||
type Ontology interface {
|
||||
// SpecURI refers to the URI location of this ontology.
|
||||
SpecURI() string
|
||||
|
||||
// The Load methods deal with determining how best to apply an ontology
|
||||
// based on the context specified by the data. This is before the data
|
||||
// is actually processed.
|
||||
|
||||
// Load loads the entire ontology.
|
||||
Load() ([]RDFNode, error)
|
||||
// LoadAsAlias loads the entire ontology with a specific alias.
|
||||
LoadAsAlias(s string) ([]RDFNode, error)
|
||||
// LoadSpecificAsAlias loads a specific element of the ontology by
|
||||
// being able to handle the specific alias as its name instead.
|
||||
LoadSpecificAsAlias(alias, name string) ([]RDFNode, error)
|
||||
// LoadElement loads a specific element of the ontology based on the
|
||||
// object definition.
|
||||
LoadElement(name string, payload map[string]interface{}) ([]RDFNode, error)
|
||||
|
||||
// The Get methods deal with determining how best to apply an ontology
|
||||
// during processing. This is a result of certain nodes having highly
|
||||
// contextual effects.
|
||||
|
||||
// GetByName returns an RDFNode associated with the given name. Note
|
||||
// that the name may either be fully-qualified (in the case it was not
|
||||
// aliased) or it may be just the element name (in the case it was
|
||||
// aliased).
|
||||
GetByName(name string) (RDFNode, error)
|
||||
}
|
||||
|
||||
// aliasedNode represents a context element that has a special reserved alias.
|
||||
type aliasedNode struct {
|
||||
Alias string
|
||||
Nodes []RDFNode
|
||||
}
|
||||
|
||||
// RDFRegistry manages the different ontologies needed to determine the
|
||||
// generated Go code.
|
||||
type RDFRegistry struct {
|
||||
ontologies map[string]Ontology
|
||||
aliases map[string]string
|
||||
aliasedNodes map[string]aliasedNode
|
||||
}
|
||||
|
||||
// NewRDFRegistry returns a new RDFRegistry.
|
||||
func NewRDFRegistry() *RDFRegistry {
|
||||
return &RDFRegistry{
|
||||
ontologies: make(map[string]Ontology),
|
||||
aliases: make(map[string]string),
|
||||
aliasedNodes: make(map[string]aliasedNode),
|
||||
}
|
||||
}
|
||||
|
||||
// clone creates a new RDFRegistry keeping only the ontologies.
|
||||
func (r *RDFRegistry) clone() *RDFRegistry {
|
||||
c := NewRDFRegistry()
|
||||
for k, v := range r.ontologies {
|
||||
c.ontologies[k] = v
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// setAlias sets an alias for a string.
|
||||
func (r *RDFRegistry) setAlias(alias, s string) error {
|
||||
if _, ok := r.aliases[alias]; ok {
|
||||
return fmt.Errorf("already have alias for %s", alias)
|
||||
}
|
||||
r.aliases[alias] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
// setAliasedNode sets an alias for a node.
|
||||
func (r *RDFRegistry) setAliasedNode(alias string, nodes []RDFNode) error {
|
||||
if _, ok := r.aliasedNodes[alias]; ok {
|
||||
return fmt.Errorf("already have aliased node for %s", alias)
|
||||
}
|
||||
r.aliasedNodes[alias] = aliasedNode{
|
||||
Alias: alias,
|
||||
Nodes: nodes,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getOngology resolves an alias to a particular Ontology.
|
||||
func (r *RDFRegistry) getOntology(alias string) (Ontology, error) {
|
||||
if ontologyName, ok := r.aliases[alias]; !ok {
|
||||
return nil, fmt.Errorf("missing alias %q", alias)
|
||||
} else if ontology, ok := r.ontologies[ontologyName]; !ok {
|
||||
return nil, fmt.Errorf("alias %q resolved but missing ontology with name %q", alias, ontologyName)
|
||||
} else {
|
||||
return ontology, nil
|
||||
}
|
||||
}
|
||||
|
||||
// AddOntology adds an RDF ontology to the registry.
|
||||
func (r *RDFRegistry) AddOntology(o Ontology) error {
|
||||
if r.ontologies == nil {
|
||||
r.ontologies = make(map[string]Ontology, 1)
|
||||
}
|
||||
specString := o.SpecURI()
|
||||
httpSpec, httpsSpec, err := ToHttpAndHttps(specString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := r.ontologies[httpSpec]; ok {
|
||||
return fmt.Errorf("ontology already registered for %q", httpSpec)
|
||||
}
|
||||
if _, ok := r.ontologies[httpsSpec]; ok {
|
||||
return fmt.Errorf("ontology already registered for %q", httpsSpec)
|
||||
}
|
||||
r.ontologies[httpSpec] = o
|
||||
r.ontologies[httpsSpec] = o
|
||||
return nil
|
||||
}
|
||||
|
||||
// reset clears the registry in preparation for loading another JSONLD context.
|
||||
func (r *RDFRegistry) reset() {
|
||||
r.aliases = make(map[string]string)
|
||||
r.aliasedNodes = make(map[string]aliasedNode)
|
||||
}
|
||||
|
||||
// getFor gets RDFKeyers based on a context's string.
|
||||
//
|
||||
// Package public.
|
||||
func (r *RDFRegistry) getFor(s string) (n []RDFNode, e error) {
|
||||
ontology, ok := r.ontologies[s]
|
||||
if !ok {
|
||||
e = fmt.Errorf("no ontology for %s", s)
|
||||
return
|
||||
}
|
||||
return ontology.Load()
|
||||
}
|
||||
|
||||
// getForAliased gets RDFKeyers based on a context's string.
|
||||
//
|
||||
// Private to this file.
|
||||
func (r *RDFRegistry) getForAliased(alias, s string) (n []RDFNode, e error) {
|
||||
ontology, ok := r.ontologies[s]
|
||||
if !ok {
|
||||
e = fmt.Errorf("no ontology for %s", s)
|
||||
return
|
||||
}
|
||||
return ontology.LoadAsAlias(alias)
|
||||
}
|
||||
|
||||
// getAliased gets RDFKeyers based on a context string and its
|
||||
// alias.
|
||||
//
|
||||
// Package public.
|
||||
func (r *RDFRegistry) getAliased(alias, s string) (n []RDFNode, e error) {
|
||||
strs := SplitAlias(s)
|
||||
if len(strs) == 1 {
|
||||
if e = r.setAlias(alias, s); e != nil {
|
||||
return
|
||||
}
|
||||
return r.getForAliased(alias, s)
|
||||
} else if len(strs) == 2 {
|
||||
var o Ontology
|
||||
o, e = r.getOntology(strs[0])
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
n, e = o.LoadSpecificAsAlias(alias, strs[1])
|
||||
return
|
||||
} else {
|
||||
e = fmt.Errorf("too many delimiters in %s", s)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// getAliasedObject gets RDFKeyers based on a context object and
|
||||
// its alias and definition.
|
||||
//
|
||||
// Package public.
|
||||
func (r *RDFRegistry) getAliasedObject(alias string, object map[string]interface{}) (n []RDFNode, e error) {
|
||||
raw, ok := object[ID]
|
||||
if !ok {
|
||||
e = fmt.Errorf("aliased object does not have %s value", ID)
|
||||
return
|
||||
}
|
||||
if element, ok := raw.(string); !ok {
|
||||
e = fmt.Errorf("element in getAliasedObject must be a string")
|
||||
return
|
||||
} else {
|
||||
strs := SplitAlias(element)
|
||||
if len(strs) == 1 {
|
||||
n, e = r.getFor(strs[0])
|
||||
} else if len(strs) == 2 {
|
||||
var o Ontology
|
||||
o, e = r.getOntology(strs[0])
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
n, e = o.LoadElement(alias, object)
|
||||
}
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
e = r.setAliasedNode(alias, n)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// getNode fetches a node based on a string. It may be aliased or not.
|
||||
//
|
||||
// Package public.
|
||||
func (r *RDFRegistry) getNode(s string) (n RDFNode, e error) {
|
||||
strs := SplitAlias(s)
|
||||
if len(strs) == 2 {
|
||||
if ontName, ok := r.aliases[strs[0]]; !ok {
|
||||
e = fmt.Errorf("no alias to ontology for %s", strs[0])
|
||||
return
|
||||
} else if ontology, ok := r.ontologies[ontName]; !ok {
|
||||
e = fmt.Errorf("no ontology named %s for alias %s", ontName, strs[0])
|
||||
return
|
||||
} else {
|
||||
n, e = ontology.GetByName(strs[1])
|
||||
return
|
||||
}
|
||||
} else if len(strs) == 1 {
|
||||
for _, ontology := range r.ontologies {
|
||||
if strings.HasPrefix(s, ontology.SpecURI()) {
|
||||
n, e = ontology.GetByName(s)
|
||||
return
|
||||
}
|
||||
}
|
||||
e = fmt.Errorf("getNode could not find ontology for %s", s)
|
||||
return
|
||||
} else {
|
||||
e = fmt.Errorf("getNode given unhandled node name: %s", s)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// resolveAlias turns an alias into its full qualifier for the ontology.
|
||||
//
|
||||
// If passed in a valid URI, it returns what was passed in.
|
||||
func (r *RDFRegistry) ResolveAlias(alias string) (url string, e error) {
|
||||
if _, ok := r.ontologies[alias]; ok {
|
||||
url = alias
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
if url, ok = r.aliases[alias]; !ok {
|
||||
e = fmt.Errorf("registry cannot resolve alias %q", alias)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
package rdfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/astool/rdf"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
rdfsSpecURI = "http://www.w3.org/2000/01/rdf-schema#"
|
||||
commentSpec = "comment"
|
||||
domainSpec = "domain"
|
||||
isDefinedBySpec = "isDefinedBy"
|
||||
rangeSpec = "range"
|
||||
subClassOfSpec = "subClassOf"
|
||||
subPropertyOfSpec = "subPropertyOf"
|
||||
)
|
||||
|
||||
// RDFSchemaOntology is the Ontology for rdfs.
|
||||
type RDFSchemaOntology struct{}
|
||||
|
||||
// SpecURI returns the URI for the RDFS spec.
|
||||
func (o *RDFSchemaOntology) SpecURI() string {
|
||||
return rdfsSpecURI
|
||||
}
|
||||
|
||||
// Load this Ontology without an alias.
|
||||
func (o *RDFSchemaOntology) Load() ([]rdf.RDFNode, error) {
|
||||
return o.LoadAsAlias("")
|
||||
}
|
||||
|
||||
// LoadAsAlias loads this ontology with an alias.
|
||||
func (o *RDFSchemaOntology) LoadAsAlias(s string) ([]rdf.RDFNode, error) {
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rdfsSpecURI,
|
||||
Alias: s,
|
||||
Name: commentSpec,
|
||||
Delegate: &comment{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rdfsSpecURI,
|
||||
Alias: s,
|
||||
Name: domainSpec,
|
||||
Delegate: &domain{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rdfsSpecURI,
|
||||
Alias: s,
|
||||
Name: isDefinedBySpec,
|
||||
Delegate: &isDefinedBy{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rdfsSpecURI,
|
||||
Alias: s,
|
||||
Name: rangeSpec,
|
||||
Delegate: &ranges{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rdfsSpecURI,
|
||||
Alias: s,
|
||||
Name: subClassOfSpec,
|
||||
Delegate: &subClassOf{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rdfsSpecURI,
|
||||
Alias: s,
|
||||
Name: subPropertyOfSpec,
|
||||
Delegate: &subPropertyOf{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadSpecificAsAlias loads a specific node as an alias.
|
||||
func (o *RDFSchemaOntology) LoadSpecificAsAlias(alias, name string) ([]rdf.RDFNode, error) {
|
||||
switch name {
|
||||
case commentSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &comment{},
|
||||
},
|
||||
}, nil
|
||||
case domainSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &domain{},
|
||||
},
|
||||
}, nil
|
||||
case isDefinedBySpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &isDefinedBy{},
|
||||
},
|
||||
}, nil
|
||||
case rangeSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &ranges{},
|
||||
},
|
||||
}, nil
|
||||
case subClassOfSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &subClassOf{},
|
||||
},
|
||||
}, nil
|
||||
case subPropertyOfSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &subPropertyOf{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("rdfs ontology cannot find %q to alias to %q", name, alias)
|
||||
}
|
||||
|
||||
// LoadElement does nothing.
|
||||
func (o *RDFSchemaOntology) LoadElement(name string, payload map[string]interface{}) ([]rdf.RDFNode, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetByName returns a bare node by name.
|
||||
func (o *RDFSchemaOntology) GetByName(name string) (rdf.RDFNode, error) {
|
||||
name = strings.TrimPrefix(name, o.SpecURI())
|
||||
switch name {
|
||||
case commentSpec:
|
||||
return &comment{}, nil
|
||||
case domainSpec:
|
||||
return &domain{}, nil
|
||||
case isDefinedBySpec:
|
||||
return &isDefinedBy{}, nil
|
||||
case rangeSpec:
|
||||
return &ranges{}, nil
|
||||
case subClassOfSpec:
|
||||
return &subClassOf{}, nil
|
||||
case subPropertyOfSpec:
|
||||
return &subPropertyOf{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("rdfs ontology could not find node for name %s", name)
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &comment{}
|
||||
|
||||
// comment sets Notes on vocabulary items.
|
||||
type comment struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (n *comment) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs comment cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (n *comment) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs comment cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the string value on Current's Notes.
|
||||
//
|
||||
// Returns an error if value isn't a string or Current can't set Notes.
|
||||
func (n *comment) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
note, ok := value.(string)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdf comment not given string value")
|
||||
}
|
||||
if ctx.Current == nil {
|
||||
return true, fmt.Errorf("rdf comment given nil Current")
|
||||
}
|
||||
noteSetter, ok := ctx.Current.(rdf.NotesSetter)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdf comment not given NotesSetter")
|
||||
}
|
||||
noteSetter.SetNotes(note)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &domain{}
|
||||
|
||||
// domain is rdfs:domain.
|
||||
type domain struct{}
|
||||
|
||||
// Enter Pushes a Reference as Current.
|
||||
func (d *domain) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Push()
|
||||
ctx.Current = make([]rdf.VocabularyReference, 0)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit Pops a slice of References and sets it on the parent Property.
|
||||
//
|
||||
// Returns an error if the popped item is not a slice of References, or if the
|
||||
// Current after Popping is not a Property.
|
||||
func (d *domain) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
i := ctx.Current
|
||||
ctx.Pop()
|
||||
vr, ok := i.([]rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdfs domain exit did not get []rdf.VocabularyReference")
|
||||
}
|
||||
vp, ok := ctx.Current.(*rdf.VocabularyProperty)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdf domain exit Current is not *rdf.VocabularyProperty")
|
||||
}
|
||||
vp.Domain = append(vp.Domain, vr...)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (d *domain) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs domain cannot be applied")
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &isDefinedBy{}
|
||||
|
||||
// isDefinedBy is rdfs:isDefinedBy.
|
||||
type isDefinedBy struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (i *isDefinedBy) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs isDefinedBy cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (i *isDefinedBy) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs isDefinedBy cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the string value as Current's URI.
|
||||
//
|
||||
// Returns an error if value is not a string or if Current cannot have a URI
|
||||
// set.
|
||||
func (i *isDefinedBy) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdfs isDefinedBy given non-string: %T", value)
|
||||
}
|
||||
u, ok := ctx.Current.(rdf.URISetter)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdfs isDefinedBy Current is not rdf.URISetter: %T", ctx.Current)
|
||||
}
|
||||
return true, u.SetURI(s)
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &ranges{}
|
||||
|
||||
// ranges is rdfs:ranges.
|
||||
type ranges struct{}
|
||||
|
||||
// Enter Pushes as a slice of References as Current.
|
||||
func (r *ranges) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Push()
|
||||
ctx.Current = make([]rdf.VocabularyReference, 0)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit Pops a slice of References and sets it on the parent Property.
|
||||
//
|
||||
// Returns an error if the popped item is not a slice of references, or if the
|
||||
// Current item after popping is not a Property.
|
||||
func (r *ranges) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
i := ctx.Current
|
||||
ctx.Pop()
|
||||
vr, ok := i.([]rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdfs ranges exit did not get []rdf.VocabularyReference")
|
||||
}
|
||||
vp, ok := ctx.Current.(*rdf.VocabularyProperty)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdf ranges exit Current is not *rdf.VocabularyProperty")
|
||||
}
|
||||
vp.Range = append(vp.Range, vr...)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (r *ranges) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs ranges cannot be applied")
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &subClassOf{}
|
||||
|
||||
// subClassOf implements rdfs:subClassOf.
|
||||
type subClassOf struct{}
|
||||
|
||||
// Enter Pushes a Reference as Current.
|
||||
func (s *subClassOf) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Push()
|
||||
ctx.Current = &rdf.VocabularyReference{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit Pops a Reference and appends it to the parent Type's Extends.
|
||||
//
|
||||
// Returns an error if the popped item is not a reference, or if the Current
|
||||
// item after popping is not a Type.
|
||||
func (s *subClassOf) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
i := ctx.Current
|
||||
ctx.Pop()
|
||||
vr, ok := i.(*rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdfs subclassof exit did not get *rdf.VocabularyReference")
|
||||
}
|
||||
vt, ok := ctx.Current.(*rdf.VocabularyType)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdf subclassof exit Current is not *rdf.VocabularyType")
|
||||
}
|
||||
vt.Extends = append(vt.Extends, *vr)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (s *subClassOf) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs subclassof cannot be applied")
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &subPropertyOf{}
|
||||
|
||||
// subPropertyOf is rdfs:subPropertyOf
|
||||
type subPropertyOf struct{}
|
||||
|
||||
// Enter Pushes a Reference as Current.
|
||||
func (s *subPropertyOf) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Push()
|
||||
ctx.Current = &rdf.VocabularyReference{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit Pops a Reference and sets it as the parent property's SubpropertyOf.
|
||||
//
|
||||
// Returns an error if the popped item is not a Reference, or if after popping
|
||||
// Current is not a Property.
|
||||
func (s *subPropertyOf) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
i := ctx.Current
|
||||
ctx.Pop()
|
||||
vr, ok := i.(*rdf.VocabularyReference)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdfs subpropertyof exit did not get *rdf.VocabularyReference")
|
||||
}
|
||||
vp, ok := ctx.Current.(*rdf.VocabularyProperty)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("rdf subpropertyof exit Current is not *rdf.VocabularyProperty")
|
||||
}
|
||||
vp.SubpropertyOf = *vr
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (s *subPropertyOf) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rdfs subpropertyof cannot be applied")
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
package rdf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Ontology = &ReferenceOntology{}
|
||||
)
|
||||
|
||||
// ReferenceOntology wraps a previously-parsed spec so it can be made known to
|
||||
// the registry.
|
||||
type ReferenceOntology struct {
|
||||
v Vocabulary
|
||||
}
|
||||
|
||||
// SpecURI returns the URI for this specification
|
||||
func (r *ReferenceOntology) SpecURI() string {
|
||||
return r.v.URI.String()
|
||||
}
|
||||
|
||||
// Load loads the ontology without an alias.
|
||||
func (r *ReferenceOntology) Load() ([]RDFNode, error) {
|
||||
return r.LoadAsAlias("")
|
||||
}
|
||||
|
||||
// LoadAsAlias loads the vocabulary ontology with an alias.
|
||||
//
|
||||
// Values cannot be loaded because their serialization and deserialization types
|
||||
// are not known at runtime if not embedded in the go-fed tool. If the error is
|
||||
// generated when running the tool, then file a bug so that the tool can
|
||||
// properly "know" about this particular value and how to serialize and
|
||||
// deserialize it properly.
|
||||
func (r *ReferenceOntology) LoadAsAlias(s string) ([]RDFNode, error) {
|
||||
var nodes []RDFNode
|
||||
for name, t := range r.v.Types {
|
||||
nodes = append(nodes, &AliasedDelegate{
|
||||
Spec: r.v.URI.String(),
|
||||
Alias: s,
|
||||
Name: name,
|
||||
Delegate: &typeReference{t: t, vocabName: r.SpecURI()},
|
||||
})
|
||||
}
|
||||
for name, p := range r.v.Properties {
|
||||
nodes = append(nodes, &AliasedDelegate{
|
||||
Spec: r.v.URI.String(),
|
||||
Alias: s,
|
||||
Name: name,
|
||||
Delegate: &propertyReference{p: p, vocabName: r.SpecURI()},
|
||||
})
|
||||
}
|
||||
// Note: Values cannot be added this way as there's no way to detect
|
||||
// at runtime what the correct serialization and deserialization scheme
|
||||
// are for particular vocabulary values. Therefore, we omit them here
|
||||
// and will emit an error.
|
||||
//
|
||||
// If this error is emitted, it means a code change to the tool is
|
||||
// required. A new ontology implementation for this vocabulary needs to
|
||||
// be added, and a hardcoded implementation of the value's serialization
|
||||
// and deserialization functions must be created. This will then let the
|
||||
// rest of the generated code properly serialize and deserialize these
|
||||
// values.
|
||||
if len(r.v.Values) > 0 {
|
||||
return nil, fmt.Errorf("known limitation: value type definitions in a new vocabulary must be embedded in the go-fed tool to ensure that the value is properly serialized and deserialized. This tool is not intelligent enough to automatically somehow deduce what encoding is necessary for new values.")
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// LoadSpecificAsAlias loads a specific RDFNode with the given alias.
|
||||
//
|
||||
// Values cannot be loaded because their serialization and deserialization types
|
||||
// are not known at runtime if not embedded in the go-fed tool. If the error is
|
||||
// generated when running the tool, then file a bug so that the tool can
|
||||
// properly "know" about this particular value and how to serialize and
|
||||
// deserialize it properly.
|
||||
func (r *ReferenceOntology) LoadSpecificAsAlias(alias, name string) ([]RDFNode, error) {
|
||||
if t, ok := r.v.Types[name]; ok {
|
||||
return []RDFNode{
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &typeReference{t: t, vocabName: r.SpecURI()},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if p, ok := r.v.Properties[name]; ok {
|
||||
return []RDFNode{
|
||||
&AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &propertyReference{p: p, vocabName: r.SpecURI()},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if _, ok := r.v.Values[name]; ok {
|
||||
// Note: Values cannot be added this way as there's no way to detect
|
||||
// at runtime what the correct serialization and deserialization scheme
|
||||
// are for particular vocabulary values. Therefore, we omit them here
|
||||
// and will emit an error.
|
||||
//
|
||||
// If this error is emitted, it means a code change to the tool is
|
||||
// required. A new ontology implementation for this vocabulary needs to
|
||||
// be added, and a hardcoded implementation of the value's serialization
|
||||
// and deserialization functions must be created. This will then let the
|
||||
// rest of the generated code properly serialize and deserialize these
|
||||
// values.
|
||||
return nil, fmt.Errorf("known limitation: value type definitions in a new vocabulary must be embedded in the go-fed tool to ensure that the value is properly serialized and deserialized. This tool is not intelligent enough to automatically somehow deduce what encoding is necessary for new values.")
|
||||
}
|
||||
return nil, fmt.Errorf("ontology (%s) cannot find %q to make alias %q", r.SpecURI(), name, alias)
|
||||
}
|
||||
|
||||
// LoadElement does nothing.
|
||||
func (r *ReferenceOntology) LoadElement(name string, payload map[string]interface{}) ([]RDFNode, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetByName returns a raw, unguarded node by name.
|
||||
//
|
||||
// Values cannot be loaded because their serialization and deserialization types
|
||||
// are not known at runtime if not embedded in the go-fed tool. If the error is
|
||||
// generated when running the tool, then file a bug so that the tool can
|
||||
// properly "know" about this particular value and how to serialize and
|
||||
// deserialize it properly.
|
||||
func (r *ReferenceOntology) GetByName(name string) (RDFNode, error) {
|
||||
if t, ok := r.v.Types[name]; ok {
|
||||
return &typeReference{t: t, vocabName: r.SpecURI()}, nil
|
||||
}
|
||||
if p, ok := r.v.Properties[name]; ok {
|
||||
return &propertyReference{p: p, vocabName: r.SpecURI()}, nil
|
||||
}
|
||||
if _, ok := r.v.Values[name]; ok {
|
||||
// Note: Values cannot be added this way as there's no way to detect
|
||||
// at runtime what the correct serialization and deserialization scheme
|
||||
// are for particular vocabulary values. Therefore, we omit them here
|
||||
// and will emit an error.
|
||||
//
|
||||
// If this error is emitted, it means a code change to the tool is
|
||||
// required. A new ontology implementation for this vocabulary needs to
|
||||
// be added, and a hardcoded implementation of the value's serialization
|
||||
// and deserialization functions must be created. This will then let the
|
||||
// rest of the generated code properly serialize and deserialize these
|
||||
// values.
|
||||
return nil, fmt.Errorf("known limitation: value type definitions in a new vocabulary must be embedded in the go-fed tool to ensure that the value is properly serialized and deserialized. This tool is not intelligent enough to automatically somehow deduce what encoding is necessary for new values.")
|
||||
}
|
||||
return nil, fmt.Errorf("ontology (%s) cannot find node for name %s", r.SpecURI(), name)
|
||||
}
|
||||
|
||||
var _ RDFNode = &typeReference{}
|
||||
|
||||
// typeReference adds a VocabularyReference for a VocabularyType in another
|
||||
// vocabulary.
|
||||
type typeReference struct {
|
||||
t VocabularyType
|
||||
vocabName string
|
||||
}
|
||||
|
||||
// Enter returns an error.
|
||||
func (*typeReference) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("typeReference cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (*typeReference) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("typeReference cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets a reference in the context.
|
||||
func (t *typeReference) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
ref, ok := ctx.Current.(*VocabularyReference)
|
||||
if !ok {
|
||||
// May be during resolve reference phase -- nothing to do.
|
||||
return true, nil
|
||||
}
|
||||
ref.Name = t.t.GetName()
|
||||
ref.URI = t.t.URI
|
||||
ref.Vocab = t.vocabName
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ RDFNode = &propertyReference{}
|
||||
|
||||
// typeReference adds a VocabularyReference for a VocabularyProperty in another
|
||||
// vocabulary.
|
||||
type propertyReference struct {
|
||||
p VocabularyProperty
|
||||
vocabName string
|
||||
}
|
||||
|
||||
// Enter returns an error.
|
||||
func (*propertyReference) Enter(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("propertyReference cannot be entered")
|
||||
}
|
||||
|
||||
// Exit returns an error.
|
||||
func (*propertyReference) Exit(key string, ctx *ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("propertyReference cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets a reference in the context.
|
||||
func (p *propertyReference) Apply(key string, value interface{}, ctx *ParsingContext) (bool, error) {
|
||||
ref, ok := ctx.Current.(*VocabularyReference)
|
||||
if !ok {
|
||||
// May be during resolve reference phase -- nothing to do.
|
||||
return true, nil
|
||||
}
|
||||
ref.Name = p.p.GetName()
|
||||
ref.URI = p.p.URI
|
||||
ref.Vocab = p.vocabName
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
// Package RFC contains ontology values that are defined in RFCs, BCPs, and
|
||||
// other miscellaneous standards.
|
||||
package rfc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/go-fed/activity/astool/codegen"
|
||||
"github.com/go-fed/activity/astool/rdf"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
rfcName = "RFC"
|
||||
rfcSpec = "https://tools.ietf.org/html/"
|
||||
bcp47Spec = "bcp47"
|
||||
mimeSpec = "rfc2045" // See also: rfc2046 and rfc6838
|
||||
relSpec = "rfc5988"
|
||||
)
|
||||
|
||||
// RFCOntology represents standards and values that originate from RFC
|
||||
// specifications.
|
||||
type RFCOntology struct {
|
||||
Package string
|
||||
}
|
||||
|
||||
// SpecURI returns the RFC specifications URI.
|
||||
func (o *RFCOntology) SpecURI() string {
|
||||
return rfcSpec
|
||||
}
|
||||
|
||||
// Load without an alias.
|
||||
func (o *RFCOntology) Load() ([]rdf.RDFNode, error) {
|
||||
return o.LoadAsAlias("")
|
||||
}
|
||||
|
||||
// LoadAsAlias loads with the given alias.
|
||||
func (o *RFCOntology) LoadAsAlias(s string) ([]rdf.RDFNode, error) {
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rfcSpec,
|
||||
Alias: s,
|
||||
Name: bcp47Spec,
|
||||
Delegate: &bcp47{pkg: o.Package},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rfcSpec,
|
||||
Alias: s,
|
||||
Name: mimeSpec,
|
||||
Delegate: &mime{pkg: o.Package},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: rfcSpec,
|
||||
Alias: s,
|
||||
Name: relSpec,
|
||||
Delegate: &rel{pkg: o.Package},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadSpecificAsAlias loads a specific item with a given alias.
|
||||
func (o *RFCOntology) LoadSpecificAsAlias(alias, name string) ([]rdf.RDFNode, error) {
|
||||
switch name {
|
||||
case bcp47Spec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &bcp47{pkg: o.Package},
|
||||
},
|
||||
}, nil
|
||||
case mimeSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &mime{pkg: o.Package},
|
||||
},
|
||||
}, nil
|
||||
case relSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &rel{pkg: o.Package},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("rfc ontology cannot find %q to alias to %q", name, alias)
|
||||
}
|
||||
|
||||
// LoadElement does nothing.
|
||||
func (o *RFCOntology) LoadElement(name string, payload map[string]interface{}) ([]rdf.RDFNode, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetByName obtains a bare node by name.
|
||||
func (o *RFCOntology) GetByName(name string) (rdf.RDFNode, error) {
|
||||
name = strings.TrimPrefix(name, o.SpecURI())
|
||||
switch name {
|
||||
case bcp47Spec:
|
||||
return &bcp47{pkg: o.Package}, nil
|
||||
case mimeSpec:
|
||||
return &mime{pkg: o.Package}, nil
|
||||
case relSpec:
|
||||
return &rel{pkg: o.Package}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("rfc ontology could not find node for name %s", name)
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &bcp47{}
|
||||
|
||||
// BCP47 represents a BCP47 value.
|
||||
//
|
||||
// No validation is done on deserialized values.
|
||||
type bcp47 struct {
|
||||
pkg string
|
||||
}
|
||||
|
||||
// Enter does nothing.
|
||||
func (b *bcp47) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("bcp47 langaugetag cannot be entered")
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (b *bcp47) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("bcp47 languagetag cannot be exited")
|
||||
}
|
||||
|
||||
// Apply adds BCP47 as a value Kind.
|
||||
func (b *bcp47) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
v, err := ctx.GetResultReferenceWithDefaults(rfcSpec, rfcName)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if len(v.Values[bcp47Spec].Name) == 0 {
|
||||
u, err := url.Parse(rfcSpec + bcp47Spec)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
val := &rdf.VocabularyValue{
|
||||
Name: bcp47Spec,
|
||||
URI: u,
|
||||
DefinitionType: jen.String(),
|
||||
Zero: "\"\"",
|
||||
IsNilable: false,
|
||||
SerializeFn: rdf.SerializeValueFunction(
|
||||
b.pkg,
|
||||
bcp47Spec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Id(codegen.This()),
|
||||
jen.Nil(),
|
||||
),
|
||||
}),
|
||||
DeserializeFn: rdf.DeserializeValueFunction(
|
||||
b.pkg,
|
||||
bcp47Spec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("s"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id(codegen.This()).Assert(jen.String()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Id("s"),
|
||||
jen.Nil(),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Lit(""),
|
||||
jen.Qual("fmt", "Errorf").Call(
|
||||
jen.Lit("%v cannot be interpreted as a string for bcp47 languagetag"),
|
||||
jen.Id(codegen.This()),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
LessFn: rdf.LessFunction(
|
||||
b.pkg,
|
||||
bcp47Spec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Id("lhs").Op("<").Id("rhs"),
|
||||
),
|
||||
}),
|
||||
}
|
||||
if err = v.SetValue(bcp47Spec, val); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &mime{}
|
||||
|
||||
// mime represents MIME values.
|
||||
type mime struct {
|
||||
pkg string
|
||||
}
|
||||
|
||||
// Enter does nothing.
|
||||
func (*mime) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("MIME media type cannot be entered")
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (*mime) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("MIME media type cannot be exited")
|
||||
}
|
||||
|
||||
// Apply adds MIME as a value Kind.
|
||||
func (m *mime) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
v, err := ctx.GetResultReferenceWithDefaults(rfcSpec, rfcName)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if len(v.Values[mimeSpec].Name) == 0 {
|
||||
u, err := url.Parse(rfcSpec + mimeSpec)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
val := &rdf.VocabularyValue{
|
||||
Name: mimeSpec,
|
||||
URI: u,
|
||||
DefinitionType: jen.String(),
|
||||
Zero: "\"\"",
|
||||
IsNilable: false,
|
||||
SerializeFn: rdf.SerializeValueFunction(
|
||||
m.pkg,
|
||||
mimeSpec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Id(codegen.This()),
|
||||
jen.Nil(),
|
||||
),
|
||||
}),
|
||||
DeserializeFn: rdf.DeserializeValueFunction(
|
||||
m.pkg,
|
||||
mimeSpec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("s"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id(codegen.This()).Assert(jen.String()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Id("s"),
|
||||
jen.Nil(),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Lit(""),
|
||||
jen.Qual("fmt", "Errorf").Call(
|
||||
jen.Lit("%v cannot be interpreted as a string for MIME media type"),
|
||||
jen.Id(codegen.This()),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
LessFn: rdf.LessFunction(
|
||||
m.pkg,
|
||||
mimeSpec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Id("lhs").Op("<").Id("rhs"),
|
||||
),
|
||||
}),
|
||||
}
|
||||
if err = v.SetValue(mimeSpec, val); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &rel{}
|
||||
|
||||
// rel is a Link Relation.
|
||||
type rel struct {
|
||||
pkg string
|
||||
}
|
||||
|
||||
// Enter does nothing.
|
||||
func (*rel) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rel cannot be entered")
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (*rel) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("rel cannot be exited")
|
||||
}
|
||||
|
||||
// Apply adds rel as a supported value Kind.
|
||||
func (r *rel) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
v, err := ctx.GetResultReferenceWithDefaults(rfcSpec, rfcName)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if len(v.Values[relSpec].Name) == 0 {
|
||||
u, err := url.Parse(rfcSpec + relSpec)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
val := &rdf.VocabularyValue{
|
||||
Name: relSpec,
|
||||
URI: u,
|
||||
DefinitionType: jen.String(),
|
||||
Zero: "\"\"",
|
||||
IsNilable: false,
|
||||
SerializeFn: rdf.SerializeValueFunction(
|
||||
r.pkg,
|
||||
relSpec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Id(codegen.This()),
|
||||
jen.Nil(),
|
||||
),
|
||||
}),
|
||||
DeserializeFn: rdf.DeserializeValueFunction(
|
||||
r.pkg,
|
||||
relSpec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.If(
|
||||
jen.List(
|
||||
jen.Id("s"),
|
||||
jen.Id("ok"),
|
||||
).Op(":=").Id(codegen.This()).Assert(jen.String()),
|
||||
jen.Id("ok"),
|
||||
).Block(
|
||||
jen.Return(
|
||||
jen.Id("s"),
|
||||
jen.Nil(),
|
||||
),
|
||||
).Else().Block(
|
||||
jen.Return(
|
||||
jen.Lit(""),
|
||||
jen.Qual("fmt", "Errorf").Call(
|
||||
jen.Lit("%v cannot be interpreted as a string for rel"),
|
||||
jen.Id(codegen.This()),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
LessFn: rdf.LessFunction(
|
||||
r.pkg,
|
||||
relSpec,
|
||||
jen.String(),
|
||||
[]jen.Code{
|
||||
jen.Return(
|
||||
jen.Id("lhs").Op("<").Id("rhs"),
|
||||
),
|
||||
}),
|
||||
}
|
||||
if err = v.SetValue(relSpec, val); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,328 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/astool/rdf"
|
||||
neturl "net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
schemaSpec = "http://schema.org/"
|
||||
exampleSpec = "workExample"
|
||||
mainEntitySpec = "mainEntity"
|
||||
urlSpec = "URL"
|
||||
nameSpec = "name"
|
||||
creativeWorkSpec = "CreativeWork"
|
||||
)
|
||||
|
||||
// SchemaOntology represents Ontologies from schema.org.
|
||||
type SchemaOntology struct{}
|
||||
|
||||
// SpecURI returns the Schema.org URI.
|
||||
func (o *SchemaOntology) SpecURI() string {
|
||||
return schemaSpec
|
||||
}
|
||||
|
||||
// Load without an alias.
|
||||
func (o *SchemaOntology) Load() ([]rdf.RDFNode, error) {
|
||||
return o.LoadAsAlias("")
|
||||
}
|
||||
|
||||
// LoadAsAlias loads with an alias.
|
||||
func (o *SchemaOntology) LoadAsAlias(s string) ([]rdf.RDFNode, error) {
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: schemaSpec,
|
||||
Alias: s,
|
||||
Name: exampleSpec,
|
||||
Delegate: &example{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: schemaSpec,
|
||||
Alias: s,
|
||||
Name: mainEntitySpec,
|
||||
Delegate: &mainEntity{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: schemaSpec,
|
||||
Alias: s,
|
||||
Name: urlSpec,
|
||||
Delegate: &url{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: schemaSpec,
|
||||
Alias: s,
|
||||
Name: nameSpec,
|
||||
Delegate: &name{},
|
||||
},
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: schemaSpec,
|
||||
Alias: s,
|
||||
Name: creativeWorkSpec,
|
||||
Delegate: &creativeWork{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadSpecificAsAlias loads a specific node and aliases it.
|
||||
func (o *SchemaOntology) LoadSpecificAsAlias(alias, n string) ([]rdf.RDFNode, error) {
|
||||
switch n {
|
||||
case exampleSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &example{},
|
||||
},
|
||||
}, nil
|
||||
case mainEntitySpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &mainEntity{},
|
||||
},
|
||||
}, nil
|
||||
case urlSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &url{},
|
||||
},
|
||||
}, nil
|
||||
case nameSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &name{},
|
||||
},
|
||||
}, nil
|
||||
case creativeWorkSpec:
|
||||
return []rdf.RDFNode{
|
||||
&rdf.AliasedDelegate{
|
||||
Spec: "",
|
||||
Alias: "",
|
||||
Name: alias,
|
||||
Delegate: &creativeWork{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("schema ontology cannot find %q to alias to %q", n, alias)
|
||||
}
|
||||
|
||||
// LoadElement does nothing.
|
||||
func (o *SchemaOntology) LoadElement(name string, payload map[string]interface{}) ([]rdf.RDFNode, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetByName returns a bare node by name.
|
||||
func (o *SchemaOntology) GetByName(n string) (rdf.RDFNode, error) {
|
||||
n = strings.TrimPrefix(n, o.SpecURI())
|
||||
switch n {
|
||||
case exampleSpec:
|
||||
return &example{}, nil
|
||||
case mainEntitySpec:
|
||||
return &mainEntity{}, nil
|
||||
case urlSpec:
|
||||
return &url{}, nil
|
||||
case nameSpec:
|
||||
return &name{}, nil
|
||||
case creativeWorkSpec:
|
||||
return &creativeWork{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("schema ontology could not find node for name %s", n)
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &example{}
|
||||
|
||||
// example is best understood by giving an example, such as this.
|
||||
type example struct{}
|
||||
|
||||
// Enter Pushes an Example as Current.
|
||||
func (e *example) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Push()
|
||||
ctx.Current = &rdf.VocabularyExample{}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit Pops an Example and sets it on the parent item.
|
||||
//
|
||||
// Exit returns an error if the popped item is not an Example, or if after
|
||||
// popping the Current item cannot have an Example added to it.
|
||||
func (e *example) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ei := ctx.Current
|
||||
ctx.Pop()
|
||||
if ve, ok := ei.(*rdf.VocabularyExample); !ok {
|
||||
return true, fmt.Errorf("schema example did not pop a *VocabularyExample")
|
||||
} else if ea, ok := ctx.Current.(rdf.ExampleAdder); !ok {
|
||||
return true, fmt.Errorf("schema example not given an ExampleAdder")
|
||||
} else {
|
||||
ea.AddExample(ve)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply returns an error.
|
||||
func (e *example) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("schema example cannot be applied")
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &mainEntity{}
|
||||
|
||||
// mainEntity reapplies itself in all sublevels and simply saves the value onto
|
||||
// Current. This saves the JSON example in raw form.
|
||||
type mainEntity struct{}
|
||||
|
||||
// Enter Pushes the Current item and tells the context to only apply itself for
|
||||
// all sublevels.
|
||||
func (m *mainEntity) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Push()
|
||||
ctx.SetOnlyApplyThisNode(m)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Exit saves the current raw JSON example onto a parent Example.
|
||||
//
|
||||
// Exit reutrns an error if Current after popping is not an Example.
|
||||
func (m *mainEntity) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
// Save the example
|
||||
example := ctx.Current
|
||||
// Undo the Enter operations
|
||||
ctx.ResetOnlyApplyThisNode()
|
||||
ctx.Pop()
|
||||
// Set the example data
|
||||
if vEx, ok := ctx.Current.(*rdf.VocabularyExample); !ok {
|
||||
return true, fmt.Errorf("mainEntity exit not given a *VocabularyExample")
|
||||
} else {
|
||||
vEx.Example = example
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Apply simply saves the value onto Current.
|
||||
func (m *mainEntity) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
ctx.Current = value
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &url{}
|
||||
|
||||
// url sets the URI on an item.
|
||||
type url struct{}
|
||||
|
||||
// Enter does nothing.
|
||||
func (u *url) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("schema url cannot be entered")
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (u *url) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("schema url cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the value as a URI onto an item.
|
||||
//
|
||||
// Returns an error if the value is not a string, or it cannot set the URI on
|
||||
// the Current item.
|
||||
func (u *url) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
if urlString, ok := value.(string); !ok {
|
||||
return true, fmt.Errorf("schema url not given a string")
|
||||
} else if uriSetter, ok := ctx.Current.(rdf.URISetter); !ok {
|
||||
return true, fmt.Errorf("schema url not given a URISetter in context")
|
||||
} else {
|
||||
return true, uriSetter.SetURI(urlString)
|
||||
}
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &name{}
|
||||
|
||||
// name sets the Name on an item.
|
||||
type name struct{}
|
||||
|
||||
// Enter does nothing.
|
||||
func (n *name) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("schema name cannot be entered")
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (n *name) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("schema name cannot be exited")
|
||||
}
|
||||
|
||||
// Apply sets the value as a name on the Current item.
|
||||
//
|
||||
// Returns an error if the value is not a string, or if the Current item cannot
|
||||
// have its name set.
|
||||
func (n *name) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
if s, ok := value.(string); !ok {
|
||||
return true, fmt.Errorf("schema name not given string")
|
||||
} else if ns, ok := ctx.Current.(rdf.NameSetter); !ok {
|
||||
return true, fmt.Errorf("schema name not given NameSetter in context")
|
||||
} else {
|
||||
var vocab string
|
||||
// Parse will interpret "ActivityStreams" as a valid URL without
|
||||
// a scheme. It will also interpret "as:Object" as a valid URL
|
||||
// with a scheme of "as".
|
||||
if u, err := neturl.Parse(s); err == nil && len(u.Scheme) > 0 && len(u.Host) > 0 {
|
||||
// If the name is a URL, use heuristics to determine the
|
||||
// name versus vocabulary part.
|
||||
//
|
||||
// The vocabulary is usually the URI without the
|
||||
// fragment or final path entry. The name is usually the
|
||||
// fragment or final path entry.
|
||||
if len(u.Fragment) > 0 {
|
||||
// Attempt to parse the fragment
|
||||
s = u.Fragment
|
||||
u.Fragment = ""
|
||||
vocab = u.String()
|
||||
} else {
|
||||
// Use the final path component
|
||||
comp := strings.Split(s, "/")
|
||||
s = comp[len(comp)-1]
|
||||
vocab = strings.Join(comp[:len(comp)-1], "/")
|
||||
}
|
||||
} else if sp := rdf.SplitAlias(s); len(sp) == 2 {
|
||||
// The name may be aliased.
|
||||
vocab = sp[0]
|
||||
s = sp[1]
|
||||
} // Else the name has no vocabulary reference.
|
||||
if len(vocab) > 0 {
|
||||
if ref, ok := ctx.Current.(*rdf.VocabularyReference); !ok {
|
||||
return true, fmt.Errorf("schema name not given *rdf.VocabularyReference in context")
|
||||
} else {
|
||||
ref.Vocab = vocab
|
||||
}
|
||||
}
|
||||
ns.SetName(s)
|
||||
ctx.Name = s
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
var _ rdf.RDFNode = &creativeWork{}
|
||||
|
||||
// creativeWork does nothing.
|
||||
type creativeWork struct{}
|
||||
|
||||
// Enter returns an error.
|
||||
func (c *creativeWork) Enter(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("schema creative work cannot be entered")
|
||||
}
|
||||
|
||||
// Exit does nothing.
|
||||
func (c *creativeWork) Exit(key string, ctx *rdf.ParsingContext) (bool, error) {
|
||||
return true, fmt.Errorf("schema creative work cannot be exited")
|
||||
}
|
||||
|
||||
// Apply does nothing.
|
||||
func (c *creativeWork) Apply(key string, value interface{}, ctx *rdf.ParsingContext) (bool, error) {
|
||||
// Do nothing -- should already be an example.
|
||||
return true, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,143 @@
|
|||
{
|
||||
"@context": [
|
||||
{
|
||||
"as": "https://www.w3.org/ns/activitystreams",
|
||||
"owl": "http://www.w3.org/2002/07/owl#",
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||
"rfc": "https://tools.ietf.org/html/",
|
||||
"schema": "http://schema.org/",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#"
|
||||
},
|
||||
{
|
||||
"domain": "rdfs:domain",
|
||||
"example": "schema:workExample",
|
||||
"isDefinedBy": "rdfs:isDefinedBy",
|
||||
"mainEntity": "schema:mainEntity",
|
||||
"members": "owl:members",
|
||||
"name": "schema:name",
|
||||
"notes": "rdfs:comment",
|
||||
"range": "rdfs:range",
|
||||
"subClassOf": "rdfs:subClassOf",
|
||||
"disjointWith": "owl:disjointWith",
|
||||
"subPropertyOf": "rdfs:subPropertyOf",
|
||||
"unionOf": "owl:unionOf",
|
||||
"url": "schema:URL"
|
||||
}
|
||||
],
|
||||
"id": "https://w3id.org/security/v1",
|
||||
"type": "owl:Ontology",
|
||||
"name": "W3IDSecurityV1",
|
||||
"members": [
|
||||
{
|
||||
"id": "https://w3id.org/security/v1#PublicKey",
|
||||
"type": "owl:Class",
|
||||
"notes": "A public key represents a public cryptographical key for a user",
|
||||
"name": "PublicKey",
|
||||
"url": "https://w3id.org/security/v1#PublicKey",
|
||||
"@wtf_typeless": true
|
||||
},
|
||||
{
|
||||
"id": "https://w3id.org/security/v1#dfn-publickey",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:ObjectProperty"
|
||||
],
|
||||
"example": {},
|
||||
"notes": "The public key for an ActivityStreams actor",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Application",
|
||||
"name": "as:Application"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Group",
|
||||
"name": "as:Group"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Organization",
|
||||
"name": "as:Organization"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Person",
|
||||
"name": "as:Person"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Service",
|
||||
"name": "as:Service"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isDefinedBy": "https://w3id.org/security/v1#dfn-publickey",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://w3id.org/security/v1#PublicKey",
|
||||
"name": "PublicKey"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "publicKey",
|
||||
"url": "https://w3id.org/security/v1#dfn-publickey"
|
||||
},
|
||||
{
|
||||
"id": "https://w3id.org/security/v1#dfn-publickeypem",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "The public key PEM encoded data for an ActivityStreams actor",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://w3id.org/security/v1#PublicKey",
|
||||
"name": "PublicKey"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isDefinedBy": "https://w3id.org/security/v1#dfn-publickeypem",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "publicKeyPem",
|
||||
"url": "https://w3id.org/security/v1#dfn-publickeypem"
|
||||
},
|
||||
{
|
||||
"id": "https://w3id.org/security/v1#dfn-owner",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"notes": "The owner of the public key for an ActivityStreams actor",
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://w3id.org/security/v1#PublicKey",
|
||||
"name": "PublicKey"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isDefinedBy": "https://w3id.org/security/v1#dfn-owner",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:anyURI"
|
||||
},
|
||||
"name": "owner",
|
||||
"url": "https://w3id.org/security/v1#dfn-owner"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
{
|
||||
"@context": [
|
||||
{
|
||||
"as": "https://www.w3.org/ns/activitystreams",
|
||||
"owl": "http://www.w3.org/2002/07/owl#",
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||
"rfc": "https://tools.ietf.org/html/",
|
||||
"schema": "http://schema.org/",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#"
|
||||
},
|
||||
{
|
||||
"domain": "rdfs:domain",
|
||||
"example": "schema:workExample",
|
||||
"isDefinedBy": "rdfs:isDefinedBy",
|
||||
"mainEntity": "schema:mainEntity",
|
||||
"members": "owl:members",
|
||||
"name": "schema:name",
|
||||
"notes": "rdfs:comment",
|
||||
"range": "rdfs:range",
|
||||
"subClassOf": "rdfs:subClassOf",
|
||||
"disjointWith": "owl:disjointWith",
|
||||
"subPropertyOf": "rdfs:subPropertyOf",
|
||||
"unionOf": "owl:unionOf",
|
||||
"url": "schema:URL"
|
||||
}
|
||||
],
|
||||
"id": "http://joinmastodon.org/ns#",
|
||||
"type": "owl:Ontology",
|
||||
"name": "Toot",
|
||||
"members": [
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#Emoji",
|
||||
"type": "owl:Class",
|
||||
"example": [
|
||||
{
|
||||
"type": "http://schema.org/CreativeWork",
|
||||
"mainEntity": {
|
||||
"id": "https://example.com/@alice/hello-world",
|
||||
"type": "Note",
|
||||
"content": "Hello world :Kappa:",
|
||||
"tag": [
|
||||
{
|
||||
"id": "https://example.com/emoji/123",
|
||||
"type": "Emoji",
|
||||
"name": ":Kappa:",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/png",
|
||||
"url": "https://example.com/files/kappa.png"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
"disjointWith": [],
|
||||
"name": "Emoji",
|
||||
"url": "https://docs.joinmastodon.org/development/activitypub/#custom-emojis"
|
||||
},
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#featured",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": {
|
||||
},
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Application",
|
||||
"name": "as:Application"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Group",
|
||||
"name": "as:Group"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Organization",
|
||||
"name": "as:Organization"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Person",
|
||||
"name": "as:Person"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Service",
|
||||
"name": "as:Service"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isDefinedBy": "https://docs.joinmastodon.org/development/activitypub/#featured-collection",
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#OrderedCollection",
|
||||
"name": "as:OrderedCollection"
|
||||
}
|
||||
},
|
||||
"name": "featured",
|
||||
"url": "https://docs.joinmastodon.org/development/activitypub/#featured-collection"
|
||||
},
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#votersCount",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": {
|
||||
},
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Question",
|
||||
"name": "as:Question"
|
||||
}
|
||||
]
|
||||
},
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:nonNegativeInteger"
|
||||
},
|
||||
"name": "votersCount"
|
||||
},
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#blurhash",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": {
|
||||
},
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Document",
|
||||
"name": "as:Document"
|
||||
}
|
||||
]
|
||||
},
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "blurhash"
|
||||
},
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#IdentityProof",
|
||||
"type": "owl:Class",
|
||||
"example": {
|
||||
},
|
||||
"subClassOf": {
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Object",
|
||||
"name": "as:Object"
|
||||
},
|
||||
"disjointWith": [],
|
||||
"name": "IdentityProof"
|
||||
},
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#signatureAlgorithm",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": {
|
||||
},
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "http://joinmastodon.org/ns#IdentityProof",
|
||||
"name": "IdentityProof"
|
||||
}
|
||||
]
|
||||
},
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "signatureAlgorithm"
|
||||
},
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#signatureValue",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": {
|
||||
},
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "http://joinmastodon.org/ns#IdentityProof",
|
||||
"name": "IdentityProof"
|
||||
}
|
||||
]
|
||||
},
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:string"
|
||||
},
|
||||
"name": "signatureValue"
|
||||
},
|
||||
{
|
||||
"id": "http://joinmastodon.org/ns#discoverable",
|
||||
"type": [
|
||||
"rdf:Property",
|
||||
"owl:FunctionalProperty"
|
||||
],
|
||||
"example": {
|
||||
},
|
||||
"domain": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": [
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Application",
|
||||
"name": "as:Application"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Group",
|
||||
"name": "as:Group"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Organization",
|
||||
"name": "as:Organization"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Person",
|
||||
"name": "as:Person"
|
||||
},
|
||||
{
|
||||
"type": "owl:Class",
|
||||
"url": "https://www.w3.org/ns/activitystreams#Service",
|
||||
"name": "as:Service"
|
||||
}
|
||||
]
|
||||
},
|
||||
"range": {
|
||||
"type": "owl:Class",
|
||||
"unionOf": "xsd:boolean"
|
||||
},
|
||||
"name": "discoverable"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
# deliverer
|
||||
|
||||
This library is completely optional, provided only for convenience.
|
||||
|
||||
An extra utility that provides a simple mechanism to asynchronously deliver
|
||||
federated messages from a `pub.Pubber` available from the `go-fed/activity/pub`
|
||||
library.
|
||||
|
||||
It implements the `pub.Deliverer` interface.
|
||||
|
||||
The parent application may provide a way to persist delivery attempts in a way
|
||||
that survives shutdown by implementing the new `DeliveryPersister ` interface.
|
||||
The sky is the limit.
|
|
@ -1,233 +0,0 @@
|
|||
package deliverer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/pub"
|
||||
"golang.org/x/time/rate"
|
||||
"math"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeliveryPersister allows applications to keep track of delivery states of
|
||||
// the messages being sent, including during retries. This permits clients to
|
||||
// also resume delivery of messages that were in the process of being delivered
|
||||
// when the application server was shut down.
|
||||
type DeliveryPersister interface {
|
||||
// Sending informs the delivery persister that the provided bytes are
|
||||
// being delivered to the specified url. It must return a unique id for
|
||||
// this delivery.
|
||||
Sending(b []byte, to *url.URL) string
|
||||
// Cancel informs the delivery persister that the provided delivery was
|
||||
// interrupted by the server cancelling. These should be retried once
|
||||
// the server is back online.
|
||||
Cancel(id string)
|
||||
// Successful informs the delivery persister that the request has been
|
||||
// successfully delivered and no further retries are needed.
|
||||
Successful(id string)
|
||||
// Retrying indicates the specified delivery is being retried.
|
||||
Retrying(id string)
|
||||
// Undeliverable indicates the specified delivery has failed and is no
|
||||
// longer being retried.
|
||||
Undeliverable(id string)
|
||||
}
|
||||
|
||||
// DeliveryOptions provides options when delivering messages to federated
|
||||
// servers. All are required unless explicitly stated otherwise.
|
||||
type DeliveryOptions struct {
|
||||
// Initial amount of time to wait before retrying delivery.
|
||||
InitialRetryTime time.Duration
|
||||
// The longest amount of time to wait before retrying delivery.
|
||||
MaximumRetryTime time.Duration
|
||||
// Rate of backing off retries. Must be at least 1.
|
||||
BackoffFactor float64
|
||||
// Maximum number of retries to do when delivering a message. Must be at
|
||||
// least 1.
|
||||
MaxRetries int
|
||||
// Global rate limiter across all deliveries, to prevent spamming
|
||||
// outbound messages.
|
||||
RateLimit *rate.Limiter
|
||||
// Persister allows implementations to save messages that are enqueued
|
||||
// for delivery between downtimes. It also permits metrics gathering and
|
||||
// monitoring of outbound messages.
|
||||
//
|
||||
// This field is optional.
|
||||
Persister DeliveryPersister
|
||||
}
|
||||
|
||||
var _ pub.Deliverer = &DelivererPool{}
|
||||
|
||||
type DelivererPool struct {
|
||||
// When present, permits clients to be notified of all state changes
|
||||
// when delivering a request to another federated server.
|
||||
//
|
||||
// Optional.
|
||||
persister DeliveryPersister
|
||||
// Limit speed of retries.
|
||||
initialRetryTime time.Duration
|
||||
maxRetryTime time.Duration
|
||||
retryTimeFactor float64
|
||||
// Limit total number of retries.
|
||||
maxNumberRetries int
|
||||
// Enforces speed limit of retries
|
||||
limiter *rate.Limiter
|
||||
// Allow graceful cancelling
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
timerId uint64
|
||||
timerMap map[uint64]*time.Timer
|
||||
mu sync.Mutex // Limits concurrent access to timerId and timerMap
|
||||
// Allow graceful error handling
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func NewDelivererPool(d DeliveryOptions) *DelivererPool {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &DelivererPool{
|
||||
persister: d.Persister,
|
||||
initialRetryTime: d.InitialRetryTime,
|
||||
maxRetryTime: d.MaximumRetryTime,
|
||||
retryTimeFactor: d.BackoffFactor,
|
||||
maxNumberRetries: d.MaxRetries,
|
||||
limiter: d.RateLimit,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
timerId: 0,
|
||||
timerMap: make(map[uint64]*time.Timer, 0),
|
||||
mu: sync.Mutex{},
|
||||
errChan: make(chan error, 0),
|
||||
}
|
||||
}
|
||||
|
||||
type retryData struct {
|
||||
nextWait time.Duration
|
||||
n int
|
||||
f func() error
|
||||
id string
|
||||
}
|
||||
|
||||
func (r retryData) NextRetry(factor float64, max time.Duration) retryData {
|
||||
w := time.Duration(int64(math.Floor((float64(r.nextWait) * factor) + 0.5)))
|
||||
if w > max {
|
||||
w = max
|
||||
}
|
||||
return retryData{
|
||||
nextWait: w,
|
||||
n: r.n + 1,
|
||||
f: r.f,
|
||||
id: r.id,
|
||||
}
|
||||
}
|
||||
|
||||
func (r retryData) ShouldRetry(max int) bool {
|
||||
return r.n < max
|
||||
}
|
||||
|
||||
// Do spawns a goroutine that retries f until it returns no error. Retry
|
||||
// behavior is determined by the DeliveryOptions passed to the DelivererPool
|
||||
// upon construction.
|
||||
func (d *DelivererPool) Do(b []byte, to *url.URL, sendFn func([]byte, *url.URL) error) {
|
||||
f := func() error {
|
||||
return sendFn(b, to)
|
||||
}
|
||||
go func() {
|
||||
id := ""
|
||||
if d.persister != nil {
|
||||
id = d.persister.Sending(b, to)
|
||||
}
|
||||
d.do(retryData{
|
||||
nextWait: d.initialRetryTime,
|
||||
n: 0,
|
||||
f: f,
|
||||
id: id,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// Restart resumes a previous attempt at delivering a payload to the specified
|
||||
// URL. Retry behavior is determined by the DeliveryOptions passed to this
|
||||
// DelivererPool upon construction, and is not governed by the previous
|
||||
// DelivererPool that attempted to deliver the message.
|
||||
func (d *DelivererPool) Restart(b []byte, to *url.URL, id string, sendFn func([]byte, *url.URL) error) {
|
||||
f := func() error {
|
||||
return sendFn(b, to)
|
||||
}
|
||||
go func() {
|
||||
d.do(retryData{
|
||||
nextWait: d.initialRetryTime,
|
||||
n: 0,
|
||||
f: f,
|
||||
id: id,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop turns down and stops any in-flight requests or retries.
|
||||
func (d *DelivererPool) Stop() {
|
||||
d.cancel()
|
||||
d.closeTimers()
|
||||
}
|
||||
|
||||
// Provides a channel streaming any errors the pool encounters, including errors
|
||||
// that it retries on.
|
||||
func (d *DelivererPool) Errors() <-chan error {
|
||||
return d.errChan
|
||||
}
|
||||
|
||||
func (d *DelivererPool) do(r retryData) {
|
||||
if err := d.limiter.Wait(d.ctx); err != nil {
|
||||
if d.persister != nil {
|
||||
d.persister.Cancel(r.id)
|
||||
}
|
||||
d.errChan <- err
|
||||
return
|
||||
}
|
||||
if err := r.f(); err != nil {
|
||||
d.errChan <- err
|
||||
if r.ShouldRetry(d.maxNumberRetries) {
|
||||
if d.persister != nil {
|
||||
d.persister.Retrying(r.id)
|
||||
}
|
||||
d.addClosableTimer(r)
|
||||
} else {
|
||||
d.errChan <- fmt.Errorf("delivery tried maximum number of times")
|
||||
if d.persister != nil {
|
||||
d.persister.Undeliverable(r.id)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if d.persister != nil {
|
||||
d.persister.Successful(r.id)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DelivererPool) addClosableTimer(r retryData) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
id := d.timerId
|
||||
d.timerId++
|
||||
d.timerMap[id] = time.AfterFunc(r.nextWait, func() {
|
||||
d.do(r.NextRetry(d.retryTimeFactor, d.maxRetryTime))
|
||||
d.removeTimer(id)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DelivererPool) removeTimer(id uint64) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if _, ok := d.timerMap[id]; ok {
|
||||
delete(d.timerMap, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DelivererPool) closeTimers() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
for _, v := range d.timerMap {
|
||||
v.Stop()
|
||||
}
|
||||
d.timerMap = make(map[uint64]*time.Timer, 0)
|
||||
}
|
|
@ -1,311 +0,0 @@
|
|||
package deliverer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-test/deep"
|
||||
"golang.org/x/time/rate"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
id1 = "id1"
|
||||
id2 = "id2"
|
||||
sending = "sending"
|
||||
cancel = "cancel"
|
||||
successful = "successful"
|
||||
retrying = "retrying"
|
||||
undeliverable = "undeliverable"
|
||||
noState = "noState"
|
||||
)
|
||||
|
||||
var (
|
||||
testBytes []byte = []byte{0, 1, 2, 3}
|
||||
testURL *url.URL
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
testURL, err = url.Parse("example.com")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var _ DeliveryPersister = &mockDeliveryPersister{}
|
||||
|
||||
type mockDeliveryPersister struct {
|
||||
t *testing.T
|
||||
i int
|
||||
mu *sync.Mutex
|
||||
id1State string
|
||||
id2State string
|
||||
}
|
||||
|
||||
func newMockDeliveryPersister(t *testing.T) *mockDeliveryPersister {
|
||||
return &mockDeliveryPersister{
|
||||
t: t,
|
||||
mu: &sync.Mutex{},
|
||||
id1State: noState,
|
||||
id2State: noState,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDeliveryPersister) Sending(b []byte, to *url.URL) string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.i == 0 {
|
||||
m.i++
|
||||
return id1
|
||||
} else if m.i == 1 {
|
||||
m.i++
|
||||
return id2
|
||||
} else {
|
||||
m.t.Fatal("too many calls to Sending")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockDeliveryPersister) Cancel(id string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if id == id1 {
|
||||
m.id1State = cancel
|
||||
} else if id == id2 {
|
||||
m.id2State = cancel
|
||||
} else {
|
||||
m.t.Fatalf("unknown Cancel id: %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDeliveryPersister) Successful(id string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if id == id1 {
|
||||
m.id1State = successful
|
||||
} else if id == id2 {
|
||||
m.id2State = successful
|
||||
} else {
|
||||
m.t.Fatalf("unknown Successful id: %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDeliveryPersister) Retrying(id string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if id == id1 {
|
||||
m.id1State = retrying
|
||||
} else if id == id2 {
|
||||
m.id2State = retrying
|
||||
} else {
|
||||
m.t.Fatalf("unknown Retrying id: %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDeliveryPersister) Undeliverable(id string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if id == id1 {
|
||||
m.id1State = undeliverable
|
||||
} else if id == id2 {
|
||||
m.id2State = undeliverable
|
||||
} else {
|
||||
m.t.Fatalf("unknown Retrying id: %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelivererPoolSuccessNoPersister(t *testing.T) {
|
||||
testSendFn := func(b []byte, u *url.URL) error {
|
||||
if diff := deep.Equal(b, testBytes); diff != nil {
|
||||
t.Fatal(diff)
|
||||
} else if u != testURL {
|
||||
t.Fatal("wrong testURL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
pool := NewDelivererPool(DeliveryOptions{
|
||||
InitialRetryTime: time.Microsecond,
|
||||
MaximumRetryTime: time.Microsecond,
|
||||
BackoffFactor: 2,
|
||||
MaxRetries: 1,
|
||||
RateLimit: rate.NewLimiter(1, 1),
|
||||
})
|
||||
pool.Do(testBytes, testURL, testSendFn)
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
}
|
||||
|
||||
func TestDelivererPoolSuccessPersister(t *testing.T) {
|
||||
testSendFn := func(b []byte, u *url.URL) error {
|
||||
if diff := deep.Equal(b, testBytes); diff != nil {
|
||||
t.Fatal(diff)
|
||||
} else if u != testURL {
|
||||
t.Fatal("wrong testURL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
p := newMockDeliveryPersister(t)
|
||||
pool := NewDelivererPool(DeliveryOptions{
|
||||
InitialRetryTime: time.Microsecond,
|
||||
MaximumRetryTime: time.Microsecond,
|
||||
BackoffFactor: 2,
|
||||
MaxRetries: 1,
|
||||
RateLimit: rate.NewLimiter(1, 1),
|
||||
Persister: p,
|
||||
})
|
||||
pool.Do(testBytes, testURL, testSendFn)
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
if p.id1State != successful {
|
||||
t.Fatalf("want: %s, got %s", successful, p.id1State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartSuccess(t *testing.T) {
|
||||
testSendFn := func(b []byte, u *url.URL) error {
|
||||
if diff := deep.Equal(b, testBytes); diff != nil {
|
||||
t.Fatal(diff)
|
||||
} else if u != testURL {
|
||||
t.Fatal("wrong testURL")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
p := newMockDeliveryPersister(t)
|
||||
pool := NewDelivererPool(DeliveryOptions{
|
||||
InitialRetryTime: time.Microsecond,
|
||||
MaximumRetryTime: time.Microsecond,
|
||||
BackoffFactor: 2,
|
||||
MaxRetries: 1,
|
||||
RateLimit: rate.NewLimiter(1, 1),
|
||||
Persister: p,
|
||||
})
|
||||
pool.Restart(testBytes, testURL, id2, testSendFn)
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
if p.id2State != successful {
|
||||
t.Fatalf("want: %s, got %s", successful, p.id1State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelivererPoolRetrying(t *testing.T) {
|
||||
testSendFn := func(b []byte, u *url.URL) error {
|
||||
if diff := deep.Equal(b, testBytes); diff != nil {
|
||||
t.Fatal(diff)
|
||||
} else if u != testURL {
|
||||
t.Fatal("wrong testURL")
|
||||
}
|
||||
return fmt.Errorf("expected")
|
||||
}
|
||||
p := newMockDeliveryPersister(t)
|
||||
pool := NewDelivererPool(DeliveryOptions{
|
||||
InitialRetryTime: time.Microsecond,
|
||||
MaximumRetryTime: time.Microsecond,
|
||||
BackoffFactor: 2,
|
||||
MaxRetries: 1,
|
||||
RateLimit: rate.NewLimiter(1000000, 10000000),
|
||||
Persister: p,
|
||||
})
|
||||
pool.Do(testBytes, testURL, testSendFn)
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
select {
|
||||
case <-pool.Errors():
|
||||
default:
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
if p.id1State != retrying {
|
||||
t.Fatalf("want: %s, got %s", retrying, p.id1State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelivererPoolUndeliverable(t *testing.T) {
|
||||
testSendFn := func(b []byte, u *url.URL) error {
|
||||
if diff := deep.Equal(b, testBytes); diff != nil {
|
||||
t.Fatal(diff)
|
||||
} else if u != testURL {
|
||||
t.Fatal("wrong testURL")
|
||||
}
|
||||
return fmt.Errorf("expected")
|
||||
}
|
||||
p := newMockDeliveryPersister(t)
|
||||
pool := NewDelivererPool(DeliveryOptions{
|
||||
InitialRetryTime: time.Microsecond,
|
||||
MaximumRetryTime: time.Microsecond,
|
||||
BackoffFactor: 2,
|
||||
MaxRetries: 1,
|
||||
RateLimit: rate.NewLimiter(1000000, 10000000),
|
||||
Persister: p,
|
||||
})
|
||||
pool.Do(testBytes, testURL, testSendFn)
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
<-pool.Errors()
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
<-pool.Errors()
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
<-pool.Errors()
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
if p.id1State != undeliverable {
|
||||
t.Fatalf("want: %s, got %s", undeliverable, p.id1State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartRetrying(t *testing.T) {
|
||||
testSendFn := func(b []byte, u *url.URL) error {
|
||||
if diff := deep.Equal(b, testBytes); diff != nil {
|
||||
t.Fatal(diff)
|
||||
} else if u != testURL {
|
||||
t.Fatal("wrong testURL")
|
||||
}
|
||||
return fmt.Errorf("expected")
|
||||
}
|
||||
p := newMockDeliveryPersister(t)
|
||||
pool := NewDelivererPool(DeliveryOptions{
|
||||
InitialRetryTime: time.Microsecond,
|
||||
MaximumRetryTime: time.Microsecond,
|
||||
BackoffFactor: 2,
|
||||
MaxRetries: 1,
|
||||
RateLimit: rate.NewLimiter(1000000, 10000000),
|
||||
Persister: p,
|
||||
})
|
||||
pool.Restart(testBytes, testURL, id2, testSendFn)
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
select {
|
||||
case <-pool.Errors():
|
||||
default:
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
if p.id2State != retrying {
|
||||
t.Fatalf("want: %s, got %s", retrying, p.id2State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartUndeliverable(t *testing.T) {
|
||||
testSendFn := func(b []byte, u *url.URL) error {
|
||||
if diff := deep.Equal(b, testBytes); diff != nil {
|
||||
t.Fatal(diff)
|
||||
} else if u != testURL {
|
||||
t.Fatal("wrong testURL")
|
||||
}
|
||||
return fmt.Errorf("expected")
|
||||
}
|
||||
p := newMockDeliveryPersister(t)
|
||||
pool := NewDelivererPool(DeliveryOptions{
|
||||
InitialRetryTime: time.Microsecond,
|
||||
MaximumRetryTime: time.Microsecond,
|
||||
BackoffFactor: 2,
|
||||
MaxRetries: 1,
|
||||
RateLimit: rate.NewLimiter(1000000, 10000000),
|
||||
Persister: p,
|
||||
})
|
||||
pool.Restart(testBytes, testURL, id2, testSendFn)
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
<-pool.Errors()
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
<-pool.Errors()
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
<-pool.Errors()
|
||||
time.Sleep(time.Microsecond * 500)
|
||||
if p.id2State != undeliverable {
|
||||
t.Fatalf("want: %s, got %s", undeliverable, p.id2State)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// +build generate
|
||||
//go:generate go run ./astool -spec astool/activitystreams.jsonld -spec astool/security-v1.jsonld -spec astool/toot.jsonld -spec astool/forgefed.jsonld -path github.com/go-fed/activity ./streams
|
||||
|
||||
package activity
|
7
go.mod
7
go.mod
|
@ -1,7 +1,10 @@
|
|||
module github.com/go-fed/activity
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/go-fed/httpsig v0.1.0
|
||||
github.com/dave/jennifer v1.3.0
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5
|
||||
github.com/go-test/deep v1.0.1
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2
|
||||
github.com/golang/mock v1.2.0
|
||||
)
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
github.com/go-fed/httpsig v0.1.0 h1:clkeIoBexg4Fmc8u5mneN5nFPdisCWHATTB8mMsP+/E=
|
||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43 h1:PvnWIWTbA7gsEBkKjt0HV9hckYfcqYv8s/ju7ArZ0do=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
|
|
@ -0,0 +1,12 @@
|
|||
github.com/dave/jennifer v1.3.0 h1:p3tl41zjjCZTNBytMwrUuiAnherNUZktlhPTKoF/sEk=
|
||||
github.com/dave/jennifer v1.3.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5 h1:WLvFZqoXnuVTBKA6U/1FnEHNQ0Rq0QM0rGhY8Tx6R1g=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43 h1:PvnWIWTbA7gsEBkKjt0HV9hckYfcqYv8s/ju7ArZ0do=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
363
pub/README.md
363
pub/README.md
|
@ -1,187 +1,270 @@
|
|||
# pub
|
||||
|
||||
Implements both the SocialAPI and FederateAPI in the ActivityPub specification.
|
||||
Implements the Social and Federating Protocols in the ActivityPub specification.
|
||||
|
||||
## Disclaimer
|
||||
## Reference & Tutorial
|
||||
|
||||
This library is designed with flexibility in mind. The cost of doing so is that
|
||||
writing an ActivityPub application requires a lot of careful considerations that
|
||||
are not trivial. ActivityPub is an Application transport layer that is also tied
|
||||
to a specific data model, making retrofits nontrivial as well.
|
||||
The [go-fed website](https://go-fed.org/) contains tutorials and reference
|
||||
materials, in addition to the rest of this README.
|
||||
|
||||
## How To Use
|
||||
|
||||
There are two ActivityPub APIs: the SocialAPI between a user and your
|
||||
ActivityPub server, and the FederateAPI between your ActivityPub server and
|
||||
another server peer. This library lets you choose one or both.
|
||||
|
||||
*Lightning intro to ActivityPub: ActivityPub uses ActivityStreams as data. This
|
||||
lives in `go-fed/activity/vocab`. ActivityPub has a concept of `actors` who can
|
||||
send, receive, and read their messages. When sending and receiving messages from
|
||||
a client (such as on their phone) to an ActivityPub server, it is via the
|
||||
SocialAPI. When it is between two ActivityPub servers, it is via the
|
||||
FederateAPI.*
|
||||
|
||||
Next, there are two kinds of ActivityPub requests to handle:
|
||||
|
||||
1. Requests that `GET` or `POST` to stuff owned by an `actor` like their `inbox`
|
||||
or `outbox`.
|
||||
1. Requests that `GET` ActivityStream objects hosted on your server.
|
||||
|
||||
The first is the most complex, and requires the creation of a `Pubber`. It is
|
||||
created depending on which APIs are to be supported:
|
||||
|
||||
```
|
||||
// Only support SocialAPI
|
||||
s := pub.NewSocialPubber(...)
|
||||
// Only support FederateAPI
|
||||
f := pub.NewFederatingPubber(...)
|
||||
// Support both APIs
|
||||
sf := pub.NewPubber(...)
|
||||
go get github.com/go-fed/activity
|
||||
```
|
||||
|
||||
Note that *only* the creation of the `Pubber` is affected by the decision of
|
||||
which API to support. Once created, the `Pubber` should be used in the same
|
||||
manner regardless of the API it is supporting. This allows your application
|
||||
to easily adopt one API first and migrate to both later by simply changing how
|
||||
the `Pubber` is created.
|
||||
The root of all ActivityPub behavior is the `Actor`, which requires you to
|
||||
implement a few interfaces:
|
||||
|
||||
To use the `Pubber`, call its methods in the HTTP handlers responsible for an
|
||||
`actor`'s `inbox` and `outbox`:
|
||||
```golang
|
||||
import (
|
||||
"github.com/go-fed/activity/pub"
|
||||
)
|
||||
|
||||
type myActivityPubApp struct { /* ... */ }
|
||||
type myAppsDatabase struct { /* ... */ }
|
||||
type myAppsClock struct { /* ... */ }
|
||||
|
||||
var (
|
||||
// Your app will implement pub.CommonBehavior, and either
|
||||
// pub.SocialProtocol, pub.FederatingProtocol, or both.
|
||||
myApp = &myActivityPubApp{}
|
||||
myCommonBehavior pub.CommonBehavior = myApp
|
||||
mySocialProtocol pub.SocialProtocol = myApp
|
||||
myFederatingProtocol pub.FederatingProtocol = myApp
|
||||
// Your app's database implementation.
|
||||
myDatabase pub.Database = &myAppsDatabase{}
|
||||
// Your app's clock.
|
||||
myClock pub.Clock = &myAppsClock{}
|
||||
)
|
||||
|
||||
// Only support the C2S Social protocol
|
||||
actor := pub.NewSocialActor(
|
||||
myCommonBehavior,
|
||||
mySocialProtocol,
|
||||
myDatabase,
|
||||
myClock)
|
||||
// OR
|
||||
//
|
||||
// Only support S2S Federating protocol
|
||||
actor = pub.NewFederatingActor(
|
||||
myCommonBehavior,
|
||||
myFederatingProtocol,
|
||||
myDatabase,
|
||||
myClock)
|
||||
// OR
|
||||
//
|
||||
// Support both C2S Social and S2S Federating protocol.
|
||||
actor = pub.NewActor(
|
||||
myCommonBehavior,
|
||||
mySocialProtocol,
|
||||
myFederatingProtocol,
|
||||
myDatabase,
|
||||
myClock)
|
||||
```
|
||||
// Given:
|
||||
// var myPubber pub.Pubber
|
||||
|
||||
Next, hook the `Actor` into your web server:
|
||||
|
||||
```golang
|
||||
// The application's actor
|
||||
var actor pub.Actor
|
||||
var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||
c := context.Background()
|
||||
// Populate c with application specific information
|
||||
if handled, err := myPubber.PostOutbox(c, w, r); err != nil {
|
||||
// Populate c with request-specific information
|
||||
if handled, err := actor.PostOutbox(c, w, r); err != nil {
|
||||
// Write to w
|
||||
return
|
||||
} else if handled {
|
||||
return
|
||||
} else if handled, err = actor.GetOutbox(c, w, r); err != nil {
|
||||
// Write to w
|
||||
return
|
||||
} else if handled {
|
||||
return
|
||||
}
|
||||
if handled, err := myPubber.GetOutbox(c, w, r); err != nil {
|
||||
// Write to w
|
||||
} else if handled {
|
||||
return
|
||||
}
|
||||
// Handle non-ActivityPub request, such as responding with an HTML
|
||||
// representation with correct view permissions.
|
||||
// else:
|
||||
//
|
||||
// Handle non-ActivityPub request, such as serving a webpage.
|
||||
}
|
||||
var inboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||
c := context.Background()
|
||||
// Populate c with application specific information
|
||||
if handled, err := myPubber.PostInbox(c, w, r); err != nil {
|
||||
// Populate c with request-specific information
|
||||
if handled, err := actor.PostInbox(c, w, r); err != nil {
|
||||
// Write to w
|
||||
return
|
||||
} else if handled {
|
||||
return
|
||||
} else if handled, err = actor.GetInbox(c, w, r); err != nil {
|
||||
// Write to w
|
||||
return
|
||||
} else if handled {
|
||||
return
|
||||
}
|
||||
if handled, err := myPubber.GetInbox(c, w, r); err != nil {
|
||||
// Write to w
|
||||
} else if handled {
|
||||
return
|
||||
}
|
||||
// Handle non-ActivityPub request, such as responding with an HTML
|
||||
// representation with correct view permissions.
|
||||
// else:
|
||||
//
|
||||
// Handle non-ActivityPub request, such as serving a webpage.
|
||||
}
|
||||
// Add the handlers to a HTTP server
|
||||
serveMux := http.NewServeMux()
|
||||
serveMux.HandleFunc("/actor/outbox", outboxHandler)
|
||||
serveMux.HandleFunc("/actor/inbox", inboxHandler)
|
||||
var server http.Server
|
||||
server.Handler = serveMux
|
||||
```
|
||||
|
||||
Finally, to handle the second kind of request, use the `HandlerFunc` within HTTP
|
||||
handler functions in a similar way. There are two ways to create `HandlerFunc`,
|
||||
which depend on decisions we will address later:
|
||||
To serve ActivityStreams data:
|
||||
|
||||
```
|
||||
asHandler := pub.ServeActivityPubObject(...)
|
||||
var activityStreamHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||
```golang
|
||||
myHander := pub.NewActivityStreamsHandler(myDatabase, myClock)
|
||||
var activityStreamsHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||
c := context.Background()
|
||||
// Populate c with application specific information
|
||||
if handled, err := asHandler(c, w, r); err != nil {
|
||||
// Populate c with request-specific information
|
||||
if handled, err := myHandler(c, w, r); err != nil {
|
||||
// Write to w
|
||||
return
|
||||
} else if handled {
|
||||
return
|
||||
}
|
||||
// Handle non-ActivityPub request, such as responding with an HTML
|
||||
// representation with correct view permissions.
|
||||
// else:
|
||||
//
|
||||
// Handle non-ActivityPub request, such as serving a webpage.
|
||||
}
|
||||
serveMux.HandleFunc("/some/data/like/a/note", activityStreamsHandler)
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
Package `pub` relies on dependency injection to provide out-of-the-box support
|
||||
for ActivityPub. The interfaces to be satisfied are:
|
||||
|
||||
* `CommonBehavior` - Behavior needed regardless of which Protocol is used.
|
||||
* `SocialProtocol` - Behavior needed for the Social Protocol.
|
||||
* `FederatingProtocol` - Behavior needed for the Federating Protocol.
|
||||
* `Database` - The data store abstraction, not tied to the `database/sql`
|
||||
package.
|
||||
* `Clock` - The server's internal clock.
|
||||
* `Transport` - Responsible for the network that serves requests and deliveries
|
||||
of ActivityStreams data. A `HttpSigTransport` type is provided.
|
||||
|
||||
These implementations form the core of an application's behavior without
|
||||
worrying about the particulars and pitfalls of the ActivityPub protocol.
|
||||
Implementing these interfaces gives you greater assurance about being
|
||||
ActivityPub compliant.
|
||||
|
||||
### Application Logic
|
||||
|
||||
The `SocialProtocol` and `FederatingProtocol` are responsible for returning
|
||||
callback functions compatible with `streams.TypeResolver`. They also return
|
||||
`SocialWrappedCallbacks` and `FederatingWrappedCallbacks`, which are nothing
|
||||
more than a bundle of default behaviors for types like `Create`, `Update`, and
|
||||
so on.
|
||||
|
||||
Applications will want to focus on implementing their specific behaviors in the
|
||||
callbacks, and have fine-grained control over customization:
|
||||
|
||||
```golang
|
||||
// Implements the FederatingProtocol interface.
|
||||
//
|
||||
// This illustration can also be applied for the Social Protocol.
|
||||
func (m *myAppsFederatingProtocol) Callbacks(c context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}) {
|
||||
// The context 'c' has request-specific logic and can be used to apply complex
|
||||
// logic building the right behaviors, if desired.
|
||||
//
|
||||
// 'c' will later be passed through to the callbacks created below.
|
||||
wrapped = pub.FederatingWrappedCallbacks{
|
||||
Create: func(ctx context.Context, create vocab.ActivityStreamsCreate) error {
|
||||
// This function is wrapped by default behavior.
|
||||
//
|
||||
// More application specific logic can be written here.
|
||||
//
|
||||
// 'ctx' will have request-specific information from the HTTP handler. It
|
||||
// is the same as the 'c' passed to the Callbacks method.
|
||||
// 'create' has, at this point, already triggered the recommended
|
||||
// ActivityPub side effect behavior. The application can process it
|
||||
// further as needed.
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// The 'other' must contain functions that satisfy the signature pattern
|
||||
// required by streams.JSONResolver.
|
||||
//
|
||||
// If they are not, at runtime errors will be returned to indicate this.
|
||||
other = []interface{}{
|
||||
// The FederatingWrappedCallbacks has default behavior for an "Update" type,
|
||||
// but since we are providing this behavior in "other" and not in the
|
||||
// FederatingWrappedCallbacks.Update member, we will entirely replace the
|
||||
// default behavior provided by go-fed. Be careful that this still
|
||||
// implements ActivityPub properly.
|
||||
func(ctx context.Context, update vocab.ActivityStreamsUpdate) error {
|
||||
// This function is NOT wrapped by default behavior.
|
||||
//
|
||||
// Application specific logic can be written here.
|
||||
//
|
||||
// 'ctx' will have request-specific information from the HTTP handler. It
|
||||
// is the same as the 'c' passed to the Callbacks method.
|
||||
// 'update' will NOT trigger the recommended ActivityPub side effect
|
||||
// behavior. The application should do so in addition to any other custom
|
||||
// side effects required.
|
||||
return nil
|
||||
},
|
||||
// The "Listen" type has no default suggested behavior in ActivityPub, so
|
||||
// this just makes this application able to handle "Listen" activities.
|
||||
func(ctx context.Context, listen vocab.ActivityStreamsListen) error {
|
||||
// This function is NOT wrapped by default behavior. There's not a
|
||||
// FederatingWrappedCallbacks.Listen member to wrap.
|
||||
//
|
||||
// Application specific logic can be written here.
|
||||
//
|
||||
// 'ctx' will have request-specific information from the HTTP handler. It
|
||||
// is the same as the 'c' passed to the Callbacks method.
|
||||
// 'listen' can be processed with side effects as the application needs.
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
That's all that's required to support ActivityPub.
|
||||
The `pub` package supports applications that grow into more custom solutions by
|
||||
overriding the default behaviors as needed.
|
||||
|
||||
## How To Create
|
||||
### ActivityStreams Extensions: Future-Proofing An Application
|
||||
|
||||
You may have noticed that using the library is deceptively straightforward. This
|
||||
is because *creating* the `Pubber` and `HandlerFunc` types is not trivial and
|
||||
requires forethought.
|
||||
Package `pub` relies on the `streams.TypeResolver` and `streams.JSONResolver`
|
||||
code generated types. As new ActivityStreams extensions are developed and their
|
||||
code is generated, `pub` will automatically pick up support for these
|
||||
extensions.
|
||||
|
||||
There are a lot of interfaces that must be satisfied in order to have a complete
|
||||
working ActivityPub server.
|
||||
The steps to rapidly implement a new extension in a `pub` application are:
|
||||
|
||||
Note that `context.Context` is passed everywhere possible, to allow your
|
||||
implementation to keep a request-specific context throughout the lifecycle of
|
||||
an ActivityPub request.
|
||||
1. Generate an OWL definition of the ActivityStreams extension. This definition
|
||||
could be the same one defining the vocabulary at the `@context` IRI.
|
||||
2. Run `astool` to autogenerate the golang types in the `streams` package.
|
||||
3. Implement the application's callbacks in the `FederatingProtocol.Callbacks`
|
||||
or `SocialProtocol.Callbacks` for the new behaviors needed.
|
||||
4. Build the application, which builds `pub`, with the newly generated `streams`
|
||||
code. No code changes in `pub` are required.
|
||||
|
||||
### Application Interface
|
||||
Whether an author of an ActivityStreams extension or an application developer,
|
||||
these quick steps should reduce the barrier to adopion in a statically-typed
|
||||
environment.
|
||||
|
||||
Regardless of which of the SocialAPI and FederateAPI chosen, the `Application`
|
||||
interface contains the set of core methods fundamental to the functionality of
|
||||
this library. It contains a lot of the storage fetching and writing, all of
|
||||
which is keyed by `*url.URL`. To protect against race conditions, this library
|
||||
will inform whether it is fetching data to read-only or fetching for read-or-
|
||||
write.
|
||||
### DelegateActor
|
||||
|
||||
Note that under some conditions, ActivityPub verifies the peer's request. It
|
||||
does so using HTTP Signatures. However, this requires knowing the other party's
|
||||
public key, and fetching this remotely is do-able. However, this library assumes
|
||||
this server already has it locally; at this time it is up to implementations to
|
||||
remotely fetch it if needed.
|
||||
For those that need a near-complete custom ActivityPub solution, or want to have
|
||||
that possibility in the future after adopting go-fed, the `DelegateActor`
|
||||
interface can be used to obtain an `Actor`:
|
||||
|
||||
### SocialAPI and FederateAPI Interfaces
|
||||
```golang
|
||||
// Use custom ActivityPub implementation
|
||||
actor = pub.NewCustomActor(
|
||||
myDelegateActor,
|
||||
isSocialProtocolEnabled,
|
||||
isFederatedProtocolEnabled,
|
||||
myAppsClock)
|
||||
```
|
||||
|
||||
These interfaces capture additional behaviors required by the SocialAPI and the
|
||||
FederateAPI.
|
||||
|
||||
The SocialAPI can additionally provide a mechanism for client authentication and
|
||||
authorization using frameworks like Oauth 2.0. Such frameworks are not natively
|
||||
supported in this library and must be supplied.
|
||||
|
||||
### Callbacker Interface
|
||||
|
||||
One of these is needed per ActivityPub API supported. For example, if both the
|
||||
SocialAPI and FederateAPI are supported, then two of these are needed.
|
||||
|
||||
Upon receiving one of these activities from a `POST` to the inbox or outbox, the
|
||||
correct callbacker will be called to handle either a SocialAPI activity or a
|
||||
FederateAPI activity.
|
||||
|
||||
This is where the bulk of implementation-specific logic is expected to reside.
|
||||
|
||||
Do note that for some of these activities, default actions will already occur.
|
||||
For example, if receiving an `Accept` in response to a sent `Follow`, this
|
||||
library automatically handles adding the correct actor into the correct
|
||||
`following` collection. This means a lot of the social and federate
|
||||
functionality is provided out of the box.
|
||||
|
||||
### Deliverer Interface
|
||||
|
||||
This is an optional interface. Since this library needs to send HTTP requests,
|
||||
it would be unwise for it to provide no way of allowing implementations to
|
||||
rate limit, persist across downtime, back off, etc. This interface is satisfied
|
||||
by the `go-fed/activity/deliverer` package which has an implementation that can
|
||||
remember to send requests across downtime.
|
||||
|
||||
If an implementation does not care to have this level of control, a synchronous
|
||||
implementation is very straightforward to make.
|
||||
|
||||
### Other Interfaces
|
||||
|
||||
Other interfaces such as `Typer` and `PubObject` are meant to limit modification
|
||||
scope or require minimal ActivityStream compatibility to be used by this
|
||||
library. As long as the `go-fed/activity/vocab` or `go-fed/activity/streams`
|
||||
packages are being used, these interfaces will be natively supported.
|
||||
|
||||
## Other Considerations
|
||||
|
||||
This library does not have an implementation report generated... yet! Once it is
|
||||
available, it will be linked here. Furthermore, the test server will also be an
|
||||
excellent tutorial resource. Unfortunately such a resource does not exist...
|
||||
yet!
|
||||
It does not guarantee that an implementation adheres to the ActivityPub
|
||||
specification. It acts as a stepping stone for applications that want to build
|
||||
up to a fully custom solution and not be locked into the `pub` package
|
||||
implementation.
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
)
|
||||
|
||||
// Activity represents any ActivityStreams Activity type.
|
||||
//
|
||||
// The Activity types provided in the streams package implement this.
|
||||
type Activity interface {
|
||||
// Activity is also a vocab.Type
|
||||
vocab.Type
|
||||
// GetActivityStreamsActor returns the "actor" property if it exists, and
|
||||
// nil otherwise.
|
||||
GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
|
||||
// GetActivityStreamsAudience returns the "audience" property if it
|
||||
// exists, and nil otherwise.
|
||||
GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty
|
||||
// GetActivityStreamsBcc returns the "bcc" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty
|
||||
// GetActivityStreamsBto returns the "bto" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty
|
||||
// GetActivityStreamsCc returns the "cc" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
|
||||
// GetActivityStreamsTo returns the "to" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsTo() vocab.ActivityStreamsToProperty
|
||||
// GetActivityStreamsAttributedTo returns the "attributedTo" property if
|
||||
// it exists, and nil otherwise.
|
||||
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
|
||||
// GetActivityStreamsObject returns the "object" property if it exists,
|
||||
// and nil otherwise.
|
||||
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
|
||||
// SetActivityStreamsActor sets the "actor" property.
|
||||
SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty)
|
||||
// SetActivityStreamsObject sets the "object" property.
|
||||
SetActivityStreamsObject(i vocab.ActivityStreamsObjectProperty)
|
||||
// SetActivityStreamsTo sets the "to" property.
|
||||
SetActivityStreamsTo(i vocab.ActivityStreamsToProperty)
|
||||
// SetActivityStreamsBto sets the "bto" property.
|
||||
SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty)
|
||||
// SetActivityStreamsBcc sets the "bcc" property.
|
||||
SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty)
|
||||
// SetActivityStreamsAttributedTo sets the "attributedTo" property.
|
||||
SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty)
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Actor represents ActivityPub's actor concept. It conceptually has an inbox
|
||||
// and outbox that receives either a POST or GET request, which triggers side
|
||||
// effects in the federating application.
|
||||
//
|
||||
// An Actor within an application may federate server-to-server (Federation
|
||||
// Protocol), client-to-server (Social API), or both. The Actor represents the
|
||||
// server in either use case.
|
||||
//
|
||||
// An actor can be created by calling NewSocialActor (only the Social Protocol
|
||||
// is supported), NewFederatingActor (only the Federating Protocol is
|
||||
// supported), NewActor (both are supported), or NewCustomActor (neither are).
|
||||
//
|
||||
// Not all Actors have the same behaviors depending on the constructor used to
|
||||
// create them. Refer to the constructor's documentation to determine the exact
|
||||
// behavior of the Actor on an application.
|
||||
//
|
||||
// The behaviors documented here are common to all Actors returned by any
|
||||
// constructor.
|
||||
type Actor interface {
|
||||
// PostInbox returns true if the request was handled as an ActivityPub
|
||||
// POST to an actor's inbox. If false, the request was not an
|
||||
// ActivityPub request and may still be handled by the caller in
|
||||
// another way, such as serving a web page.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the Actor was constructed with the Federated Protocol enabled,
|
||||
// side effects will occur.
|
||||
//
|
||||
// If the Federated Protocol is not enabled, writes the
|
||||
// http.StatusMethodNotAllowed status code in the response. No side
|
||||
// effects occur.
|
||||
//
|
||||
// The request and data of your application will be interpreted as
|
||||
// having an HTTPS protocol scheme.
|
||||
PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
||||
// PostInboxScheme is similar to PostInbox, except clients are able to
|
||||
// specify which protocol scheme to handle the incoming request and the
|
||||
// data stored within the application (HTTP, HTTPS, etc).
|
||||
PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error)
|
||||
// GetInbox returns true if the request was handled as an ActivityPub
|
||||
// GET to an actor's inbox. If false, the request was not an ActivityPub
|
||||
// request and may still be handled by the caller in another way, such
|
||||
// as serving a web page.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the request is an ActivityPub request, the Actor will defer to the
|
||||
// application to determine the correct authorization of the request and
|
||||
// the resulting OrderedCollection to respond with. The Actor handles
|
||||
// serializing this OrderedCollection and responding with the correct
|
||||
// headers and http.StatusOK.
|
||||
GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
||||
// PostOutbox returns true if the request was handled as an ActivityPub
|
||||
// POST to an actor's outbox. If false, the request was not an
|
||||
// ActivityPub request and may still be handled by the caller in another
|
||||
// way, such as serving a web page.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the Actor was constructed with the Social Protocol enabled, side
|
||||
// effects will occur.
|
||||
//
|
||||
// If the Social Protocol is not enabled, writes the
|
||||
// http.StatusMethodNotAllowed status code in the response. No side
|
||||
// effects occur.
|
||||
//
|
||||
// If the Social and Federated Protocol are both enabled, it will handle
|
||||
// the side effects of receiving an ActivityStream Activity, and then
|
||||
// federate the Activity to peers.
|
||||
//
|
||||
// The request will be interpreted as having an HTTPS scheme.
|
||||
PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
||||
// PostOutboxScheme is similar to PostOutbox, except clients are able to
|
||||
// specify which protocol scheme to handle the incoming request and the
|
||||
// data stored within the application (HTTP, HTTPS, etc).
|
||||
PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error)
|
||||
// GetOutbox returns true if the request was handled as an ActivityPub
|
||||
// GET to an actor's outbox. If false, the request was not an
|
||||
// ActivityPub request.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the request is an ActivityPub request, the Actor will defer to the
|
||||
// application to determine the correct authorization of the request and
|
||||
// the resulting OrderedCollection to respond with. The Actor handles
|
||||
// serializing this OrderedCollection and responding with the correct
|
||||
// headers and http.StatusOK.
|
||||
GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
|
||||
}
|
||||
|
||||
// FederatingActor is an Actor that allows programmatically delivering an
|
||||
// Activity to a federating peer.
|
||||
type FederatingActor interface {
|
||||
Actor
|
||||
// Send a federated activity.
|
||||
//
|
||||
// The provided url must be the outbox of the sender. All processing of
|
||||
// the activity occurs similarly to the C2S flow:
|
||||
// - If t is not an Activity, it is wrapped in a Create activity.
|
||||
// - A new ID is generated for the activity.
|
||||
// - The activity is added to the specified outbox.
|
||||
// - The activity is prepared and delivered to recipients.
|
||||
//
|
||||
// Note that this function will only behave as expected if the
|
||||
// implementation has been constructed to support federation. This
|
||||
// method will guaranteed work for non-custom Actors. For custom actors,
|
||||
// care should be used to not call this method if only C2S is supported.
|
||||
Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error)
|
||||
}
|
|
@ -0,0 +1,494 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// baseActor must satisfy the Actor interface.
|
||||
var _ Actor = &baseActor{}
|
||||
|
||||
// baseActor is an application-independent ActivityPub implementation. It does
|
||||
// not implement the entire protocol, and relies on a delegate to do so. It
|
||||
// only implements the part of the protocol that is side-effect-free, allowing
|
||||
// an existing application to write a DelegateActor that glues their application
|
||||
// into the ActivityPub world.
|
||||
//
|
||||
// It is preferred to use a DelegateActor provided by this library, so that the
|
||||
// application does not need to worry about the ActivityPub implementation.
|
||||
type baseActor struct {
|
||||
// delegate contains application-specific delegation logic.
|
||||
delegate DelegateActor
|
||||
// enableSocialProtocol enables or disables the Social API, the client to
|
||||
// server part of ActivityPub. Useful if permitting remote clients to
|
||||
// act on behalf of the users of the client application.
|
||||
enableSocialProtocol bool
|
||||
// enableFederatedProtocol enables or disables the Federated Protocol, or the
|
||||
// server to server part of ActivityPub. Useful to permit integrating
|
||||
// with the rest of the federative web.
|
||||
enableFederatedProtocol bool
|
||||
// clock simply tracks the current time.
|
||||
clock Clock
|
||||
}
|
||||
|
||||
// baseActorFederating must satisfy the FederatingActor interface.
|
||||
var _ FederatingActor = &baseActorFederating{}
|
||||
|
||||
// baseActorFederating is a baseActor that also satisfies the FederatingActor
|
||||
// interface.
|
||||
//
|
||||
// The baseActor is preserved as an Actor which will not successfully cast to a
|
||||
// FederatingActor.
|
||||
type baseActorFederating struct {
|
||||
baseActor
|
||||
}
|
||||
|
||||
// NewSocialActor builds a new Actor concept that handles only the Social
|
||||
// Protocol part of ActivityPub.
|
||||
//
|
||||
// This Actor can be created once in an application and reused to handle
|
||||
// multiple requests concurrently and for different endpoints.
|
||||
//
|
||||
// It leverages as much of go-fed as possible to ensure the implementation is
|
||||
// compliant with the ActivityPub specification, while providing enough freedom
|
||||
// to be productive without shooting one's self in the foot.
|
||||
//
|
||||
// Do not try to use NewSocialActor and NewFederatingActor together to cover
|
||||
// both the Social and Federating parts of the protocol. Instead, use NewActor.
|
||||
func NewSocialActor(c CommonBehavior,
|
||||
c2s SocialProtocol,
|
||||
db Database,
|
||||
clock Clock) Actor {
|
||||
return &baseActor{
|
||||
delegate: &sideEffectActor{
|
||||
common: c,
|
||||
c2s: c2s,
|
||||
db: db,
|
||||
clock: clock,
|
||||
},
|
||||
enableSocialProtocol: true,
|
||||
clock: clock,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFederatingActor builds a new Actor concept that handles only the Federating
|
||||
// Protocol part of ActivityPub.
|
||||
//
|
||||
// This Actor can be created once in an application and reused to handle
|
||||
// multiple requests concurrently and for different endpoints.
|
||||
//
|
||||
// It leverages as much of go-fed as possible to ensure the implementation is
|
||||
// compliant with the ActivityPub specification, while providing enough freedom
|
||||
// to be productive without shooting one's self in the foot.
|
||||
//
|
||||
// Do not try to use NewSocialActor and NewFederatingActor together to cover
|
||||
// both the Social and Federating parts of the protocol. Instead, use NewActor.
|
||||
func NewFederatingActor(c CommonBehavior,
|
||||
s2s FederatingProtocol,
|
||||
db Database,
|
||||
clock Clock) FederatingActor {
|
||||
return &baseActorFederating{
|
||||
baseActor{
|
||||
delegate: &sideEffectActor{
|
||||
common: c,
|
||||
s2s: s2s,
|
||||
db: db,
|
||||
clock: clock,
|
||||
},
|
||||
enableFederatedProtocol: true,
|
||||
clock: clock,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewActor builds a new Actor concept that handles both the Social and
|
||||
// Federating Protocol parts of ActivityPub.
|
||||
//
|
||||
// This Actor can be created once in an application and reused to handle
|
||||
// multiple requests concurrently and for different endpoints.
|
||||
//
|
||||
// It leverages as much of go-fed as possible to ensure the implementation is
|
||||
// compliant with the ActivityPub specification, while providing enough freedom
|
||||
// to be productive without shooting one's self in the foot.
|
||||
func NewActor(c CommonBehavior,
|
||||
c2s SocialProtocol,
|
||||
s2s FederatingProtocol,
|
||||
db Database,
|
||||
clock Clock) FederatingActor {
|
||||
return &baseActorFederating{
|
||||
baseActor{
|
||||
delegate: &sideEffectActor{
|
||||
common: c,
|
||||
c2s: c2s,
|
||||
s2s: s2s,
|
||||
db: db,
|
||||
clock: clock,
|
||||
},
|
||||
enableSocialProtocol: true,
|
||||
enableFederatedProtocol: true,
|
||||
clock: clock,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomActor allows clients to create a custom ActivityPub implementation
|
||||
// for the Social Protocol, Federating Protocol, or both.
|
||||
//
|
||||
// It still uses the library as a high-level scaffold, which has the benefit of
|
||||
// allowing applications to grow into a custom ActivityPub solution without
|
||||
// having to refactor the code that passes HTTP requests into the Actor.
|
||||
//
|
||||
// It is possible to create a DelegateActor that is not ActivityPub compliant.
|
||||
// Use with due care.
|
||||
func NewCustomActor(delegate DelegateActor,
|
||||
enableSocialProtocol, enableFederatedProtocol bool,
|
||||
clock Clock) FederatingActor {
|
||||
return &baseActorFederating{
|
||||
baseActor{
|
||||
delegate: delegate,
|
||||
enableSocialProtocol: enableSocialProtocol,
|
||||
enableFederatedProtocol: enableFederatedProtocol,
|
||||
clock: clock,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PostInbox implements the generic algorithm for handling a POST request to an
|
||||
// actor's inbox independent on an application. It relies on a delegate to
|
||||
// implement application specific functionality.
|
||||
//
|
||||
// Only supports serving data with identifiers having the HTTPS scheme.
|
||||
func (b *baseActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
return b.PostInboxScheme(c, w, r, "https")
|
||||
}
|
||||
|
||||
// PostInbox implements the generic algorithm for handling a POST request to an
|
||||
// actor's inbox independent on an application. It relies on a delegate to
|
||||
// implement application specific functionality.
|
||||
//
|
||||
// Specifying the "scheme" allows for retrieving ActivityStreams content with
|
||||
// identifiers such as HTTP, HTTPS, or other protocol schemes.
|
||||
func (b *baseActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
|
||||
// Do nothing if it is not an ActivityPub POST request.
|
||||
if !isActivityPubPost(r) {
|
||||
return false, nil
|
||||
}
|
||||
// If the Federated Protocol is not enabled, then this endpoint is not
|
||||
// enabled.
|
||||
if !b.enableFederatedProtocol {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return true, nil
|
||||
}
|
||||
// Check the peer request is authentic.
|
||||
c, authenticated, err := b.delegate.AuthenticatePostInbox(c, w, r)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if !authenticated {
|
||||
return true, nil
|
||||
}
|
||||
// Begin processing the request, but have not yet applied
|
||||
// authorization (ex: blocks). Obtain the activity reject unknown
|
||||
// activities.
|
||||
raw, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err = json.Unmarshal(raw, &m); err != nil {
|
||||
return true, err
|
||||
}
|
||||
asValue, err := streams.ToType(c, m)
|
||||
if err != nil && !streams.IsUnmatchedErr(err) {
|
||||
return true, err
|
||||
} else if streams.IsUnmatchedErr(err) {
|
||||
// Respond with bad request -- we do not understand the type.
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return true, nil
|
||||
}
|
||||
activity, ok := asValue.(Activity)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("activity streams value is not an Activity: %T", asValue)
|
||||
}
|
||||
if activity.GetJSONLDId() == nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return true, nil
|
||||
}
|
||||
// Allow server implementations to set context data with a hook.
|
||||
c, err = b.delegate.PostInboxRequestBodyHook(c, r, activity)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Check authorization of the activity.
|
||||
authorized, err := b.delegate.AuthorizePostInbox(c, w, activity)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if !authorized {
|
||||
return true, nil
|
||||
}
|
||||
// Post the activity to the actor's inbox and trigger side effects for
|
||||
// that particular Activity type. It is up to the delegate to resolve
|
||||
// the given map.
|
||||
inboxId := requestId(r, scheme)
|
||||
err = b.delegate.PostInbox(c, inboxId, activity)
|
||||
if err != nil {
|
||||
// Special case: We know it is a bad request if the object or
|
||||
// target properties needed to be populated, but weren't.
|
||||
//
|
||||
// Send the rejection to the peer.
|
||||
if err == ErrObjectRequired || err == ErrTargetRequired {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return true, nil
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
// Our side effects are complete, now delegate determining whether to
|
||||
// do inbox forwarding, as well as the action to do it.
|
||||
if err := b.delegate.InboxForwarding(c, inboxId, activity); err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Request has been processed. Begin responding to the request.
|
||||
//
|
||||
// Simply respond with an OK status to the peer.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetInbox implements the generic algorithm for handling a GET request to an
|
||||
// actor's inbox independent on an application. It relies on a delegate to
|
||||
// implement application specific functionality.
|
||||
func (b *baseActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
// Do nothing if it is not an ActivityPub GET request.
|
||||
if !isActivityPubGet(r) {
|
||||
return false, nil
|
||||
}
|
||||
// Delegate authenticating and authorizing the request.
|
||||
c, authenticated, err := b.delegate.AuthenticateGetInbox(c, w, r)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if !authenticated {
|
||||
return true, nil
|
||||
}
|
||||
// Everything is good to begin processing the request.
|
||||
oc, err := b.delegate.GetInbox(c, r)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Deduplicate the 'orderedItems' property by ID.
|
||||
err = dedupeOrderedItems(oc)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Request has been processed. Begin responding to the request.
|
||||
//
|
||||
// Serialize the OrderedCollection.
|
||||
m, err := streams.Serialize(oc)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Write the response.
|
||||
addResponseHeaders(w.Header(), b.clock, raw)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
n, err := w.Write(raw)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if n != len(raw) {
|
||||
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PostOutbox implements the generic algorithm for handling a POST request to an
|
||||
// actor's outbox independent on an application. It relies on a delegate to
|
||||
// implement application specific functionality.
|
||||
//
|
||||
// Only supports serving data with identifiers having the HTTPS scheme.
|
||||
func (b *baseActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
return b.PostOutboxScheme(c, w, r, "https")
|
||||
}
|
||||
|
||||
// PostOutbox implements the generic algorithm for handling a POST request to an
|
||||
// actor's outbox independent on an application. It relies on a delegate to
|
||||
// implement application specific functionality.
|
||||
//
|
||||
// Specifying the "scheme" allows for retrieving ActivityStreams content with
|
||||
// identifiers such as HTTP, HTTPS, or other protocol schemes.
|
||||
func (b *baseActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
|
||||
// Do nothing if it is not an ActivityPub POST request.
|
||||
if !isActivityPubPost(r) {
|
||||
return false, nil
|
||||
}
|
||||
// If the Social API is not enabled, then this endpoint is not enabled.
|
||||
if !b.enableSocialProtocol {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return true, nil
|
||||
}
|
||||
// Delegate authenticating and authorizing the request.
|
||||
c, authenticated, err := b.delegate.AuthenticatePostOutbox(c, w, r)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if !authenticated {
|
||||
return true, nil
|
||||
}
|
||||
// Everything is good to begin processing the request.
|
||||
raw, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err = json.Unmarshal(raw, &m); err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Note that converting to a Type will NOT successfully convert types
|
||||
// not known to go-fed. This prevents accidentally wrapping an Activity
|
||||
// type unknown to go-fed in a Create below. Instead,
|
||||
// streams.ErrUnhandledType will be returned here.
|
||||
asValue, err := streams.ToType(c, m)
|
||||
if err != nil && !streams.IsUnmatchedErr(err) {
|
||||
return true, err
|
||||
} else if streams.IsUnmatchedErr(err) {
|
||||
// Respond with bad request -- we do not understand the type.
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return true, nil
|
||||
}
|
||||
// Allow server implementations to set context data with a hook.
|
||||
c, err = b.delegate.PostOutboxRequestBodyHook(c, r, asValue)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// The HTTP request steps are complete, complete the rest of the outbox
|
||||
// and delivery process.
|
||||
outboxId := requestId(r, scheme)
|
||||
activity, err := b.deliver(c, outboxId, asValue, m)
|
||||
// Special case: We know it is a bad request if the object or
|
||||
// target properties needed to be populated, but weren't.
|
||||
//
|
||||
// Send the rejection to the client.
|
||||
if err == ErrObjectRequired || err == ErrTargetRequired {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return true, nil
|
||||
} else if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Respond to the request with the new Activity's IRI location.
|
||||
w.Header().Set(locationHeader, activity.GetJSONLDId().Get().String())
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetOutbox implements the generic algorithm for handling a Get request to an
|
||||
// actor's outbox independent on an application. It relies on a delegate to
|
||||
// implement application specific functionality.
|
||||
func (b *baseActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
// Do nothing if it is not an ActivityPub GET request.
|
||||
if !isActivityPubGet(r) {
|
||||
return false, nil
|
||||
}
|
||||
// Delegate authenticating and authorizing the request.
|
||||
c, authenticated, err := b.delegate.AuthenticateGetOutbox(c, w, r)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if !authenticated {
|
||||
return true, nil
|
||||
}
|
||||
// Everything is good to begin processing the request.
|
||||
oc, err := b.delegate.GetOutbox(c, r)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Request has been processed. Begin responding to the request.
|
||||
//
|
||||
// Serialize the OrderedCollection.
|
||||
m, err := streams.Serialize(oc)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// Write the response.
|
||||
addResponseHeaders(w.Header(), b.clock, raw)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
n, err := w.Write(raw)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if n != len(raw) {
|
||||
return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// deliver delegates all outbox handling steps and optionally will federate the
|
||||
// activity if the federated protocol is enabled.
|
||||
//
|
||||
// This function is not exported so an Actor that only supports C2S cannot be
|
||||
// type casted to a FederatingActor. It doesn't exactly fit the Send method
|
||||
// signature anyways.
|
||||
//
|
||||
// Note: 'm' is nilable.
|
||||
func (b *baseActor) deliver(c context.Context, outbox *url.URL, asValue vocab.Type, m map[string]interface{}) (activity Activity, err error) {
|
||||
// If the value is not an Activity or type extending from Activity, then
|
||||
// we need to wrap it in a Create Activity.
|
||||
if !streams.IsOrExtendsActivityStreamsActivity(asValue) {
|
||||
asValue, err = b.delegate.WrapInCreate(c, asValue, outbox)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// At this point, this should be a safe conversion. If this error is
|
||||
// triggered, then there is either a bug in the delegation of
|
||||
// WrapInCreate, behavior is not lining up in the generated ExtendedBy
|
||||
// code, or something else is incorrect with the type system.
|
||||
var ok bool
|
||||
activity, ok = asValue.(Activity)
|
||||
if !ok {
|
||||
err = fmt.Errorf("activity streams value is not an Activity: %T", asValue)
|
||||
return
|
||||
}
|
||||
// Delegate generating new IDs for the activity and all new objects.
|
||||
if err = b.delegate.AddNewIDs(c, activity); err != nil {
|
||||
return
|
||||
}
|
||||
// Post the activity to the actor's outbox and trigger side effects for
|
||||
// that particular Activity type.
|
||||
//
|
||||
// Since 'm' is nil-able and side effects may need access to literal nil
|
||||
// values, such as for Update activities, ensure 'm' is non-nil.
|
||||
if m == nil {
|
||||
m, err = asValue.Serialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
deliverable, err := b.delegate.PostOutbox(c, activity, outbox, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Request has been processed and all side effects internal to this
|
||||
// application server have finished. Begin side effects affecting other
|
||||
// servers and/or the client who sent this request.
|
||||
//
|
||||
// If we are federating and the type is a deliverable one, then deliver
|
||||
// the activity to federating peers.
|
||||
if b.enableFederatedProtocol && deliverable {
|
||||
if err = b.delegate.Deliver(c, outbox, activity); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send is programmatically accessible if the federated protocol is enabled.
|
||||
func (b *baseActorFederating) Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error) {
|
||||
return b.deliver(c, outbox, t, nil)
|
||||
}
|
|
@ -0,0 +1,756 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/golang/mock/gomock"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBaseActorSocialProtocol tests the Actor returned with NewCustomActor
|
||||
// and only having the SocialProtocol enabled.
|
||||
func TestBaseActorSocialProtocol(t *testing.T) {
|
||||
// Set up test case
|
||||
setupData()
|
||||
ctx := context.Background()
|
||||
setupFn := func(ctl *gomock.Controller) (delegate *MockDelegateActor, clock *MockClock, a Actor) {
|
||||
delegate = NewMockDelegateActor(ctl)
|
||||
clock = NewMockClock(ctl)
|
||||
a = NewCustomActor(
|
||||
delegate,
|
||||
/*enableSocialProtocol=*/ true,
|
||||
/*enableFederatedProtocol=*/ false,
|
||||
clock)
|
||||
return
|
||||
}
|
||||
// Run tests
|
||||
t.Run("PostInboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toPostInboxRequest(testCreate)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("PostInboxNotAllowed", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxRequest(testCreate))
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusMethodNotAllowed)
|
||||
})
|
||||
t.Run("GetInboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toGetInboxRequest()
|
||||
// Run the test
|
||||
handled, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("GetInboxDeniesIfNotAuthenticated", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetInboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetInbox(ctx, resp, req).DoAndReturn(func(ctx context.Context, resp http.ResponseWriter, req *http.Request) (context.Context, bool, error) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
return ctx, false, nil
|
||||
})
|
||||
// Run the test
|
||||
handled, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusForbidden)
|
||||
})
|
||||
t.Run("GetInboxRespondsWithDataAndHeaders", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, clock, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetInboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().GetInbox(ctx, req).Return(testOrderedCollectionUniqueElems, nil)
|
||||
clock.EXPECT().Now().Return(now())
|
||||
// Run the test
|
||||
handled, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusOK)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(contentTypeHeader), "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
|
||||
assertEqual(t, respV.Header.Get(dateHeader), nowDateHeader())
|
||||
assertNotEqual(t, len(respV.Header.Get(digestHeader)), 0)
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, []byte(testOrderedCollectionUniqueElemsString))
|
||||
})
|
||||
t.Run("GetInboxDeduplicatesData", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, clock, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetInboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().GetInbox(ctx, req).Return(testOrderedCollectionDupedElems, nil)
|
||||
clock.EXPECT().Now().Return(now())
|
||||
// Run the test
|
||||
_, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
respV := resp.Result()
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, []byte(testOrderedCollectionDedupedElemsString))
|
||||
})
|
||||
t.Run("PostOutboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toPostOutboxRequest(testCreateNoId)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("PostOutboxDeniesIfNotAuthenticated", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).DoAndReturn(func(ctx context.Context, resp http.ResponseWriter, req *http.Request) (context.Context, bool, error) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
return ctx, false, nil
|
||||
})
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusForbidden)
|
||||
})
|
||||
t.Run("PostOutboxBadRequestIfUnknownType", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxUnknownRequest())
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusBadRequest)
|
||||
})
|
||||
t.Run("PostOutboxRespondsWithDataAndHeaders", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostOutboxRequestBodyHook(ctx, req, toDeserializedForm(testCreateNoId)).Return(ctx, nil)
|
||||
delegate.EXPECT().AddNewIDs(ctx, toDeserializedForm(testCreateNoId)).DoAndReturn(func(c context.Context, activity Activity) error {
|
||||
withNewId(activity)
|
||||
return nil
|
||||
})
|
||||
delegate.EXPECT().PostOutbox(
|
||||
ctx,
|
||||
withNewId(toDeserializedForm(testCreateNoId)),
|
||||
mustParse(testMyOutboxIRI),
|
||||
mustSerialize(testCreateNoId),
|
||||
).Return(true, nil)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusCreated)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(locationHeader), testNewActivityIRI)
|
||||
})
|
||||
t.Run("PostOutboxWrapsInCreate", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testMyNote))
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostOutboxRequestBodyHook(ctx, req, toDeserializedForm(testMyNote)).Return(ctx, nil)
|
||||
delegate.EXPECT().WrapInCreate(ctx, toDeserializedForm(testMyNote), mustParse(testMyOutboxIRI)).DoAndReturn(func(c context.Context, t vocab.Type, u *url.URL) (vocab.ActivityStreamsCreate, error) {
|
||||
return wrappedInCreate(t), nil
|
||||
})
|
||||
delegate.EXPECT().AddNewIDs(ctx, wrappedInCreate(toDeserializedForm(testMyNote))).DoAndReturn(func(c context.Context, activity Activity) error {
|
||||
withNewId(activity)
|
||||
return nil
|
||||
})
|
||||
delegate.EXPECT().PostOutbox(
|
||||
ctx,
|
||||
withNewId(wrappedInCreate(toDeserializedForm(testMyNote))),
|
||||
mustParse(testMyOutboxIRI),
|
||||
mustSerialize(toDeserializedForm(testMyNote)),
|
||||
).Return(true, nil)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusCreated)
|
||||
})
|
||||
t.Run("PostOutboxBadRequestForErrObjectRequired", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostOutboxRequestBodyHook(ctx, req, toDeserializedForm(testCreateNoId)).Return(ctx, nil)
|
||||
delegate.EXPECT().AddNewIDs(ctx, toDeserializedForm(testCreateNoId)).DoAndReturn(func(c context.Context, activity Activity) error {
|
||||
withNewId(activity)
|
||||
return nil
|
||||
})
|
||||
delegate.EXPECT().PostOutbox(
|
||||
ctx,
|
||||
withNewId(toDeserializedForm(testCreateNoId)),
|
||||
mustParse(testMyOutboxIRI),
|
||||
mustSerialize(testCreateNoId),
|
||||
).Return(true, ErrObjectRequired)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusBadRequest)
|
||||
})
|
||||
t.Run("PostOutboxBadRequestForErrTargetRequired", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostOutboxRequestBodyHook(ctx, req, toDeserializedForm(testCreateNoId)).Return(ctx, nil)
|
||||
delegate.EXPECT().AddNewIDs(ctx, toDeserializedForm(testCreateNoId)).DoAndReturn(func(c context.Context, activity Activity) error {
|
||||
withNewId(activity)
|
||||
return nil
|
||||
})
|
||||
delegate.EXPECT().PostOutbox(
|
||||
ctx,
|
||||
withNewId(toDeserializedForm(testCreateNoId)),
|
||||
mustParse(testMyOutboxIRI),
|
||||
mustSerialize(testCreateNoId),
|
||||
).Return(true, ErrTargetRequired)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusBadRequest)
|
||||
})
|
||||
t.Run("GetOutboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toGetOutboxRequest()
|
||||
// Run the test
|
||||
handled, err := a.GetOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("GetOutboxDeniesIfNotAuthenticated", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetOutboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetOutbox(ctx, resp, req).DoAndReturn(func(ctx context.Context, resp http.ResponseWriter, req *http.Request) (context.Context, bool, error) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
return ctx, false, nil
|
||||
})
|
||||
// Run the test
|
||||
handled, err := a.GetOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusForbidden)
|
||||
})
|
||||
t.Run("GetOutboxRespondsWithDataAndHeaders", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, clock, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetOutboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().GetOutbox(ctx, req).Return(testOrderedCollectionUniqueElems, nil)
|
||||
clock.EXPECT().Now().Return(now())
|
||||
// Run the test
|
||||
handled, err := a.GetOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusOK)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(contentTypeHeader), "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
|
||||
assertEqual(t, respV.Header.Get(dateHeader), nowDateHeader())
|
||||
assertNotEqual(t, len(respV.Header.Get(digestHeader)), 0)
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, []byte(testOrderedCollectionUniqueElemsString))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBaseActorFederatingProtocol tests the Actor returned with
|
||||
// NewCustomActor and only having the FederatingProtocol enabled.
|
||||
func TestBaseActorFederatingProtocol(t *testing.T) {
|
||||
// Set up test case
|
||||
setupData()
|
||||
ctx := context.Background()
|
||||
setupFn := func(ctl *gomock.Controller) (delegate *MockDelegateActor, clock *MockClock, a Actor) {
|
||||
delegate = NewMockDelegateActor(ctl)
|
||||
clock = NewMockClock(ctl)
|
||||
a = NewCustomActor(
|
||||
delegate,
|
||||
/*enableSocialProtocol=*/ false,
|
||||
/*enableFederatedProtocol=*/ true,
|
||||
clock)
|
||||
return
|
||||
}
|
||||
// Run tests
|
||||
t.Run("PostInboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toPostInboxRequest(testCreate)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("PostInboxDeniesIfNotAuthenticated", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxRequest(testCreate))
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).DoAndReturn(func(ctx context.Context, resp http.ResponseWriter, req *http.Request) (context.Context, bool, error) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
return ctx, false, nil
|
||||
})
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusForbidden)
|
||||
})
|
||||
t.Run("PostInboxBadRequestIfUnknownType", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxUnknownRequest())
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusBadRequest)
|
||||
})
|
||||
t.Run("PostInboxBadRequestIfActivityHasNoId", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusBadRequest)
|
||||
})
|
||||
t.Run("PostInboxDeniesIfNotAuthorized", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxRequest(testCreate))
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostInboxRequestBodyHook(ctx, req, toDeserializedForm(testCreate)).Return(ctx, nil)
|
||||
delegate.EXPECT().AuthorizePostInbox(ctx, resp, toDeserializedForm(testCreate)).DoAndReturn(func(ctx context.Context, resp http.ResponseWriter, activity Activity) (bool, error) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
return false, nil
|
||||
})
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusForbidden)
|
||||
})
|
||||
t.Run("PostInboxRespondsWithStatus", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxRequest(testCreate))
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostInboxRequestBodyHook(ctx, req, toDeserializedForm(testCreate)).Return(ctx, nil)
|
||||
delegate.EXPECT().AuthorizePostInbox(ctx, resp, toDeserializedForm(testCreate)).Return(true, nil)
|
||||
delegate.EXPECT().PostInbox(ctx, mustParse(testMyInboxIRI), toDeserializedForm(testCreate)).Return(nil)
|
||||
delegate.EXPECT().InboxForwarding(ctx, mustParse(testMyInboxIRI), toDeserializedForm(testCreate)).Return(nil)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusOK)
|
||||
})
|
||||
t.Run("PostInboxBadRequestForErrObjectRequired", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxRequest(testCreate))
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostInboxRequestBodyHook(ctx, req, toDeserializedForm(testCreate)).Return(ctx, nil)
|
||||
delegate.EXPECT().AuthorizePostInbox(ctx, resp, toDeserializedForm(testCreate)).Return(true, nil)
|
||||
delegate.EXPECT().PostInbox(ctx, mustParse(testMyInboxIRI), toDeserializedForm(testCreate)).Return(ErrObjectRequired)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusBadRequest)
|
||||
})
|
||||
t.Run("PostInboxBadRequestForErrTargetRequired", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxRequest(testCreate))
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostInboxRequestBodyHook(ctx, req, toDeserializedForm(testCreate)).Return(ctx, nil)
|
||||
delegate.EXPECT().AuthorizePostInbox(ctx, resp, toDeserializedForm(testCreate)).Return(true, nil)
|
||||
delegate.EXPECT().PostInbox(ctx, mustParse(testMyInboxIRI), toDeserializedForm(testCreate)).Return(ErrTargetRequired)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusBadRequest)
|
||||
})
|
||||
t.Run("GetInboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toGetInboxRequest()
|
||||
// Run the test
|
||||
handled, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("GetInboxDeniesIfNotAuthenticated", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetInboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetInbox(ctx, resp, req).DoAndReturn(func(ctx context.Context, resp http.ResponseWriter, req *http.Request) (context.Context, bool, error) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
return ctx, false, nil
|
||||
})
|
||||
// Run the test
|
||||
handled, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusForbidden)
|
||||
})
|
||||
t.Run("GetInboxRespondsWithDataAndHeaders", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, clock, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetInboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().GetInbox(ctx, req).Return(testOrderedCollectionUniqueElems, nil)
|
||||
clock.EXPECT().Now().Return(now())
|
||||
// Run the test
|
||||
handled, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusOK)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(contentTypeHeader), "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
|
||||
assertEqual(t, respV.Header.Get(dateHeader), nowDateHeader())
|
||||
assertNotEqual(t, len(respV.Header.Get(digestHeader)), 0)
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, []byte(testOrderedCollectionUniqueElemsString))
|
||||
})
|
||||
t.Run("GetInboxDeduplicatesData", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, clock, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetInboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().GetInbox(ctx, req).Return(testOrderedCollectionDupedElems, nil)
|
||||
clock.EXPECT().Now().Return(now())
|
||||
// Run the test
|
||||
_, err := a.GetInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
respV := resp.Result()
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, []byte(testOrderedCollectionDedupedElemsString))
|
||||
})
|
||||
t.Run("PostOutboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toPostOutboxRequest(testCreateNoId)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("PostOutboxNotAllowed", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusMethodNotAllowed)
|
||||
})
|
||||
t.Run("GetOutboxIgnoresNonActivityPubRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toGetOutboxRequest()
|
||||
// Run the test
|
||||
handled, err := a.GetOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, false)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("GetOutboxDeniesIfNotAuthenticated", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetOutboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetOutbox(ctx, resp, req).DoAndReturn(func(ctx context.Context, resp http.ResponseWriter, req *http.Request) (context.Context, bool, error) {
|
||||
resp.WriteHeader(http.StatusForbidden)
|
||||
return ctx, false, nil
|
||||
})
|
||||
// Run the test
|
||||
handled, err := a.GetOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusForbidden)
|
||||
})
|
||||
t.Run("GetOutboxRespondsWithDataAndHeaders", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, clock, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toGetOutboxRequest())
|
||||
delegate.EXPECT().AuthenticateGetOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().GetOutbox(ctx, req).Return(testOrderedCollectionUniqueElems, nil)
|
||||
clock.EXPECT().Now().Return(now())
|
||||
// Run the test
|
||||
handled, err := a.GetOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusOK)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(contentTypeHeader), "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
|
||||
assertEqual(t, respV.Header.Get(dateHeader), nowDateHeader())
|
||||
assertNotEqual(t, len(respV.Header.Get(digestHeader)), 0)
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, []byte(testOrderedCollectionUniqueElemsString))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBaseActor tests the Actor returned with NewCustomActor and having both
|
||||
// the SocialProtocol and FederatingProtocol enabled.
|
||||
func TestBaseActor(t *testing.T) {
|
||||
// Set up test case
|
||||
setupData()
|
||||
ctx := context.Background()
|
||||
setupFn := func(ctl *gomock.Controller) (delegate *MockDelegateActor, clock *MockClock, a Actor) {
|
||||
delegate = NewMockDelegateActor(ctl)
|
||||
clock = NewMockClock(ctl)
|
||||
a = NewCustomActor(
|
||||
delegate,
|
||||
/*enableSocialProtocol=*/ true,
|
||||
/*enableFederatedProtocol=*/ true,
|
||||
clock)
|
||||
return
|
||||
}
|
||||
// Run tests
|
||||
t.Run("PostInboxRespondsWithStatus", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostInboxRequest(testCreate))
|
||||
delegate.EXPECT().AuthenticatePostInbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostInboxRequestBodyHook(ctx, req, toDeserializedForm(testCreate)).Return(ctx, nil)
|
||||
delegate.EXPECT().AuthorizePostInbox(ctx, resp, toDeserializedForm(testCreate)).Return(true, nil)
|
||||
delegate.EXPECT().PostInbox(ctx, mustParse(testMyInboxIRI), toDeserializedForm(testCreate)).Return(nil)
|
||||
delegate.EXPECT().InboxForwarding(ctx, mustParse(testMyInboxIRI), toDeserializedForm(testCreate)).Return(nil)
|
||||
// Run the test
|
||||
handled, err := a.PostInbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusOK)
|
||||
})
|
||||
t.Run("PostOutboxRespondsWithDataAndHeaders", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostOutboxRequestBodyHook(ctx, req, toDeserializedForm(testCreateNoId)).Return(ctx, nil)
|
||||
delegate.EXPECT().AddNewIDs(ctx, toDeserializedForm(testCreateNoId)).DoAndReturn(func(c context.Context, activity Activity) error {
|
||||
withNewId(activity)
|
||||
return nil
|
||||
})
|
||||
delegate.EXPECT().PostOutbox(
|
||||
ctx,
|
||||
withNewId(toDeserializedForm(testCreateNoId)),
|
||||
mustParse(testMyOutboxIRI),
|
||||
mustSerialize(testCreateNoId),
|
||||
).Return(false, nil)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusCreated)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(locationHeader), testNewActivityIRI)
|
||||
})
|
||||
t.Run("PostOutboxFederates", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
delegate, _, a := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(toPostOutboxRequest(testCreateNoId))
|
||||
delegate.EXPECT().AuthenticatePostOutbox(ctx, resp, req).Return(ctx, true, nil)
|
||||
delegate.EXPECT().PostOutboxRequestBodyHook(ctx, req, toDeserializedForm(testCreateNoId)).Return(ctx, nil)
|
||||
delegate.EXPECT().AddNewIDs(ctx, toDeserializedForm(testCreateNoId)).DoAndReturn(func(c context.Context, activity Activity) error {
|
||||
withNewId(activity)
|
||||
return nil
|
||||
})
|
||||
delegate.EXPECT().PostOutbox(
|
||||
ctx,
|
||||
withNewId(toDeserializedForm(testCreateNoId)),
|
||||
mustParse(testMyOutboxIRI),
|
||||
mustSerialize(testCreateNoId),
|
||||
).Return(true, nil)
|
||||
delegate.EXPECT().Deliver(ctx, mustParse(testMyOutboxIRI), withNewId(toDeserializedForm(testCreateNoId))).Return(nil)
|
||||
// Run the test
|
||||
handled, err := a.PostOutbox(ctx, resp, req)
|
||||
// Verify results
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, handled, true)
|
||||
assertEqual(t, resp.Code, http.StatusCreated)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(locationHeader), testNewActivityIRI)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Clock determines the time.
|
||||
type Clock interface {
|
||||
// Now returns the current time.
|
||||
Now() time.Time
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Common contains functions required for both the Social API and Federating
|
||||
// Protocol.
|
||||
//
|
||||
// It is passed to the library as a dependency injection from the client
|
||||
// application.
|
||||
type CommonBehavior interface {
|
||||
// AuthenticateGetInbox delegates the authentication of a GET to an
|
||||
// inbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetInbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// AuthenticateGetOutbox delegates the authentication of a GET to an
|
||||
// outbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetOutbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// GetOutbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
//
|
||||
// AuthenticateGetOutbox will be called prior to this.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
|
||||
// NewTransport returns a new Transport on behalf of a specific actor.
|
||||
//
|
||||
// The actorBoxIRI will be either the inbox or outbox of an actor who is
|
||||
// attempting to do the dereferencing or delivery. Any authentication
|
||||
// scheme applied on the request must be based on this actor. The
|
||||
// request must contain some sort of credential of the user, such as a
|
||||
// HTTP Signature.
|
||||
//
|
||||
// The gofedAgent passed in should be used by the Transport
|
||||
// implementation in the User-Agent, as well as the application-specific
|
||||
// user agent string. The gofedAgent will indicate this library's use as
|
||||
// well as the library's version number.
|
||||
//
|
||||
// Any server-wide rate-limiting that needs to occur should happen in a
|
||||
// Transport implementation. This factory function allows this to be
|
||||
// created, so peer servers are not DOS'd.
|
||||
//
|
||||
// Any retry logic should also be handled by the Transport
|
||||
// implementation.
|
||||
//
|
||||
// Note that the library will not maintain a long-lived pointer to the
|
||||
// returned Transport so that any private credentials are able to be
|
||||
// garbage collected.
|
||||
NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error)
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
// Lock takes a lock for the object at the specified id. If an error
|
||||
// is returned, the lock must not have been taken.
|
||||
//
|
||||
// The lock must be able to succeed for an id that does not exist in
|
||||
// the database. This means acquiring the lock does not guarantee the
|
||||
// entry exists in the database.
|
||||
//
|
||||
// Locks are encouraged to be lightweight and in the Go layer, as some
|
||||
// processes require tight loops acquiring and releasing locks.
|
||||
//
|
||||
// Used to ensure race conditions in multiple requests do not occur.
|
||||
Lock(c context.Context, id *url.URL) error
|
||||
// Unlock makes the lock for the object at the specified id available.
|
||||
// If an error is returned, the lock must have still been freed.
|
||||
//
|
||||
// Used to ensure race conditions in multiple requests do not occur.
|
||||
Unlock(c context.Context, id *url.URL) error
|
||||
// InboxContains returns true if the OrderedCollection at 'inbox'
|
||||
// contains the specified 'id'.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error)
|
||||
// GetInbox returns the first ordered collection page of the outbox at
|
||||
// the specified IRI, for prepending new items.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error)
|
||||
// SetInbox saves the inbox value given from GetInbox, with new items
|
||||
// prepended. Note that the new items must not be added as independent
|
||||
// database entries. Separate calls to Create will do that.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error
|
||||
// Owns returns true if the database has an entry for the IRI and it
|
||||
// exists in the database.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Owns(c context.Context, id *url.URL) (owns bool, err error)
|
||||
// ActorForOutbox fetches the actor's IRI for the given outbox IRI.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error)
|
||||
// ActorForInbox fetches the actor's IRI for the given outbox IRI.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error)
|
||||
// OutboxForInbox fetches the corresponding actor's outbox IRI for the
|
||||
// actor's inbox IRI.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error)
|
||||
// InboxForActor fetches the inbox corresponding to the given actorIRI.
|
||||
//
|
||||
// It is acceptable to just return nil for the inboxIRI. In this case, the library will
|
||||
// attempt to resolve the inbox of the actor by remote dereferencing instead.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
InboxForActor(c context.Context, actorIRI *url.URL) (inboxIRI *url.URL, err error)
|
||||
// Exists returns true if the database has an entry for the specified
|
||||
// id. It may not be owned by this application instance.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Exists(c context.Context, id *url.URL) (exists bool, err error)
|
||||
// Get returns the database entry for the specified id.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Get(c context.Context, id *url.URL) (value vocab.Type, err error)
|
||||
// Create adds a new entry to the database which must be able to be
|
||||
// keyed by its id.
|
||||
//
|
||||
// Note that Activity values received from federated peers may also be
|
||||
// created in the database this way if the Federating Protocol is
|
||||
// enabled. The client may freely decide to store only the id instead of
|
||||
// the entire value.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
//
|
||||
// Under certain conditions and network activities, Create may be called
|
||||
// multiple times for the same ActivityStreams object.
|
||||
Create(c context.Context, asType vocab.Type) error
|
||||
// Update sets an existing entry to the database based on the value's
|
||||
// id.
|
||||
//
|
||||
// Note that Activity values received from federated peers may also be
|
||||
// updated in the database this way if the Federating Protocol is
|
||||
// enabled. The client may freely decide to store only the id instead of
|
||||
// the entire value.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Update(c context.Context, asType vocab.Type) error
|
||||
// Delete removes the entry with the given id.
|
||||
//
|
||||
// Delete is only called for federated objects. Deletes from the Social
|
||||
// Protocol instead call Update to create a Tombstone.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Delete(c context.Context, id *url.URL) error
|
||||
// GetOutbox returns the first ordered collection page of the outbox
|
||||
// at the specified IRI, for prepending new items.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
GetOutbox(c context.Context, outboxIRI *url.URL) (outbox vocab.ActivityStreamsOrderedCollectionPage, err error)
|
||||
// SetOutbox saves the outbox value given from GetOutbox, with new items
|
||||
// prepended. Note that the new items must not be added as independent
|
||||
// database entries. Separate calls to Create will do that.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error
|
||||
// NewID creates a new IRI id for the provided activity or object. The
|
||||
// implementation does not need to set the 'id' property and simply
|
||||
// needs to determine the value.
|
||||
//
|
||||
// The go-fed library will handle setting the 'id' property on the
|
||||
// activity or object provided with the value returned.
|
||||
NewID(c context.Context, t vocab.Type) (id *url.URL, err error)
|
||||
// Followers obtains the Followers Collection for an actor with the
|
||||
// given id.
|
||||
//
|
||||
// If modified, the library will then call Update.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error)
|
||||
// Following obtains the Following Collection for an actor with the
|
||||
// given id.
|
||||
//
|
||||
// If modified, the library will then call Update.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Following(c context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error)
|
||||
// Liked obtains the Liked Collection for an actor with the
|
||||
// given id.
|
||||
//
|
||||
// If modified, the library will then call Update.
|
||||
//
|
||||
// The library makes this call only after acquiring a lock first.
|
||||
Liked(c context.Context, actorIRI *url.URL) (liked vocab.ActivityStreamsCollection, err error)
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// DelegateActor contains the detailed interface an application must satisfy in
|
||||
// order to implement the ActivityPub specification.
|
||||
//
|
||||
// Note that an implementation of this interface is implicitly provided in the
|
||||
// calls to NewActor, NewSocialActor, and NewFederatingActor.
|
||||
//
|
||||
// Implementing the DelegateActor requires familiarity with the ActivityPub
|
||||
// specification because it does not a strong enough abstraction for the client
|
||||
// application to ignore the ActivityPub spec. It is very possible to implement
|
||||
// this interface and build a foot-gun that trashes the fediverse without being
|
||||
// ActivityPub compliant. Please use with due consideration.
|
||||
//
|
||||
// Alternatively, build an application that uses the parts of the pub library
|
||||
// that do not require implementing a DelegateActor so that the ActivityPub
|
||||
// implementation is completely provided out of the box.
|
||||
type DelegateActor interface {
|
||||
// Hook callback after parsing the request body for a federated request
|
||||
// to the Actor's inbox.
|
||||
//
|
||||
// Can be used to set contextual information based on the Activity
|
||||
// received.
|
||||
//
|
||||
// Only called if the Federated Protocol is enabled.
|
||||
//
|
||||
// Warning: Neither authentication nor authorization has taken place at
|
||||
// this time. Doing anything beyond setting contextual information is
|
||||
// strongly discouraged.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostInbox. In this case, the DelegateActor implementation must not
|
||||
// write a response to the ResponseWriter as is expected that the caller
|
||||
// to PostInbox will do so when handling the error.
|
||||
PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error)
|
||||
// Hook callback after parsing the request body for a client request
|
||||
// to the Actor's outbox.
|
||||
//
|
||||
// Can be used to set contextual information based on the
|
||||
// ActivityStreams object received.
|
||||
//
|
||||
// Only called if the Social API is enabled.
|
||||
//
|
||||
// Warning: Neither authentication nor authorization has taken place at
|
||||
// this time. Doing anything beyond setting contextual information is
|
||||
// strongly discouraged.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostOutbox. In this case, the DelegateActor implementation must not
|
||||
// write a response to the ResponseWriter as is expected that the caller
|
||||
// to PostOutbox will do so when handling the error.
|
||||
PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error)
|
||||
// AuthenticatePostInbox delegates the authentication of a POST to an
|
||||
// inbox.
|
||||
//
|
||||
// Only called if the Federated Protocol is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostInbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// AuthenticateGetInbox delegates the authentication of a GET to an
|
||||
// inbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetInbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// AuthorizePostInbox delegates the authorization of an activity that
|
||||
// has been sent by POST to an inbox.
|
||||
//
|
||||
// Only called if the Federated Protocol is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostInbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authorized' is ignored.
|
||||
//
|
||||
// If no error is returned, but authorization fails, then authorized
|
||||
// must be false and error nil. It is expected that the implementation
|
||||
// handles writing to the ResponseWriter in this case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authorized must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthorizePostInbox(c context.Context, w http.ResponseWriter, activity Activity) (authorized bool, err error)
|
||||
// PostInbox delegates the side effects of adding to the inbox and
|
||||
// determining if it is a request that should be blocked.
|
||||
//
|
||||
// Only called if the Federated Protocol is enabled.
|
||||
//
|
||||
// As a side effect, PostInbox sets the federated data in the inbox, but
|
||||
// not on its own in the database, as InboxForwarding (which is called
|
||||
// later) must decide whether it has seen this activity before in order
|
||||
// to determine whether to do the forwarding algorithm.
|
||||
//
|
||||
// If the error is ErrObjectRequired or ErrTargetRequired, then a Bad
|
||||
// Request status is sent in the response.
|
||||
PostInbox(c context.Context, inboxIRI *url.URL, activity Activity) error
|
||||
// InboxForwarding delegates inbox forwarding logic when a POST request
|
||||
// is received in the Actor's inbox.
|
||||
//
|
||||
// Only called if the Federated Protocol is enabled.
|
||||
//
|
||||
// The delegate is responsible for determining whether to do the inbox
|
||||
// forwarding, as well as actually conducting it if it determines it
|
||||
// needs to.
|
||||
//
|
||||
// As a side effect, InboxForwarding must set the federated data in the
|
||||
// database, independently of the inbox, however it sees fit in order to
|
||||
// determine whether it has seen the activity before.
|
||||
//
|
||||
// The provided url is the inbox of the recipient of the Activity. The
|
||||
// Activity is examined for the information about who to inbox forward
|
||||
// to.
|
||||
//
|
||||
// If an error is returned, it is returned to the caller of PostInbox.
|
||||
InboxForwarding(c context.Context, inboxIRI *url.URL, activity Activity) error
|
||||
// PostOutbox delegates the logic for side effects and adding to the
|
||||
// outbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled. In the case of the Social API being enabled, side
|
||||
// effects of the Activity must occur.
|
||||
//
|
||||
// The delegate is responsible for adding the activity to the database's
|
||||
// general storage for independent retrieval, and not just within the
|
||||
// actor's outbox.
|
||||
//
|
||||
// If the error is ErrObjectRequired or ErrTargetRequired, then a Bad
|
||||
// Request status is sent in the response.
|
||||
//
|
||||
// Note that 'rawJSON' is an unfortunate consequence where an 'Update'
|
||||
// Activity is the only one that explicitly cares about 'null' values in
|
||||
// JSON. Since go-fed does not differentiate between 'null' values and
|
||||
// values that are simply not present, the 'rawJSON' map is ONLY needed
|
||||
// for this narrow and specific use case.
|
||||
PostOutbox(c context.Context, a Activity, outboxIRI *url.URL, rawJSON map[string]interface{}) (deliverable bool, e error)
|
||||
// AddNewIDs sets new URL ids on the activity. It also does so for all
|
||||
// 'object' properties if the Activity is a Create type.
|
||||
//
|
||||
// Only called if the Social API is enabled.
|
||||
//
|
||||
// If an error is returned, it is returned to the caller of PostOutbox.
|
||||
AddNewIDs(c context.Context, a Activity) error
|
||||
// Deliver sends a federated message. Called only if federation is
|
||||
// enabled.
|
||||
//
|
||||
// Called if the Federated Protocol is enabled.
|
||||
//
|
||||
// The provided url is the outbox of the sender. The Activity contains
|
||||
// the information about the intended recipients.
|
||||
//
|
||||
// If an error is returned, it is returned to the caller of PostOutbox.
|
||||
Deliver(c context.Context, outbox *url.URL, activity Activity) error
|
||||
// AuthenticatePostOutbox delegates the authentication and authorization
|
||||
// of a POST to an outbox.
|
||||
//
|
||||
// Only called if the Social API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostOutbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// AuthenticateGetOutbox delegates the authentication of a GET to an
|
||||
// outbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetOutbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// WrapInCreate wraps the provided object in a Create ActivityStreams
|
||||
// activity. The provided URL is the actor's outbox endpoint.
|
||||
//
|
||||
// Only called if the Social API is enabled.
|
||||
WrapInCreate(c context.Context, value vocab.Type, outboxIRI *url.URL) (vocab.ActivityStreamsCreate, error)
|
||||
// GetOutbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
//
|
||||
// AuthenticateGetOutbox will be called prior to this.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
|
||||
// GetInbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
//
|
||||
// AuthenticateGetInbox will be called prior to this.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// Package pub implements the ActivityPub protocol.
|
||||
//
|
||||
// Note that every time the ActivityStreams types are changed (added, removed)
|
||||
// due to code generation, the internal function toASType needs to be modified
|
||||
// to know about these types.
|
||||
//
|
||||
// Note that every version change should also include a change in the version.go
|
||||
// file.
|
||||
package pub
|
1263
pub/fed.go
1263
pub/fed.go
File diff suppressed because it is too large
Load Diff
6185
pub/fed_test.go
6185
pub/fed_test.go
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,124 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// FederatingProtocol contains behaviors an application needs to satisfy for the
|
||||
// full ActivityPub S2S implementation to be supported by this library.
|
||||
//
|
||||
// It is only required if the client application wants to support the server-to-
|
||||
// server, or federating, protocol.
|
||||
//
|
||||
// It is passed to the library as a dependency injection from the client
|
||||
// application.
|
||||
type FederatingProtocol interface {
|
||||
// Hook callback after parsing the request body for a federated request
|
||||
// to the Actor's inbox.
|
||||
//
|
||||
// Can be used to set contextual information based on the Activity
|
||||
// received.
|
||||
//
|
||||
// Only called if the Federated Protocol is enabled.
|
||||
//
|
||||
// Warning: Neither authentication nor authorization has taken place at
|
||||
// this time. Doing anything beyond setting contextual information is
|
||||
// strongly discouraged.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostInbox. In this case, the DelegateActor implementation must not
|
||||
// write a response to the ResponseWriter as is expected that the caller
|
||||
// to PostInbox will do so when handling the error.
|
||||
PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error)
|
||||
// AuthenticatePostInbox delegates the authentication of a POST to an
|
||||
// inbox.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostInbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// Blocked should determine whether to permit a set of actors given by
|
||||
// their ids are able to interact with this particular end user due to
|
||||
// being blocked or other application-specific logic.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostInbox.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then blocked must be true and error nil. An http.StatusForbidden
|
||||
// will be written in the wresponse.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// blocked must be false and error nil. The request will continue
|
||||
// to be processed.
|
||||
Blocked(c context.Context, actorIRIs []*url.URL) (blocked bool, err error)
|
||||
// FederatingCallbacks returns the application logic that handles
|
||||
// ActivityStreams received from federating peers.
|
||||
//
|
||||
// Note that certain types of callbacks will be 'wrapped' with default
|
||||
// behaviors supported natively by the library. Other callbacks
|
||||
// compatible with streams.TypeResolver can be specified by 'other'.
|
||||
//
|
||||
// For example, setting the 'Create' field in the
|
||||
// FederatingWrappedCallbacks lets an application dependency inject
|
||||
// additional behaviors they want to take place, including the default
|
||||
// behavior supplied by this library. This is guaranteed to be compliant
|
||||
// with the ActivityPub Social protocol.
|
||||
//
|
||||
// To override the default behavior, instead supply the function in
|
||||
// 'other', which does not guarantee the application will be compliant
|
||||
// with the ActivityPub Social Protocol.
|
||||
//
|
||||
// Applications are not expected to handle every single ActivityStreams
|
||||
// type and extension. The unhandled ones are passed to DefaultCallback.
|
||||
FederatingCallbacks(c context.Context) (wrapped FederatingWrappedCallbacks, other []interface{}, err error)
|
||||
// DefaultCallback is called for types that go-fed can deserialize but
|
||||
// are not handled by the application's callbacks returned in the
|
||||
// Callbacks method.
|
||||
//
|
||||
// Applications are not expected to handle every single ActivityStreams
|
||||
// type and extension, so the unhandled ones are passed to
|
||||
// DefaultCallback.
|
||||
DefaultCallback(c context.Context, activity Activity) error
|
||||
// MaxInboxForwardingRecursionDepth determines how deep to search within
|
||||
// an activity to determine if inbox forwarding needs to occur.
|
||||
//
|
||||
// Zero or negative numbers indicate infinite recursion.
|
||||
MaxInboxForwardingRecursionDepth(c context.Context) int
|
||||
// MaxDeliveryRecursionDepth determines how deep to search within
|
||||
// collections owned by peers when they are targeted to receive a
|
||||
// delivery.
|
||||
//
|
||||
// Zero or negative numbers indicate infinite recursion.
|
||||
MaxDeliveryRecursionDepth(c context.Context) int
|
||||
// FilterForwarding allows the implementation to apply business logic
|
||||
// such as blocks, spam filtering, and so on to a list of potential
|
||||
// Collections and OrderedCollections of recipients when inbox
|
||||
// forwarding has been triggered.
|
||||
//
|
||||
// The activity is provided as a reference for more intelligent
|
||||
// logic to be used, but the implementation must not modify it.
|
||||
FilterForwarding(c context.Context, potentialRecipients []*url.URL, a Activity) (filteredRecipients []*url.URL, err error)
|
||||
// GetInbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
//
|
||||
// AuthenticateGetInbox will be called prior to this.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
|
||||
}
|
|
@ -0,0 +1,907 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// OnFollowBehavior enumerates the different default actions that the go-fed
|
||||
// library can provide when receiving a Follow Activity from a peer.
|
||||
type OnFollowBehavior int
|
||||
|
||||
const (
|
||||
// OnFollowDoNothing does not take any action when a Follow Activity
|
||||
// is received.
|
||||
OnFollowDoNothing OnFollowBehavior = iota
|
||||
// OnFollowAutomaticallyAccept triggers the side effect of sending an
|
||||
// Accept of this Follow request in response.
|
||||
OnFollowAutomaticallyAccept
|
||||
// OnFollowAutomaticallyAccept triggers the side effect of sending a
|
||||
// Reject of this Follow request in response.
|
||||
OnFollowAutomaticallyReject
|
||||
)
|
||||
|
||||
// FederatingWrappedCallbacks lists the callback functions that already have
|
||||
// some side effect behavior provided by the pub library.
|
||||
//
|
||||
// These functions are wrapped for the Federating Protocol.
|
||||
type FederatingWrappedCallbacks struct {
|
||||
// Create handles additional side effects for the Create ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping callback for the Federating Protocol ensures the
|
||||
// 'object' property is created in the database.
|
||||
//
|
||||
// Create calls Create for each object in the federated Activity.
|
||||
Create func(context.Context, vocab.ActivityStreamsCreate) error
|
||||
// Update handles additional side effects for the Update ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping callback for the Federating Protocol ensures the
|
||||
// 'object' property is updated in the database.
|
||||
//
|
||||
// Update calls Update on the federated entry from the database, with a
|
||||
// new value.
|
||||
Update func(context.Context, vocab.ActivityStreamsUpdate) error
|
||||
// Delete handles additional side effects for the Delete ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// Delete removes the federated entry from the database.
|
||||
Delete func(context.Context, vocab.ActivityStreamsDelete) error
|
||||
// Follow handles additional side effects for the Follow ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function can have one of several default behaviors,
|
||||
// depending on the value of the OnFollow setting.
|
||||
Follow func(context.Context, vocab.ActivityStreamsFollow) error
|
||||
// OnFollow determines what action to take for this particular callback
|
||||
// if a Follow Activity is handled.
|
||||
OnFollow OnFollowBehavior
|
||||
// Accept handles additional side effects for the Accept ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function determines if this 'Accept' is in response to a
|
||||
// 'Follow'. If so, then the 'actor' is added to the original 'actor's
|
||||
// 'following' collection.
|
||||
//
|
||||
// Otherwise, no side effects are done by go-fed.
|
||||
Accept func(context.Context, vocab.ActivityStreamsAccept) error
|
||||
// Reject handles additional side effects for the Reject ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function has no default side effects. However, if this
|
||||
// 'Reject' is in response to a 'Follow' then the client MUST NOT go
|
||||
// forward with adding the 'actor' to the original 'actor's 'following'
|
||||
// collection by the client application.
|
||||
Reject func(context.Context, vocab.ActivityStreamsReject) error
|
||||
// Add handles additional side effects for the Add ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function will add the 'object' IRIs to a specific
|
||||
// 'target' collection if the 'target' collection(s) live on this
|
||||
// server.
|
||||
Add func(context.Context, vocab.ActivityStreamsAdd) error
|
||||
// Remove handles additional side effects for the Remove ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function will remove all 'object' IRIs from a specific
|
||||
// 'target' collection if the 'target' collection(s) live on this
|
||||
// server.
|
||||
Remove func(context.Context, vocab.ActivityStreamsRemove) error
|
||||
// Like handles additional side effects for the Like ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function will add the activity to the "likes" collection
|
||||
// on all 'object' targets owned by this server.
|
||||
Like func(context.Context, vocab.ActivityStreamsLike) error
|
||||
// Announce handles additional side effects for the Announce
|
||||
// ActivityStreams type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function will add the activity to the "shares"
|
||||
// collection on all 'object' targets owned by this server.
|
||||
Announce func(context.Context, vocab.ActivityStreamsAnnounce) error
|
||||
// Undo handles additional side effects for the Undo ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function ensures the 'actor' on the 'Undo'
|
||||
// is be the same as the 'actor' on all Activities being undone.
|
||||
// It enforces that the actors on the Undo must correspond to all of the
|
||||
// 'object' actors in some manner.
|
||||
//
|
||||
// It is expected that the application will implement the proper
|
||||
// reversal of activities that are being undone.
|
||||
Undo func(context.Context, vocab.ActivityStreamsUndo) error
|
||||
// Block handles additional side effects for the Block ActivityStreams
|
||||
// type, specific to the application using go-fed.
|
||||
//
|
||||
// The wrapping function provides no default side effects. It simply
|
||||
// calls the wrapped function. However, note that Blocks should not be
|
||||
// received from a federated peer, as delivering Blocks explicitly
|
||||
// deviates from the original ActivityPub specification.
|
||||
Block func(context.Context, vocab.ActivityStreamsBlock) error
|
||||
|
||||
// Sidechannel data -- this is set at request handling time. These must
|
||||
// be set before the callbacks are used.
|
||||
|
||||
// db is the Database the FederatingWrappedCallbacks should use.
|
||||
db Database
|
||||
// inboxIRI is the inboxIRI that is handling this callback.
|
||||
inboxIRI *url.URL
|
||||
// addNewIds creates new 'id' entries on an activity and its objects if
|
||||
// it is a Create activity.
|
||||
addNewIds func(c context.Context, activity Activity) error
|
||||
// deliver delivers an outgoing message.
|
||||
deliver func(c context.Context, outboxIRI *url.URL, activity Activity) error
|
||||
// newTransport creates a new Transport.
|
||||
newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error)
|
||||
}
|
||||
|
||||
// callbacks returns the WrappedCallbacks members into a single interface slice
|
||||
// for use in streams.Resolver callbacks.
|
||||
//
|
||||
// If the given functions have a type that collides with the default behavior,
|
||||
// then disable our default behavior
|
||||
func (w FederatingWrappedCallbacks) callbacks(fns []interface{}) []interface{} {
|
||||
enableCreate := true
|
||||
enableUpdate := true
|
||||
enableDelete := true
|
||||
enableFollow := true
|
||||
enableAccept := true
|
||||
enableReject := true
|
||||
enableAdd := true
|
||||
enableRemove := true
|
||||
enableLike := true
|
||||
enableAnnounce := true
|
||||
enableUndo := true
|
||||
enableBlock := true
|
||||
for _, fn := range fns {
|
||||
switch fn.(type) {
|
||||
default:
|
||||
continue
|
||||
case func(context.Context, vocab.ActivityStreamsCreate) error:
|
||||
enableCreate = false
|
||||
case func(context.Context, vocab.ActivityStreamsUpdate) error:
|
||||
enableUpdate = false
|
||||
case func(context.Context, vocab.ActivityStreamsDelete) error:
|
||||
enableDelete = false
|
||||
case func(context.Context, vocab.ActivityStreamsFollow) error:
|
||||
enableFollow = false
|
||||
case func(context.Context, vocab.ActivityStreamsAccept) error:
|
||||
enableAccept = false
|
||||
case func(context.Context, vocab.ActivityStreamsReject) error:
|
||||
enableReject = false
|
||||
case func(context.Context, vocab.ActivityStreamsAdd) error:
|
||||
enableAdd = false
|
||||
case func(context.Context, vocab.ActivityStreamsRemove) error:
|
||||
enableRemove = false
|
||||
case func(context.Context, vocab.ActivityStreamsLike) error:
|
||||
enableLike = false
|
||||
case func(context.Context, vocab.ActivityStreamsAnnounce) error:
|
||||
enableAnnounce = false
|
||||
case func(context.Context, vocab.ActivityStreamsUndo) error:
|
||||
enableUndo = false
|
||||
case func(context.Context, vocab.ActivityStreamsBlock) error:
|
||||
enableBlock = false
|
||||
}
|
||||
}
|
||||
if enableCreate {
|
||||
fns = append(fns, w.create)
|
||||
}
|
||||
if enableUpdate {
|
||||
fns = append(fns, w.update)
|
||||
}
|
||||
if enableDelete {
|
||||
fns = append(fns, w.deleteFn)
|
||||
}
|
||||
if enableFollow {
|
||||
fns = append(fns, w.follow)
|
||||
}
|
||||
if enableAccept {
|
||||
fns = append(fns, w.accept)
|
||||
}
|
||||
if enableReject {
|
||||
fns = append(fns, w.reject)
|
||||
}
|
||||
if enableAdd {
|
||||
fns = append(fns, w.add)
|
||||
}
|
||||
if enableRemove {
|
||||
fns = append(fns, w.remove)
|
||||
}
|
||||
if enableLike {
|
||||
fns = append(fns, w.like)
|
||||
}
|
||||
if enableAnnounce {
|
||||
fns = append(fns, w.announce)
|
||||
}
|
||||
if enableUndo {
|
||||
fns = append(fns, w.undo)
|
||||
}
|
||||
if enableBlock {
|
||||
fns = append(fns, w.block)
|
||||
}
|
||||
return fns
|
||||
}
|
||||
|
||||
// create implements the federating Create activity side effects.
|
||||
func (w FederatingWrappedCallbacks) create(c context.Context, a vocab.ActivityStreamsCreate) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
|
||||
t := iter.GetType()
|
||||
if t == nil && iter.IsIRI() {
|
||||
// Attempt to dereference the IRI instead
|
||||
tport, err := w.newTransport(c, w.inboxIRI, goFedUserAgent())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := tport.Dereference(c, iter.GetIRI())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err = json.Unmarshal(b, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err = streams.ToType(c, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if t == nil {
|
||||
return fmt.Errorf("cannot handle federated create: object is neither a value nor IRI")
|
||||
}
|
||||
id, err := GetId(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.db.Lock(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, id)
|
||||
if err := w.db.Create(c, t); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
if err := loopFn(iter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Create != nil {
|
||||
return w.Create(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// update implements the federating Update activity side effects.
|
||||
func (w FederatingWrappedCallbacks) update(c context.Context, a vocab.ActivityStreamsUpdate) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
if err := mustHaveActivityOriginMatchObjects(a); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
|
||||
t := iter.GetType()
|
||||
if t == nil {
|
||||
return fmt.Errorf("update requires an object to be wholly provided")
|
||||
}
|
||||
id, err := GetId(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.db.Lock(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, id)
|
||||
if err := w.db.Update(c, t); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
if err := loopFn(iter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Update != nil {
|
||||
return w.Update(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteFn implements the federating Delete activity side effects.
|
||||
func (w FederatingWrappedCallbacks) deleteFn(c context.Context, a vocab.ActivityStreamsDelete) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
if err := mustHaveActivityOriginMatchObjects(a); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.db.Lock(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, id)
|
||||
if err := w.db.Delete(c, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
if err := loopFn(iter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Delete != nil {
|
||||
return w.Delete(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// follow implements the federating Follow activity side effects.
|
||||
func (w FederatingWrappedCallbacks) follow(c context.Context, a vocab.ActivityStreamsFollow) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
// Check that we own at least one of the 'object' properties, and ensure
|
||||
// it is to the actor that owns this inbox.
|
||||
//
|
||||
// If not then don't send a response. It was federated to us as an FYI,
|
||||
// by mistake, or some other reason.
|
||||
if err := w.db.Lock(c, w.inboxIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock not deferred.
|
||||
actorIRI, err := w.db.ActorForInbox(c, w.inboxIRI)
|
||||
if err != nil {
|
||||
w.db.Unlock(c, w.inboxIRI)
|
||||
return err
|
||||
}
|
||||
w.db.Unlock(c, w.inboxIRI)
|
||||
// Unlock must be called by now and every branch above.
|
||||
isMe := false
|
||||
if w.OnFollow != OnFollowDoNothing {
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id.String() == actorIRI.String() {
|
||||
isMe = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if isMe {
|
||||
// Prepare the response.
|
||||
var response Activity
|
||||
if w.OnFollow == OnFollowAutomaticallyAccept {
|
||||
response = streams.NewActivityStreamsAccept()
|
||||
} else if w.OnFollow == OnFollowAutomaticallyReject {
|
||||
response = streams.NewActivityStreamsReject()
|
||||
} else {
|
||||
return fmt.Errorf("unknown OnFollowBehavior: %d", w.OnFollow)
|
||||
}
|
||||
// Set us as the 'actor'.
|
||||
me := streams.NewActivityStreamsActorProperty()
|
||||
response.SetActivityStreamsActor(me)
|
||||
me.AppendIRI(actorIRI)
|
||||
// Set the Follow as the 'object' property.
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
response.SetActivityStreamsObject(op)
|
||||
op.AppendActivityStreamsFollow(a)
|
||||
// Add all actors on the original Follow to the 'to' property.
|
||||
recipients := make([]*url.URL, 0)
|
||||
to := streams.NewActivityStreamsToProperty()
|
||||
response.SetActivityStreamsTo(to)
|
||||
followActors := a.GetActivityStreamsActor()
|
||||
for iter := followActors.Begin(); iter != followActors.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to.AppendIRI(id)
|
||||
recipients = append(recipients, id)
|
||||
}
|
||||
if w.OnFollow == OnFollowAutomaticallyAccept {
|
||||
// If automatically accepting, then also update our
|
||||
// followers collection with the new actors.
|
||||
//
|
||||
// If automatically rejecting, do not update the
|
||||
// followers collection.
|
||||
if err := w.db.Lock(c, actorIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock not deferred.
|
||||
followers, err := w.db.Followers(c, actorIRI)
|
||||
if err != nil {
|
||||
w.db.Unlock(c, actorIRI)
|
||||
return err
|
||||
}
|
||||
items := followers.GetActivityStreamsItems()
|
||||
if items == nil {
|
||||
items = streams.NewActivityStreamsItemsProperty()
|
||||
followers.SetActivityStreamsItems(items)
|
||||
}
|
||||
for _, elem := range recipients {
|
||||
items.PrependIRI(elem)
|
||||
}
|
||||
if err = w.db.Update(c, followers); err != nil {
|
||||
w.db.Unlock(c, actorIRI)
|
||||
return err
|
||||
}
|
||||
w.db.Unlock(c, actorIRI)
|
||||
// Unlock must be called by now and every branch above.
|
||||
}
|
||||
// Lock without defer!
|
||||
w.db.Lock(c, w.inboxIRI)
|
||||
outboxIRI, err := w.db.OutboxForInbox(c, w.inboxIRI)
|
||||
if err != nil {
|
||||
w.db.Unlock(c, w.inboxIRI)
|
||||
return err
|
||||
}
|
||||
w.db.Unlock(c, w.inboxIRI)
|
||||
// Everything must be unlocked by now.
|
||||
if err := w.addNewIds(c, response); err != nil {
|
||||
return err
|
||||
} else if err := w.deliver(c, outboxIRI, response); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Follow != nil {
|
||||
return w.Follow(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// accept implements the federating Accept activity side effects.
|
||||
func (w FederatingWrappedCallbacks) accept(c context.Context, a vocab.ActivityStreamsAccept) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op != nil && op.Len() > 0 {
|
||||
// Get this actor's id.
|
||||
if err := w.db.Lock(c, w.inboxIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock not deferred.
|
||||
actorIRI, err := w.db.ActorForInbox(c, w.inboxIRI)
|
||||
if err != nil {
|
||||
w.db.Unlock(c, w.inboxIRI)
|
||||
return err
|
||||
}
|
||||
w.db.Unlock(c, w.inboxIRI)
|
||||
// Unlock must be called by now and every branch above.
|
||||
//
|
||||
// Determine if we are in a follow on the 'object' property.
|
||||
//
|
||||
// TODO: Handle Accept multiple Follow.
|
||||
var maybeMyFollowIRI *url.URL
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
t := iter.GetType()
|
||||
if t == nil && iter.IsIRI() {
|
||||
// Attempt to dereference the IRI instead
|
||||
tport, err := w.newTransport(c, w.inboxIRI, goFedUserAgent())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := tport.Dereference(c, iter.GetIRI())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err = json.Unmarshal(b, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err = streams.ToType(c, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if t == nil {
|
||||
return fmt.Errorf("cannot handle federated create: object is neither a value nor IRI")
|
||||
}
|
||||
// Ensure it is a Follow.
|
||||
if !streams.IsOrExtendsActivityStreamsFollow(t) {
|
||||
continue
|
||||
}
|
||||
follow, ok := t.(Activity)
|
||||
if !ok {
|
||||
return fmt.Errorf("a Follow in an Accept does not satisfy the Activity interface")
|
||||
}
|
||||
followId, err := GetId(follow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure that we are one of the actors on the Follow.
|
||||
actors := follow.GetActivityStreamsActor()
|
||||
for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id.String() == actorIRI.String() {
|
||||
maybeMyFollowIRI = followId
|
||||
break
|
||||
}
|
||||
}
|
||||
// Continue breaking if we found ourselves
|
||||
if maybeMyFollowIRI != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
// If we received an Accept whose 'object' is a Follow with an
|
||||
// Accept that we sent, add to the following collection.
|
||||
if maybeMyFollowIRI != nil {
|
||||
// Verify our Follow request exists and the peer didn't
|
||||
// fabricate it.
|
||||
activityActors := a.GetActivityStreamsActor()
|
||||
if activityActors == nil || activityActors.Len() == 0 {
|
||||
return fmt.Errorf("an Accept with a Follow has no actors")
|
||||
}
|
||||
// This may be a duplicate check if we dereferenced the
|
||||
// Follow above. TODO: Separate this logic to avoid
|
||||
// redundancy.
|
||||
//
|
||||
// Use an anonymous function to properly scope the
|
||||
// database lock, immediately call it.
|
||||
err = func() error {
|
||||
if err := w.db.Lock(c, maybeMyFollowIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, maybeMyFollowIRI)
|
||||
t, err := w.db.Get(c, maybeMyFollowIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !streams.IsOrExtendsActivityStreamsFollow(t) {
|
||||
return fmt.Errorf("peer gave an Accept wrapping a Follow but provided a non-Follow id")
|
||||
}
|
||||
follow, ok := t.(Activity)
|
||||
if !ok {
|
||||
return fmt.Errorf("a Follow in an Accept does not satisfy the Activity interface")
|
||||
}
|
||||
// Ensure that we are one of the actors on the Follow.
|
||||
ok = false
|
||||
actors := follow.GetActivityStreamsActor()
|
||||
for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id.String() == actorIRI.String() {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("peer gave an Accept wrapping a Follow but we are not the actor on that Follow")
|
||||
}
|
||||
// Build map of original Accept actors
|
||||
acceptActors := make(map[string]bool)
|
||||
for iter := activityActors.Begin(); iter != activityActors.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
acceptActors[id.String()] = false
|
||||
}
|
||||
// Verify all actor(s) were on the original Follow.
|
||||
followObj := follow.GetActivityStreamsObject()
|
||||
for iter := followObj.Begin(); iter != followObj.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := acceptActors[id.String()]; ok {
|
||||
acceptActors[id.String()] = true
|
||||
}
|
||||
}
|
||||
for _, found := range acceptActors {
|
||||
if !found {
|
||||
return fmt.Errorf("peer gave an Accept wrapping a Follow but was not an object in the original Follow")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add the peer to our following collection.
|
||||
if err := w.db.Lock(c, actorIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock not deferred.
|
||||
following, err := w.db.Following(c, actorIRI)
|
||||
if err != nil {
|
||||
w.db.Unlock(c, actorIRI)
|
||||
return err
|
||||
}
|
||||
items := following.GetActivityStreamsItems()
|
||||
if items == nil {
|
||||
items = streams.NewActivityStreamsItemsProperty()
|
||||
following.SetActivityStreamsItems(items)
|
||||
}
|
||||
for iter := activityActors.Begin(); iter != activityActors.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
w.db.Unlock(c, actorIRI)
|
||||
return err
|
||||
}
|
||||
items.PrependIRI(id)
|
||||
}
|
||||
if err = w.db.Update(c, following); err != nil {
|
||||
w.db.Unlock(c, actorIRI)
|
||||
return err
|
||||
}
|
||||
w.db.Unlock(c, actorIRI)
|
||||
// Unlock must be called by now and every branch above.
|
||||
}
|
||||
}
|
||||
if w.Accept != nil {
|
||||
return w.Accept(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reject implements the federating Reject activity side effects.
|
||||
func (w FederatingWrappedCallbacks) reject(c context.Context, a vocab.ActivityStreamsReject) error {
|
||||
if w.Reject != nil {
|
||||
return w.Reject(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// add implements the federating Add activity side effects.
|
||||
func (w FederatingWrappedCallbacks) add(c context.Context, a vocab.ActivityStreamsAdd) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
target := a.GetActivityStreamsTarget()
|
||||
if target == nil || target.Len() == 0 {
|
||||
return ErrTargetRequired
|
||||
}
|
||||
if err := add(c, op, target, w.db); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Add != nil {
|
||||
return w.Add(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove implements the federating Remove activity side effects.
|
||||
func (w FederatingWrappedCallbacks) remove(c context.Context, a vocab.ActivityStreamsRemove) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
target := a.GetActivityStreamsTarget()
|
||||
if target == nil || target.Len() == 0 {
|
||||
return ErrTargetRequired
|
||||
}
|
||||
if err := remove(c, op, target, w.db); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Remove != nil {
|
||||
return w.Remove(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// like implements the federating Like activity side effects.
|
||||
func (w FederatingWrappedCallbacks) like(c context.Context, a vocab.ActivityStreamsLike) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
id, err := GetId(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
|
||||
objId, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.db.Lock(c, objId); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, objId)
|
||||
if owns, err := w.db.Owns(c, objId); err != nil {
|
||||
return err
|
||||
} else if !owns {
|
||||
return nil
|
||||
}
|
||||
t, err := w.db.Get(c, objId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l, ok := t.(likeser)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot add Like to likes collection for type %T", t)
|
||||
}
|
||||
// Get 'likes' property on the object, creating default if
|
||||
// necessary.
|
||||
likes := l.GetActivityStreamsLikes()
|
||||
if likes == nil {
|
||||
likes = streams.NewActivityStreamsLikesProperty()
|
||||
l.SetActivityStreamsLikes(likes)
|
||||
}
|
||||
// Get 'likes' value, defaulting to a collection.
|
||||
likesT := likes.GetType()
|
||||
if likesT == nil {
|
||||
col := streams.NewActivityStreamsCollection()
|
||||
likesT = col
|
||||
likes.SetActivityStreamsCollection(col)
|
||||
}
|
||||
// Prepend the activity's 'id' on the 'likes' Collection or
|
||||
// OrderedCollection.
|
||||
if col, ok := likesT.(itemser); ok {
|
||||
items := col.GetActivityStreamsItems()
|
||||
if items == nil {
|
||||
items = streams.NewActivityStreamsItemsProperty()
|
||||
col.SetActivityStreamsItems(items)
|
||||
}
|
||||
items.PrependIRI(id)
|
||||
} else if oCol, ok := likesT.(orderedItemser); ok {
|
||||
oItems := oCol.GetActivityStreamsOrderedItems()
|
||||
if oItems == nil {
|
||||
oItems = streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oCol.SetActivityStreamsOrderedItems(oItems)
|
||||
}
|
||||
oItems.PrependIRI(id)
|
||||
} else {
|
||||
return fmt.Errorf("likes type is neither a Collection nor an OrderedCollection: %T", likesT)
|
||||
}
|
||||
err = w.db.Update(c, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
if err := loopFn(iter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Like != nil {
|
||||
return w.Like(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// announce implements the federating Announce activity side effects.
|
||||
func (w FederatingWrappedCallbacks) announce(c context.Context, a vocab.ActivityStreamsAnnounce) error {
|
||||
id, err := GetId(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
op := a.GetActivityStreamsObject()
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error {
|
||||
objId, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.db.Lock(c, objId); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, objId)
|
||||
if owns, err := w.db.Owns(c, objId); err != nil {
|
||||
return err
|
||||
} else if !owns {
|
||||
return nil
|
||||
}
|
||||
t, err := w.db.Get(c, objId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, ok := t.(shareser)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot add Announce to Shares collection for type %T", t)
|
||||
}
|
||||
// Get 'shares' property on the object, creating default if
|
||||
// necessary.
|
||||
shares := s.GetActivityStreamsShares()
|
||||
if shares == nil {
|
||||
shares = streams.NewActivityStreamsSharesProperty()
|
||||
s.SetActivityStreamsShares(shares)
|
||||
}
|
||||
// Get 'shares' value, defaulting to a collection.
|
||||
sharesT := shares.GetType()
|
||||
if sharesT == nil {
|
||||
col := streams.NewActivityStreamsCollection()
|
||||
sharesT = col
|
||||
shares.SetActivityStreamsCollection(col)
|
||||
}
|
||||
// Prepend the activity's 'id' on the 'shares' Collection or
|
||||
// OrderedCollection.
|
||||
if col, ok := sharesT.(itemser); ok {
|
||||
items := col.GetActivityStreamsItems()
|
||||
if items == nil {
|
||||
items = streams.NewActivityStreamsItemsProperty()
|
||||
col.SetActivityStreamsItems(items)
|
||||
}
|
||||
items.PrependIRI(id)
|
||||
} else if oCol, ok := sharesT.(orderedItemser); ok {
|
||||
oItems := oCol.GetActivityStreamsOrderedItems()
|
||||
if oItems == nil {
|
||||
oItems = streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oCol.SetActivityStreamsOrderedItems(oItems)
|
||||
}
|
||||
oItems.PrependIRI(id)
|
||||
} else {
|
||||
return fmt.Errorf("shares type is neither a Collection nor an OrderedCollection: %T", sharesT)
|
||||
}
|
||||
err = w.db.Update(c, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if op != nil {
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
if err := loopFn(iter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.Announce != nil {
|
||||
return w.Announce(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// undo implements the federating Undo activity side effects.
|
||||
func (w FederatingWrappedCallbacks) undo(c context.Context, a vocab.ActivityStreamsUndo) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
actors := a.GetActivityStreamsActor()
|
||||
if err := mustHaveActivityActorsMatchObjectActors(c, actors, op, w.newTransport, w.inboxIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Undo != nil {
|
||||
return w.Undo(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// block implements the federating Block activity side effects.
|
||||
func (w FederatingWrappedCallbacks) block(c context.Context, a vocab.ActivityStreamsBlock) error {
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
if w.Block != nil {
|
||||
return w.Block(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
216
pub/handlers.go
216
pub/handlers.go
|
@ -2,140 +2,112 @@ package pub
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/vocab"
|
||||
"github.com/go-fed/httpsig"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
)
|
||||
|
||||
// ServeActivityPubObject will serve the ActivityPub object with the given IRI
|
||||
// in the request. Note that requests may be signed with HTTP signatures or be
|
||||
// permitted without any authentication scheme. To change this default behavior,
|
||||
// use ServeActivityPubObjectWithVerificationMethod instead.
|
||||
func ServeActivityPubObject(a Application, clock Clock) HandlerFunc {
|
||||
return func(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
return serveActivityPubObject(c, a, clock, w, r, nil)
|
||||
}
|
||||
var ErrNotFound = errors.New("go-fed/activity: ActivityStreams data not found")
|
||||
|
||||
// HandlerFunc determines whether an incoming HTTP request is an ActivityStreams
|
||||
// GET request, and if so attempts to serve ActivityStreams data.
|
||||
//
|
||||
// If an error is returned, then the calling function is responsible for writing
|
||||
// to the ResponseWriter as part of error handling.
|
||||
//
|
||||
// If 'isASRequest' is false and there is no error, then the calling function
|
||||
// may continue processing the request, and the HandlerFunc will not have
|
||||
// written anything to the ResponseWriter. For example, a webpage may be served
|
||||
// instead.
|
||||
//
|
||||
// If 'isASRequest' is true and there is no error, then the HandlerFunc
|
||||
// successfully served the request and wrote to the ResponseWriter.
|
||||
//
|
||||
// Callers are responsible for authorized access to this resource.
|
||||
type HandlerFunc func(c context.Context, w http.ResponseWriter, r *http.Request) (isASRequest bool, err error)
|
||||
|
||||
// NewActivityStreamsHandler creates a HandlerFunc to serve ActivityStreams
|
||||
// requests which are coming from other clients or servers that wish to obtain
|
||||
// an ActivityStreams representation of data.
|
||||
//
|
||||
// Strips retrieved ActivityStreams values of sensitive fields ('bto' and 'bcc')
|
||||
// before responding with them. Sets the appropriate HTTP status code for
|
||||
// Tombstone Activities as well.
|
||||
//
|
||||
// Defaults to supporting content to be retrieved by HTTPS only.
|
||||
func NewActivityStreamsHandler(db Database, clock Clock) HandlerFunc {
|
||||
return NewActivityStreamsHandlerScheme(db, clock, "https")
|
||||
}
|
||||
|
||||
// ServeActivityPubObjectWithVerificationMethod will serve the ActivityPub
|
||||
// object with the given IRI in the request. The rules for accessing the data
|
||||
// are governed by the SocialAPIVerifier's behavior and may permit accessing
|
||||
// data without having any credentials in the request.
|
||||
func ServeActivityPubObjectWithVerificationMethod(a Application, clock Clock, verifierFn func(context.Context) SocialAPIVerifier) HandlerFunc {
|
||||
return func(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
if verifierFn != nil {
|
||||
verifier := verifierFn(c)
|
||||
return serveActivityPubObject(c, a, clock, w, r, verifier)
|
||||
} else {
|
||||
return serveActivityPubObject(c, a, clock, w, r, nil)
|
||||
// NewActivityStreamsHandlerScheme creates a HandlerFunc to serve
|
||||
// ActivityStreams requests which are coming from other clients or servers that
|
||||
// wish to obtain an ActivityStreams representation of data provided by the
|
||||
// specified protocol scheme.
|
||||
//
|
||||
// Strips retrieved ActivityStreams values of sensitive fields ('bto' and 'bcc')
|
||||
// before responding with them. Sets the appropriate HTTP status code for
|
||||
// Tombstone Activities as well.
|
||||
//
|
||||
// Specifying the "scheme" allows for retrieving ActivityStreams content with
|
||||
// identifiers such as HTTP, HTTPS, or other protocol schemes.
|
||||
//
|
||||
// Returns ErrNotFound when the database does not retrieve any data and no
|
||||
// errors occurred during retrieval.
|
||||
func NewActivityStreamsHandlerScheme(db Database, clock Clock, scheme string) HandlerFunc {
|
||||
return func(c context.Context, w http.ResponseWriter, r *http.Request) (isASRequest bool, err error) {
|
||||
// Do nothing if it is not an ActivityPub GET request
|
||||
if !isActivityPubGet(r) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveActivityPubObject(c context.Context, a Application, clock Clock, w http.ResponseWriter, r *http.Request, verifier SocialAPIVerifier) (handled bool, err error) {
|
||||
handled = isActivityPubGet(r)
|
||||
if !handled {
|
||||
return
|
||||
}
|
||||
id := r.URL
|
||||
if !a.Owns(c, id) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var verifiedUser *url.URL
|
||||
// By default, permit unsigned access to resource. however, if there is
|
||||
// an HTTP Signature present, it must pass validation.
|
||||
authenticated := false
|
||||
authorized := false
|
||||
if verifier != nil {
|
||||
verifiedUser, authenticated, authorized, err = verifier.Verify(r)
|
||||
isASRequest = true
|
||||
id := requestId(r, scheme)
|
||||
// Lock and obtain a copy of the requested ActivityStreams value
|
||||
err = db.Lock(c, id)
|
||||
if err != nil {
|
||||
return
|
||||
} else if authenticated && !authorized {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
} else if !authenticated && !authorized {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
} else if !authenticated && authorized {
|
||||
// Protect against bad implementations: There is no
|
||||
// recognized reason for an implementation to pass back
|
||||
// a non-nil verifiedUser that is authorized but not
|
||||
// authenticated.
|
||||
//
|
||||
// Force HTTP Signature validation to trigger by
|
||||
// ensuring the verifiedUser is nil.
|
||||
if verifiedUser != nil {
|
||||
verifiedUser = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if verifiedUser == nil {
|
||||
var v httpsig.Verifier
|
||||
v, err = httpsig.NewVerifier(r)
|
||||
if err != nil { // Unsigned request
|
||||
if !authenticated && authorized { // Must pass HTTP Signature verification
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
err = nil
|
||||
return
|
||||
} // Else permit unsigned requests access
|
||||
} else { // Signed request
|
||||
var publicKey crypto.PublicKey
|
||||
var algo httpsig.Algorithm
|
||||
var user *url.URL
|
||||
publicKey, algo, user, err = a.GetPublicKey(c, v.KeyId())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = v.Verify(publicKey, algo)
|
||||
if err != nil && !authenticated { // Failed and must pass HTTP Signature verification
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
err = nil
|
||||
return
|
||||
} else if err == nil {
|
||||
verifiedUser = user
|
||||
} // Else failed HTTP Signature verification but we still allow access.
|
||||
// WARNING: Unlock not deferred
|
||||
t, err := db.Get(c, id)
|
||||
if err != nil {
|
||||
db.Unlock(c, id)
|
||||
return
|
||||
}
|
||||
db.Unlock(c, id)
|
||||
// Unlock must have been called by this point and in every
|
||||
// branch above
|
||||
if t == nil {
|
||||
err = ErrNotFound
|
||||
return
|
||||
}
|
||||
// Remove sensitive fields.
|
||||
clearSensitiveFields(t)
|
||||
// Serialize the fetched value.
|
||||
m, err := streams.Serialize(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Construct the response.
|
||||
addResponseHeaders(w.Header(), clock, raw)
|
||||
// Write the response.
|
||||
if streams.IsOrExtendsActivityStreamsTombstone(t) {
|
||||
w.WriteHeader(http.StatusGone)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := w.Write(raw)
|
||||
if err != nil {
|
||||
return
|
||||
} else if n != len(raw) {
|
||||
err = fmt.Errorf("only wrote %d of %d bytes", n, len(raw))
|
||||
return
|
||||
}
|
||||
}
|
||||
var pObj PubObject
|
||||
if verifiedUser != nil {
|
||||
pObj, err = a.GetAsVerifiedUser(c, r.URL, verifiedUser, Read)
|
||||
} else {
|
||||
pObj, err = a.Get(c, r.URL, Read)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if obj, ok := pObj.(vocab.ObjectType); ok {
|
||||
clearSensitiveFields(obj)
|
||||
}
|
||||
var m map[string]interface{}
|
||||
m, err = pObj.Serialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addJSONLDContext(m)
|
||||
var b []byte
|
||||
b, err = json.Marshal(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addResponseHeaders(w.Header(), clock, b)
|
||||
if vocab.HasTypeTombstone(pObj) {
|
||||
w.WriteHeader(http.StatusGone)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := w.Write(b)
|
||||
if err != nil {
|
||||
return
|
||||
} else if n != len(b) {
|
||||
err = fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(b))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -2,697 +2,104 @@ package pub
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"github.com/go-fed/activity/vocab"
|
||||
"github.com/go-fed/httpsig"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
func TestServeActivityPubObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
app *MockApplication
|
||||
clock *MockClock
|
||||
input *http.Request
|
||||
expectedCode int
|
||||
expectedObjFn func() vocab.Serializer
|
||||
expectHandled bool
|
||||
}{
|
||||
{
|
||||
name: "unsigned request",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "http signature request",
|
||||
input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) {
|
||||
if publicKeyId != testPublicKeyId {
|
||||
t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId)
|
||||
}
|
||||
return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil
|
||||
},
|
||||
getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
} else if u := user.String(); u != samIRIString {
|
||||
t.Fatalf("expected %s, got %s", samIRIString, u)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "not owned",
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusNotFound,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "not activitypub get",
|
||||
input: httptest.NewRequest("GET", noteURIString, nil),
|
||||
expectHandled: false,
|
||||
},
|
||||
{
|
||||
name: "bad http signature",
|
||||
input: BadSignature(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) {
|
||||
if publicKeyId != testPublicKeyId {
|
||||
t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId)
|
||||
}
|
||||
return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "remove bto & bcc",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
testNote.AppendBtoIRI(samIRI)
|
||||
testNote.AppendBccIRI(sallyIRI)
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "tombstone is status gone",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != testNewIRIString {
|
||||
t.Fatalf("expected %s, got %s", testNewIRIString, s)
|
||||
}
|
||||
tombstone := &vocab.Tombstone{}
|
||||
tombstone.SetId(testNewIRI)
|
||||
return tombstone, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != testNewIRIString {
|
||||
t.Fatalf("expected %s, got %s", testNewIRIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", testNewIRIString, nil)),
|
||||
expectedCode: http.StatusGone,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
tombstone := &vocab.Tombstone{}
|
||||
tombstone.SetId(testNewIRI)
|
||||
return tombstone
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
// TestActivityStreamsHandler tests the handler for serving ActivityPub
|
||||
// requests.
|
||||
func TestActivityStreamsHandler(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
setupFn := func(ctl *gomock.Controller) (db *MockDatabase, clock *MockClock, hf HandlerFunc) {
|
||||
db = NewMockDatabase(ctl)
|
||||
clock = NewMockClock(ctl)
|
||||
hf = NewActivityStreamsHandler(db, clock)
|
||||
return
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Logf("Running table test case %q", test.name)
|
||||
t.Run("IgnoresIfNotActivityPubGetRequest", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
_, _, hf := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
fnUnderTest := ServeActivityPubObject(test.app, test.clock)
|
||||
handled, err := fnUnderTest(context.Background(), resp, test.input)
|
||||
if err != nil {
|
||||
t.Fatalf("(%q) %s", test.name, err)
|
||||
} else if handled != test.expectHandled {
|
||||
t.Fatalf("(%q) expected %v, got %v", test.name, test.expectHandled, handled)
|
||||
} else if test.expectedCode != 0 {
|
||||
if resp.Code != test.expectedCode {
|
||||
t.Fatalf("(%q) expected %d, got %d", test.name, test.expectedCode, resp.Code)
|
||||
}
|
||||
} else if test.expectedObjFn != nil {
|
||||
if err := VocabEquals(resp.Body, test.expectedObjFn()); err != nil {
|
||||
t.Fatalf("(%q) unexpected object: %s", test.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeActivityPubObjectWithVerificationMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
app *MockApplication
|
||||
clock *MockClock
|
||||
verifier *MockSocialAPIVerifier
|
||||
input *http.Request
|
||||
expectedCode int
|
||||
expectedObjFn func() vocab.Serializer
|
||||
expectHandled bool
|
||||
}{
|
||||
{
|
||||
name: "unsigned request",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
get: func(c context.Context, id *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "http signature request",
|
||||
input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) {
|
||||
if publicKeyId != testPublicKeyId {
|
||||
t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId)
|
||||
}
|
||||
return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil
|
||||
},
|
||||
getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
} else if u := user.String(); u != samIRIString {
|
||||
t.Fatalf("expected %s, got %s", samIRIString, u)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "not owned",
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusNotFound,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "not activitypub get",
|
||||
input: httptest.NewRequest("GET", noteURIString, nil),
|
||||
expectHandled: false,
|
||||
},
|
||||
{
|
||||
name: "bad http signature",
|
||||
input: BadSignature(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) {
|
||||
if publicKeyId != testPublicKeyId {
|
||||
t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId)
|
||||
}
|
||||
return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "unsigned request passes verifier",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
} else if u := user.String(); u != samIRIString {
|
||||
t.Fatalf("expected %s, got %s", samIRIString, u)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
verifier: &MockSocialAPIVerifier{
|
||||
t: t,
|
||||
verify: func(r *http.Request) (*url.URL, bool, bool, error) {
|
||||
return samIRI, true, true, nil
|
||||
},
|
||||
},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "http signature request passes verifier",
|
||||
input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
} else if u := user.String(); u != samIRIString {
|
||||
t.Fatalf("expected %s, got %s", samIRIString, u)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
verifier: &MockSocialAPIVerifier{
|
||||
t: t,
|
||||
verify: func(r *http.Request) (*url.URL, bool, bool, error) {
|
||||
return samIRI, true, true, nil
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "verifier authed unauthz",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
verifier: &MockSocialAPIVerifier{
|
||||
t: t,
|
||||
verify: func(r *http.Request) (*url.URL, bool, bool, error) {
|
||||
return samIRI, true, false, nil
|
||||
},
|
||||
},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "verifier unauthed unauthz",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
verifier: &MockSocialAPIVerifier{
|
||||
t: t,
|
||||
verify: func(r *http.Request) (*url.URL, bool, bool, error) {
|
||||
return nil, false, false, nil
|
||||
},
|
||||
},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusBadRequest,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "verifier unauthed authz unsigned fails",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
verifier: &MockSocialAPIVerifier{
|
||||
t: t,
|
||||
verify: func(r *http.Request) (*url.URL, bool, bool, error) {
|
||||
return nil, false, true, nil
|
||||
},
|
||||
},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusBadRequest,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "verifier unauthed authz signed success",
|
||||
input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))),
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) {
|
||||
if publicKeyId != testPublicKeyId {
|
||||
t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId)
|
||||
}
|
||||
return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil
|
||||
},
|
||||
getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
} else if u := user.String(); u != samIRIString {
|
||||
t.Fatalf("expected %s, got %s", samIRIString, u)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
verifier: &MockSocialAPIVerifier{
|
||||
t: t,
|
||||
verify: func(r *http.Request) (*url.URL, bool, bool, error) {
|
||||
return nil, false, true, nil
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "verifier unauthed authz unsigned fails with bad impl returning user",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
verifier: &MockSocialAPIVerifier{
|
||||
t: t,
|
||||
verify: func(r *http.Request) (*url.URL, bool, bool, error) {
|
||||
return samIRI, false, true, nil
|
||||
},
|
||||
},
|
||||
input: ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil)),
|
||||
expectedCode: http.StatusBadRequest,
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "remove bto & bcc",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) {
|
||||
if publicKeyId != testPublicKeyId {
|
||||
t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId)
|
||||
}
|
||||
return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil
|
||||
},
|
||||
getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
} else if u := user.String(); u != samIRIString {
|
||||
t.Fatalf("expected %s, got %s", samIRIString, u)
|
||||
}
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
testNote.AppendBtoIRI(samIRI)
|
||||
testNote.AppendBccIRI(sallyIRI)
|
||||
return testNote, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != noteURIString {
|
||||
t.Fatalf("expected %s, got %s", noteURIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
input: Sign(ActivityPubRequest(httptest.NewRequest("GET", noteURIString, nil))),
|
||||
expectedCode: http.StatusOK,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
testNote = &vocab.Note{}
|
||||
testNote.SetId(noteIRI)
|
||||
testNote.AppendNameString(noteName)
|
||||
testNote.AppendContentString("This is a simple note")
|
||||
return testNote
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
{
|
||||
name: "tombstone is status gone",
|
||||
app: &MockApplication{
|
||||
t: t,
|
||||
getPublicKey: func(c context.Context, publicKeyId string) (crypto.PublicKey, httpsig.Algorithm, *url.URL, error) {
|
||||
if publicKeyId != testPublicKeyId {
|
||||
t.Fatalf("expected %s, got %s", testPublicKeyId, publicKeyId)
|
||||
}
|
||||
return testPrivateKey.Public(), httpsig.RSA_SHA256, samIRI, nil
|
||||
},
|
||||
getAsVerifiedUser: func(c context.Context, id, user *url.URL, rw RWType) (PubObject, error) {
|
||||
if rw != Read {
|
||||
t.Fatalf("expected RWType of %d, got %d", Read, rw)
|
||||
} else if s := id.String(); s != testNewIRIString {
|
||||
t.Fatalf("expected %s, got %s", testNewIRIString, s)
|
||||
} else if u := user.String(); u != samIRIString {
|
||||
t.Fatalf("expected %s, got %s", samIRIString, u)
|
||||
}
|
||||
tombstone := &vocab.Tombstone{}
|
||||
tombstone.SetId(testNewIRI)
|
||||
return tombstone, nil
|
||||
},
|
||||
owns: func(c context.Context, id *url.URL) bool {
|
||||
if s := id.String(); s != testNewIRIString {
|
||||
t.Fatalf("expected %s, got %s", testNewIRIString, s)
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
clock: &MockClock{now},
|
||||
input: Sign(ActivityPubRequest(httptest.NewRequest("GET", testNewIRIString, nil))),
|
||||
expectedCode: http.StatusGone,
|
||||
expectedObjFn: func() vocab.Serializer {
|
||||
tombstone := &vocab.Tombstone{}
|
||||
tombstone.SetId(testNewIRI)
|
||||
return tombstone
|
||||
},
|
||||
expectHandled: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Logf("Running table test case %q", test.name)
|
||||
req := httptest.NewRequest("GET", testNoteId1, nil)
|
||||
// Run & Verify
|
||||
isAPReq, err := hf(ctx, resp, req)
|
||||
assertEqual(t, isAPReq, false)
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("ReturnsErrorWhenDatabaseFetchReturnsError", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
mockDb, _, hf := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
var fnUnderTest HandlerFunc
|
||||
if test.verifier != nil {
|
||||
verifierFn := func(c context.Context) SocialAPIVerifier {
|
||||
return test.verifier
|
||||
}
|
||||
fnUnderTest = ServeActivityPubObjectWithVerificationMethod(test.app, test.clock, verifierFn)
|
||||
} else {
|
||||
fnUnderTest = ServeActivityPubObjectWithVerificationMethod(test.app, test.clock, nil)
|
||||
}
|
||||
handled, err := fnUnderTest(context.Background(), resp, test.input)
|
||||
if err != nil {
|
||||
t.Fatalf("(%q) %s", test.name, err)
|
||||
} else if handled != test.expectHandled {
|
||||
t.Fatalf("(%q) expected %v, got %v", test.name, test.expectHandled, handled)
|
||||
} else if test.expectedCode != 0 {
|
||||
if resp.Code != test.expectedCode {
|
||||
t.Fatalf("(%q) expected %d, got %d", test.name, test.expectedCode, resp.Code)
|
||||
}
|
||||
} else if test.expectedObjFn != nil {
|
||||
if err := VocabEquals(resp.Body, test.expectedObjFn()); err != nil {
|
||||
t.Fatalf("(%q) unexpected object: %s", test.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
req := toAPRequest(httptest.NewRequest("GET", testNoteId1, nil))
|
||||
testErr := fmt.Errorf("test error")
|
||||
// Mock
|
||||
mockDb.EXPECT().Lock(ctx, mustParse(testNoteId1))
|
||||
mockDb.EXPECT().Get(ctx, mustParse(testNoteId1)).Return(nil, testErr)
|
||||
mockDb.EXPECT().Unlock(ctx, mustParse(testNoteId1))
|
||||
// Run & Verify
|
||||
isAPReq, err := hf(ctx, resp, req)
|
||||
assertEqual(t, isAPReq, true)
|
||||
assertEqual(t, err, testErr)
|
||||
assertEqual(t, len(resp.Result().Header), 0)
|
||||
})
|
||||
t.Run("ServesTombstoneWithStatusGone", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
mockDb, mockClock, hf := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(httptest.NewRequest("GET", testNoteId1, nil))
|
||||
// Mock
|
||||
mockDb.EXPECT().Lock(ctx, mustParse(testNoteId1))
|
||||
mockDb.EXPECT().Get(ctx, mustParse(testNoteId1)).Return(testTombstone, nil)
|
||||
mockDb.EXPECT().Unlock(ctx, mustParse(testNoteId1))
|
||||
mockClock.EXPECT().Now().Return(now())
|
||||
// Run & Verify
|
||||
isAPReq, err := hf(ctx, resp, req)
|
||||
assertEqual(t, isAPReq, true)
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, resp.Code, http.StatusGone)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(contentTypeHeader), "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
|
||||
assertEqual(t, respV.Header.Get(dateHeader), nowDateHeader())
|
||||
assertNotEqual(t, len(respV.Header.Get(digestHeader)), 0)
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, mustSerializeToBytes(testTombstone))
|
||||
})
|
||||
t.Run("ServesContentWithStatusOk", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
mockDb, mockClock, hf := setupFn(ctl)
|
||||
resp := httptest.NewRecorder()
|
||||
req := toAPRequest(httptest.NewRequest("GET", testNoteId1, nil))
|
||||
// Mock
|
||||
mockDb.EXPECT().Lock(ctx, mustParse(testNoteId1))
|
||||
mockDb.EXPECT().Get(ctx, mustParse(testNoteId1)).Return(testMyNote, nil)
|
||||
mockDb.EXPECT().Unlock(ctx, mustParse(testNoteId1))
|
||||
mockClock.EXPECT().Now().Return(now())
|
||||
// Run & Verify
|
||||
isAPReq, err := hf(ctx, resp, req)
|
||||
assertEqual(t, isAPReq, true)
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, resp.Code, http.StatusOK)
|
||||
respV := resp.Result()
|
||||
assertEqual(t, respV.Header.Get(contentTypeHeader), "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
|
||||
assertEqual(t, respV.Header.Get(dateHeader), nowDateHeader())
|
||||
assertNotEqual(t, len(respV.Header.Get(digestHeader)), 0)
|
||||
b, err := ioutil.ReadAll(respV.Body)
|
||||
assertEqual(t, err, nil)
|
||||
assertByteEqual(t, b, mustSerializeToBytes(testMyNote))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,399 +0,0 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/vocab"
|
||||
"github.com/go-fed/httpsig"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HandlerFunc returns true if it was able to handle the request as an
|
||||
// ActivityPub request. If it handled the request then the error should be
|
||||
// checked. The response will have already been written to when handled and
|
||||
// there was no error. Client applications can freely choose how to handle the
|
||||
// request if this function does not handle it.
|
||||
//
|
||||
// Note that if the handler attempted to handle the request but returned an
|
||||
// error, it is up to the client application to determine what headers and
|
||||
// response to send to the requester.
|
||||
type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request) (bool, error)
|
||||
|
||||
// Clock determines the time.
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// HttpClient sends http requests.
|
||||
type HttpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// SocialAPIVerifier will verify incoming requests from clients and is meant to
|
||||
// encapsulate authentication functionality by standards such as OAuth (RFC
|
||||
// 6749).
|
||||
type SocialAPIVerifier interface {
|
||||
// Verify will determine the authenticated user for the given request,
|
||||
// returning false if verification fails. If the request is entirely
|
||||
// missing the required fields in order to authenticate, this function
|
||||
// must return nil and false for all values to permit attempting
|
||||
// validation by HTTP Signatures. If there was an internal error
|
||||
// determining the authentication of the request, it is returned.
|
||||
//
|
||||
// Return values are interpreted as follows:
|
||||
// (userFoo, true, true, <nil>) => userFoo passed authentication and is authorized
|
||||
// (<any>, true, false, <nil>) => a user passed authentication but failed authorization (Permission denied)
|
||||
// (<any>, false, false, <nil>) => authentication failed: deny access (Bad request)
|
||||
// (<nil>, false, true, <nil>) => authentication failed: must pass HTTP Signature verification or will be Permission Denied
|
||||
// (<nil>, true, true, <nil>) => "I don't care, try to validate using HTTP Signatures. If that still doesn't work, permit raw requests access anyway."
|
||||
// (<any>, <any>, <any>, error) => an internal error occurred during validation
|
||||
//
|
||||
// Be very careful that the 'authenticatedUser' value is non-nil when
|
||||
// returning 'authn' and 'authz' values of true, or else the library
|
||||
// will use the most permissive logic instead of the most restrictive as
|
||||
// outlined above.
|
||||
Verify(r *http.Request) (authenticatedUser *url.URL, authn, authz bool, err error)
|
||||
// VerifyForOutbox is the same as Verify, except that the request must
|
||||
// authenticate the owner of the provided outbox IRI.
|
||||
//
|
||||
// Return values are interpreted as follows:
|
||||
// (true, true, <nil>) => user for the outbox passed authentication and is authorized
|
||||
// (true, false, <nil>) => a user passed authentication but failed authorization for this outbox (Permission denied)
|
||||
// (false, true, <nil>) => authentication failed: must pass HTTP Signature verification or will be Permission Denied
|
||||
// (false, false, <nil>) => authentication failed: deny access (Bad request)
|
||||
// (<any>, <any>, error) => an internal error occurred during validation
|
||||
VerifyForOutbox(r *http.Request, outbox *url.URL) (authn, authz bool, err error)
|
||||
}
|
||||
|
||||
// Application is provided by users of this library in order to implement a
|
||||
// social-federative-web application.
|
||||
//
|
||||
// The contexts provided in these calls are passed through this library without
|
||||
// modification, allowing implementations to pass-through request-scoped data in
|
||||
// order to properly handle the request.
|
||||
type Application interface {
|
||||
// Owns returns true if the provided id is owned by this server.
|
||||
Owns(c context.Context, id *url.URL) bool
|
||||
// Get fetches the ActivityStream representation of the given id.
|
||||
Get(c context.Context, id *url.URL, rw RWType) (PubObject, error)
|
||||
// GetAsVerifiedUser fetches the ActivityStream representation of the
|
||||
// given id with the provided IRI representing the authenticated user
|
||||
// making the request.
|
||||
GetAsVerifiedUser(c context.Context, id, authdUser *url.URL, rw RWType) (PubObject, error)
|
||||
// Has determines if the server already knows about the object or
|
||||
// Activity specified by the given id.
|
||||
Has(c context.Context, id *url.URL) (bool, error)
|
||||
// Set should write or overwrite the value of the provided object for
|
||||
// its 'id'.
|
||||
Set(c context.Context, o PubObject) error
|
||||
// GetInbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
GetInbox(c context.Context, r *http.Request, rw RWType) (vocab.OrderedCollectionType, error)
|
||||
// GetOutbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
GetOutbox(c context.Context, r *http.Request, rw RWType) (vocab.OrderedCollectionType, error)
|
||||
// NewId takes in a client id token and returns an ActivityStreams IRI
|
||||
// id for a new Activity posted to the outbox. The object is provided
|
||||
// as a Typer so clients can use it to decide how to generate the IRI.
|
||||
NewId(c context.Context, t Typer) *url.URL
|
||||
// GetPublicKey fetches the public key for a user based on the public
|
||||
// key id. It also determines which algorithm to use to verify the
|
||||
// signature.
|
||||
GetPublicKey(c context.Context, publicKeyId string) (pubKey crypto.PublicKey, algo httpsig.Algorithm, user *url.URL, err error)
|
||||
// CanAdd returns true if the provided object is allowed to be added to
|
||||
// the given target collection. Applicable to either or both of the
|
||||
// SocialAPI and FederateAPI.
|
||||
CanAdd(c context.Context, o vocab.ObjectType, t vocab.ObjectType) bool
|
||||
// CanRemove returns true if the provided object is allowed to be
|
||||
// removed from the given target collection. Applicable to either or
|
||||
// both of the SocialAPI and FederateAPI.
|
||||
CanRemove(c context.Context, o vocab.ObjectType, t vocab.ObjectType) bool
|
||||
}
|
||||
|
||||
// RWType indicates the kind of reading being done.
|
||||
type RWType int
|
||||
|
||||
const (
|
||||
// Read indicates the object is only being read.
|
||||
Read RWType = iota
|
||||
// ReadWrite indicates the object is being mutated as well.
|
||||
ReadWrite
|
||||
)
|
||||
|
||||
// SocialAPI is provided by users of this library and designed to handle
|
||||
// receiving messages from ActivityPub clients through the Social API.
|
||||
type SocialAPI interface {
|
||||
// ActorIRI returns the actor's IRI associated with the given request.
|
||||
ActorIRI(c context.Context, r *http.Request) (*url.URL, error)
|
||||
// GetSocialAPIVerifier returns the authentication mechanism used for
|
||||
// incoming ActivityPub client requests. It is optional and allowed to
|
||||
// return null.
|
||||
//
|
||||
// Note that regardless of what this implementation returns, HTTP
|
||||
// Signatures is supported natively as a fallback.
|
||||
GetSocialAPIVerifier(c context.Context) SocialAPIVerifier
|
||||
// GetPublicKeyForOutbox fetches the public key for a user based on the
|
||||
// public key id. It also determines which algorithm to use to verify
|
||||
// the signature.
|
||||
//
|
||||
// Note that a key difference from Application's GetPublicKey is that
|
||||
// this function must make sure that the actor whose boxIRI is passed in
|
||||
// matches the public key id that is requested, or return an error.
|
||||
GetPublicKeyForOutbox(c context.Context, publicKeyId string, boxIRI *url.URL) (crypto.PublicKey, httpsig.Algorithm, error)
|
||||
}
|
||||
|
||||
// FederateAPI is provided by users of this library and designed to handle
|
||||
// receiving messages from ActivityPub servers through the Federative API.
|
||||
type FederateAPI interface {
|
||||
// OnFollow determines whether to take any automatic reactions in
|
||||
// response to this follow. Note that if this application does not own
|
||||
// an object on the activity, then the 'AutomaticAccept' and
|
||||
// 'AutomaticReject' results will behave as if they were 'DoNothing'.
|
||||
OnFollow(c context.Context, s *streams.Follow) FollowResponse
|
||||
// Unblocked should return an error if the provided actor ids are not
|
||||
// able to interact with this particular end user due to being blocked
|
||||
// or other application-specific logic. This error is passed
|
||||
// transparently back to the request thread via PostInbox.
|
||||
//
|
||||
// If nil error is returned, then the received activity is processed as
|
||||
// a normal unblocked interaction.
|
||||
Unblocked(c context.Context, actorIRIs []*url.URL) error
|
||||
// FilterForwarding is invoked when a received activity needs to be
|
||||
// forwarded to specific inboxes owned by this server in order to avoid
|
||||
// the ghost reply problem. The IRIs provided are collections owned by
|
||||
// this server that the federate peer requested inbox forwarding to.
|
||||
//
|
||||
// Implementors must apply some sort of filtering to the provided IRI
|
||||
// collections. Without any filtering, the resulting application is
|
||||
// vulnerable to becoming a spam bot for a malicious federate peer.
|
||||
// Typical implementations will filter the iris down to be only the
|
||||
// follower collections owned by the actors targeted in the activity.
|
||||
FilterForwarding(c context.Context, activity vocab.ActivityType, iris []*url.URL) ([]*url.URL, error)
|
||||
// NewSigner returns a new httpsig.Signer for which deliveries can be
|
||||
// signed by the actor delivering the Activity. Let me take this moment
|
||||
// to really level with you, dear anonymous reader-of-documentation. You
|
||||
// want to use httpsig.RSA_SHA256 as the algorithm. Otherwise, your app
|
||||
// will not federate correctly and peers will reject the signatures. All
|
||||
// other known implementations using HTTP Signatures use RSA_SHA256,
|
||||
// hardcoded just like your implementation will be.
|
||||
//
|
||||
// Some people might think it funny to split the federation and use
|
||||
// their own algorithm. And while I give you the power to build the
|
||||
// largest foot-gun possible to blow away your limbs because I respect
|
||||
// your freedom, you as a developer have the responsibility to also be
|
||||
// cognizant of the wider community you are building for. Don't be a
|
||||
// dick.
|
||||
//
|
||||
// The headers available for inclusion in the signature are:
|
||||
// Date
|
||||
// User-Agent
|
||||
NewSigner() (httpsig.Signer, error)
|
||||
// PrivateKey fetches the private key and its associated public key ID.
|
||||
// The given URL is the inbox or outbox for the actor whose key is
|
||||
// needed.
|
||||
PrivateKey(boxIRI *url.URL) (privKey crypto.PrivateKey, pubKeyId string, err error)
|
||||
}
|
||||
|
||||
// SocialApp is an implementation only for the Social API part of the
|
||||
// ActivityPub specification.
|
||||
type SocialApplication interface {
|
||||
Application
|
||||
SocialAPI
|
||||
}
|
||||
|
||||
// FederateApp is an implementation only for the Federating API part of the
|
||||
// ActivityPub specification.
|
||||
type FederateApplication interface {
|
||||
Application
|
||||
FederateAPI
|
||||
}
|
||||
|
||||
// SocialFederateApplication is an implementation for both the Social API and
|
||||
// the Federating API parts of the ActivityPub specification.
|
||||
type SocialFederateApplication interface {
|
||||
Application
|
||||
SocialAPI
|
||||
FederateAPI
|
||||
}
|
||||
|
||||
// FollowResponse instructs how to proceed upon immediately receiving a request
|
||||
// to follow.
|
||||
type FollowResponse int
|
||||
|
||||
const (
|
||||
AutomaticAccept FollowResponse = iota
|
||||
AutomaticReject
|
||||
DoNothing
|
||||
)
|
||||
|
||||
// Callbacker provides an Application hooks into the lifecycle of the
|
||||
// ActivityPub processes for both client-to-server and server-to-server
|
||||
// interactions. These callbacks are called after their spec-compliant actions
|
||||
// are completed, but before inbox forwarding and before delivery.
|
||||
//
|
||||
// Note that at minimum, for inbox forwarding to work correctly, these
|
||||
// Activities must be stored in the client application as a system of record.
|
||||
//
|
||||
// Note that modifying the ActivityStream objects in a callback may cause
|
||||
// unintentionally non-standard behavior if modifying core attributes, but
|
||||
// otherwise affords clients powerful flexibility. Use responsibly.
|
||||
type Callbacker interface {
|
||||
// Create Activity callback.
|
||||
Create(c context.Context, s *streams.Create) error
|
||||
// Update Activity callback.
|
||||
Update(c context.Context, s *streams.Update) error
|
||||
// Delete Activity callback.
|
||||
Delete(c context.Context, s *streams.Delete) error
|
||||
// Add Activity callback.
|
||||
Add(c context.Context, s *streams.Add) error
|
||||
// Remove Activity callback.
|
||||
Remove(c context.Context, s *streams.Remove) error
|
||||
// Like Activity callback.
|
||||
Like(c context.Context, s *streams.Like) error
|
||||
// Block Activity callback. By default, this implmentation does not
|
||||
// dictate how blocking should be implemented, so it is up to the
|
||||
// application to enforce this by implementing the FederateApp
|
||||
// interface.
|
||||
Block(c context.Context, s *streams.Block) error
|
||||
// Follow Activity callback. In the special case of server-to-server
|
||||
// delivery of a Follow activity, this implementation supports the
|
||||
// option of automatically replying with an 'Accept', 'Reject', or
|
||||
// waiting for human interaction as provided in the FederateApp
|
||||
// interface.
|
||||
//
|
||||
// In the special case that the FederateApp returned AutomaticAccept,
|
||||
// this library automatically handles adding the 'actor' to the
|
||||
// 'followers' collection of the 'object'.
|
||||
Follow(c context.Context, s *streams.Follow) error
|
||||
// Undo Activity callback. It is up to the client to provide support
|
||||
// for all 'Undo' operations; this implementation does not attempt to
|
||||
// provide a generic implementation.
|
||||
Undo(c context.Context, s *streams.Undo) error
|
||||
// Accept Activity callback. In the special case that this 'Accept'
|
||||
// activity has an 'object' of 'Follow' type, then the library will
|
||||
// handle adding the 'actor' to the 'following' collection of the
|
||||
// original 'actor' who requested the 'Follow'.
|
||||
Accept(c context.Context, s *streams.Accept) error
|
||||
// Reject Activity callback. Note that in the special case that this
|
||||
// 'Reject' activity has an 'object' of 'Follow' type, then the client
|
||||
// MUST NOT add the 'actor' to the 'following' collection of the
|
||||
// original 'actor' who requested the 'Follow'.
|
||||
Reject(c context.Context, s *streams.Reject) error
|
||||
}
|
||||
|
||||
// Deliverer schedules federated ActivityPub messages for delivery, possibly
|
||||
// asynchronously.
|
||||
type Deliverer interface {
|
||||
// Do schedules a message to be sent to a specific URL endpoint by
|
||||
// using toDo.
|
||||
Do(b []byte, to *url.URL, toDo func(b []byte, u *url.URL) error)
|
||||
}
|
||||
|
||||
// PubObject is an ActivityPub Object.
|
||||
type PubObject interface {
|
||||
vocab.Serializer
|
||||
Typer
|
||||
GetId() *url.URL
|
||||
SetId(*url.URL)
|
||||
HasId() bool
|
||||
AppendType(interface{})
|
||||
RemoveType(int)
|
||||
}
|
||||
|
||||
// Typer is an object that has a type.
|
||||
type Typer interface {
|
||||
vocab.Typer
|
||||
}
|
||||
|
||||
// typeIder is a Typer with additional generic capabilities.
|
||||
type typeIder interface {
|
||||
Typer
|
||||
SetId(v *url.URL)
|
||||
Serialize() (m map[string]interface{}, e error)
|
||||
}
|
||||
|
||||
// actor is an object that is an ActivityPub Actor. The specification is more
|
||||
// strict than what we include here, only for our internal use.
|
||||
type actor interface {
|
||||
IsInboxAnyURI() (ok bool)
|
||||
GetInboxAnyURI() (v *url.URL)
|
||||
IsInboxOrderedCollection() (ok bool)
|
||||
GetInboxOrderedCollection() (v vocab.OrderedCollectionType)
|
||||
}
|
||||
|
||||
var _ actor = &vocab.Object{}
|
||||
|
||||
// actorObject is an object that has "actor" or "attributedTo" properties,
|
||||
// representing the author or originator of the object.
|
||||
type actorObject interface {
|
||||
IsInboxAnyURI() (ok bool)
|
||||
GetInboxAnyURI() (v *url.URL)
|
||||
IsInboxOrderedCollection() (ok bool)
|
||||
GetInboxOrderedCollection() (v vocab.OrderedCollectionType)
|
||||
AttributedToLen() (l int)
|
||||
IsAttributedToObject(index int) (ok bool)
|
||||
GetAttributedToObject(index int) (v vocab.ObjectType)
|
||||
IsAttributedToLink(index int) (ok bool)
|
||||
GetAttributedToLink(index int) (v vocab.LinkType)
|
||||
IsAttributedToIRI(index int) (ok bool)
|
||||
GetAttributedToIRI(index int) (v *url.URL)
|
||||
ActorLen() (l int)
|
||||
IsActorObject(index int) (ok bool)
|
||||
GetActorObject(index int) (v vocab.ObjectType)
|
||||
IsActorLink(index int) (ok bool)
|
||||
GetActorLink(index int) (v vocab.LinkType)
|
||||
IsActorIRI(index int) (ok bool)
|
||||
GetActorIRI(index int) (v *url.URL)
|
||||
}
|
||||
|
||||
// deliverableObject is an object that is able to be sent to recipients via the
|
||||
// "to", "bto", "cc", "bcc", and "audience" objects and/or links and/or IRIs.
|
||||
type deliverableObject interface {
|
||||
actorObject
|
||||
ToLen() (l int)
|
||||
IsToObject(index int) (ok bool)
|
||||
GetToObject(index int) (v vocab.ObjectType)
|
||||
IsToLink(index int) (ok bool)
|
||||
GetToLink(index int) (v vocab.LinkType)
|
||||
IsToIRI(index int) (ok bool)
|
||||
GetToIRI(index int) (v *url.URL)
|
||||
BtoLen() (l int)
|
||||
IsBtoObject(index int) (ok bool)
|
||||
GetBtoObject(index int) (v vocab.ObjectType)
|
||||
RemoveBtoObject(index int)
|
||||
IsBtoLink(index int) (ok bool)
|
||||
GetBtoLink(index int) (v vocab.LinkType)
|
||||
RemoveBtoLink(index int)
|
||||
IsBtoIRI(index int) (ok bool)
|
||||
GetBtoIRI(index int) (v *url.URL)
|
||||
RemoveBtoIRI(index int)
|
||||
CcLen() (l int)
|
||||
IsCcObject(index int) (ok bool)
|
||||
GetCcObject(index int) (v vocab.ObjectType)
|
||||
IsCcLink(index int) (ok bool)
|
||||
GetCcLink(index int) (v vocab.LinkType)
|
||||
IsCcIRI(index int) (ok bool)
|
||||
GetCcIRI(index int) (v *url.URL)
|
||||
BccLen() (l int)
|
||||
IsBccObject(index int) (ok bool)
|
||||
GetBccObject(index int) (v vocab.ObjectType)
|
||||
RemoveBccObject(index int)
|
||||
IsBccLink(index int) (ok bool)
|
||||
GetBccLink(index int) (v vocab.LinkType)
|
||||
RemoveBccLink(index int)
|
||||
IsBccIRI(index int) (ok bool)
|
||||
GetBccIRI(index int) (v *url.URL)
|
||||
RemoveBccIRI(index int)
|
||||
AudienceLen() (l int)
|
||||
IsAudienceObject(index int) (ok bool)
|
||||
GetAudienceObject(index int) (v vocab.ObjectType)
|
||||
IsAudienceLink(index int) (ok bool)
|
||||
GetAudienceLink(index int) (v vocab.LinkType)
|
||||
IsAudienceIRI(index int) (ok bool)
|
||||
GetAudienceIRI(index int) (v *url.URL)
|
||||
}
|
2125
pub/internal.go
2125
pub/internal.go
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,47 @@
|
|||
package pub
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: clock.go
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockClock is a mock of Clock interface
|
||||
type MockClock struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockClockMockRecorder
|
||||
}
|
||||
|
||||
// MockClockMockRecorder is the mock recorder for MockClock
|
||||
type MockClockMockRecorder struct {
|
||||
mock *MockClock
|
||||
}
|
||||
|
||||
// NewMockClock creates a new mock instance
|
||||
func NewMockClock(ctrl *gomock.Controller) *MockClock {
|
||||
mock := &MockClock{ctrl: ctrl}
|
||||
mock.recorder = &MockClockMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockClock) EXPECT() *MockClockMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Now mocks base method
|
||||
func (m *MockClock) Now() time.Time {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Now")
|
||||
ret0, _ := ret[0].(time.Time)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Now indicates an expected call of Now
|
||||
func (mr *MockClockMockRecorder) Now() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClock)(nil).Now))
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: pub/common_behavior.go
|
||||
|
||||
// Package pub is a generated GoMock package.
|
||||
package pub
|
||||
|
||||
import (
|
||||
context "context"
|
||||
vocab "github.com/go-fed/activity/streams/vocab"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "net/http"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockCommonBehavior is a mock of CommonBehavior interface
|
||||
type MockCommonBehavior struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockCommonBehaviorMockRecorder
|
||||
}
|
||||
|
||||
// MockCommonBehaviorMockRecorder is the mock recorder for MockCommonBehavior
|
||||
type MockCommonBehaviorMockRecorder struct {
|
||||
mock *MockCommonBehavior
|
||||
}
|
||||
|
||||
// NewMockCommonBehavior creates a new mock instance
|
||||
func NewMockCommonBehavior(ctrl *gomock.Controller) *MockCommonBehavior {
|
||||
mock := &MockCommonBehavior{ctrl: ctrl}
|
||||
mock.recorder = &MockCommonBehaviorMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockCommonBehavior) EXPECT() *MockCommonBehaviorMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AuthenticateGetInbox mocks base method
|
||||
func (m *MockCommonBehavior) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticateGetInbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticateGetInbox indicates an expected call of AuthenticateGetInbox
|
||||
func (mr *MockCommonBehaviorMockRecorder) AuthenticateGetInbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateGetInbox", reflect.TypeOf((*MockCommonBehavior)(nil).AuthenticateGetInbox), c, w, r)
|
||||
}
|
||||
|
||||
// AuthenticateGetOutbox mocks base method
|
||||
func (m *MockCommonBehavior) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticateGetOutbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticateGetOutbox indicates an expected call of AuthenticateGetOutbox
|
||||
func (mr *MockCommonBehaviorMockRecorder) AuthenticateGetOutbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateGetOutbox", reflect.TypeOf((*MockCommonBehavior)(nil).AuthenticateGetOutbox), c, w, r)
|
||||
}
|
||||
|
||||
// GetOutbox mocks base method
|
||||
func (m *MockCommonBehavior) GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOutbox", c, r)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOutbox indicates an expected call of GetOutbox
|
||||
func (mr *MockCommonBehaviorMockRecorder) GetOutbox(c, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOutbox", reflect.TypeOf((*MockCommonBehavior)(nil).GetOutbox), c, r)
|
||||
}
|
||||
|
||||
// NewTransport mocks base method
|
||||
func (m *MockCommonBehavior) NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (Transport, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "NewTransport", c, actorBoxIRI, gofedAgent)
|
||||
ret0, _ := ret[0].(Transport)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// NewTransport indicates an expected call of NewTransport
|
||||
func (mr *MockCommonBehaviorMockRecorder) NewTransport(c, actorBoxIRI, gofedAgent interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTransport", reflect.TypeOf((*MockCommonBehavior)(nil).NewTransport), c, actorBoxIRI, gofedAgent)
|
||||
}
|
|
@ -0,0 +1,345 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: database.go
|
||||
|
||||
// Package pub is a generated GoMock package.
|
||||
package pub
|
||||
|
||||
import (
|
||||
context "context"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
|
||||
vocab "github.com/go-fed/activity/streams/vocab"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDatabase is a mock of Database interface.
|
||||
type MockDatabase struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDatabaseMockRecorder
|
||||
}
|
||||
|
||||
// MockDatabaseMockRecorder is the mock recorder for MockDatabase.
|
||||
type MockDatabaseMockRecorder struct {
|
||||
mock *MockDatabase
|
||||
}
|
||||
|
||||
// NewMockDatabase creates a new mock instance.
|
||||
func NewMockDatabase(ctrl *gomock.Controller) *MockDatabase {
|
||||
mock := &MockDatabase{ctrl: ctrl}
|
||||
mock.recorder = &MockDatabaseMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockDatabase) EXPECT() *MockDatabaseMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ActorForInbox mocks base method.
|
||||
func (m *MockDatabase) ActorForInbox(c context.Context, inboxIRI *url.URL) (*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ActorForInbox", c, inboxIRI)
|
||||
ret0, _ := ret[0].(*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ActorForInbox indicates an expected call of ActorForInbox.
|
||||
func (mr *MockDatabaseMockRecorder) ActorForInbox(c, inboxIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActorForInbox", reflect.TypeOf((*MockDatabase)(nil).ActorForInbox), c, inboxIRI)
|
||||
}
|
||||
|
||||
// ActorForOutbox mocks base method.
|
||||
func (m *MockDatabase) ActorForOutbox(c context.Context, outboxIRI *url.URL) (*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ActorForOutbox", c, outboxIRI)
|
||||
ret0, _ := ret[0].(*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ActorForOutbox indicates an expected call of ActorForOutbox.
|
||||
func (mr *MockDatabaseMockRecorder) ActorForOutbox(c, outboxIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActorForOutbox", reflect.TypeOf((*MockDatabase)(nil).ActorForOutbox), c, outboxIRI)
|
||||
}
|
||||
|
||||
// Create mocks base method.
|
||||
func (m *MockDatabase) Create(c context.Context, asType vocab.Type) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Create", c, asType)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Create indicates an expected call of Create.
|
||||
func (mr *MockDatabaseMockRecorder) Create(c, asType interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDatabase)(nil).Create), c, asType)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockDatabase) Delete(c context.Context, id *url.URL) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", c, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockDatabaseMockRecorder) Delete(c, id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDatabase)(nil).Delete), c, id)
|
||||
}
|
||||
|
||||
// Exists mocks base method.
|
||||
func (m *MockDatabase) Exists(c context.Context, id *url.URL) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Exists", c, id)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Exists indicates an expected call of Exists.
|
||||
func (mr *MockDatabaseMockRecorder) Exists(c, id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockDatabase)(nil).Exists), c, id)
|
||||
}
|
||||
|
||||
// Followers mocks base method.
|
||||
func (m *MockDatabase) Followers(c context.Context, actorIRI *url.URL) (vocab.ActivityStreamsCollection, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Followers", c, actorIRI)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsCollection)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Followers indicates an expected call of Followers.
|
||||
func (mr *MockDatabaseMockRecorder) Followers(c, actorIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Followers", reflect.TypeOf((*MockDatabase)(nil).Followers), c, actorIRI)
|
||||
}
|
||||
|
||||
// Following mocks base method.
|
||||
func (m *MockDatabase) Following(c context.Context, actorIRI *url.URL) (vocab.ActivityStreamsCollection, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Following", c, actorIRI)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsCollection)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Following indicates an expected call of Following.
|
||||
func (mr *MockDatabaseMockRecorder) Following(c, actorIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Following", reflect.TypeOf((*MockDatabase)(nil).Following), c, actorIRI)
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockDatabase) Get(c context.Context, id *url.URL) (vocab.Type, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", c, id)
|
||||
ret0, _ := ret[0].(vocab.Type)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockDatabaseMockRecorder) Get(c, id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDatabase)(nil).Get), c, id)
|
||||
}
|
||||
|
||||
// GetInbox mocks base method.
|
||||
func (m *MockDatabase) GetInbox(c context.Context, inboxIRI *url.URL) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInbox", c, inboxIRI)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetInbox indicates an expected call of GetInbox.
|
||||
func (mr *MockDatabaseMockRecorder) GetInbox(c, inboxIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInbox", reflect.TypeOf((*MockDatabase)(nil).GetInbox), c, inboxIRI)
|
||||
}
|
||||
|
||||
// GetOutbox mocks base method.
|
||||
func (m *MockDatabase) GetOutbox(c context.Context, outboxIRI *url.URL) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOutbox", c, outboxIRI)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOutbox indicates an expected call of GetOutbox.
|
||||
func (mr *MockDatabaseMockRecorder) GetOutbox(c, outboxIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOutbox", reflect.TypeOf((*MockDatabase)(nil).GetOutbox), c, outboxIRI)
|
||||
}
|
||||
|
||||
// InboxContains mocks base method.
|
||||
func (m *MockDatabase) InboxContains(c context.Context, inbox, id *url.URL) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InboxContains", c, inbox, id)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InboxContains indicates an expected call of InboxContains.
|
||||
func (mr *MockDatabaseMockRecorder) InboxContains(c, inbox, id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InboxContains", reflect.TypeOf((*MockDatabase)(nil).InboxContains), c, inbox, id)
|
||||
}
|
||||
|
||||
// InboxForActor mocks base method.
|
||||
func (m *MockDatabase) InboxForActor(c context.Context, actorIRI *url.URL) (*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InboxForActor", c, actorIRI)
|
||||
ret0, _ := ret[0].(*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InboxForActor indicates an expected call of InboxForActor.
|
||||
func (mr *MockDatabaseMockRecorder) InboxForActor(c, actorIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InboxForActor", reflect.TypeOf((*MockDatabase)(nil).InboxForActor), c, actorIRI)
|
||||
}
|
||||
|
||||
// Liked mocks base method.
|
||||
func (m *MockDatabase) Liked(c context.Context, actorIRI *url.URL) (vocab.ActivityStreamsCollection, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Liked", c, actorIRI)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsCollection)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Liked indicates an expected call of Liked.
|
||||
func (mr *MockDatabaseMockRecorder) Liked(c, actorIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Liked", reflect.TypeOf((*MockDatabase)(nil).Liked), c, actorIRI)
|
||||
}
|
||||
|
||||
// Lock mocks base method.
|
||||
func (m *MockDatabase) Lock(c context.Context, id *url.URL) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Lock", c, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Lock indicates an expected call of Lock.
|
||||
func (mr *MockDatabaseMockRecorder) Lock(c, id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockDatabase)(nil).Lock), c, id)
|
||||
}
|
||||
|
||||
// NewID mocks base method.
|
||||
func (m *MockDatabase) NewID(c context.Context, t vocab.Type) (*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "NewID", c, t)
|
||||
ret0, _ := ret[0].(*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// NewID indicates an expected call of NewID.
|
||||
func (mr *MockDatabaseMockRecorder) NewID(c, t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewID", reflect.TypeOf((*MockDatabase)(nil).NewID), c, t)
|
||||
}
|
||||
|
||||
// OutboxForInbox mocks base method.
|
||||
func (m *MockDatabase) OutboxForInbox(c context.Context, inboxIRI *url.URL) (*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OutboxForInbox", c, inboxIRI)
|
||||
ret0, _ := ret[0].(*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// OutboxForInbox indicates an expected call of OutboxForInbox.
|
||||
func (mr *MockDatabaseMockRecorder) OutboxForInbox(c, inboxIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OutboxForInbox", reflect.TypeOf((*MockDatabase)(nil).OutboxForInbox), c, inboxIRI)
|
||||
}
|
||||
|
||||
// Owns mocks base method.
|
||||
func (m *MockDatabase) Owns(c context.Context, id *url.URL) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Owns", c, id)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Owns indicates an expected call of Owns.
|
||||
func (mr *MockDatabaseMockRecorder) Owns(c, id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Owns", reflect.TypeOf((*MockDatabase)(nil).Owns), c, id)
|
||||
}
|
||||
|
||||
// SetInbox mocks base method.
|
||||
func (m *MockDatabase) SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetInbox", c, inbox)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetInbox indicates an expected call of SetInbox.
|
||||
func (mr *MockDatabaseMockRecorder) SetInbox(c, inbox interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetInbox", reflect.TypeOf((*MockDatabase)(nil).SetInbox), c, inbox)
|
||||
}
|
||||
|
||||
// SetOutbox mocks base method.
|
||||
func (m *MockDatabase) SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetOutbox", c, outbox)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetOutbox indicates an expected call of SetOutbox.
|
||||
func (mr *MockDatabaseMockRecorder) SetOutbox(c, outbox interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOutbox", reflect.TypeOf((*MockDatabase)(nil).SetOutbox), c, outbox)
|
||||
}
|
||||
|
||||
// Unlock mocks base method.
|
||||
func (m *MockDatabase) Unlock(c context.Context, id *url.URL) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Unlock", c, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Unlock indicates an expected call of Unlock.
|
||||
func (mr *MockDatabaseMockRecorder) Unlock(c, id interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockDatabase)(nil).Unlock), c, id)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockDatabase) Update(c context.Context, asType vocab.Type) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", c, asType)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockDatabaseMockRecorder) Update(c, asType interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockDatabase)(nil).Update), c, asType)
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: delegate_actor.go
|
||||
|
||||
// Package pub is a generated GoMock package.
|
||||
package pub
|
||||
|
||||
import (
|
||||
context "context"
|
||||
vocab "github.com/go-fed/activity/streams/vocab"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "net/http"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockDelegateActor is a mock of DelegateActor interface
|
||||
type MockDelegateActor struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDelegateActorMockRecorder
|
||||
}
|
||||
|
||||
// MockDelegateActorMockRecorder is the mock recorder for MockDelegateActor
|
||||
type MockDelegateActorMockRecorder struct {
|
||||
mock *MockDelegateActor
|
||||
}
|
||||
|
||||
// NewMockDelegateActor creates a new mock instance
|
||||
func NewMockDelegateActor(ctrl *gomock.Controller) *MockDelegateActor {
|
||||
mock := &MockDelegateActor{ctrl: ctrl}
|
||||
mock.recorder = &MockDelegateActorMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockDelegateActor) EXPECT() *MockDelegateActorMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// PostInboxRequestBodyHook mocks base method
|
||||
func (m *MockDelegateActor) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostInboxRequestBodyHook", c, r, activity)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PostInboxRequestBodyHook indicates an expected call of PostInboxRequestBodyHook
|
||||
func (mr *MockDelegateActorMockRecorder) PostInboxRequestBodyHook(c, r, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostInboxRequestBodyHook", reflect.TypeOf((*MockDelegateActor)(nil).PostInboxRequestBodyHook), c, r, activity)
|
||||
}
|
||||
|
||||
// PostOutboxRequestBodyHook mocks base method
|
||||
func (m *MockDelegateActor) PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostOutboxRequestBodyHook", c, r, data)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PostOutboxRequestBodyHook indicates an expected call of PostOutboxRequestBodyHook
|
||||
func (mr *MockDelegateActorMockRecorder) PostOutboxRequestBodyHook(c, r, data interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostOutboxRequestBodyHook", reflect.TypeOf((*MockDelegateActor)(nil).PostOutboxRequestBodyHook), c, r, data)
|
||||
}
|
||||
|
||||
// AuthenticatePostInbox mocks base method
|
||||
func (m *MockDelegateActor) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticatePostInbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticatePostInbox indicates an expected call of AuthenticatePostInbox
|
||||
func (mr *MockDelegateActorMockRecorder) AuthenticatePostInbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePostInbox", reflect.TypeOf((*MockDelegateActor)(nil).AuthenticatePostInbox), c, w, r)
|
||||
}
|
||||
|
||||
// AuthenticateGetInbox mocks base method
|
||||
func (m *MockDelegateActor) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticateGetInbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticateGetInbox indicates an expected call of AuthenticateGetInbox
|
||||
func (mr *MockDelegateActorMockRecorder) AuthenticateGetInbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateGetInbox", reflect.TypeOf((*MockDelegateActor)(nil).AuthenticateGetInbox), c, w, r)
|
||||
}
|
||||
|
||||
// AuthorizePostInbox mocks base method
|
||||
func (m *MockDelegateActor) AuthorizePostInbox(c context.Context, w http.ResponseWriter, activity Activity) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthorizePostInbox", c, w, activity)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthorizePostInbox indicates an expected call of AuthorizePostInbox
|
||||
func (mr *MockDelegateActorMockRecorder) AuthorizePostInbox(c, w, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizePostInbox", reflect.TypeOf((*MockDelegateActor)(nil).AuthorizePostInbox), c, w, activity)
|
||||
}
|
||||
|
||||
// PostInbox mocks base method
|
||||
func (m *MockDelegateActor) PostInbox(c context.Context, inboxIRI *url.URL, activity Activity) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostInbox", c, inboxIRI, activity)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PostInbox indicates an expected call of PostInbox
|
||||
func (mr *MockDelegateActorMockRecorder) PostInbox(c, inboxIRI, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostInbox", reflect.TypeOf((*MockDelegateActor)(nil).PostInbox), c, inboxIRI, activity)
|
||||
}
|
||||
|
||||
// InboxForwarding mocks base method
|
||||
func (m *MockDelegateActor) InboxForwarding(c context.Context, inboxIRI *url.URL, activity Activity) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InboxForwarding", c, inboxIRI, activity)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// InboxForwarding indicates an expected call of InboxForwarding
|
||||
func (mr *MockDelegateActorMockRecorder) InboxForwarding(c, inboxIRI, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InboxForwarding", reflect.TypeOf((*MockDelegateActor)(nil).InboxForwarding), c, inboxIRI, activity)
|
||||
}
|
||||
|
||||
// PostOutbox mocks base method
|
||||
func (m *MockDelegateActor) PostOutbox(c context.Context, a Activity, outboxIRI *url.URL, rawJSON map[string]interface{}) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostOutbox", c, a, outboxIRI, rawJSON)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PostOutbox indicates an expected call of PostOutbox
|
||||
func (mr *MockDelegateActorMockRecorder) PostOutbox(c, a, outboxIRI, rawJSON interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostOutbox", reflect.TypeOf((*MockDelegateActor)(nil).PostOutbox), c, a, outboxIRI, rawJSON)
|
||||
}
|
||||
|
||||
// AddNewIDs mocks base method
|
||||
func (m *MockDelegateActor) AddNewIDs(c context.Context, a Activity) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddNewIDs", c, a)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddNewIDs indicates an expected call of AddNewIDs
|
||||
func (mr *MockDelegateActorMockRecorder) AddNewIDs(c, a interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddNewIDs", reflect.TypeOf((*MockDelegateActor)(nil).AddNewIDs), c, a)
|
||||
}
|
||||
|
||||
// Deliver mocks base method
|
||||
func (m *MockDelegateActor) Deliver(c context.Context, outbox *url.URL, activity Activity) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Deliver", c, outbox, activity)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Deliver indicates an expected call of Deliver
|
||||
func (mr *MockDelegateActorMockRecorder) Deliver(c, outbox, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deliver", reflect.TypeOf((*MockDelegateActor)(nil).Deliver), c, outbox, activity)
|
||||
}
|
||||
|
||||
// AuthenticatePostOutbox mocks base method
|
||||
func (m *MockDelegateActor) AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticatePostOutbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticatePostOutbox indicates an expected call of AuthenticatePostOutbox
|
||||
func (mr *MockDelegateActorMockRecorder) AuthenticatePostOutbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePostOutbox", reflect.TypeOf((*MockDelegateActor)(nil).AuthenticatePostOutbox), c, w, r)
|
||||
}
|
||||
|
||||
// AuthenticateGetOutbox mocks base method
|
||||
func (m *MockDelegateActor) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticateGetOutbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticateGetOutbox indicates an expected call of AuthenticateGetOutbox
|
||||
func (mr *MockDelegateActorMockRecorder) AuthenticateGetOutbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateGetOutbox", reflect.TypeOf((*MockDelegateActor)(nil).AuthenticateGetOutbox), c, w, r)
|
||||
}
|
||||
|
||||
// WrapInCreate mocks base method
|
||||
func (m *MockDelegateActor) WrapInCreate(c context.Context, value vocab.Type, outboxIRI *url.URL) (vocab.ActivityStreamsCreate, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WrapInCreate", c, value, outboxIRI)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsCreate)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// WrapInCreate indicates an expected call of WrapInCreate
|
||||
func (mr *MockDelegateActorMockRecorder) WrapInCreate(c, value, outboxIRI interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WrapInCreate", reflect.TypeOf((*MockDelegateActor)(nil).WrapInCreate), c, value, outboxIRI)
|
||||
}
|
||||
|
||||
// GetOutbox mocks base method
|
||||
func (m *MockDelegateActor) GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOutbox", c, r)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOutbox indicates an expected call of GetOutbox
|
||||
func (mr *MockDelegateActorMockRecorder) GetOutbox(c, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOutbox", reflect.TypeOf((*MockDelegateActor)(nil).GetOutbox), c, r)
|
||||
}
|
||||
|
||||
// GetInbox mocks base method
|
||||
func (m *MockDelegateActor) GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInbox", c, r)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetInbox indicates an expected call of GetInbox
|
||||
func (mr *MockDelegateActorMockRecorder) GetInbox(c, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInbox", reflect.TypeOf((*MockDelegateActor)(nil).GetInbox), c, r)
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: federating_protocol.go
|
||||
|
||||
// Package pub is a generated GoMock package.
|
||||
package pub
|
||||
|
||||
import (
|
||||
context "context"
|
||||
vocab "github.com/go-fed/activity/streams/vocab"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "net/http"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockFederatingProtocol is a mock of FederatingProtocol interface
|
||||
type MockFederatingProtocol struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockFederatingProtocolMockRecorder
|
||||
}
|
||||
|
||||
// MockFederatingProtocolMockRecorder is the mock recorder for MockFederatingProtocol
|
||||
type MockFederatingProtocolMockRecorder struct {
|
||||
mock *MockFederatingProtocol
|
||||
}
|
||||
|
||||
// NewMockFederatingProtocol creates a new mock instance
|
||||
func NewMockFederatingProtocol(ctrl *gomock.Controller) *MockFederatingProtocol {
|
||||
mock := &MockFederatingProtocol{ctrl: ctrl}
|
||||
mock.recorder = &MockFederatingProtocolMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockFederatingProtocol) EXPECT() *MockFederatingProtocolMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// PostInboxRequestBodyHook mocks base method
|
||||
func (m *MockFederatingProtocol) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostInboxRequestBodyHook", c, r, activity)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PostInboxRequestBodyHook indicates an expected call of PostInboxRequestBodyHook
|
||||
func (mr *MockFederatingProtocolMockRecorder) PostInboxRequestBodyHook(c, r, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostInboxRequestBodyHook", reflect.TypeOf((*MockFederatingProtocol)(nil).PostInboxRequestBodyHook), c, r, activity)
|
||||
}
|
||||
|
||||
// AuthenticatePostInbox mocks base method
|
||||
func (m *MockFederatingProtocol) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticatePostInbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticatePostInbox indicates an expected call of AuthenticatePostInbox
|
||||
func (mr *MockFederatingProtocolMockRecorder) AuthenticatePostInbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePostInbox", reflect.TypeOf((*MockFederatingProtocol)(nil).AuthenticatePostInbox), c, w, r)
|
||||
}
|
||||
|
||||
// Blocked mocks base method
|
||||
func (m *MockFederatingProtocol) Blocked(c context.Context, actorIRIs []*url.URL) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Blocked", c, actorIRIs)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Blocked indicates an expected call of Blocked
|
||||
func (mr *MockFederatingProtocolMockRecorder) Blocked(c, actorIRIs interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Blocked", reflect.TypeOf((*MockFederatingProtocol)(nil).Blocked), c, actorIRIs)
|
||||
}
|
||||
|
||||
// FederatingCallbacks mocks base method
|
||||
func (m *MockFederatingProtocol) FederatingCallbacks(c context.Context) (FederatingWrappedCallbacks, []interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FederatingCallbacks", c)
|
||||
ret0, _ := ret[0].(FederatingWrappedCallbacks)
|
||||
ret1, _ := ret[1].([]interface{})
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// FederatingCallbacks indicates an expected call of FederatingCallbacks
|
||||
func (mr *MockFederatingProtocolMockRecorder) FederatingCallbacks(c interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FederatingCallbacks", reflect.TypeOf((*MockFederatingProtocol)(nil).FederatingCallbacks), c)
|
||||
}
|
||||
|
||||
// DefaultCallback mocks base method
|
||||
func (m *MockFederatingProtocol) DefaultCallback(c context.Context, activity Activity) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DefaultCallback", c, activity)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DefaultCallback indicates an expected call of DefaultCallback
|
||||
func (mr *MockFederatingProtocolMockRecorder) DefaultCallback(c, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultCallback", reflect.TypeOf((*MockFederatingProtocol)(nil).DefaultCallback), c, activity)
|
||||
}
|
||||
|
||||
// MaxInboxForwardingRecursionDepth mocks base method
|
||||
func (m *MockFederatingProtocol) MaxInboxForwardingRecursionDepth(c context.Context) int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "MaxInboxForwardingRecursionDepth", c)
|
||||
ret0, _ := ret[0].(int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// MaxInboxForwardingRecursionDepth indicates an expected call of MaxInboxForwardingRecursionDepth
|
||||
func (mr *MockFederatingProtocolMockRecorder) MaxInboxForwardingRecursionDepth(c interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxInboxForwardingRecursionDepth", reflect.TypeOf((*MockFederatingProtocol)(nil).MaxInboxForwardingRecursionDepth), c)
|
||||
}
|
||||
|
||||
// MaxDeliveryRecursionDepth mocks base method
|
||||
func (m *MockFederatingProtocol) MaxDeliveryRecursionDepth(c context.Context) int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "MaxDeliveryRecursionDepth", c)
|
||||
ret0, _ := ret[0].(int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// MaxDeliveryRecursionDepth indicates an expected call of MaxDeliveryRecursionDepth
|
||||
func (mr *MockFederatingProtocolMockRecorder) MaxDeliveryRecursionDepth(c interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxDeliveryRecursionDepth", reflect.TypeOf((*MockFederatingProtocol)(nil).MaxDeliveryRecursionDepth), c)
|
||||
}
|
||||
|
||||
// FilterForwarding mocks base method
|
||||
func (m *MockFederatingProtocol) FilterForwarding(c context.Context, potentialRecipients []*url.URL, a Activity) ([]*url.URL, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FilterForwarding", c, potentialRecipients, a)
|
||||
ret0, _ := ret[0].([]*url.URL)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FilterForwarding indicates an expected call of FilterForwarding
|
||||
func (mr *MockFederatingProtocolMockRecorder) FilterForwarding(c, potentialRecipients, a interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterForwarding", reflect.TypeOf((*MockFederatingProtocol)(nil).FilterForwarding), c, potentialRecipients, a)
|
||||
}
|
||||
|
||||
// GetInbox mocks base method
|
||||
func (m *MockFederatingProtocol) GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInbox", c, r)
|
||||
ret0, _ := ret[0].(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetInbox indicates an expected call of GetInbox
|
||||
func (mr *MockFederatingProtocolMockRecorder) GetInbox(c, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInbox", reflect.TypeOf((*MockFederatingProtocol)(nil).GetInbox), c, r)
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ../../httpsig/httpsig.go
|
||||
|
||||
// Package pub is a generated GoMock package.
|
||||
package pub
|
||||
|
||||
import (
|
||||
crypto "crypto"
|
||||
httpsig "github.com/go-fed/httpsig"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "net/http"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockSigner is a mock of Signer interface
|
||||
type MockSigner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSignerMockRecorder
|
||||
}
|
||||
|
||||
// MockSignerMockRecorder is the mock recorder for MockSigner
|
||||
type MockSignerMockRecorder struct {
|
||||
mock *MockSigner
|
||||
}
|
||||
|
||||
// NewMockSigner creates a new mock instance
|
||||
func NewMockSigner(ctrl *gomock.Controller) *MockSigner {
|
||||
mock := &MockSigner{ctrl: ctrl}
|
||||
mock.recorder = &MockSignerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockSigner) EXPECT() *MockSignerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// SignRequest mocks base method
|
||||
func (m *MockSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SignRequest", pKey, pubKeyId, r, body)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SignRequest indicates an expected call of SignRequest
|
||||
func (mr *MockSignerMockRecorder) SignRequest(pKey, pubKeyId, r, body interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignRequest", reflect.TypeOf((*MockSigner)(nil).SignRequest), pKey, pubKeyId, r, body)
|
||||
}
|
||||
|
||||
// SignResponse mocks base method
|
||||
func (m *MockSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SignResponse", pKey, pubKeyId, r, body)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SignResponse indicates an expected call of SignResponse
|
||||
func (mr *MockSignerMockRecorder) SignResponse(pKey, pubKeyId, r, body interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignResponse", reflect.TypeOf((*MockSigner)(nil).SignResponse), pKey, pubKeyId, r, body)
|
||||
}
|
||||
|
||||
// MockSSHSigner is a mock of SSHSigner interface
|
||||
type MockSSHSigner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSSHSignerMockRecorder
|
||||
}
|
||||
|
||||
// MockSSHSignerMockRecorder is the mock recorder for MockSSHSigner
|
||||
type MockSSHSignerMockRecorder struct {
|
||||
mock *MockSSHSigner
|
||||
}
|
||||
|
||||
// NewMockSSHSigner creates a new mock instance
|
||||
func NewMockSSHSigner(ctrl *gomock.Controller) *MockSSHSigner {
|
||||
mock := &MockSSHSigner{ctrl: ctrl}
|
||||
mock.recorder = &MockSSHSignerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockSSHSigner) EXPECT() *MockSSHSignerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// SignRequest mocks base method
|
||||
func (m *MockSSHSigner) SignRequest(pubKeyId string, r *http.Request, body []byte) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SignRequest", pubKeyId, r, body)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SignRequest indicates an expected call of SignRequest
|
||||
func (mr *MockSSHSignerMockRecorder) SignRequest(pubKeyId, r, body interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignRequest", reflect.TypeOf((*MockSSHSigner)(nil).SignRequest), pubKeyId, r, body)
|
||||
}
|
||||
|
||||
// SignResponse mocks base method
|
||||
func (m *MockSSHSigner) SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SignResponse", pubKeyId, r, body)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SignResponse indicates an expected call of SignResponse
|
||||
func (mr *MockSSHSignerMockRecorder) SignResponse(pubKeyId, r, body interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignResponse", reflect.TypeOf((*MockSSHSigner)(nil).SignResponse), pubKeyId, r, body)
|
||||
}
|
||||
|
||||
// MockVerifier is a mock of Verifier interface
|
||||
type MockVerifier struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockVerifierMockRecorder
|
||||
}
|
||||
|
||||
// MockVerifierMockRecorder is the mock recorder for MockVerifier
|
||||
type MockVerifierMockRecorder struct {
|
||||
mock *MockVerifier
|
||||
}
|
||||
|
||||
// NewMockVerifier creates a new mock instance
|
||||
func NewMockVerifier(ctrl *gomock.Controller) *MockVerifier {
|
||||
mock := &MockVerifier{ctrl: ctrl}
|
||||
mock.recorder = &MockVerifierMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockVerifier) EXPECT() *MockVerifierMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// KeyId mocks base method
|
||||
func (m *MockVerifier) KeyId() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "KeyId")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// KeyId indicates an expected call of KeyId
|
||||
func (mr *MockVerifierMockRecorder) KeyId() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyId", reflect.TypeOf((*MockVerifier)(nil).KeyId))
|
||||
}
|
||||
|
||||
// Verify mocks base method
|
||||
func (m *MockVerifier) Verify(pKey crypto.PublicKey, algo httpsig.Algorithm) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Verify", pKey, algo)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Verify indicates an expected call of Verify
|
||||
func (mr *MockVerifierMockRecorder) Verify(pKey, algo interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockVerifier)(nil).Verify), pKey, algo)
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: social_protocol.go
|
||||
|
||||
// Package pub is a generated GoMock package.
|
||||
package pub
|
||||
|
||||
import (
|
||||
context "context"
|
||||
vocab "github.com/go-fed/activity/streams/vocab"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "net/http"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockSocialProtocol is a mock of SocialProtocol interface
|
||||
type MockSocialProtocol struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSocialProtocolMockRecorder
|
||||
}
|
||||
|
||||
// MockSocialProtocolMockRecorder is the mock recorder for MockSocialProtocol
|
||||
type MockSocialProtocolMockRecorder struct {
|
||||
mock *MockSocialProtocol
|
||||
}
|
||||
|
||||
// NewMockSocialProtocol creates a new mock instance
|
||||
func NewMockSocialProtocol(ctrl *gomock.Controller) *MockSocialProtocol {
|
||||
mock := &MockSocialProtocol{ctrl: ctrl}
|
||||
mock.recorder = &MockSocialProtocolMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockSocialProtocol) EXPECT() *MockSocialProtocolMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// PostOutboxRequestBodyHook mocks base method
|
||||
func (m *MockSocialProtocol) PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostOutboxRequestBodyHook", c, r, data)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PostOutboxRequestBodyHook indicates an expected call of PostOutboxRequestBodyHook
|
||||
func (mr *MockSocialProtocolMockRecorder) PostOutboxRequestBodyHook(c, r, data interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostOutboxRequestBodyHook", reflect.TypeOf((*MockSocialProtocol)(nil).PostOutboxRequestBodyHook), c, r, data)
|
||||
}
|
||||
|
||||
// AuthenticatePostOutbox mocks base method
|
||||
func (m *MockSocialProtocol) AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticatePostOutbox", c, w, r)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// AuthenticatePostOutbox indicates an expected call of AuthenticatePostOutbox
|
||||
func (mr *MockSocialProtocolMockRecorder) AuthenticatePostOutbox(c, w, r interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePostOutbox", reflect.TypeOf((*MockSocialProtocol)(nil).AuthenticatePostOutbox), c, w, r)
|
||||
}
|
||||
|
||||
// SocialCallbacks mocks base method
|
||||
func (m *MockSocialProtocol) SocialCallbacks(c context.Context) (SocialWrappedCallbacks, []interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SocialCallbacks", c)
|
||||
ret0, _ := ret[0].(SocialWrappedCallbacks)
|
||||
ret1, _ := ret[1].([]interface{})
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// SocialCallbacks indicates an expected call of SocialCallbacks
|
||||
func (mr *MockSocialProtocolMockRecorder) SocialCallbacks(c interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SocialCallbacks", reflect.TypeOf((*MockSocialProtocol)(nil).SocialCallbacks), c)
|
||||
}
|
||||
|
||||
// DefaultCallback mocks base method
|
||||
func (m *MockSocialProtocol) DefaultCallback(c context.Context, activity Activity) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DefaultCallback", c, activity)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DefaultCallback indicates an expected call of DefaultCallback
|
||||
func (mr *MockSocialProtocolMockRecorder) DefaultCallback(c, activity interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultCallback", reflect.TypeOf((*MockSocialProtocol)(nil).DefaultCallback), c, activity)
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: transport.go
|
||||
|
||||
// Package pub is a generated GoMock package.
|
||||
package pub
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
http "net/http"
|
||||
url "net/url"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockTransport is a mock of Transport interface
|
||||
type MockTransport struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTransportMockRecorder
|
||||
}
|
||||
|
||||
// MockTransportMockRecorder is the mock recorder for MockTransport
|
||||
type MockTransportMockRecorder struct {
|
||||
mock *MockTransport
|
||||
}
|
||||
|
||||
// NewMockTransport creates a new mock instance
|
||||
func NewMockTransport(ctrl *gomock.Controller) *MockTransport {
|
||||
mock := &MockTransport{ctrl: ctrl}
|
||||
mock.recorder = &MockTransportMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockTransport) EXPECT() *MockTransportMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Dereference mocks base method
|
||||
func (m *MockTransport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Dereference", c, iri)
|
||||
ret0, _ := ret[0].([]byte)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Dereference indicates an expected call of Dereference
|
||||
func (mr *MockTransportMockRecorder) Dereference(c, iri interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dereference", reflect.TypeOf((*MockTransport)(nil).Dereference), c, iri)
|
||||
}
|
||||
|
||||
// Deliver mocks base method
|
||||
func (m *MockTransport) Deliver(c context.Context, b []byte, to *url.URL) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Deliver", c, b, to)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Deliver indicates an expected call of Deliver
|
||||
func (mr *MockTransportMockRecorder) Deliver(c, b, to interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deliver", reflect.TypeOf((*MockTransport)(nil).Deliver), c, b, to)
|
||||
}
|
||||
|
||||
// BatchDeliver mocks base method
|
||||
func (m *MockTransport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BatchDeliver", c, b, recipients)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// BatchDeliver indicates an expected call of BatchDeliver
|
||||
func (mr *MockTransportMockRecorder) BatchDeliver(c, b, recipients interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchDeliver", reflect.TypeOf((*MockTransport)(nil).BatchDeliver), c, b, recipients)
|
||||
}
|
||||
|
||||
// MockHttpClient is a mock of HttpClient interface
|
||||
type MockHttpClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockHttpClientMockRecorder
|
||||
}
|
||||
|
||||
// MockHttpClientMockRecorder is the mock recorder for MockHttpClient
|
||||
type MockHttpClientMockRecorder struct {
|
||||
mock *MockHttpClient
|
||||
}
|
||||
|
||||
// NewMockHttpClient creates a new mock instance
|
||||
func NewMockHttpClient(ctrl *gomock.Controller) *MockHttpClient {
|
||||
mock := &MockHttpClient{ctrl: ctrl}
|
||||
mock.recorder = &MockHttpClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockHttpClient) EXPECT() *MockHttpClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Do mocks base method
|
||||
func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Do", req)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Do indicates an expected call of Do
|
||||
func (mr *MockHttpClientMockRecorder) Do(req interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockHttpClient)(nil).Do), req)
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// inReplyToer is an ActivityStreams type with an 'inReplyTo' property
|
||||
type inReplyToer interface {
|
||||
GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty
|
||||
}
|
||||
|
||||
// objecter is an ActivityStreams type with an 'object' property
|
||||
type objecter interface {
|
||||
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
|
||||
}
|
||||
|
||||
// targeter is an ActivityStreams type with a 'target' property
|
||||
type targeter interface {
|
||||
GetActivityStreamsTarget() vocab.ActivityStreamsTargetProperty
|
||||
}
|
||||
|
||||
// tagger is an ActivityStreams type with a 'tag' property
|
||||
type tagger interface {
|
||||
GetActivityStreamsTag() vocab.ActivityStreamsTagProperty
|
||||
}
|
||||
|
||||
// hrefer is an ActivityStreams type with a 'href' property
|
||||
type hrefer interface {
|
||||
GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty
|
||||
}
|
||||
|
||||
// itemser is an ActivityStreams type with an 'items' property
|
||||
type itemser interface {
|
||||
GetActivityStreamsItems() vocab.ActivityStreamsItemsProperty
|
||||
SetActivityStreamsItems(vocab.ActivityStreamsItemsProperty)
|
||||
}
|
||||
|
||||
// orderedItemser is an ActivityStreams type with an 'orderedItems' property
|
||||
type orderedItemser interface {
|
||||
GetActivityStreamsOrderedItems() vocab.ActivityStreamsOrderedItemsProperty
|
||||
SetActivityStreamsOrderedItems(vocab.ActivityStreamsOrderedItemsProperty)
|
||||
}
|
||||
|
||||
// publisheder is an ActivityStreams type with a 'published' property
|
||||
type publisheder interface {
|
||||
GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty
|
||||
}
|
||||
|
||||
// updateder is an ActivityStreams type with an 'updateder' property
|
||||
type updateder interface {
|
||||
GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty
|
||||
}
|
||||
|
||||
// toer is an ActivityStreams type with a 'to' property
|
||||
type toer interface {
|
||||
GetActivityStreamsTo() vocab.ActivityStreamsToProperty
|
||||
SetActivityStreamsTo(i vocab.ActivityStreamsToProperty)
|
||||
}
|
||||
|
||||
// btoer is an ActivityStreams type with a 'bto' property
|
||||
type btoer interface {
|
||||
GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty
|
||||
SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty)
|
||||
}
|
||||
|
||||
// ccer is an ActivityStreams type with a 'cc' property
|
||||
type ccer interface {
|
||||
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
|
||||
SetActivityStreamsCc(i vocab.ActivityStreamsCcProperty)
|
||||
}
|
||||
|
||||
// bccer is an ActivityStreams type with a 'bcc' property
|
||||
type bccer interface {
|
||||
GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty
|
||||
SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty)
|
||||
}
|
||||
|
||||
// audiencer is an ActivityStreams type with an 'audience' property
|
||||
type audiencer interface {
|
||||
GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty
|
||||
SetActivityStreamsAudience(i vocab.ActivityStreamsAudienceProperty)
|
||||
}
|
||||
|
||||
// inboxer is an ActivityStreams type with an 'inbox' property
|
||||
type inboxer interface {
|
||||
GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty
|
||||
}
|
||||
|
||||
// attributedToer is an ActivityStreams type with an 'attributedTo' property
|
||||
type attributedToer interface {
|
||||
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
|
||||
SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty)
|
||||
}
|
||||
|
||||
// likeser is an ActivityStreams type with a 'likes' property
|
||||
type likeser interface {
|
||||
GetActivityStreamsLikes() vocab.ActivityStreamsLikesProperty
|
||||
SetActivityStreamsLikes(i vocab.ActivityStreamsLikesProperty)
|
||||
}
|
||||
|
||||
// shareser is an ActivityStreams type with a 'shares' property
|
||||
type shareser interface {
|
||||
GetActivityStreamsShares() vocab.ActivityStreamsSharesProperty
|
||||
SetActivityStreamsShares(i vocab.ActivityStreamsSharesProperty)
|
||||
}
|
||||
|
||||
// actorer is an ActivityStreams type with an 'actor' property
|
||||
type actorer interface {
|
||||
GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
|
||||
SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty)
|
||||
}
|
||||
|
||||
// appendIRIer is an ActivityStreams type that can Append IRIs.
|
||||
type appendIRIer interface {
|
||||
AppendIRI(v *url.URL)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
// Package pub provides generic support for the ActivityPub Social API and
|
||||
// Federation Protocol for client-to-server and server-to-server interactions.
|
||||
package pub
|
|
@ -0,0 +1,707 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
testMyInboxIRI = "https://example.com/addison/inbox"
|
||||
testMyOutboxIRI = "https://example.com/addison/outbox"
|
||||
testFederatedActivityIRI = "https://other.example.com/activity/1"
|
||||
testFederatedActivityIRI2 = "https://other.example.com/activity/2"
|
||||
testFederatedActorIRI = "https://other.example.com/dakota"
|
||||
testFederatedActorIRI2 = "https://other.example.com/addison"
|
||||
testFederatedActorIRI3 = "https://other.example.com/sam"
|
||||
testFederatedActorIRI4 = "https://other.example.com/jessie"
|
||||
testFederatedInboxIRI = "https://other.example.com/dakota/inbox"
|
||||
testFederatedInboxIRI2 = "https://other.example.com/addison/inbox"
|
||||
testNoteId1 = "https://example.com/note/1"
|
||||
testNoteId2 = "https://example.com/note/2"
|
||||
testNewActivityIRI = "https://example.com/new/1"
|
||||
testNewActivityIRI2 = "https://example.com/new/2"
|
||||
testNewActivityIRI3 = "https://example.com/new/3"
|
||||
testToIRI = "https://maybe.example.com/to/1"
|
||||
testToIRI2 = "https://maybe.example.com/to/2"
|
||||
testCcIRI = "https://maybe.example.com/cc/1"
|
||||
testCcIRI2 = "https://maybe.example.com/cc/2"
|
||||
testAudienceIRI = "https://maybe.example.com/audience/1"
|
||||
testAudienceIRI2 = "https://maybe.example.com/audience/2"
|
||||
testPersonIRI = "https://maybe.example.com/person"
|
||||
testServiceIRI = "https://maybe.example.com/service"
|
||||
testTagIRI = "https://example.com/tag/1"
|
||||
testTagIRI2 = "https://example.com/tag/2"
|
||||
inReplyToIRI = "https://example.com/inReplyTo/1"
|
||||
inReplyToIRI2 = "https://example.com/inReplyTo/2"
|
||||
)
|
||||
|
||||
// mustParse parses a URL or panics.
|
||||
func mustParse(s string) *url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// assertEqual ensures two values are equal.
|
||||
func assertEqual(t *testing.T, a, b interface{}) {
|
||||
if a != b {
|
||||
t.Errorf("expected equal: %v != %v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// assertByteEqual ensures two byte slices are equal.
|
||||
func assertByteEqual(t *testing.T, a, b []byte) {
|
||||
if string(a) != string(b) {
|
||||
t.Errorf("expected equal:\n%s\n\n%s", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// assertNotEqual ensures two values are not equal.
|
||||
func assertNotEqual(t *testing.T, a, b interface{}) {
|
||||
if a == b {
|
||||
t.Errorf("expected not equal: %v != %v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// testErr is a test error.
|
||||
testErr = errors.New("test error")
|
||||
// testFederatedNote is a test Note from a federated peer.
|
||||
testFederatedNote vocab.ActivityStreamsNote
|
||||
// testFederatedNote2 is a test Note from a federated peer.
|
||||
testFederatedNote2 vocab.ActivityStreamsNote
|
||||
// testMyNote is a test Note owned by this server.
|
||||
testMyNote vocab.ActivityStreamsNote
|
||||
// testMyNoteNoId is a test Note owned by this server.
|
||||
testMyNoteNoId vocab.ActivityStreamsNote
|
||||
// testMyCreate is a test Create Activity.
|
||||
testMyCreate vocab.ActivityStreamsCreate
|
||||
// testCreate is a test Create Activity.
|
||||
testCreate vocab.ActivityStreamsCreate
|
||||
// testCreate2 is a test Create Activity with two actors.
|
||||
testCreate2 vocab.ActivityStreamsCreate
|
||||
// testCreateNoId is a test Create Activity without an 'id' set.
|
||||
testCreateNoId vocab.ActivityStreamsCreate
|
||||
// testOrderedCollectionUniqueElems is a collection with only unique
|
||||
// ids.
|
||||
testOrderedCollectionUniqueElems vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testOrderedCollectionUniqueElemsString is the JSON-LD version of the
|
||||
// testOrderedCollectionUniqueElems value
|
||||
testOrderedCollectionUniqueElemsString string
|
||||
// testOrderedCollectionDupedElems is a collection with duplicated ids.
|
||||
testOrderedCollectionDupedElems vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testOrderedCollectionDedupedElemsString is the JSON-LD version of the
|
||||
// testOrderedCollectionDedupedElems value with duplicates removed
|
||||
testOrderedCollectionDedupedElemsString string
|
||||
// testEmptyOrderedCollection is an empty OrderedCollectionPage.
|
||||
testEmptyOrderedCollection vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testOrderedCollectionWithNewId has the new id
|
||||
testOrderedCollectionWithNewId vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testOrderedCollectionWithNewId has the second new id
|
||||
testOrderedCollectionWithNewId2 vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testOrderedCollectionWithBothNewIds has both new ids.
|
||||
testOrderedCollectionWithBothNewIds vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testOrderedCollectionWithFederatedId has the federated Activity id.
|
||||
testOrderedCollectionWithFederatedId vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testMyListen is a test Listen C2S Activity.
|
||||
testMyListen vocab.ActivityStreamsListen
|
||||
// testMyListenNoId is a test Listen C2S Activity without an id.
|
||||
testMyListenNoId vocab.ActivityStreamsListen
|
||||
// testListen is a test Listen Activity.
|
||||
testListen vocab.ActivityStreamsListen
|
||||
// testOrderedCollectionWithFederatedId2 has the second federated
|
||||
// Activity id.
|
||||
testOrderedCollectionWithFederatedId2 vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testOrderedCollectionWithBothFederatedIds has both federated Activity id.
|
||||
testOrderedCollectionWithBothFederatedIds vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testPerson is a Person.
|
||||
testPerson vocab.ActivityStreamsPerson
|
||||
// testMyPerson is my Person.
|
||||
testMyPerson vocab.ActivityStreamsPerson
|
||||
// testFederatedPerson1 is a federated Person.
|
||||
testFederatedPerson1 vocab.ActivityStreamsPerson
|
||||
// testFederatedPerson2 is a federated Person.
|
||||
testFederatedPerson2 vocab.ActivityStreamsPerson
|
||||
// testService is a Service.
|
||||
testService vocab.ActivityStreamsService
|
||||
// testCollectionOfActors is a collection of actors.
|
||||
testCollectionOfActors vocab.ActivityStreamsCollectionPage
|
||||
// testOrderedCollectionOfActors is an ordered collection of actors.
|
||||
testOrderedCollectionOfActors vocab.ActivityStreamsOrderedCollectionPage
|
||||
// testNestedInReplyTo is an Activity with an 'object' with an 'inReplyTo'
|
||||
testNestedInReplyTo vocab.ActivityStreamsListen
|
||||
// testFollow is a test Follow Activity.
|
||||
testFollow vocab.ActivityStreamsFollow
|
||||
// testTombstone is a test Tombsone.
|
||||
testTombstone vocab.ActivityStreamsTombstone
|
||||
)
|
||||
|
||||
// The test data cannot be created at init time since that is when the hooks of
|
||||
// the `streams` package are set up. So initialize the data in this call instead
|
||||
// of at init time.
|
||||
func setupData() {
|
||||
// testFederatedNote
|
||||
func() {
|
||||
testFederatedNote = streams.NewActivityStreamsNote()
|
||||
name := streams.NewActivityStreamsNameProperty()
|
||||
name.AppendXMLSchemaString("A Federated Note")
|
||||
testFederatedNote.SetActivityStreamsName(name)
|
||||
content := streams.NewActivityStreamsContentProperty()
|
||||
content.AppendXMLSchemaString("This is a simple note being federated.")
|
||||
testFederatedNote.SetActivityStreamsContent(content)
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testNoteId1))
|
||||
testFederatedNote.SetJSONLDId(id)
|
||||
}()
|
||||
// testFederatedNote2
|
||||
func() {
|
||||
testFederatedNote2 = streams.NewActivityStreamsNote()
|
||||
name := streams.NewActivityStreamsNameProperty()
|
||||
name.AppendXMLSchemaString("A second federated note")
|
||||
testFederatedNote2.SetActivityStreamsName(name)
|
||||
content := streams.NewActivityStreamsContentProperty()
|
||||
content.AppendXMLSchemaString("This is a simple second note being federated.")
|
||||
testFederatedNote2.SetActivityStreamsContent(content)
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testNoteId2))
|
||||
testFederatedNote2.SetJSONLDId(id)
|
||||
}()
|
||||
// testMyNote
|
||||
func() {
|
||||
testMyNote = streams.NewActivityStreamsNote()
|
||||
name := streams.NewActivityStreamsNameProperty()
|
||||
name.AppendXMLSchemaString("My Note")
|
||||
testMyNote.SetActivityStreamsName(name)
|
||||
content := streams.NewActivityStreamsContentProperty()
|
||||
content.AppendXMLSchemaString("This is a simple note of mine.")
|
||||
testMyNote.SetActivityStreamsContent(content)
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testNoteId1))
|
||||
testMyNote.SetJSONLDId(id)
|
||||
}()
|
||||
// testMyNoteNoId
|
||||
func() {
|
||||
testMyNoteNoId = streams.NewActivityStreamsNote()
|
||||
name := streams.NewActivityStreamsNameProperty()
|
||||
name.AppendXMLSchemaString("My Note")
|
||||
testMyNoteNoId.SetActivityStreamsName(name)
|
||||
content := streams.NewActivityStreamsContentProperty()
|
||||
content.AppendXMLSchemaString("This is a simple note of mine.")
|
||||
testMyNoteNoId.SetActivityStreamsContent(content)
|
||||
}()
|
||||
// testMyCreate
|
||||
func() {
|
||||
testMyCreate = streams.NewActivityStreamsCreate()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testNewActivityIRI))
|
||||
testMyCreate.SetJSONLDId(id)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendActivityStreamsNote(testMyNote)
|
||||
testMyCreate.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testCreate
|
||||
func() {
|
||||
testCreate = streams.NewActivityStreamsCreate()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testFederatedActivityIRI))
|
||||
testCreate.SetJSONLDId(id)
|
||||
actor := streams.NewActivityStreamsActorProperty()
|
||||
actor.AppendIRI(mustParse(testFederatedActorIRI))
|
||||
testCreate.SetActivityStreamsActor(actor)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendActivityStreamsNote(testFederatedNote)
|
||||
testCreate.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testCreate2
|
||||
func() {
|
||||
testCreate2 = streams.NewActivityStreamsCreate()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testFederatedActivityIRI))
|
||||
testCreate2.SetJSONLDId(id)
|
||||
actor := streams.NewActivityStreamsActorProperty()
|
||||
actor.AppendIRI(mustParse(testFederatedActorIRI))
|
||||
actor.AppendIRI(mustParse(testFederatedActorIRI2))
|
||||
testCreate2.SetActivityStreamsActor(actor)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendActivityStreamsNote(testFederatedNote)
|
||||
testCreate2.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testCreateNoId
|
||||
func() {
|
||||
testCreateNoId = streams.NewActivityStreamsCreate()
|
||||
actor := streams.NewActivityStreamsActorProperty()
|
||||
actor.AppendIRI(mustParse(testFederatedActorIRI))
|
||||
testCreateNoId.SetActivityStreamsActor(actor)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendActivityStreamsNote(testFederatedNote)
|
||||
testCreateNoId.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testOrderedCollectionUniqueElems and
|
||||
// testOrderedCollectionUniqueElemsString
|
||||
func() {
|
||||
testOrderedCollectionUniqueElems = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testNoteId1))
|
||||
oi.AppendIRI(mustParse(testNoteId2))
|
||||
testOrderedCollectionUniqueElems.SetActivityStreamsOrderedItems(oi)
|
||||
testOrderedCollectionUniqueElemsString = `{"@context":"https://www.w3.org/ns/activitystreams","orderedItems":["https://example.com/note/1","https://example.com/note/2"],"type":"OrderedCollectionPage"}`
|
||||
}()
|
||||
// testOrderedCollectionDupedElems and
|
||||
// testOrderedCollectionDedupedElemsString
|
||||
func() {
|
||||
testOrderedCollectionDupedElems = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testNoteId1))
|
||||
oi.AppendIRI(mustParse(testNoteId1))
|
||||
testOrderedCollectionDupedElems.SetActivityStreamsOrderedItems(oi)
|
||||
testOrderedCollectionDedupedElemsString = `{"@context":"https://www.w3.org/ns/activitystreams","orderedItems":"https://example.com/note/1","type":"OrderedCollectionPage"}`
|
||||
}()
|
||||
// testEmptyOrderedCollection
|
||||
func() {
|
||||
testEmptyOrderedCollection = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
}()
|
||||
// testOrderedCollectionWithNewId
|
||||
func() {
|
||||
testOrderedCollectionWithNewId = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testNewActivityIRI))
|
||||
testOrderedCollectionWithNewId.SetActivityStreamsOrderedItems(oi)
|
||||
}()
|
||||
// testOrderedCollectionWithNewId2
|
||||
func() {
|
||||
testOrderedCollectionWithNewId2 = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testNewActivityIRI2))
|
||||
testOrderedCollectionWithNewId2.SetActivityStreamsOrderedItems(oi)
|
||||
}()
|
||||
// testOrderedCollectionWithBothNewIds
|
||||
func() {
|
||||
testOrderedCollectionWithBothNewIds = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testNewActivityIRI))
|
||||
oi.AppendIRI(mustParse(testNewActivityIRI2))
|
||||
testOrderedCollectionWithBothNewIds.SetActivityStreamsOrderedItems(oi)
|
||||
}()
|
||||
// testOrderedCollectionWithFederatedId
|
||||
func() {
|
||||
testOrderedCollectionWithFederatedId = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testFederatedActivityIRI))
|
||||
testOrderedCollectionWithFederatedId.SetActivityStreamsOrderedItems(oi)
|
||||
}()
|
||||
// testMyListen
|
||||
func() {
|
||||
testMyListen = streams.NewActivityStreamsListen()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testNewActivityIRI))
|
||||
testMyListen.SetJSONLDId(id)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendActivityStreamsNote(testMyNote)
|
||||
testMyListen.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testMyListenNoId
|
||||
func() {
|
||||
testMyListenNoId = streams.NewActivityStreamsListen()
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendActivityStreamsNote(testMyNoteNoId)
|
||||
testMyListenNoId.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testListen
|
||||
func() {
|
||||
testListen = streams.NewActivityStreamsListen()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testFederatedActivityIRI))
|
||||
testListen.SetJSONLDId(id)
|
||||
actor := streams.NewActivityStreamsActorProperty()
|
||||
actor.AppendIRI(mustParse(testFederatedActorIRI))
|
||||
testListen.SetActivityStreamsActor(actor)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendActivityStreamsNote(testFederatedNote)
|
||||
testListen.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testOrderedCollectionWithFederatedId2
|
||||
func() {
|
||||
testOrderedCollectionWithFederatedId2 = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testFederatedActivityIRI2))
|
||||
testOrderedCollectionWithFederatedId2.SetActivityStreamsOrderedItems(oi)
|
||||
}()
|
||||
// testOrderedCollectionWithBothFederatedIds
|
||||
func() {
|
||||
testOrderedCollectionWithBothFederatedIds = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testFederatedActivityIRI))
|
||||
oi.AppendIRI(mustParse(testFederatedActivityIRI2))
|
||||
testOrderedCollectionWithBothFederatedIds.SetActivityStreamsOrderedItems(oi)
|
||||
}()
|
||||
// testPerson
|
||||
func() {
|
||||
testPerson = streams.NewActivityStreamsPerson()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testPersonIRI))
|
||||
testPerson.SetJSONLDId(id)
|
||||
}()
|
||||
// testMyPerson
|
||||
func() {
|
||||
testMyPerson = streams.NewActivityStreamsPerson()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testPersonIRI))
|
||||
testMyPerson.SetJSONLDId(id)
|
||||
inbox := streams.NewActivityStreamsInboxProperty()
|
||||
inbox.SetIRI(mustParse(testMyInboxIRI))
|
||||
testMyPerson.SetActivityStreamsInbox(inbox)
|
||||
outbox := streams.NewActivityStreamsOutboxProperty()
|
||||
outbox.SetIRI(mustParse(testMyOutboxIRI))
|
||||
testMyPerson.SetActivityStreamsOutbox(outbox)
|
||||
}()
|
||||
// testFederatedPerson1
|
||||
func() {
|
||||
testFederatedPerson1 = streams.NewActivityStreamsPerson()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.SetIRI(mustParse(testFederatedActorIRI))
|
||||
testFederatedPerson1.SetJSONLDId(id)
|
||||
inbox := streams.NewActivityStreamsInboxProperty()
|
||||
inbox.SetIRI(mustParse(testFederatedInboxIRI))
|
||||
testFederatedPerson1.SetActivityStreamsInbox(inbox)
|
||||
}()
|
||||
// testFederatedPerson2
|
||||
func() {
|
||||
testFederatedPerson2 = streams.NewActivityStreamsPerson()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testFederatedActorIRI2))
|
||||
testFederatedPerson2.SetJSONLDId(id)
|
||||
inbox := streams.NewActivityStreamsInboxProperty()
|
||||
inbox.SetIRI(mustParse(testFederatedInboxIRI2))
|
||||
testFederatedPerson2.SetActivityStreamsInbox(inbox)
|
||||
}()
|
||||
// testService
|
||||
func() {
|
||||
testService = streams.NewActivityStreamsService()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testServiceIRI))
|
||||
testService.SetJSONLDId(id)
|
||||
}()
|
||||
// testCollectionOfActors
|
||||
func() {
|
||||
testCollectionOfActors = streams.NewActivityStreamsCollectionPage()
|
||||
i := streams.NewActivityStreamsItemsProperty()
|
||||
i.AppendIRI(mustParse(testFederatedActorIRI))
|
||||
i.AppendIRI(mustParse(testFederatedActorIRI2))
|
||||
testCollectionOfActors.SetActivityStreamsItems(i)
|
||||
}()
|
||||
// testOrderedCollectionOfActors
|
||||
func() {
|
||||
testOrderedCollectionOfActors = streams.NewActivityStreamsOrderedCollectionPage()
|
||||
oi := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
oi.AppendIRI(mustParse(testFederatedActorIRI3))
|
||||
oi.AppendIRI(mustParse(testFederatedActorIRI4))
|
||||
testOrderedCollectionOfActors.SetActivityStreamsOrderedItems(oi)
|
||||
}()
|
||||
// testNestedInReplyTo
|
||||
func() {
|
||||
testNestedInReplyTo = streams.NewActivityStreamsListen()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testFederatedActivityIRI))
|
||||
testNestedInReplyTo.SetJSONLDId(id)
|
||||
actor := streams.NewActivityStreamsActorProperty()
|
||||
actor.AppendIRI(mustParse(testFederatedActorIRI))
|
||||
testNestedInReplyTo.SetActivityStreamsActor(actor)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
// Note
|
||||
note := streams.NewActivityStreamsNote()
|
||||
name := streams.NewActivityStreamsNameProperty()
|
||||
name.AppendXMLSchemaString("A Federated Note")
|
||||
note.SetActivityStreamsName(name)
|
||||
content := streams.NewActivityStreamsContentProperty()
|
||||
content.AppendXMLSchemaString("This is a simple note being federated.")
|
||||
note.SetActivityStreamsContent(content)
|
||||
noteId := streams.NewJSONLDIdProperty()
|
||||
noteId.Set(mustParse(testNoteId1))
|
||||
note.SetJSONLDId(noteId)
|
||||
irt := streams.NewActivityStreamsInReplyToProperty()
|
||||
irt.AppendIRI(mustParse(inReplyToIRI))
|
||||
irt.AppendIRI(mustParse(inReplyToIRI2))
|
||||
note.SetActivityStreamsInReplyTo(irt)
|
||||
// Listen
|
||||
op.AppendActivityStreamsNote(note)
|
||||
testNestedInReplyTo.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testFollow
|
||||
func() {
|
||||
testFollow = streams.NewActivityStreamsFollow()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testFederatedActivityIRI))
|
||||
testFollow.SetJSONLDId(id)
|
||||
actor := streams.NewActivityStreamsActorProperty()
|
||||
actor.AppendIRI(mustParse(testFederatedActorIRI2))
|
||||
testFollow.SetActivityStreamsActor(actor)
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendIRI(mustParse(testFederatedActorIRI))
|
||||
testFollow.SetActivityStreamsObject(op)
|
||||
}()
|
||||
// testTombstone
|
||||
func() {
|
||||
testTombstone = streams.NewActivityStreamsTombstone()
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testFederatedActivityIRI))
|
||||
testTombstone.SetJSONLDId(id)
|
||||
}()
|
||||
}
|
||||
|
||||
// wrappedInCreate returns a Create activity wrapping the given type.
|
||||
func wrappedInCreate(t vocab.Type) vocab.ActivityStreamsCreate {
|
||||
create := streams.NewActivityStreamsCreate()
|
||||
op := streams.NewActivityStreamsObjectProperty()
|
||||
op.AppendType(t)
|
||||
create.SetActivityStreamsObject(op)
|
||||
return create
|
||||
}
|
||||
|
||||
// mustSerializeToBytes serializes a type to bytes or panics.
|
||||
func mustSerializeToBytes(t vocab.Type) []byte {
|
||||
m := mustSerialize(t)
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// mustSerialize serializes a type or panics.
|
||||
func mustSerialize(t vocab.Type) map[string]interface{} {
|
||||
m, err := streams.Serialize(t)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// toDeserializedForm serializes and deserializes a type so that it works with
|
||||
// mock expectations.
|
||||
func toDeserializedForm(t vocab.Type) vocab.Type {
|
||||
m := mustSerialize(t)
|
||||
asValue, err := streams.ToType(context.Background(), m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return asValue
|
||||
}
|
||||
|
||||
// withNewId sets a new id property on the activity
|
||||
func withNewId(t vocab.Type) Activity {
|
||||
a, ok := t.(Activity)
|
||||
if !ok {
|
||||
panic("activity streams value is not an Activity")
|
||||
}
|
||||
id := streams.NewJSONLDIdProperty()
|
||||
id.Set(mustParse(testNewActivityIRI))
|
||||
a.SetJSONLDId(id)
|
||||
return a
|
||||
}
|
||||
|
||||
// now returns the "current" time for tests.
|
||||
func now() time.Time {
|
||||
l, err := time.LoadLocation("America/New_York")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return time.Date(2000, 2, 3, 4, 5, 6, 7, l)
|
||||
}
|
||||
|
||||
// nowDateHeader returns the "current" time formatted in a form expected by the
|
||||
// Date header in HTTP responses.
|
||||
func nowDateHeader() string {
|
||||
return now().UTC().Format("Mon, 02 Jan 2006 15:04:05") + " GMT"
|
||||
}
|
||||
|
||||
// toAPRequests adds the appropriate Content-Type or Accept headers to indicate
|
||||
// that the HTTP request is an ActivityPub one. Also sets the Date header with
|
||||
// the "current" test time.
|
||||
func toAPRequest(r *http.Request) *http.Request {
|
||||
if r.Method == "POST" {
|
||||
existing, ok := r.Header[contentTypeHeader]
|
||||
if ok {
|
||||
r.Header[contentTypeHeader] = append(existing, activityStreamsMediaTypes[0])
|
||||
} else {
|
||||
r.Header[contentTypeHeader] = []string{activityStreamsMediaTypes[0]}
|
||||
}
|
||||
} else if r.Method == "GET" {
|
||||
existing, ok := r.Header[acceptHeader]
|
||||
if ok {
|
||||
r.Header[acceptHeader] = append(existing, activityStreamsMediaTypes[0])
|
||||
} else {
|
||||
r.Header[acceptHeader] = []string{activityStreamsMediaTypes[0]}
|
||||
}
|
||||
} else {
|
||||
panic("cannot toAPRequest with method " + r.Method)
|
||||
}
|
||||
r.Header[dateHeader] = []string{now().UTC().Format("Mon, 02 Jan 2006 15:04:05") + " GMT"}
|
||||
return r
|
||||
}
|
||||
|
||||
// toPostInboxRequest creates a new POST HTTP request with the given type as
|
||||
// the payload.
|
||||
func toPostInboxRequest(t vocab.Type) *http.Request {
|
||||
m, err := streams.Serialize(t)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf := bytes.NewBuffer(b)
|
||||
return httptest.NewRequest("POST", testMyInboxIRI, buf)
|
||||
}
|
||||
|
||||
// toPostOutboxRequest creates a new POST HTTP request with the given type as
|
||||
// the payload.
|
||||
func toPostOutboxRequest(t vocab.Type) *http.Request {
|
||||
m, err := streams.Serialize(t)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf := bytes.NewBuffer(b)
|
||||
return httptest.NewRequest("POST", testMyOutboxIRI, buf)
|
||||
}
|
||||
|
||||
// toPostOutboxUnknownRequest creates a new POST HTTP request with an unknown
|
||||
// type in the payload.
|
||||
func toPostOutboxUnknownRequest() *http.Request {
|
||||
s := `{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "http://www.types.example/ProductOffer",
|
||||
"id": "http://www.example.com/spam"
|
||||
}`
|
||||
b := []byte(s)
|
||||
buf := bytes.NewBuffer(b)
|
||||
return httptest.NewRequest("POST", testMyOutboxIRI, buf)
|
||||
}
|
||||
|
||||
// toPostInboxUnknownRequest creates a new POST HTTP request with an unknown
|
||||
// type in the payload.
|
||||
func toPostInboxUnknownRequest() *http.Request {
|
||||
s := `{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "http://www.types.example/ProductOffer",
|
||||
"id": "http://www.example.com/spam"
|
||||
}`
|
||||
b := []byte(s)
|
||||
buf := bytes.NewBuffer(b)
|
||||
return httptest.NewRequest("POST", testMyInboxIRI, buf)
|
||||
}
|
||||
|
||||
// toGetInboxRequest creates a new GET HTTP request.
|
||||
func toGetInboxRequest() *http.Request {
|
||||
return httptest.NewRequest("GET", testMyInboxIRI, nil)
|
||||
}
|
||||
|
||||
// toGetOutboxRequest creates a new GET HTTP request.
|
||||
func toGetOutboxRequest() *http.Request {
|
||||
return httptest.NewRequest("GET", testMyOutboxIRI, nil)
|
||||
}
|
||||
|
||||
// addToIds adds two IRIs to the 'to' property
|
||||
func addToIds(t Activity) Activity {
|
||||
to := streams.NewActivityStreamsToProperty()
|
||||
to.AppendIRI(mustParse(testToIRI))
|
||||
to.AppendIRI(mustParse(testToIRI2))
|
||||
t.SetActivityStreamsTo(to)
|
||||
return t
|
||||
}
|
||||
|
||||
// mustAddCcIds adds two IRIs to the 'cc' property
|
||||
func mustAddCcIds(t Activity) Activity {
|
||||
if ccer, ok := t.(ccer); ok {
|
||||
cc := streams.NewActivityStreamsCcProperty()
|
||||
cc.AppendIRI(mustParse(testCcIRI))
|
||||
cc.AppendIRI(mustParse(testCcIRI2))
|
||||
ccer.SetActivityStreamsCc(cc)
|
||||
} else {
|
||||
panic(fmt.Sprintf("activity is not ccer: %T", t))
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// mustAddAudienceIds adds two IRIs to the 'audience' property
|
||||
func mustAddAudienceIds(t Activity) Activity {
|
||||
if audiencer, ok := t.(audiencer); ok {
|
||||
aud := streams.NewActivityStreamsAudienceProperty()
|
||||
aud.AppendIRI(mustParse(testAudienceIRI))
|
||||
aud.AppendIRI(mustParse(testAudienceIRI2))
|
||||
audiencer.SetActivityStreamsAudience(aud)
|
||||
} else {
|
||||
panic(fmt.Sprintf("activity is not audiencer: %T", t))
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// setTagger is an ActivityStreams type with a 'tag' property
|
||||
type setTagger interface {
|
||||
SetActivityStreamsTag(vocab.ActivityStreamsTagProperty)
|
||||
}
|
||||
|
||||
// mustAddTagIds adds two IRIs to the 'tag' property
|
||||
func mustAddTagIds(t Activity) Activity {
|
||||
if st, ok := t.(setTagger); ok {
|
||||
tag := streams.NewActivityStreamsTagProperty()
|
||||
tag.AppendIRI(mustParse(testTagIRI))
|
||||
tag.AppendIRI(mustParse(testTagIRI2))
|
||||
st.SetActivityStreamsTag(tag)
|
||||
} else {
|
||||
panic(fmt.Sprintf("activity is not setTagger: %T", t))
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// setInReplyToer is an ActivityStreams type with a 'inReplyTo' property
|
||||
type setInReplyToer interface {
|
||||
SetActivityStreamsInReplyTo(vocab.ActivityStreamsInReplyToProperty)
|
||||
}
|
||||
|
||||
// mustAddInReplyToIds adds two IRIs to the 'inReplyTo' property
|
||||
func mustAddInReplyToIds(t Activity) Activity {
|
||||
if st, ok := t.(setInReplyToer); ok {
|
||||
irt := streams.NewActivityStreamsInReplyToProperty()
|
||||
irt.AppendIRI(mustParse(inReplyToIRI))
|
||||
irt.AppendIRI(mustParse(inReplyToIRI2))
|
||||
st.SetActivityStreamsInReplyTo(irt)
|
||||
} else {
|
||||
panic(fmt.Sprintf("activity is not setInReplyToer: %T", t))
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// newObjectWithId creates a generic object with a given id.
|
||||
func newObjectWithId(id string) vocab.ActivityStreamsObject {
|
||||
obj := streams.NewActivityStreamsObject()
|
||||
i := streams.NewJSONLDIdProperty()
|
||||
i.Set(mustParse(id))
|
||||
obj.SetJSONLDId(i)
|
||||
return obj
|
||||
}
|
||||
|
||||
// newActivityWithId creates a generic Activity with a given id.
|
||||
func newActivityWithId(id string) vocab.ActivityStreamsActivity {
|
||||
a := streams.NewActivityStreamsActivity()
|
||||
i := streams.NewJSONLDIdProperty()
|
||||
i.Set(mustParse(id))
|
||||
a.SetJSONLDId(i)
|
||||
return a
|
||||
}
|
167
pub/resolvers.go
167
pub/resolvers.go
|
@ -1,167 +0,0 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/vocab"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ToPubObject transforms a json-deserialized ActivityStream object into a
|
||||
// PubObject for use with the pub library. Note that for an object to be an
|
||||
// ActivityPub object, it must have an 'id' and at least one 'type'.
|
||||
func ToPubObject(m map[string]interface{}) (t []PubObject, e error) {
|
||||
r := &streams.Resolver{
|
||||
AnyObjectCallback: func(i vocab.ObjectType) error {
|
||||
if !i.HasId() {
|
||||
return fmt.Errorf("object type does not have an id: %q", i)
|
||||
} else if i.TypeLen() == 0 {
|
||||
return fmt.Errorf("object type does not have a type: %q", i)
|
||||
}
|
||||
t = append(t, i)
|
||||
return nil
|
||||
},
|
||||
AnyLinkCallback: func(i vocab.LinkType) error {
|
||||
if !i.HasId() {
|
||||
return fmt.Errorf("link type does not have an id: %q", i)
|
||||
} else if i.TypeLen() == 0 {
|
||||
return fmt.Errorf("link type does not have a type: %q", i)
|
||||
}
|
||||
t = append(t, i)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
e = r.Deserialize(m)
|
||||
return t, e
|
||||
}
|
||||
|
||||
func getActorObject(m map[string]interface{}) (actorObject, error) {
|
||||
var a actorObject
|
||||
err := toActorObjectResolver(&a).Deserialize(m)
|
||||
return a, err
|
||||
}
|
||||
|
||||
func toActorObjectResolver(a *actorObject) *streams.Resolver {
|
||||
return &streams.Resolver{
|
||||
AnyObjectCallback: func(i vocab.ObjectType) error {
|
||||
if o, ok := i.(actorObject); ok {
|
||||
*a = o
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func toActorResolver(a *actor) *streams.Resolver {
|
||||
return &streams.Resolver{
|
||||
AnyObjectCallback: func(i vocab.ObjectType) error {
|
||||
if o, ok := i.(actor); ok {
|
||||
*a = o
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func toActorCollectionResolver(a *actor, c **streams.Collection, oc **streams.OrderedCollection, cp **streams.CollectionPage, ocp **streams.OrderedCollectionPage) *streams.Resolver {
|
||||
r := toActorResolver(a)
|
||||
r.CollectionCallback = func(i *streams.Collection) error {
|
||||
*c = i
|
||||
return nil
|
||||
}
|
||||
r.OrderedCollectionCallback = func(i *streams.OrderedCollection) error {
|
||||
*oc = i
|
||||
return nil
|
||||
}
|
||||
r.CollectionPageCallback = func(i *streams.CollectionPage) error {
|
||||
*cp = i
|
||||
return nil
|
||||
}
|
||||
r.OrderedCollectionPageCallback = func(i *streams.OrderedCollectionPage) error {
|
||||
*ocp = i
|
||||
return nil
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func toIdResolver(ok *bool, u **url.URL) *streams.Resolver {
|
||||
return &streams.Resolver{
|
||||
AnyObjectCallback: func(i vocab.ObjectType) error {
|
||||
*ok = i.HasId()
|
||||
if *ok {
|
||||
*u = i.GetId()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func toCollectionPage(m map[string]interface{}) (c *streams.CollectionPage, err error) {
|
||||
r := &streams.Resolver{
|
||||
CollectionPageCallback: func(i *streams.CollectionPage) error {
|
||||
c = i
|
||||
return nil
|
||||
},
|
||||
}
|
||||
err = r.Deserialize(m)
|
||||
return
|
||||
}
|
||||
|
||||
func toOrderedCollectionPage(m map[string]interface{}) (c *streams.OrderedCollectionPage, err error) {
|
||||
r := &streams.Resolver{
|
||||
OrderedCollectionPageCallback: func(i *streams.OrderedCollectionPage) error {
|
||||
c = i
|
||||
return nil
|
||||
},
|
||||
}
|
||||
err = r.Deserialize(m)
|
||||
return
|
||||
}
|
||||
|
||||
func toTypeIder(m map[string]interface{}) (tid typeIder, err error) {
|
||||
var t []typeIder
|
||||
r := &streams.Resolver{
|
||||
AnyObjectCallback: func(i vocab.ObjectType) error {
|
||||
t = append(t, i)
|
||||
return nil
|
||||
},
|
||||
AnyLinkCallback: func(i vocab.LinkType) error {
|
||||
t = append(t, i)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
err = r.Deserialize(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// This should not be more than 1 as clients are not permitted to send
|
||||
// an array of objects/links.
|
||||
if len(t) != 1 {
|
||||
err = fmt.Errorf("too many object/links: %d", len(t))
|
||||
return
|
||||
}
|
||||
tid = t[0]
|
||||
return
|
||||
}
|
||||
|
||||
func toAnyActivity(m map[string]interface{}) (o vocab.ActivityType, err error) {
|
||||
r := &streams.Resolver{
|
||||
AnyActivityCallback: func(i vocab.ActivityType) error {
|
||||
o = i
|
||||
return nil
|
||||
},
|
||||
}
|
||||
err = r.Deserialize(m)
|
||||
return
|
||||
}
|
||||
|
||||
func toAnyObject(m map[string]interface{}) (o vocab.ObjectType, err error) {
|
||||
r := &streams.Resolver{
|
||||
AnyObjectCallback: func(i vocab.ObjectType) error {
|
||||
o = i
|
||||
return nil
|
||||
},
|
||||
}
|
||||
err = r.Deserialize(m)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,857 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
)
|
||||
|
||||
// sideEffectActor must satisfy the DelegateActor interface.
|
||||
var _ DelegateActor = &sideEffectActor{}
|
||||
|
||||
// sideEffectActor is a DelegateActor that handles the ActivityPub
|
||||
// implementation side effects, but requires a more opinionated application to
|
||||
// be written.
|
||||
//
|
||||
// Note that when using the sideEffectActor with an application that good-faith
|
||||
// implements its required interfaces, the ActivityPub specification is
|
||||
// guaranteed to be correctly followed.
|
||||
type sideEffectActor struct {
|
||||
common CommonBehavior
|
||||
s2s FederatingProtocol
|
||||
c2s SocialProtocol
|
||||
db Database
|
||||
clock Clock
|
||||
}
|
||||
|
||||
// PostInboxRequestBodyHook defers to the delegate.
|
||||
func (a *sideEffectActor) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error) {
|
||||
return a.s2s.PostInboxRequestBodyHook(c, r, activity)
|
||||
}
|
||||
|
||||
// PostOutboxRequestBodyHook defers to the delegate.
|
||||
func (a *sideEffectActor) PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) {
|
||||
return a.c2s.PostOutboxRequestBodyHook(c, r, data)
|
||||
}
|
||||
|
||||
// AuthenticatePostInbox defers to the delegate to authenticate the request.
|
||||
func (a *sideEffectActor) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
|
||||
return a.s2s.AuthenticatePostInbox(c, w, r)
|
||||
}
|
||||
|
||||
// AuthenticateGetInbox defers to the delegate to authenticate the request.
|
||||
func (a *sideEffectActor) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
|
||||
return a.common.AuthenticateGetInbox(c, w, r)
|
||||
}
|
||||
|
||||
// AuthenticatePostOutbox defers to the delegate to authenticate the request.
|
||||
func (a *sideEffectActor) AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
|
||||
return a.c2s.AuthenticatePostOutbox(c, w, r)
|
||||
}
|
||||
|
||||
// AuthenticateGetOutbox defers to the delegate to authenticate the request.
|
||||
func (a *sideEffectActor) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
|
||||
return a.common.AuthenticateGetOutbox(c, w, r)
|
||||
}
|
||||
|
||||
// GetOutbox delegates to the SocialProtocol.
|
||||
func (a *sideEffectActor) GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
return a.common.GetOutbox(c, r)
|
||||
}
|
||||
|
||||
// GetInbox delegates to the FederatingProtocol.
|
||||
func (a *sideEffectActor) GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
return a.s2s.GetInbox(c, r)
|
||||
}
|
||||
|
||||
// AuthorizePostInbox defers to the federating protocol whether the peer request
|
||||
// is authorized based on the actors' ids.
|
||||
func (a *sideEffectActor) AuthorizePostInbox(c context.Context, w http.ResponseWriter, activity Activity) (authorized bool, err error) {
|
||||
authorized = false
|
||||
actor := activity.GetActivityStreamsActor()
|
||||
if actor == nil {
|
||||
err = fmt.Errorf("no actors in post to inbox")
|
||||
return
|
||||
}
|
||||
var iris []*url.URL
|
||||
for i := 0; i < actor.Len(); i++ {
|
||||
iter := actor.At(i)
|
||||
if iter.IsIRI() {
|
||||
iris = append(iris, iter.GetIRI())
|
||||
} else if t := iter.GetType(); t != nil {
|
||||
iris = append(iris, activity.GetJSONLDId().Get())
|
||||
} else {
|
||||
err = fmt.Errorf("actor at index %d is missing an id", i)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Determine if the actor(s) sending this request are blocked.
|
||||
var blocked bool
|
||||
if blocked, err = a.s2s.Blocked(c, iris); err != nil {
|
||||
return
|
||||
} else if blocked {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
authorized = true
|
||||
return
|
||||
}
|
||||
|
||||
// PostInbox handles the side effects of determining whether to block the peer's
|
||||
// request, adding the activity to the actor's inbox, and triggering side
|
||||
// effects based on the activity's type.
|
||||
func (a *sideEffectActor) PostInbox(c context.Context, inboxIRI *url.URL, activity Activity) error {
|
||||
isNew, err := a.addToInboxIfNew(c, inboxIRI, activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isNew {
|
||||
wrapped, other, err := a.s2s.FederatingCallbacks(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Populate side channels.
|
||||
wrapped.db = a.db
|
||||
wrapped.inboxIRI = inboxIRI
|
||||
wrapped.newTransport = a.common.NewTransport
|
||||
wrapped.deliver = a.Deliver
|
||||
wrapped.addNewIds = a.AddNewIDs
|
||||
res, err := streams.NewTypeResolver(wrapped.callbacks(other)...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = res.Resolve(c, activity); err != nil && !streams.IsUnmatchedErr(err) {
|
||||
return err
|
||||
} else if streams.IsUnmatchedErr(err) {
|
||||
err = a.s2s.DefaultCallback(c, activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InboxForwarding implements the 3-part inbox forwarding algorithm specified in
|
||||
// the ActivityPub specification. Does not modify the Activity, but may send
|
||||
// outbound requests as a side effect.
|
||||
//
|
||||
// InboxForwarding sets the federated data in the database.
|
||||
func (a *sideEffectActor) InboxForwarding(c context.Context, inboxIRI *url.URL, activity Activity) error {
|
||||
// 1. Must be first time we have seen this Activity.
|
||||
//
|
||||
// Obtain the id of the activity
|
||||
id := activity.GetJSONLDId()
|
||||
// Acquire a lock for the id. To be held for the rest of execution.
|
||||
err := a.db.Lock(c, id.Get())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock is not deferred
|
||||
//
|
||||
// If the database already contains the activity, exit early.
|
||||
exists, err := a.db.Exists(c, id.Get())
|
||||
if err != nil {
|
||||
a.db.Unlock(c, id.Get())
|
||||
return err
|
||||
} else if exists {
|
||||
a.db.Unlock(c, id.Get())
|
||||
return nil
|
||||
}
|
||||
// Attempt to create the activity entry.
|
||||
err = a.db.Create(c, activity)
|
||||
if err != nil {
|
||||
a.db.Unlock(c, id.Get())
|
||||
return err
|
||||
}
|
||||
a.db.Unlock(c, id.Get())
|
||||
// Unlock by this point and in every branch above.
|
||||
//
|
||||
// 2. The values of 'to', 'cc', or 'audience' are Collections owned by
|
||||
// this server.
|
||||
var r []*url.URL
|
||||
to := activity.GetActivityStreamsTo()
|
||||
if to != nil {
|
||||
for iter := to.Begin(); iter != to.End(); iter = iter.Next() {
|
||||
val, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
cc := activity.GetActivityStreamsCc()
|
||||
if cc != nil {
|
||||
for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() {
|
||||
val, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
audience := activity.GetActivityStreamsAudience()
|
||||
if audience != nil {
|
||||
for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() {
|
||||
val, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
// Find all IRIs owned by this server. We need to find all of them so
|
||||
// that forwarding can properly occur.
|
||||
var myIRIs []*url.URL
|
||||
for _, iri := range r {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = a.db.Lock(c, iri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock is not deferred
|
||||
if owns, err := a.db.Owns(c, iri); err != nil {
|
||||
a.db.Unlock(c, iri)
|
||||
return err
|
||||
} else if !owns {
|
||||
a.db.Unlock(c, iri)
|
||||
continue
|
||||
}
|
||||
a.db.Unlock(c, iri)
|
||||
// Unlock by this point and in every branch above.
|
||||
myIRIs = append(myIRIs, iri)
|
||||
}
|
||||
// Finally, load our IRIs to determine if they are a Collection or
|
||||
// OrderedCollection.
|
||||
//
|
||||
// Load the unfiltered IRIs.
|
||||
var colIRIs []*url.URL
|
||||
col := make(map[string]itemser)
|
||||
oCol := make(map[string]orderedItemser)
|
||||
for _, iri := range myIRIs {
|
||||
err = a.db.Lock(c, iri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Not Unlocked
|
||||
t, err := a.db.Get(c, iri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if streams.IsOrExtendsActivityStreamsOrderedCollection(t) {
|
||||
if im, ok := t.(orderedItemser); ok {
|
||||
oCol[iri.String()] = im
|
||||
colIRIs = append(colIRIs, iri)
|
||||
defer a.db.Unlock(c, iri)
|
||||
} else {
|
||||
a.db.Unlock(c, iri)
|
||||
}
|
||||
} else if streams.IsOrExtendsActivityStreamsCollection(t) {
|
||||
if im, ok := t.(itemser); ok {
|
||||
col[iri.String()] = im
|
||||
colIRIs = append(colIRIs, iri)
|
||||
defer a.db.Unlock(c, iri)
|
||||
} else {
|
||||
a.db.Unlock(c, iri)
|
||||
}
|
||||
} else {
|
||||
a.db.Unlock(c, iri)
|
||||
}
|
||||
}
|
||||
// If we own none of the Collection IRIs in 'to', 'cc', or 'audience'
|
||||
// then no need to do inbox forwarding. We have nothing to forward to.
|
||||
if len(colIRIs) == 0 {
|
||||
return nil
|
||||
}
|
||||
// 3. The values of 'inReplyTo', 'object', 'target', or 'tag' are owned
|
||||
// by this server. This is only a boolean trigger: As soon as we get
|
||||
// a hit that we own something, then we should do inbox forwarding.
|
||||
maxDepth := a.s2s.MaxInboxForwardingRecursionDepth(c)
|
||||
ownsValue, err := a.hasInboxForwardingValues(c, inboxIRI, activity, maxDepth, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If we don't own any of the 'inReplyTo', 'object', 'target', or 'tag'
|
||||
// values, then no need to do inbox forwarding.
|
||||
if !ownsValue {
|
||||
return nil
|
||||
}
|
||||
// Do the inbox forwarding since the above conditions hold true. Support
|
||||
// the behavior of letting the application filter out the resulting
|
||||
// collections to be targeted.
|
||||
toSend, err := a.s2s.FilterForwarding(c, colIRIs, activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients := make([]*url.URL, 0, len(toSend))
|
||||
for _, iri := range toSend {
|
||||
if c, ok := col[iri.String()]; ok {
|
||||
if it := c.GetActivityStreamsItems(); it != nil {
|
||||
for iter := it.Begin(); iter != it.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, id)
|
||||
}
|
||||
}
|
||||
} else if oc, ok := oCol[iri.String()]; ok {
|
||||
if oit := oc.GetActivityStreamsOrderedItems(); oit != nil {
|
||||
for iter := oit.Begin(); iter != oit.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return a.deliverToRecipients(c, inboxIRI, activity, recipients)
|
||||
}
|
||||
|
||||
// PostOutbox handles the side effects of adding the activity to the actor's
|
||||
// outbox, and triggering side effects based on the activity's type.
|
||||
//
|
||||
// This implementation assumes all types are meant to be delivered except for
|
||||
// the ActivityStreams Block type.
|
||||
func (a *sideEffectActor) PostOutbox(c context.Context, activity Activity, outboxIRI *url.URL, rawJSON map[string]interface{}) (deliverable bool, err error) {
|
||||
// TODO: Determine this if c2s is nil
|
||||
deliverable = true
|
||||
if a.c2s != nil {
|
||||
var wrapped SocialWrappedCallbacks
|
||||
var other []interface{}
|
||||
wrapped, other, err = a.c2s.SocialCallbacks(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Populate side channels.
|
||||
wrapped.db = a.db
|
||||
wrapped.outboxIRI = outboxIRI
|
||||
wrapped.rawActivity = rawJSON
|
||||
wrapped.clock = a.clock
|
||||
wrapped.newTransport = a.common.NewTransport
|
||||
undeliverable := false
|
||||
wrapped.undeliverable = &undeliverable
|
||||
var res *streams.TypeResolver
|
||||
res, err = streams.NewTypeResolver(wrapped.callbacks(other)...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = res.Resolve(c, activity); err != nil && !streams.IsUnmatchedErr(err) {
|
||||
return
|
||||
} else if streams.IsUnmatchedErr(err) {
|
||||
deliverable = true
|
||||
err = a.c2s.DefaultCallback(c, activity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
deliverable = !undeliverable
|
||||
}
|
||||
}
|
||||
err = a.addToOutbox(c, outboxIRI, activity)
|
||||
return
|
||||
}
|
||||
|
||||
// AddNewIDs creates new 'id' entries on an activity and its objects if it is a
|
||||
// Create activity.
|
||||
func (a *sideEffectActor) AddNewIDs(c context.Context, activity Activity) error {
|
||||
id, err := a.db.NewID(c, activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activityId := streams.NewJSONLDIdProperty()
|
||||
activityId.Set(id)
|
||||
activity.SetJSONLDId(activityId)
|
||||
if streams.IsOrExtendsActivityStreamsCreate(activity) {
|
||||
o, ok := activity.(objecter)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot add new id for Create: %T has no object property", activity)
|
||||
}
|
||||
if oProp := o.GetActivityStreamsObject(); oProp != nil {
|
||||
for iter := oProp.Begin(); iter != oProp.End(); iter = iter.Next() {
|
||||
t := iter.GetType()
|
||||
if t == nil {
|
||||
return fmt.Errorf("cannot add new id for object in Create: object is not embedded as a value literal")
|
||||
}
|
||||
id, err = a.db.NewID(c, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objId := streams.NewJSONLDIdProperty()
|
||||
objId.Set(id)
|
||||
t.SetJSONLDId(objId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deliver will complete the peer-to-peer sending of a federated message to
|
||||
// another server.
|
||||
//
|
||||
// Must be called if at least the federated protocol is supported.
|
||||
func (a *sideEffectActor) Deliver(c context.Context, outboxIRI *url.URL, activity Activity) error {
|
||||
recipients, err := a.prepare(c, outboxIRI, activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.deliverToRecipients(c, outboxIRI, activity, recipients)
|
||||
}
|
||||
|
||||
// WrapInCreate wraps an object with a Create activity.
|
||||
func (a *sideEffectActor) WrapInCreate(c context.Context, obj vocab.Type, outboxIRI *url.URL) (create vocab.ActivityStreamsCreate, err error) {
|
||||
err = a.db.Lock(c, outboxIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// WARNING: No deferring the Unlock
|
||||
actorIRI, err := a.db.ActorForOutbox(c, outboxIRI)
|
||||
if err != nil {
|
||||
a.db.Unlock(c, outboxIRI)
|
||||
return
|
||||
}
|
||||
a.db.Unlock(c, outboxIRI)
|
||||
// Unlock the lock at this point and every branch above
|
||||
return wrapInCreate(c, obj, actorIRI)
|
||||
}
|
||||
|
||||
// deliverToRecipients will take a prepared Activity and send it to specific
|
||||
// recipients on behalf of an actor.
|
||||
func (a *sideEffectActor) deliverToRecipients(c context.Context, boxIRI *url.URL, activity Activity, recipients []*url.URL) error {
|
||||
m, err := streams.Serialize(activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tp, err := a.common.NewTransport(c, boxIRI, goFedUserAgent())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tp.BatchDeliver(c, b, recipients)
|
||||
}
|
||||
|
||||
// addToOutbox adds the activity to the outbox and creates the activity in the
|
||||
// internal database as its own entry.
|
||||
func (a *sideEffectActor) addToOutbox(c context.Context, outboxIRI *url.URL, activity Activity) error {
|
||||
// Set the activity in the database first.
|
||||
id := activity.GetJSONLDId()
|
||||
err := a.db.Lock(c, id.Get())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock not deferred
|
||||
err = a.db.Create(c, activity)
|
||||
if err != nil {
|
||||
a.db.Unlock(c, id.Get())
|
||||
return err
|
||||
}
|
||||
a.db.Unlock(c, id.Get())
|
||||
// WARNING: Unlock(c, id) should be called by this point and in every
|
||||
// return before here.
|
||||
//
|
||||
// Acquire a lock to read the outbox. Defer release.
|
||||
err = a.db.Lock(c, outboxIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.db.Unlock(c, outboxIRI)
|
||||
outbox, err := a.db.GetOutbox(c, outboxIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Prepend the activity to the list of 'orderedItems'.
|
||||
oi := outbox.GetActivityStreamsOrderedItems()
|
||||
if oi == nil {
|
||||
oi = streams.NewActivityStreamsOrderedItemsProperty()
|
||||
}
|
||||
oi.PrependIRI(id.Get())
|
||||
outbox.SetActivityStreamsOrderedItems(oi)
|
||||
// Save in the database.
|
||||
err = a.db.SetOutbox(c, outbox)
|
||||
return err
|
||||
}
|
||||
|
||||
// addToInboxIfNew will add the activity to the inbox at the specified IRI if
|
||||
// the activity's ID has not yet been added to the inbox.
|
||||
//
|
||||
// It does not add the activity to this database's know federated data.
|
||||
//
|
||||
// Returns true when the activity is novel.
|
||||
func (a *sideEffectActor) addToInboxIfNew(c context.Context, inboxIRI *url.URL, activity Activity) (isNew bool, err error) {
|
||||
// Acquire a lock to read the inbox. Defer release.
|
||||
err = a.db.Lock(c, inboxIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer a.db.Unlock(c, inboxIRI)
|
||||
// Obtain the id of the activity
|
||||
id := activity.GetJSONLDId()
|
||||
// If the inbox already contains the URL, early exit.
|
||||
contains, err := a.db.InboxContains(c, inboxIRI, id.Get())
|
||||
if err != nil {
|
||||
return
|
||||
} else if contains {
|
||||
return
|
||||
}
|
||||
// It is a new id, acquire the inbox.
|
||||
isNew = true
|
||||
inbox, err := a.db.GetInbox(c, inboxIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Prepend the activity to the list of 'orderedItems'.
|
||||
oi := inbox.GetActivityStreamsOrderedItems()
|
||||
if oi == nil {
|
||||
oi = streams.NewActivityStreamsOrderedItemsProperty()
|
||||
}
|
||||
oi.PrependIRI(id.Get())
|
||||
inbox.SetActivityStreamsOrderedItems(oi)
|
||||
// Save in the database.
|
||||
err = a.db.SetInbox(c, inbox)
|
||||
return
|
||||
}
|
||||
|
||||
// Given an ActivityStreams value, recursively examines ownership of the id or
|
||||
// href and the ones on properties applicable to inbox forwarding.
|
||||
//
|
||||
// Recursion may be limited by providing a 'maxDepth' greater than zero. A
|
||||
// value of zero or a negative number will result in infinite recursion.
|
||||
func (a *sideEffectActor) hasInboxForwardingValues(c context.Context, inboxIRI *url.URL, val vocab.Type, maxDepth, currDepth int) (bool, error) {
|
||||
// Stop recurring if we are exceeding the maximum depth and the maximum
|
||||
// is a positive number.
|
||||
if maxDepth > 0 && currDepth >= maxDepth {
|
||||
return false, nil
|
||||
}
|
||||
// Determine if we own the 'id' of any values on the properties we care
|
||||
// about.
|
||||
types, iris := getInboxForwardingValues(val)
|
||||
// For IRIs, simply check if we own them.
|
||||
for _, iri := range iris {
|
||||
err := a.db.Lock(c, iri)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// WARNING: Unlock is not deferred
|
||||
if owns, err := a.db.Owns(c, iri); err != nil {
|
||||
a.db.Unlock(c, iri)
|
||||
return false, err
|
||||
} else if owns {
|
||||
a.db.Unlock(c, iri)
|
||||
return true, nil
|
||||
}
|
||||
a.db.Unlock(c, iri)
|
||||
// Unlock by this point and in every branch above
|
||||
}
|
||||
// For embedded literals, check the id.
|
||||
for _, val := range types {
|
||||
id, err := GetId(val)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = a.db.Lock(c, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// WARNING: Unlock is not deferred
|
||||
if owns, err := a.db.Owns(c, id); err != nil {
|
||||
a.db.Unlock(c, id)
|
||||
return false, err
|
||||
} else if owns {
|
||||
a.db.Unlock(c, id)
|
||||
return true, nil
|
||||
}
|
||||
a.db.Unlock(c, id)
|
||||
// Unlock by this point and in every branch above
|
||||
}
|
||||
// Recur Preparation: Try fetching the IRIs so we can recur into them.
|
||||
for _, iri := range iris {
|
||||
// Dereferencing the IRI.
|
||||
tport, err := a.common.NewTransport(c, inboxIRI, goFedUserAgent())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
b, err := tport.Dereference(c, iri)
|
||||
if err != nil {
|
||||
// Do not fail the entire process if the data is
|
||||
// missing.
|
||||
continue
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err = json.Unmarshal(b, &m); err != nil {
|
||||
return false, err
|
||||
}
|
||||
t, err := streams.ToType(c, m)
|
||||
if err != nil {
|
||||
// Do not fail the entire process if we cannot handle
|
||||
// the type.
|
||||
continue
|
||||
}
|
||||
types = append(types, t)
|
||||
}
|
||||
// Recur.
|
||||
for _, nextVal := range types {
|
||||
if has, err := a.hasInboxForwardingValues(c, inboxIRI, nextVal, maxDepth, currDepth+1); err != nil {
|
||||
return false, err
|
||||
} else if has {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// prepare takes a deliverableObject and returns a list of the proper recipient
|
||||
// target URIs. Additionally, the deliverableObject will have any hidden
|
||||
// hidden recipients ("bto" and "bcc") stripped from it.
|
||||
//
|
||||
// Only call if both the social and federated protocol are supported.
|
||||
func (a *sideEffectActor) prepare(c context.Context, outboxIRI *url.URL, activity Activity) (r []*url.URL, err error) {
|
||||
// Get inboxes of recipients
|
||||
if to := activity.GetActivityStreamsTo(); to != nil {
|
||||
for iter := to.Begin(); iter != to.End(); iter = iter.Next() {
|
||||
var val *url.URL
|
||||
val, err = ToId(iter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
if bto := activity.GetActivityStreamsBto(); bto != nil {
|
||||
for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() {
|
||||
var val *url.URL
|
||||
val, err = ToId(iter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
if cc := activity.GetActivityStreamsCc(); cc != nil {
|
||||
for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() {
|
||||
var val *url.URL
|
||||
val, err = ToId(iter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
if bcc := activity.GetActivityStreamsBcc(); bcc != nil {
|
||||
for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() {
|
||||
var val *url.URL
|
||||
val, err = ToId(iter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
if audience := activity.GetActivityStreamsAudience(); audience != nil {
|
||||
for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() {
|
||||
var val *url.URL
|
||||
val, err = ToId(iter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r = append(r, val)
|
||||
}
|
||||
}
|
||||
// 1. When an object is being delivered to the originating actor's
|
||||
// followers, a server MAY reduce the number of receiving actors
|
||||
// delivered to by identifying all followers which share the same
|
||||
// sharedInbox who would otherwise be individual recipients and
|
||||
// instead deliver objects to said sharedInbox.
|
||||
// 2. If an object is addressed to the Public special collection, a
|
||||
// server MAY deliver that object to all known sharedInbox endpoints
|
||||
// on the network.
|
||||
r = filterURLs(r, IsPublic)
|
||||
|
||||
// first check if the implemented database logic can return any inboxes
|
||||
// from our list of actor IRIs.
|
||||
foundInboxesFromDB := []*url.URL{}
|
||||
foundActorsFromDB := []*url.URL{}
|
||||
for _, actorIRI := range r {
|
||||
// BEGIN LOCK
|
||||
err = a.db.Lock(c, actorIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inbox, err := a.db.InboxForActor(c, actorIRI)
|
||||
if err != nil {
|
||||
// bail on error
|
||||
a.db.Unlock(c, actorIRI)
|
||||
return nil, err
|
||||
}
|
||||
if inbox != nil {
|
||||
// we have a hit
|
||||
foundInboxesFromDB = append(foundInboxesFromDB, inbox)
|
||||
foundActorsFromDB = append(foundActorsFromDB, actorIRI)
|
||||
}
|
||||
|
||||
// END LOCK
|
||||
a.db.Unlock(c, actorIRI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// for every actor we found an inbox for in the db, we should
|
||||
// remove it from the list of actors we still need to dereference
|
||||
for _, actorIRI := range foundActorsFromDB {
|
||||
r = removeOne(r, actorIRI)
|
||||
}
|
||||
|
||||
// look for any actors' inboxes that weren't already discovered above;
|
||||
// find these by making dereference calls to remote instances
|
||||
t, err := a.common.NewTransport(c, outboxIRI, goFedUserAgent())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
foundActorsFromRemote, err := a.resolveActors(c, t, r, 0, a.s2s.MaxDeliveryRecursionDepth(c))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
foundInboxesFromRemote, err := getInboxes(foundActorsFromRemote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// combine this list of dereferenced inbox IRIs with the inboxes we already
|
||||
// found in the db, to make a complete list of target IRIs
|
||||
targets := []*url.URL{}
|
||||
targets = append(targets, foundInboxesFromDB...)
|
||||
targets = append(targets, foundInboxesFromRemote...)
|
||||
|
||||
// Get inboxes of sender.
|
||||
err = a.db.Lock(c, outboxIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// WARNING: No deferring the Unlock
|
||||
actorIRI, err := a.db.ActorForOutbox(c, outboxIRI)
|
||||
if err != nil {
|
||||
a.db.Unlock(c, outboxIRI)
|
||||
return
|
||||
}
|
||||
a.db.Unlock(c, outboxIRI)
|
||||
// Get the inbox on the sender.
|
||||
err = a.db.Lock(c, actorIRI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// BEGIN LOCK
|
||||
thisActor, err := a.db.Get(c, actorIRI)
|
||||
a.db.Unlock(c, actorIRI)
|
||||
// END LOCK -- Still need to handle err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Post-processing
|
||||
var ignore *url.URL
|
||||
ignore, err = getInbox(thisActor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = dedupeIRIs(targets, []*url.URL{ignore})
|
||||
stripHiddenRecipients(activity)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// resolveActors takes a list of Actor id URIs and returns them as concrete
|
||||
// instances of actorObject. It attempts to apply recursively when it encounters
|
||||
// a target that is a Collection or OrderedCollection.
|
||||
//
|
||||
// If maxDepth is zero or negative, then recursion is infinitely applied.
|
||||
//
|
||||
// If a recipient is a Collection or OrderedCollection, then the server MUST
|
||||
// dereference the collection, WITH the user's credentials.
|
||||
//
|
||||
// Note that this also applies to CollectionPage and OrderedCollectionPage.
|
||||
func (a *sideEffectActor) resolveActors(c context.Context, t Transport, r []*url.URL, depth, maxDepth int) (actors []vocab.Type, err error) {
|
||||
if maxDepth > 0 && depth >= maxDepth {
|
||||
return
|
||||
}
|
||||
for _, u := range r {
|
||||
var act vocab.Type
|
||||
var more []*url.URL
|
||||
// TODO: Determine if more logic is needed here for inaccessible
|
||||
// collections owned by peer servers.
|
||||
act, more, err = a.dereferenceForResolvingInboxes(c, t, u)
|
||||
if err != nil {
|
||||
// Missing recipient -- skip.
|
||||
continue
|
||||
}
|
||||
var recurActors []vocab.Type
|
||||
recurActors, err = a.resolveActors(c, t, more, depth+1, maxDepth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if act != nil {
|
||||
actors = append(actors, act)
|
||||
}
|
||||
actors = append(actors, recurActors...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// dereferenceForResolvingInboxes dereferences an IRI solely for finding an
|
||||
// actor's inbox IRI to deliver to.
|
||||
//
|
||||
// The returned actor could be nil, if it wasn't an actor (ex: a Collection or
|
||||
// OrderedCollection).
|
||||
func (a *sideEffectActor) dereferenceForResolvingInboxes(c context.Context, t Transport, actorIRI *url.URL) (actor vocab.Type, moreActorIRIs []*url.URL, err error) {
|
||||
var resp []byte
|
||||
resp, err = t.Dereference(c, actorIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err = json.Unmarshal(resp, &m); err != nil {
|
||||
return
|
||||
}
|
||||
actor, err = streams.ToType(c, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Attempt to see if the 'actor' is really some sort of type that has
|
||||
// an 'items' or 'orderedItems' property.
|
||||
if v, ok := actor.(itemser); ok {
|
||||
if i := v.GetActivityStreamsItems(); i != nil {
|
||||
for iter := i.Begin(); iter != i.End(); iter = iter.Next() {
|
||||
var id *url.URL
|
||||
id, err = ToId(iter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
moreActorIRIs = append(moreActorIRIs, id)
|
||||
}
|
||||
}
|
||||
actor = nil
|
||||
} else if v, ok := actor.(orderedItemser); ok {
|
||||
if i := v.GetActivityStreamsOrderedItems(); i != nil {
|
||||
for iter := i.Begin(); iter != i.End(); iter = iter.Next() {
|
||||
var id *url.URL
|
||||
id, err = ToId(iter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
moreActorIRIs = append(moreActorIRIs, id)
|
||||
}
|
||||
}
|
||||
actor = nil
|
||||
}
|
||||
return
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,82 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SocialProtocol contains behaviors an application needs to satisfy for the
|
||||
// full ActivityPub C2S implementation to be supported by this library.
|
||||
//
|
||||
// It is only required if the client application wants to support the client-to-
|
||||
// server, or social, protocol.
|
||||
//
|
||||
// It is passed to the library as a dependency injection from the client
|
||||
// application.
|
||||
type SocialProtocol interface {
|
||||
// Hook callback after parsing the request body for a client request
|
||||
// to the Actor's outbox.
|
||||
//
|
||||
// Can be used to set contextual information based on the
|
||||
// ActivityStreams object received.
|
||||
//
|
||||
// Only called if the Social API is enabled.
|
||||
//
|
||||
// Warning: Neither authentication nor authorization has taken place at
|
||||
// this time. Doing anything beyond setting contextual information is
|
||||
// strongly discouraged.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostOutbox. In this case, the DelegateActor implementation must not
|
||||
// write a response to the ResponseWriter as is expected that the caller
|
||||
// to PostOutbox will do so when handling the error.
|
||||
PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error)
|
||||
// AuthenticatePostOutbox delegates the authentication of a POST to an
|
||||
// outbox.
|
||||
//
|
||||
// Only called if the Social API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// PostOutbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
|
||||
// SocialCallbacks returns the application logic that handles
|
||||
// ActivityStreams received from C2S clients.
|
||||
//
|
||||
// Note that certain types of callbacks will be 'wrapped' with default
|
||||
// behaviors supported natively by the library. Other callbacks
|
||||
// compatible with streams.TypeResolver can be specified by 'other'.
|
||||
//
|
||||
// For example, setting the 'Create' field in the SocialWrappedCallbacks
|
||||
// lets an application dependency inject additional behaviors they want
|
||||
// to take place, including the default behavior supplied by this
|
||||
// library. This is guaranteed to be compliant with the ActivityPub
|
||||
// Social protocol.
|
||||
//
|
||||
// To override the default behavior, instead supply the function in
|
||||
// 'other', which does not guarantee the application will be compliant
|
||||
// with the ActivityPub Social Protocol.
|
||||
//
|
||||
// Applications are not expected to handle every single ActivityStreams
|
||||
// type and extension. The unhandled ones are passed to DefaultCallback.
|
||||
SocialCallbacks(c context.Context) (wrapped SocialWrappedCallbacks, other []interface{}, err error)
|
||||
// DefaultCallback is called for types that go-fed can deserialize but
|
||||
// are not handled by the application's callbacks returned in the
|
||||
// Callbacks method.
|
||||
//
|
||||
// Applications are not expected to handle every single ActivityStreams
|
||||
// type and extension, so the unhandled ones are passed to
|
||||
// DefaultCallback.
|
||||
DefaultCallback(c context.Context, activity Activity) error
|
||||
}
|
|
@ -0,0 +1,531 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// SocialWrappedCallbacks lists the callback functions that already have some
|
||||
// side effect behavior provided by the pub library.
|
||||
//
|
||||
// These functions are wrapped for the Social Protocol.
|
||||
type SocialWrappedCallbacks struct {
|
||||
// Create handles additional side effects for the Create ActivityStreams
|
||||
// type.
|
||||
//
|
||||
// The wrapping callback copies the actor(s) to the 'attributedTo'
|
||||
// property and copies recipients between the Create activity and all
|
||||
// objects. It then saves the entry in the database.
|
||||
Create func(context.Context, vocab.ActivityStreamsCreate) error
|
||||
// Update handles additional side effects for the Update ActivityStreams
|
||||
// type.
|
||||
//
|
||||
// The wrapping callback applies new top-level values on an object to
|
||||
// the stored objects. Any top-level null literals will be deleted on
|
||||
// the stored objects as well.
|
||||
Update func(context.Context, vocab.ActivityStreamsUpdate) error
|
||||
// Delete handles additional side effects for the Delete ActivityStreams
|
||||
// type.
|
||||
//
|
||||
// The wrapping callback replaces the object(s) with tombstones in the
|
||||
// database.
|
||||
Delete func(context.Context, vocab.ActivityStreamsDelete) error
|
||||
// Follow handles additional side effects for the Follow ActivityStreams
|
||||
// type.
|
||||
//
|
||||
// The wrapping callback only ensures the 'Follow' has at least one
|
||||
// 'object' entry, but otherwise has no default side effect.
|
||||
Follow func(context.Context, vocab.ActivityStreamsFollow) error
|
||||
// Add handles additional side effects for the Add ActivityStreams
|
||||
// type.
|
||||
//
|
||||
//
|
||||
// The wrapping function will add the 'object' IRIs to a specific
|
||||
// 'target' collection if the 'target' collection(s) live on this
|
||||
// server.
|
||||
Add func(context.Context, vocab.ActivityStreamsAdd) error
|
||||
// Remove handles additional side effects for the Remove ActivityStreams
|
||||
// type.
|
||||
//
|
||||
// The wrapping function will remove all 'object' IRIs from a specific
|
||||
// 'target' collection if the 'target' collection(s) live on this
|
||||
// server.
|
||||
Remove func(context.Context, vocab.ActivityStreamsRemove) error
|
||||
// Like handles additional side effects for the Like ActivityStreams
|
||||
// type.
|
||||
//
|
||||
// The wrapping function will add the objects on the activity to the
|
||||
// "liked" collection of this actor.
|
||||
Like func(context.Context, vocab.ActivityStreamsLike) error
|
||||
// Undo handles additional side effects for the Undo ActivityStreams
|
||||
// type.
|
||||
//
|
||||
//
|
||||
// The wrapping function ensures the 'actor' on the 'Undo'
|
||||
// is be the same as the 'actor' on all Activities being undone.
|
||||
// It enforces that the actors on the Undo must correspond to all of the
|
||||
// 'object' actors in some manner.
|
||||
//
|
||||
// It is expected that the application will implement the proper
|
||||
// reversal of activities that are being undone.
|
||||
Undo func(context.Context, vocab.ActivityStreamsUndo) error
|
||||
// Block handles additional side effects for the Block ActivityStreams
|
||||
// type.
|
||||
//
|
||||
// The wrapping callback only ensures the 'Block' has at least one
|
||||
// 'object' entry, but otherwise has no default side effect. It is up
|
||||
// to the wrapped application function to properly enforce the new
|
||||
// blocking behavior.
|
||||
//
|
||||
// Note that go-fed does not federate 'Block' activities received in the
|
||||
// Social Protocol.
|
||||
Block func(context.Context, vocab.ActivityStreamsBlock) error
|
||||
|
||||
// Sidechannel data -- this is set at request handling time. These must
|
||||
// be set before the callbacks are used.
|
||||
|
||||
// db is the Database the SocialWrappedCallbacks should use. It must be
|
||||
// set before calling the callbacks.
|
||||
db Database
|
||||
// outboxIRI is the outboxIRI that is handling this callback.
|
||||
outboxIRI *url.URL
|
||||
// rawActivity is the JSON map literal received when deserializing the
|
||||
// request body.
|
||||
rawActivity map[string]interface{}
|
||||
// clock is the server's clock.
|
||||
clock Clock
|
||||
// newTransport creates a new Transport.
|
||||
newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error)
|
||||
// undeliverable is a sidechannel out, indicating if the handled activity
|
||||
// should not be delivered to a peer.
|
||||
//
|
||||
// Its provided default value will always be used when a custom function
|
||||
// is called.
|
||||
undeliverable *bool
|
||||
}
|
||||
|
||||
// callbacks returns the WrappedCallbacks members into a single interface slice
|
||||
// for use in streams.Resolver callbacks.
|
||||
//
|
||||
// If the given functions have a type that collides with the default behavior,
|
||||
// then disable our default behavior
|
||||
func (w SocialWrappedCallbacks) callbacks(fns []interface{}) []interface{} {
|
||||
enableCreate := true
|
||||
enableUpdate := true
|
||||
enableDelete := true
|
||||
enableFollow := true
|
||||
enableAdd := true
|
||||
enableRemove := true
|
||||
enableLike := true
|
||||
enableUndo := true
|
||||
enableBlock := true
|
||||
for _, fn := range fns {
|
||||
switch fn.(type) {
|
||||
default:
|
||||
continue
|
||||
case func(context.Context, vocab.ActivityStreamsCreate) error:
|
||||
enableCreate = false
|
||||
case func(context.Context, vocab.ActivityStreamsUpdate) error:
|
||||
enableUpdate = false
|
||||
case func(context.Context, vocab.ActivityStreamsDelete) error:
|
||||
enableDelete = false
|
||||
case func(context.Context, vocab.ActivityStreamsFollow) error:
|
||||
enableFollow = false
|
||||
case func(context.Context, vocab.ActivityStreamsAdd) error:
|
||||
enableAdd = false
|
||||
case func(context.Context, vocab.ActivityStreamsRemove) error:
|
||||
enableRemove = false
|
||||
case func(context.Context, vocab.ActivityStreamsLike) error:
|
||||
enableLike = false
|
||||
case func(context.Context, vocab.ActivityStreamsUndo) error:
|
||||
enableUndo = false
|
||||
case func(context.Context, vocab.ActivityStreamsBlock) error:
|
||||
enableBlock = false
|
||||
}
|
||||
}
|
||||
if enableCreate {
|
||||
fns = append(fns, w.create)
|
||||
}
|
||||
if enableUpdate {
|
||||
fns = append(fns, w.update)
|
||||
}
|
||||
if enableDelete {
|
||||
fns = append(fns, w.deleteFn)
|
||||
}
|
||||
if enableFollow {
|
||||
fns = append(fns, w.follow)
|
||||
}
|
||||
if enableAdd {
|
||||
fns = append(fns, w.add)
|
||||
}
|
||||
if enableRemove {
|
||||
fns = append(fns, w.remove)
|
||||
}
|
||||
if enableLike {
|
||||
fns = append(fns, w.like)
|
||||
}
|
||||
if enableUndo {
|
||||
fns = append(fns, w.undo)
|
||||
}
|
||||
if enableBlock {
|
||||
fns = append(fns, w.block)
|
||||
}
|
||||
return fns
|
||||
}
|
||||
|
||||
// create implements the social Create activity side effects.
|
||||
func (w SocialWrappedCallbacks) create(c context.Context, a vocab.ActivityStreamsCreate) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
// Obtain all actor IRIs.
|
||||
actors := a.GetActivityStreamsActor()
|
||||
createActorIds := make(map[string]*url.URL)
|
||||
if actors != nil {
|
||||
createActorIds = make(map[string]*url.URL, actors.Len())
|
||||
for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createActorIds[id.String()] = id
|
||||
}
|
||||
}
|
||||
// Obtain each object's 'attributedTo' IRIs.
|
||||
objectAttributedToIds := make([]map[string]*url.URL, op.Len())
|
||||
for i := range objectAttributedToIds {
|
||||
objectAttributedToIds[i] = make(map[string]*url.URL)
|
||||
}
|
||||
for i := 0; i < op.Len(); i++ {
|
||||
t := op.At(i).GetType()
|
||||
attrToer, ok := t.(attributedToer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
attr := attrToer.GetActivityStreamsAttributedTo()
|
||||
if attr == nil {
|
||||
attr = streams.NewActivityStreamsAttributedToProperty()
|
||||
attrToer.SetActivityStreamsAttributedTo(attr)
|
||||
}
|
||||
for iter := attr.Begin(); iter != attr.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objectAttributedToIds[i][id.String()] = id
|
||||
}
|
||||
}
|
||||
// Put all missing actor IRIs onto all object attributedTo properties.
|
||||
for k, v := range createActorIds {
|
||||
for i, attributedToMap := range objectAttributedToIds {
|
||||
if _, ok := attributedToMap[k]; !ok {
|
||||
t := op.At(i).GetType()
|
||||
attrToer, ok := t.(attributedToer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
attr := attrToer.GetActivityStreamsAttributedTo()
|
||||
attr.AppendIRI(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Put all missing object attributedTo IRIs onto the actor property
|
||||
// if there is one.
|
||||
if actors != nil {
|
||||
for _, attributedToMap := range objectAttributedToIds {
|
||||
for k, v := range attributedToMap {
|
||||
if _, ok := createActorIds[k]; !ok {
|
||||
actors.AppendIRI(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy over the 'to', 'bto', 'cc', 'bcc', and 'audience' recipients
|
||||
// between the activity and all child objects and vice versa.
|
||||
if err := normalizeRecipients(a); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(i int) error {
|
||||
obj := op.At(i).GetType()
|
||||
id, err := GetId(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.db.Lock(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, id)
|
||||
if err := w.db.Create(c, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Persist all objects we've created, which will include sensitive
|
||||
// recipients such as 'bcc' and 'bto'.
|
||||
for i := 0; i < op.Len(); i++ {
|
||||
if err := loopFn(i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Create != nil {
|
||||
return w.Create(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// update implements the social Update activity side effects.
|
||||
func (w SocialWrappedCallbacks) update(c context.Context, a vocab.ActivityStreamsUpdate) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
// Obtain all object ids, which should be owned by this server.
|
||||
objIds := make([]*url.URL, 0, op.Len())
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objIds = append(objIds, id)
|
||||
}
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(idx int, loopId *url.URL) error {
|
||||
err := w.db.Lock(c, loopId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, loopId)
|
||||
t, err := w.db.Get(c, loopId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m, err := t.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Copy over new top-level values.
|
||||
objType := op.At(idx).GetType()
|
||||
if objType == nil {
|
||||
return fmt.Errorf("object at index %d is not a literal type value", idx)
|
||||
}
|
||||
newM, err := objType.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range newM {
|
||||
m[k] = v
|
||||
}
|
||||
// Delete top-level values where the raw Activity had nils.
|
||||
for k, v := range w.rawActivity {
|
||||
if _, ok := m[k]; v == nil && ok {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
newT, err := streams.ToType(c, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = w.db.Update(c, newT); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for i, id := range objIds {
|
||||
if err := loopFn(i, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Update != nil {
|
||||
return w.Update(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteFn implements the social Delete activity side effects.
|
||||
func (w SocialWrappedCallbacks) deleteFn(c context.Context, a vocab.ActivityStreamsDelete) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
// Obtain all object ids, which should be owned by this server.
|
||||
objIds := make([]*url.URL, 0, op.Len())
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
id, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objIds = append(objIds, id)
|
||||
}
|
||||
// Create anonymous loop function to be able to properly scope the defer
|
||||
// for the database lock at each iteration.
|
||||
loopFn := func(idx int, loopId *url.URL) error {
|
||||
err := w.db.Lock(c, loopId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, loopId)
|
||||
t, err := w.db.Get(c, loopId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tomb := toTombstone(t, loopId, w.clock.Now())
|
||||
if err := w.db.Update(c, tomb); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for i, id := range objIds {
|
||||
if err := loopFn(i, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Delete != nil {
|
||||
return w.Delete(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// follow implements the social Follow activity side effects.
|
||||
func (w SocialWrappedCallbacks) follow(c context.Context, a vocab.ActivityStreamsFollow) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
if w.Follow != nil {
|
||||
return w.Follow(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// add implements the social Add activity side effects.
|
||||
func (w SocialWrappedCallbacks) add(c context.Context, a vocab.ActivityStreamsAdd) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
target := a.GetActivityStreamsTarget()
|
||||
if target == nil || target.Len() == 0 {
|
||||
return ErrTargetRequired
|
||||
}
|
||||
if err := add(c, op, target, w.db); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Add != nil {
|
||||
return w.Add(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove implements the social Remove activity side effects.
|
||||
func (w SocialWrappedCallbacks) remove(c context.Context, a vocab.ActivityStreamsRemove) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
target := a.GetActivityStreamsTarget()
|
||||
if target == nil || target.Len() == 0 {
|
||||
return ErrTargetRequired
|
||||
}
|
||||
if err := remove(c, op, target, w.db); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Remove != nil {
|
||||
return w.Remove(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// like implements the social Like activity side effects.
|
||||
func (w SocialWrappedCallbacks) like(c context.Context, a vocab.ActivityStreamsLike) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
// Get this actor's IRI.
|
||||
if err := w.db.Lock(c, w.outboxIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
// WARNING: Unlock not deferred.
|
||||
actorIRI, err := w.db.ActorForOutbox(c, w.outboxIRI)
|
||||
if err != nil {
|
||||
w.db.Unlock(c, w.outboxIRI)
|
||||
return err
|
||||
}
|
||||
w.db.Unlock(c, w.outboxIRI)
|
||||
// Unlock must be called by now and every branch above.
|
||||
//
|
||||
// Now obtain this actor's 'liked' collection.
|
||||
if err := w.db.Lock(c, actorIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.db.Unlock(c, actorIRI)
|
||||
liked, err := w.db.Liked(c, actorIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
likedItems := liked.GetActivityStreamsItems()
|
||||
if likedItems == nil {
|
||||
likedItems = streams.NewActivityStreamsItemsProperty()
|
||||
liked.SetActivityStreamsItems(likedItems)
|
||||
}
|
||||
for iter := op.Begin(); iter != op.End(); iter = iter.Next() {
|
||||
objId, err := ToId(iter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
likedItems.PrependIRI(objId)
|
||||
}
|
||||
err = w.db.Update(c, liked)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Like != nil {
|
||||
return w.Like(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// undo implements the social Undo activity side effects.
|
||||
func (w SocialWrappedCallbacks) undo(c context.Context, a vocab.ActivityStreamsUndo) error {
|
||||
*w.undeliverable = false
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
actors := a.GetActivityStreamsActor()
|
||||
if err := mustHaveActivityActorsMatchObjectActors(c, actors, op, w.newTransport, w.outboxIRI); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Undo != nil {
|
||||
return w.Undo(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// block implements the social Block activity side effects.
|
||||
func (w SocialWrappedCallbacks) block(c context.Context, a vocab.ActivityStreamsBlock) error {
|
||||
*w.undeliverable = true
|
||||
op := a.GetActivityStreamsObject()
|
||||
if op == nil || op.Len() == 0 {
|
||||
return ErrObjectRequired
|
||||
}
|
||||
if w.Block != nil {
|
||||
return w.Block(c, a)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
)
|
||||
|
||||
const (
|
||||
// acceptHeaderValue is the Accept header value indicating that the
|
||||
// response should contain an ActivityStreams object.
|
||||
acceptHeaderValue = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
)
|
||||
|
||||
// isSuccess returns true if the HTTP status code is either OK, Created, or
|
||||
// Accepted.
|
||||
func isSuccess(code int) bool {
|
||||
return code == http.StatusOK ||
|
||||
code == http.StatusCreated ||
|
||||
code == http.StatusAccepted
|
||||
}
|
||||
|
||||
// Transport makes ActivityStreams calls to other servers in order to send or
|
||||
// receive ActivityStreams data.
|
||||
//
|
||||
// It is responsible for setting the appropriate request headers, signing the
|
||||
// requests if needed, and facilitating the traffic between this server and
|
||||
// another.
|
||||
//
|
||||
// The transport is exclusively used to issue requests on behalf of an actor,
|
||||
// and is never sending requests on behalf of the server in general.
|
||||
//
|
||||
// It may be reused multiple times, but never concurrently.
|
||||
type Transport interface {
|
||||
// Dereference fetches the ActivityStreams object located at this IRI
|
||||
// with a GET request.
|
||||
Dereference(c context.Context, iri *url.URL) ([]byte, error)
|
||||
// Deliver sends an ActivityStreams object.
|
||||
Deliver(c context.Context, b []byte, to *url.URL) error
|
||||
// BatchDeliver sends an ActivityStreams object to multiple recipients.
|
||||
BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error
|
||||
}
|
||||
|
||||
// Transport must be implemented by HttpSigTransport.
|
||||
var _ Transport = &HttpSigTransport{}
|
||||
|
||||
// HttpSigTransport makes a dereference call using HTTP signatures to
|
||||
// authenticate the request on behalf of a particular actor.
|
||||
//
|
||||
// No rate limiting is applied.
|
||||
//
|
||||
// Only one request is tried per call.
|
||||
type HttpSigTransport struct {
|
||||
client HttpClient
|
||||
appAgent string
|
||||
gofedAgent string
|
||||
clock Clock
|
||||
getSigner httpsig.Signer
|
||||
getSignerMu *sync.Mutex
|
||||
postSigner httpsig.Signer
|
||||
postSignerMu *sync.Mutex
|
||||
pubKeyId string
|
||||
privKey crypto.PrivateKey
|
||||
}
|
||||
|
||||
// NewHttpSigTransport returns a new Transport.
|
||||
//
|
||||
// It sends requests specifically on behalf of a specific actor on this server.
|
||||
// The actor's credentials are used to add an HTTP Signature to requests, which
|
||||
// requires an actor's private key, a unique identifier for their public key,
|
||||
// and an HTTP Signature signing algorithm.
|
||||
//
|
||||
// The client lets users issue requests through any HTTP client, including the
|
||||
// standard library's HTTP client.
|
||||
//
|
||||
// The appAgent uniquely identifies the calling application's requests, so peers
|
||||
// may aid debugging the requests incoming from this server. Note that the
|
||||
// agent string will also include one for go-fed, so at minimum peer servers can
|
||||
// reach out to the go-fed library to aid in notifying implementors of malformed
|
||||
// or unsupported requests.
|
||||
func NewHttpSigTransport(
|
||||
client HttpClient,
|
||||
appAgent string,
|
||||
clock Clock,
|
||||
getSigner, postSigner httpsig.Signer,
|
||||
pubKeyId string,
|
||||
privKey crypto.PrivateKey) *HttpSigTransport {
|
||||
return &HttpSigTransport{
|
||||
client: client,
|
||||
appAgent: appAgent,
|
||||
gofedAgent: goFedUserAgent(),
|
||||
clock: clock,
|
||||
getSigner: getSigner,
|
||||
getSignerMu: &sync.Mutex{},
|
||||
postSigner: postSigner,
|
||||
postSignerMu: &sync.Mutex{},
|
||||
pubKeyId: pubKeyId,
|
||||
privKey: privKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Dereference sends a GET request signed with an HTTP Signature to obtain an
|
||||
// ActivityStreams value.
|
||||
func (h HttpSigTransport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", iri.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(c)
|
||||
req.Header.Add(acceptHeader, acceptHeaderValue)
|
||||
req.Header.Add("Accept-Charset", "utf-8")
|
||||
req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
|
||||
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent))
|
||||
req.Header.Set("Host", iri.Host)
|
||||
h.getSignerMu.Lock()
|
||||
err = h.getSigner.SignRequest(h.privKey, h.pubKeyId, req, nil)
|
||||
h.getSignerMu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
|
||||
}
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// Deliver sends a POST request with an HTTP Signature.
|
||||
func (h HttpSigTransport) Deliver(c context.Context, b []byte, to *url.URL) error {
|
||||
req, err := http.NewRequest("POST", to.String(), bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(c)
|
||||
req.Header.Add(contentTypeHeader, contentTypeHeaderValue)
|
||||
req.Header.Add("Accept-Charset", "utf-8")
|
||||
req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
|
||||
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent))
|
||||
req.Header.Set("Host", to.Host)
|
||||
h.postSignerMu.Lock()
|
||||
err = h.postSigner.SignRequest(h.privKey, h.pubKeyId, req, b)
|
||||
h.postSignerMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if !isSuccess(resp.StatusCode) {
|
||||
return fmt.Errorf("POST request to %s failed (%d): %s", to.String(), resp.StatusCode, resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDeliver sends concurrent POST requests. Returns an error if any of the
|
||||
// requests had an error.
|
||||
func (h HttpSigTransport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, len(recipients))
|
||||
for _, recipient := range recipients {
|
||||
wg.Add(1)
|
||||
go func(r *url.URL) {
|
||||
defer wg.Done()
|
||||
if err := h.Deliver(c, b, r); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}(recipient)
|
||||
}
|
||||
wg.Wait()
|
||||
errs := make([]string, 0, len(recipients))
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case e := <-errCh:
|
||||
errs = append(errs, e.Error())
|
||||
default:
|
||||
break outer
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("batch deliver had at least one failure: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HttpClient sends http requests, and is an abstraction only needed by the
|
||||
// HttpSigTransport. The standard library's Client satisfies this interface.
|
||||
type HttpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// HttpClient must be implemented by http.Client.
|
||||
var _ HttpClient = &http.Client{}
|
|
@ -0,0 +1,159 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
const (
|
||||
testAppAgent = "testApp"
|
||||
testPubKeyId = "myPubKeyId"
|
||||
)
|
||||
|
||||
var (
|
||||
testPrivKey = []byte("some private key")
|
||||
testRespBody = []byte("test resp body")
|
||||
httpSigSetupFn = func(ctl *gomock.Controller) (t *HttpSigTransport, c *MockClock, hc *MockHttpClient, gs, ps *MockSigner) {
|
||||
c = NewMockClock(ctl)
|
||||
hc = NewMockHttpClient(ctl)
|
||||
gs = NewMockSigner(ctl)
|
||||
ps = NewMockSigner(ctl)
|
||||
t = NewHttpSigTransport(
|
||||
hc,
|
||||
testAppAgent,
|
||||
c,
|
||||
gs,
|
||||
ps,
|
||||
testPubKeyId,
|
||||
testPrivKey)
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
func TestHttpSigTransportDereference(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Run("ReturnsErrorWhenHTTPStatusError", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
tp, c, hc, gs, _ := httpSigSetupFn(ctl)
|
||||
resp := &http.Response{}
|
||||
testErr := fmt.Errorf("test error")
|
||||
// Mock
|
||||
c.EXPECT().Now().Return(now())
|
||||
gs.EXPECT().SignRequest(testPrivKey, testPubKeyId, gomock.Any(), nil)
|
||||
hc.EXPECT().Do(gomock.Any()).Return(resp, testErr)
|
||||
// Run & Verify
|
||||
b, err := tp.Dereference(ctx, mustParse(testNoteId1))
|
||||
assertEqual(t, len(b), 0)
|
||||
assertEqual(t, err, testErr)
|
||||
})
|
||||
t.Run("Dereferences", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
tp, c, hc, gs, _ := httpSigSetupFn(ctl)
|
||||
expectReq, err := http.NewRequest("GET", testNoteId1, nil)
|
||||
assertEqual(t, err, nil)
|
||||
expectReq = expectReq.WithContext(ctx)
|
||||
expectReq.Header.Add(acceptHeader, acceptHeaderValue)
|
||||
expectReq.Header.Add("Accept-Charset", "utf-8")
|
||||
expectReq.Header.Add("Date", nowDateHeader())
|
||||
expectReq.Header.Add("User-Agent", fmt.Sprintf("%s %s", testAppAgent, goFedUserAgent()))
|
||||
respR := httptest.NewRecorder()
|
||||
respR.Write(testRespBody)
|
||||
resp := respR.Result()
|
||||
// Mock
|
||||
c.EXPECT().Now().Return(now())
|
||||
gs.EXPECT().SignRequest(testPrivKey, testPubKeyId, expectReq, nil)
|
||||
hc.EXPECT().Do(expectReq).Return(resp, nil)
|
||||
// Run & Verify
|
||||
b, err := tp.Dereference(ctx, mustParse(testNoteId1))
|
||||
assertByteEqual(t, b, testRespBody)
|
||||
assertEqual(t, err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHttpSigTransportDeliver(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Run("ReturnsErrorWhenHTTPStatusError", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
tp, c, hc, _, ps := httpSigSetupFn(ctl)
|
||||
resp := &http.Response{}
|
||||
testErr := fmt.Errorf("test error")
|
||||
// Mock
|
||||
c.EXPECT().Now().Return(now())
|
||||
ps.EXPECT().SignRequest(testPrivKey, testPubKeyId, gomock.Any(), gomock.Any())
|
||||
hc.EXPECT().Do(gomock.Any()).Return(resp, testErr)
|
||||
// Run & Verify
|
||||
err := tp.Deliver(ctx, testRespBody, mustParse(testNoteId1))
|
||||
assertEqual(t, err, testErr)
|
||||
})
|
||||
t.Run("Delivers", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
tp, c, hc, _, ps := httpSigSetupFn(ctl)
|
||||
// gomock cannot handle http.NewRequest w/ Body differences.
|
||||
respR := httptest.NewRecorder()
|
||||
respR.WriteHeader(http.StatusOK)
|
||||
resp := respR.Result()
|
||||
// Mock
|
||||
c.EXPECT().Now().Return(now())
|
||||
ps.EXPECT().SignRequest(testPrivKey, testPubKeyId, gomock.Any(), testRespBody)
|
||||
hc.EXPECT().Do(gomock.Any()).Return(resp, nil)
|
||||
// Run & Verify
|
||||
err := tp.Deliver(ctx, testRespBody, mustParse(testFederatedActorIRI))
|
||||
assertEqual(t, err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHttpSigTransportBatchDeliver(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Run("BatchDelivers", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
tp, c, hc, _, ps := httpSigSetupFn(ctl)
|
||||
// gomock cannot handle http.NewRequest w/ Body differences.
|
||||
respR := httptest.NewRecorder()
|
||||
respR.WriteHeader(http.StatusOK)
|
||||
resp := respR.Result()
|
||||
// Mock
|
||||
c.EXPECT().Now().Return(now()).Times(2)
|
||||
ps.EXPECT().SignRequest(testPrivKey, testPubKeyId, gomock.Any(), testRespBody).Times(2)
|
||||
hc.EXPECT().Do(gomock.Any()).Return(resp, nil).Times(2)
|
||||
// Run & Verify
|
||||
err := tp.BatchDeliver(ctx, testRespBody, []*url.URL{mustParse(testFederatedActorIRI), mustParse(testFederatedActorIRI2)})
|
||||
assertEqual(t, err, nil)
|
||||
})
|
||||
t.Run("ReturnsErrorWhenOneErrors", func(t *testing.T) {
|
||||
// Setup
|
||||
ctl := gomock.NewController(t)
|
||||
defer ctl.Finish()
|
||||
tp, c, hc, _, ps := httpSigSetupFn(ctl)
|
||||
// gomock cannot handle http.NewRequest w/ Body differences.
|
||||
respR := httptest.NewRecorder()
|
||||
respR.WriteHeader(http.StatusOK)
|
||||
resp := respR.Result()
|
||||
errResp := &http.Response{}
|
||||
testErr := fmt.Errorf("test error")
|
||||
// Mock
|
||||
c.EXPECT().Now().Return(now()).Times(2)
|
||||
ps.EXPECT().SignRequest(testPrivKey, testPubKeyId, gomock.Any(), testRespBody).Times(2)
|
||||
first := hc.EXPECT().Do(gomock.Any()).Return(resp, nil)
|
||||
hc.EXPECT().Do(gomock.Any()).Return(errResp, testErr).After(first)
|
||||
// Run & Verify
|
||||
err := tp.BatchDeliver(ctx, testRespBody, []*url.URL{mustParse(testFederatedActorIRI), mustParse(testFederatedActorIRI2)})
|
||||
assertNotEqual(t, err, nil)
|
||||
|
||||
})
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,76 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHeaderIsActivityPubMediaType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
"Mastodon Accept Header",
|
||||
"application/activity+json, application/ld+json",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Plain Type",
|
||||
"application/activity+json",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Missing Profile",
|
||||
"application/ld+json",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"With Profile",
|
||||
"application/ld+json ; profile=https://www.w3.org/ns/activitystreams",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"With Quoted Profile",
|
||||
"application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"With Profile (End Space)",
|
||||
"application/ld+json; profile=https://www.w3.org/ns/activitystreams",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"With Quoted Profile (End Space)",
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"With Profile (Begin Space)",
|
||||
"application/ld+json ;profile=https://www.w3.org/ns/activitystreams",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"With Quoted Profile (Begin Space)",
|
||||
"application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"With Profile (No Space)",
|
||||
"application/ld+json;profile=https://www.w3.org/ns/activitystreams",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"With Quoted Profile (No Space)",
|
||||
"application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if actual := headerIsActivityPubMediaType(test.input); actual != test.expected {
|
||||
t.Fatalf("expected %v, got %v", test.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package pub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version string, used in the User-Agent
|
||||
version = "v1.0.0"
|
||||
)
|
||||
|
||||
// goFedUserAgent returns the user agent string for the go-fed library.
|
||||
func goFedUserAgent() string {
|
||||
return fmt.Sprintf("(go-fed/activity %s)", version)
|
||||
}
|
|
@ -1,138 +1,152 @@
|
|||
# streams
|
||||
|
||||
Please read the `README.md` in the `go-fed/activity/vocab` package first. This
|
||||
library is a convenience layer on top of the `go-fed/activity/vocab` library, so
|
||||
this README builds off of that one.
|
||||
ActivityStreams vocabularies automatically code-generated with `astool`.
|
||||
|
||||
This library is entirely code-generated by the
|
||||
`go-fed/activity/tools/streams/gen` library and `go-fed/activity/tools/streams`
|
||||
tool. Run `go generate` to refresh the library, which requires `$GOPATH/bin` to
|
||||
be on your `$PATH`.
|
||||
## Reference & Tutorial
|
||||
|
||||
## What it does
|
||||
The [go-fed website](https://go-fed.org/) contains tutorials and reference
|
||||
materials, in addition to the rest of this README.
|
||||
|
||||
This library provides a `Resolver`, which is simply a collection of callbacks
|
||||
that clients can specify to handle specific ActivtyStream data types. The
|
||||
`Resolver.Deserialize` method turns a JSON-decoded `map[string]interface{}`
|
||||
into its proper type, passed to the corresponding callback.
|
||||
|
||||
For example, given the data:
|
||||
## How To Use
|
||||
|
||||
```
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Note",
|
||||
"name": "Equivalent Exchange",
|
||||
"content": "I'll give half of my life to you and you give half of yours to me!",
|
||||
"attachment": "https://example.com/attachment"
|
||||
go get github.com/go-fed/activity
|
||||
```
|
||||
|
||||
All generated types and properties are interfaces in
|
||||
`github.com/go-fed/streams/vocab`, but note that the constructors and supporting
|
||||
functions live in `github.com/go-fed/streams`.
|
||||
|
||||
To create a type and set properties:
|
||||
|
||||
```golang
|
||||
var actorURL *url.URL = // ...
|
||||
|
||||
// A new "Create" Activity.
|
||||
create := streams.NewActivityStreamsCreate()
|
||||
// A new "actor" property.
|
||||
actor := streams.NewActivityStreamsActorProperty()
|
||||
actor.AppendIRI(actorURL)
|
||||
// Set the "actor" property on the "Create" Activity.
|
||||
create.SetActivityStreamsActor(actor)
|
||||
```
|
||||
|
||||
To process properties on a type:
|
||||
|
||||
```golang
|
||||
// Returns true if the "Update" has at least one "object" with an IRI value.
|
||||
func hasObjectWithIRIValue(update vocab.ActivityStreamsUpdate) bool {
|
||||
objectProperty := update.GetActivityStreamsObject()
|
||||
// Any property may be nil if it was either empty in the original JSON or
|
||||
// never set on the golang type.
|
||||
if objectProperty == nil {
|
||||
return false
|
||||
}
|
||||
// The "object" property is non-functional: it could have multiple values. The
|
||||
// generated code has slightly different methods for a functional property
|
||||
// versus a non-functional one.
|
||||
//
|
||||
// While it may be easy to ignore multiple values in other languages
|
||||
// (accidentally or purposefully), go-fed is designed to make it hard to do
|
||||
// so.
|
||||
for iter := objectProperty.Begin(); iter != objectProperty.End(); iter = iter.Next() {
|
||||
// If this particular value is an IRI, return true.
|
||||
if iter.IsIRI() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// All values are literal embedded values and not IRIs.
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
in `b []byte` one can do the following:
|
||||
The ActivityStreams type hierarchy of "extends" and "disjoint" is not the same
|
||||
as the Object Oriented definition of inheritance. It is also not the same as
|
||||
golang's interface duck-typing. Helper functions are provided to guarantee that
|
||||
an application's logic can correctly apply the type hierarchy.
|
||||
|
||||
```
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return err
|
||||
```golang
|
||||
thing := // Pick a type from streams.NewActivityStreams<Type>()
|
||||
if streams.ActivityStreamsObjectIsDisjointWith(thing) {
|
||||
fmt.Printf("The \"Object\" type is Disjoint with the %T type.\n", thing)
|
||||
}
|
||||
r := &Resolver {
|
||||
NoteCallback: func(n *Note) error {
|
||||
// 1) Use the Note concrete type here
|
||||
// 2) Errors are propagated transparently
|
||||
},
|
||||
if streams.ActivityStreamsLinkIsExtendedBy(thing) {
|
||||
fmt.Printf("The %T type Extends from the \"Link\" type.\n", thing)
|
||||
}
|
||||
if handled, err := r.Deserialize(m); err != nil {
|
||||
// 3) Any errors from #2 can be handled, or the payload is an unknown type.
|
||||
return err
|
||||
} else if !handled {
|
||||
// 4) The callback to handle the concrete type was not set.
|
||||
if streams.ActivityStreamsActivityExtends(thing) {
|
||||
fmt.Printf("The \"Activity\" type extends from the %T type.\n", thing)
|
||||
}
|
||||
```
|
||||
|
||||
Only set the callbacks that are interesting. There is no need to set every
|
||||
callback, unless your application requires it.
|
||||
When given a generic JSON payload, it can be resolved to a concrete type by
|
||||
creating a `streams.JSONResolver` and giving it a callback function that accepts
|
||||
the interesting concrete type:
|
||||
|
||||
## Using concrete types
|
||||
|
||||
The convenience layer provides easy access to properties with specific types.
|
||||
However, because ActivityStreams is fundamentally built off of JSON-LD and
|
||||
still permits large degree of freedom when it comes to obtaining a concrete type
|
||||
for a property, the convenience API is built to give clients the freedom to
|
||||
choose how best to federate.
|
||||
|
||||
For every type in this package (except `Resolver`), there is an equivalent type
|
||||
in the `activity/vocab` package. It takes only a call to `Raw` to go from this
|
||||
convenience API to the full API:
|
||||
|
||||
```
|
||||
r := &Resolver {
|
||||
NoteCallback: func(n *Note) error {
|
||||
// Raw is available for all ActivityStream types
|
||||
vocabNote := n.Raw()
|
||||
},
|
||||
```golang
|
||||
// Callbacks must be in the form:
|
||||
// func(context.Context, <TypeInterface>) error
|
||||
createCallback := func(c context.Context, create vocab.ActivityStreamsCreate) error {
|
||||
// Do something with 'create'
|
||||
fmt.Printf("createCallback called: %T\n", create)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
To determine whether the call to `Raw` is needed, the "get" and "has" methods
|
||||
use `Resolution` and `Presence` types to inform client code. The client is free
|
||||
to support as many types as is feasible within the specific application.
|
||||
|
||||
Reusing the `Note` example above that has an `attachment`, the following is
|
||||
client code that tries to handle every possible type that `attachment` can
|
||||
take. **The W3C does not require client applications to support all of these
|
||||
use cases.**
|
||||
|
||||
```
|
||||
r := &Resolver {}
|
||||
r.NoteCallback = func(n *Note) error {
|
||||
if n.LenAttachment() == 1 {
|
||||
if presence := n.HasAttachment(0); p == ConvenientPresence {
|
||||
// A new or existing Resolver can be used. This is the convenient getter.
|
||||
if resolution, err := n.ResolveAttachment(r, 0); err != nil {
|
||||
return err
|
||||
} else if resolution == RawResolutionNeeded {
|
||||
vocabNote := n.Raw()
|
||||
// Use the full API
|
||||
if vocabNote.IsAttachmentIRI(0) {
|
||||
...
|
||||
} else ...
|
||||
}
|
||||
} else if p == RawPresence {
|
||||
vocabNote := n.Raw()
|
||||
// Use the full API
|
||||
if vocabNote.IsAttachmentIRI(0) {
|
||||
...
|
||||
} else ...
|
||||
}
|
||||
}
|
||||
updateCallback := func(c context.Context, update vocab.ActivityStreamsUpdate) error {
|
||||
// Do something with 'update'
|
||||
fmt.Printf("updateCallback called: %T\n", update)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Serializing data
|
||||
|
||||
Creating a raw type and serializing it is straightforward:
|
||||
|
||||
```
|
||||
n := &Note{}
|
||||
n.AddName("I'll see you again")
|
||||
n.AddContent("You don't have to be alone when I leave")
|
||||
// The "type" property is automatically handled...
|
||||
m, err := n.Serialize()
|
||||
jsonResolver, err := streams.NewJSONResolver(createCallback, updateCallback)
|
||||
if err != nil {
|
||||
return err
|
||||
// Something in the setup was wrong. For example, a callback has an
|
||||
// unsupported signature and would never be called
|
||||
panic(err)
|
||||
}
|
||||
// Create a context, which allows you to pass data opaquely through the
|
||||
// JSONResolver.
|
||||
c := context.Background()
|
||||
// Example 15 of the ActivityStreams specification.
|
||||
b := []byte(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"summary": "Sally created a note",
|
||||
"type": "Create",
|
||||
"actor": {
|
||||
"type": "Person",
|
||||
"name": "Sally"
|
||||
},
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"name": "A Simple Note",
|
||||
"content": "This is a simple note"
|
||||
}
|
||||
}`)
|
||||
var jsonMap map[string]interface{}
|
||||
if err = json.Unmarshal(b, &jsonMap); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// The createCallback function will be called.
|
||||
err = jsonResolver.Resolve(c, jsonMap)
|
||||
if err != nil && !streams.IsUnmatchedErr(err) {
|
||||
// Something went wrong
|
||||
panic(err)
|
||||
} else if streams.IsUnmatchedErr(err) {
|
||||
// Everything went right but the callback didn't match or the ActivityStreams
|
||||
// type is one that wasn't code generated.
|
||||
fmt.Println("No match: ", err)
|
||||
}
|
||||
// ...but "@context" is not.
|
||||
m["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
b, err := json.Marshal(m)
|
||||
```
|
||||
|
||||
The only caveat is that clients must set `"@context"` manually at this time.
|
||||
A `streams.TypeResolver` is similar but uses the golang types instead. It
|
||||
accepts the generic `vocab.Type`. This is the abstraction when needing to handle
|
||||
any ActivityStreams type. The function `ToType` can convert a JSON-decoded-map
|
||||
into this kind of value if needed.
|
||||
|
||||
## What it doesn't do
|
||||
A `streams.PredicatedTypeResolver` lets you apply a boolean predicate function
|
||||
that acts as a check whether a callback is allowed to be invoked.
|
||||
|
||||
Please see the same section in the `go-fed/activity/vocab` package.
|
||||
## FAQ
|
||||
|
||||
## Other considerations
|
||||
### Why Are Empty Properties Nil And Not Zero-Valued?
|
||||
|
||||
This library is entirely code-generated. Please see the same section in the
|
||||
`go-fed/activity/vocab` package for more details.
|
||||
Due to implementation design decisions, it would require a lot of plumbing to
|
||||
ensure this would work properly. It would also require allocation of a
|
||||
non-trivial amount of memory.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2452
streams/gen_add.go
2452
streams/gen_add.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2093
streams/gen_audio.go
2093
streams/gen_audio.go
File diff suppressed because it is too large
Load Diff
2452
streams/gen_block.go
2452
streams/gen_block.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue