Compare commits

...

396 Commits

Author SHA1 Message Date
Cory J Slep b14b50eeca
Merge pull request #154 from superseriousbusiness/as-sensitive
add 'sensitive' object property
2022-01-19 08:36:22 +01:00
Cory J Slep 143897756e
Merge pull request #153 from superseriousbusiness/InboxForActor
Inbox for actor
2022-01-19 08:35:45 +01:00
Cory J Slep d2deaac73f
Merge pull request #155 from muesli/github-workflows
Add GitHub build workflow, remove TravisCI config
2022-01-19 08:32:12 +01:00
Christian Muehlhaeuser 7f61b78d08
Add GitHub build workflow, remove TravisCI config 2022-01-09 09:51:14 +01:00
tsmethurst a5791f8fcc add 'sensitive' object property 2021-09-30 13:47:23 +02:00
tsmethurst bffb780f1b go fmt ./... 2021-09-26 11:32:43 +02:00
tsmethurst 511885d31b regenerate mock 2021-09-26 11:32:33 +02:00
tsmethurst d35c464c58 add InboxForActor func 2021-09-26 11:32:22 +02:00
tsmethurst 6e8450d07d add convenience func for removing entry from slice 2021-09-26 11:32:13 +02:00
Cory J Slep d866ba75dd
Merge pull request #151 from superseriousbusiness/manuallyApprovesFollowers
add manuallyApprovesFollowers property on actors
2021-08-03 23:28:04 +02:00
tsmethurst 5498bae4cf add manuallyApprovesFollowers property on actors 2021-08-03 15:52:03 +02:00
Cory J Slep 4655f8f1e1
Merge pull request #148 from weex/master
Fix #145: Change example domains to standards
2021-08-01 13:02:20 +02:00
David Sterry b4d577f69b Fix #145 2021-06-23 10:11:37 -07:00
Cory J Slep e0de0863dc
Merge pull request #147 from BenLubar-PR/host-header
ensure host header is set
2021-04-26 21:46:15 +02:00
Cory J Slep 27010241b1
Merge pull request #146 from komarov/patch-1
cosmetic: fix some copy-pasted names
2021-04-26 21:29:10 +02:00
Ben Lubar 042ba2ce5e
ensure host header is set 2021-04-07 10:22:27 -05:00
komarov 5e8c5c0bb8
cosmetic: fix some copy-pasted names 2021-02-28 12:39:23 +01:00
Cory Slep d282a50439 Add badge for Matrix chat room 2020-12-23 12:22:50 +01:00
Cory Slep 8d07c6c337 Make license badge blue 2020-12-22 21:51:01 +01:00
Cory Slep 6daa914620 Add badges 2020-12-22 21:49:21 +01:00
Cory Slep a386d74920 Add travis.yml 2020-12-22 21:47:04 +01:00
Cory Slep 472d90163f Add PostInboxScheme and PostOutboxScheme to Actor 2020-12-13 23:45:52 +01:00
Cory Slep be7f23bde7 Return ErrNotFound when handler retrieves no data 2020-12-13 23:32:23 +01:00
Cory Slep 6cbfb30afa Support custom schemes for handlers
This allows clients to serve data over HTTP or HTTPS, which should only
be done for development purposes.
2020-12-13 23:25:58 +01:00
Cory Slep c7bbac61b6 Update READMEs and CHANGELOG for v1.0.0 2020-07-09 17:34:37 +02:00
Cory Slep c2285ceec2 Rename Callbacks to disambiguate between S2S & C2S 2020-07-06 19:41:49 +02:00
Cory Slep 0430970bd3 Add generated forgefed vocabulary 2020-07-05 22:38:06 +02:00
Cory Slep ee53bc0369 Change delegate actor to use upper 'ID', fix tests 2020-07-05 22:37:26 +02:00
Cory Slep 574f22607b Add forgefed vocabulary to go generate command 2020-07-05 21:58:08 +02:00
Cory J Slep 2da4dc1d6e
Merge pull request #136 from BenLubar-PR/forgefed
Implement ForgeFed vocabulary.
2020-07-05 21:56:36 +02:00
Cory Slep fbb513751a Merge branch 'master' of https://github.com/go-fed/activity 2020-07-05 21:47:43 +02:00
Cory Slep a3a3f48d19 Regenerate vocabular with source property 2020-07-05 21:47:30 +02:00
Cory Slep 2d143b225a Add source property to activitystreams OWL file 2020-07-05 21:47:08 +02:00
Cory Slep fc8e2d8c30 Add toot spec to go generate command 2020-07-05 21:46:37 +02:00
Cory J Slep 415c3275ef
Merge pull request #133 from sevki/patch-1
NewId => NewID
2020-07-05 21:22:45 +02:00
Sevki 7c20a89a61
Update database.go 2020-07-05 20:07:47 +01:00
Sevki a8fb705a22
Merge branch 'master' into patch-1 2020-07-05 20:06:55 +01:00
Cory Slep acc84125fa Run go mod tidy 2020-07-05 20:59:37 +02:00
Cory Slep e17e28bddb Remove unused jsonLDContext constant 2020-07-05 20:56:10 +02:00
Cory Slep f4f19b61b3 Update astool's README 2020-07-05 20:37:26 +02:00
Cory Slep 107afb6f17 Avoid a byte copy when delivering a HTTP request
Added tests for the HTTP sig transport.
2020-07-05 17:06:52 +02:00
Cory Slep c3b24964d4 Add unit tests for the AS handler 2020-07-05 15:54:31 +02:00
Cory Slep 311ab07a5c Use equal bytes function in tests when asserting 2020-07-05 15:53:42 +02:00
Cory Slep 18fcfcf812 Add tests for wrapping in create. 2020-07-05 15:22:18 +02:00
Cory Slep 78c96972b2 Clear sensitive fields by setting nil instead of removing 2020-07-05 15:22:00 +02:00
Cory Slep fa448c7559 Improve bto & bcc stripping before delivery.
Added unit tests covering the delivery call.
2020-07-05 14:10:33 +02:00
Cory Slep 5ad09ded3e Add unit tests for federated block 2020-07-04 18:30:17 +02:00
Cory Slep a8662e547a Add unit tests for federated undo 2020-07-04 18:25:35 +02:00
Cory Slep d003641810 Add unit tests for federated announce 2020-07-04 17:47:17 +02:00
Cory Slep c58f23bd17 Add unit tests for federated like 2020-07-04 10:41:42 +02:00
Cory Slep 72022bfe14 Add unit tests for federated remove 2020-07-04 10:00:48 +02:00
Cory Slep 4c60792b09 Add unit tests for federated add 2020-07-04 09:42:25 +02:00
Cory Slep 110d8bd3e5 Add unit tests for federated follows 2020-07-03 18:44:52 +02:00
Cory Slep 8af6ed9d4f Add unit tests for federated deletes 2020-07-03 18:06:52 +02:00
Cory Slep a1ac83aee8 Add unit tests for federated updates 2020-07-03 17:50:08 +02:00
Cory Slep c994dc39f0 Add tests and fix shadowing bug in Accept handling 2020-07-02 23:03:01 +02:00
Cory Slep d11b96b9ed Implement 18 more tests in the federation callbacks. 2020-07-02 22:10:35 +02:00
Cory Slep f2a497bdc2 Fix broken unit tests. 2020-07-02 21:15:02 +02:00
Cory J Slep 95bfff952e
Merge pull request #128 from BenLubar-PR/toot-grammar
Toot grammar
2020-05-26 08:08:58 +02:00
Cory J Slep d6627c1750
Merge pull request #132 from Sigafoos/bugfix/s_inbox_outbox
Replace 'inbox' with 'outbox' when appropriate
2020-05-26 08:08:33 +02:00
Cory J Slep 91fc2338f2
Merge pull request #135 from kissen/fix/likecallback
Fix: Use correct database method to get actor IRI
2020-05-26 08:06:34 +02:00
Cory J Slep ceedc04ee1
Merge pull request #134 from kissen/master
Fix nil derefences
2020-05-26 08:05:49 +02:00
Cory J Slep 28e35e3541
Merge pull request #137 from BenLubar-PR/generate
add file to allow `go generate` to update generated code
2020-05-26 08:04:50 +02:00
Ben Lubar 908aa3e9ae
add file to allow `go generate` to update generated code 2020-05-09 12:34:32 -05:00
Ben Lubar aa98aaa1cf
Implement ForgeFed vocabulary.
Caveat: the "description" property is probably wrong; it should match the definition of the ActivityPub(!) "source" property which is currently not in go-fed.
2020-05-08 19:09:02 -05:00
Andreas Schärtl 2efc3220fa Fix: Use correct database method to get actor IRI 2020-04-01 15:02:36 +02:00
Andreas Schärtl 01a23ee4d1 Fix: Avoid nil-derferences when dealing with Create side-effects 2020-04-01 14:50:36 +02:00
Andreas Schärtl 5f6df51cb8 Fix: Avoid nil-dereferences when wrapping object in Create activity 2020-04-01 14:50:34 +02:00
Sevki e7c3ac2533
Id => ID
`Id` is causing the linters to complain when implementing the db interface.
from the go project https://github.com/golang/go/wiki/CodeReviewComments#initialisms
> This rule also applies to "ID" when it is short for "identifier" (which is pretty much all cases when it's not the "id" as in "ego", "superego"), so write "appID" instead of "appId".
2020-03-20 15:37:09 +00:00
Dan Conley f502dfd041
Replace 'inbox' with 'outbox' when appropriate
As I was ~~wholesale copying~~ using the `database` interface definition
as a reference while creating my Federating implementation I noticed
some instances of a variable being named `inboxIRI` in an outbox method.
2020-02-28 21:55:38 -05:00
Cory Slep de06e3ccdc Lenient delivery to followers.
When delivering content, do not fail the whole process if a single
recipient is unavailable. Instead, skip delivering to that individual
actor.
2020-02-04 22:33:52 +01:00
Ben Lubar b27393e8dd
add toot-namespaced types and objects except toot:focalPoint for #122 2019-12-04 16:30:01 -06:00
Ben Lubar ffcaafbd9f
regenerate streams package with no changes to avoid messy diffs 2019-12-04 16:30:01 -06:00
Cory Slep e8a7301360 Make caller of handlers responsible for authorization 2019-11-11 00:43:52 +01:00
Cory Slep b977c30ce5 Add apcore as a client: toot own horn 2019-10-23 22:29:48 +02:00
Cory Slep 9acafe5f97 Sort @context when testing
Eliminates flakiness.
2019-10-23 19:06:19 +02:00
Cory Slep 84ee361478 Fix tests for the PublicKey and PropertyValue
There is still a context-order-dependent stability issue in the tests
impacting the PublicKey test, which must be fixed.

The fix for the PropertyValue test involved wrapping an
ActivityStreamsObject directly (instead of a Type). Note that only
serialization is supported in this case.
2019-10-22 23:42:56 +02:00
Cory Slep 3fc23c8c61 Remove debugging print statement 2019-10-21 22:34:16 +02:00
Cory Slep 0acf044b02 Merge remote-tracking branch 'refs/remotes/origin/master'
Pretty sure I just merged new code that would make Ben cry.
2019-10-21 22:22:30 +02:00
Cory Slep 82ca5994b4 Generate the new code update syntax.
This is a breaking change.

Sorry.

But it is for the long-term health.

Better now, than next year.

Expect a major version bump for next release.
2019-10-21 22:12:44 +02:00
Cory Slep 07df3adee5 "id" and "type" are JSONLD, not ActivityStream, properties
This was a fucking nightmare, as expected. All because the PublicKey
type is not really a derivation of the ActivityStreams "Object", which I
had been hackily relying on for things to inherit the JSONLD "id" and
"type" properties.

This breaks the "id" and "type" JSONLD properties into first-class
properties known within the tool, which for now is a patchy job of duct
tape to cover the leaks.

If someone wants "PublicKey" to have more default supported properties,
I will kindly ask them to fork a code generation that suits them. This
took way too much effort to treat PublicKey like a grab bag.

This isn't the fix for, but is on the road to fixing, the known bug
about aggressive deserialization of "PublicKey" into other types. In
fact, this is on the way to *correctly* fix it without a horrible patch
in the generated code (imagine being hacky in []jen.Code{...}, that's
too much poo to put into the meta code).

The things I do to support incomplete ontologies and major Federation
players that, for whatever reason on this god-forsaken planet, decided
to adopt the ontology and type everything except "PublicKey" (I don't
count "endpoints" because that is a pile of steaming poo for another
different reason in addition to this one, and is completely optional).
2019-10-21 22:06:37 +02:00
Cory J Slep 9cdc46afd4
Merge pull request #125 from BenLubar-PR/lint
Fix linter warnings; mark generated files as generated
2019-10-21 20:35:27 +02:00
Cory J Slep 00c504a6f2
Merge pull request #126 from BenLubar-PR/subtests
Simplify table-based tests using subtests
2019-10-21 20:34:35 +02:00
Ben Lubar 8d7b0196cd
add standard generated code header
doing this as a separate commit to avoid messy diffs for the important part of this change
2019-10-19 17:45:12 -05:00
Ben Lubar 05dd8e7024
regenerate streams with new astool 2019-10-19 17:43:37 -05:00
Ben Lubar 3ff5423014
fix linter warnings. 2019-10-19 17:43:21 -05:00
Ben Lubar d8ecddbe23
[streams] simplify table-based tests using subtests 2019-10-19 16:51:58 -05:00
Cory Slep 809fd1f041 Add unit tests for known bugs.
Aggressive PublicKey deserialization and Type masquerading bugs.
2019-10-19 13:30:16 +02:00
Cory Slep 00d19306e4 Fix JSON-LD conformance.
No longer create "default aliases" for JSON-LD vocabularies. This would
break compatibility since the way JSON-LD propagates aliases is pushed
downwards, and not propagated upwards. Since a lot of implementations
don't actually care about JSON-LD, do a lot of hacking to make sure that
others that expect hardcoded contexts and the like will still be able to
handle our output.

Still need to hook together the "well known alias" so it can generate
contexts that match the community, but that will be for things like the
custom mastodon and litepub extensions.

I don't want to ever have to work with JSON-LD in a static language ever
ever again. This is shit code.
2019-10-09 21:25:44 +02:00
Cory Slep 138e620834 Regenerate code with example fixes. 2019-10-09 19:59:41 +02:00
Cory J Slep c61e64cede
Merge pull request #120 from walfie/fix-type
Fix some `type`s in examples
2019-10-09 19:57:59 +02:00
Walfie e0b45cb7aa Fix some `type`s in examples
Also fixes spacing of some bracketed items
2019-10-01 19:28:03 -04:00
Cory Slep cd97d44b99 Fix exmaple spec 2019-09-26 20:38:25 +02:00
Cory Slep e084b904b1 Regenerate types using proper aliasing 2019-09-26 20:32:32 +02:00
Cory Slep b5a6786f9e Fix astool bug to properly alias properties and types 2019-09-26 20:31:48 +02:00
Cory J Slep bf196748f9
Merge pull request #118 from Zauberstuhl/security-v1
Separate w3id.org/security/v1 from activitystreams vocab

Note: contexts are improperly serializing properties, to be fixed.
2019-09-25 19:44:28 +02:00
Lukas Matt c8e9a9ffc9 Generate streams vocab with astool
./astool/astool -spec astool/activitystreams.jsonld \
  -spec astool/security-v1.jsonld ./streams
2019-09-16 22:49:41 +02:00
Lukas Matt 4db4275adf Revert "Add pseudo-type PublicKey for actor types"
This reverts commit 9b139af408.
2019-09-16 22:48:09 +02:00
Lukas Matt 85d191bffb Add https://w3id.org/security/v1 extension
* update go-mod httpsig
2019-09-16 22:47:07 +02:00
Cory Slep d87793a589 Add context.Context as an output to Authorize interfaces 2019-09-14 16:35:48 +02:00
Cory Slep 9b139af408 Add pseudo-type PublicKey for actor types 2019-09-13 22:49:56 +02:00
Cory Slep 79acb4ef1c gofmt 2019-09-13 12:31:07 +02:00
Cory Slep 224629ca54 No longer manually set Digest header 2019-09-13 12:31:00 +02:00
Cory Slep 92ecc4f524 Update to latest httpsig API 2019-09-13 12:18:49 +02:00
Cory Slep 351ebe64b7 Migrate serialize from pub to streams; export 2019-08-09 17:29:08 +02:00
Cory Slep f655c76e91 Permit hooks to return the modified context 2019-06-17 21:11:51 +02:00
Cory Slep c365730b4e go-fed improvements based on experience.
1. Obtaining callbacks are allowed to return errors
2. Hooks for setting context information based on request body is now
   possible.
2019-06-17 20:19:11 +02:00
Cory Slep f339304ce4 Fix race condition in signer 2019-05-11 11:33:55 +02:00
Cory Slep 28ffa4c8ad 201 and 202 are also OK statuses 2019-05-04 10:02:35 +02:00
Cory Slep 8dd3c056b3 Add ids to Accept/Rejects to Follows; clean out nested @context in serialization 2019-05-03 23:39:40 +02:00
Cory Slep 3bc578e7dd Fix numerous bugs. Identify more. 2019-05-03 21:44:18 +02:00
Cory Slep 78a8ee667f Fix @context value for activitystreams vocabulary.
The activitystreams.jsonld had its id set incorrectly.
2019-04-13 00:00:52 +02:00
Cory Slep 06954abe84 Fix ID construction from requests 2019-04-13 00:00:35 +02:00
Cory Slep d20d97a526 Add two different signers for GET vs POST requests 2019-04-04 21:38:08 +02:00
Cory Slep ca18901fcc Bugfix for programmatic delivery 2019-04-03 23:35:57 +02:00
Cory Slep a65388c27b Better abstract programmatic sending of activities. 2019-03-26 22:00:55 +01:00
Cory Slep d535bc95f3 Add FederatingActor and implementation. 2019-03-24 17:18:22 +01:00
Cory Slep b9a95751dc Add Insert methods for nonfunctional properties. 2019-03-24 14:25:36 +01:00
Cory Slep b5eecc692c Add SetType nonfunctional property method 2019-03-24 10:55:23 +01:00
Cory Slep 1ea8e2c7f3 Add more unit tests for new id generation.
Fix major bugs: was using IsExtendedBy checks instead of IsOrExtends
from the streams package.

For example, if checking for an OrderedCollection, was
checking its child types and failing if it was an OrderedCollection. Now
this is fixed, so OrderedCollection and child types are accepted.
2019-03-13 21:19:38 +01:00
Cory Slep 42b799393a Fix astool bug incorrectly referencing an unknown variable in generated code 2019-03-13 21:14:06 +01:00
Cory Slep 1ed044f7cf Add IsOrExtends to astool; regenerate ActivityStreams code.
The IsOrExtends will return true if an object is a type T or extends
from that type in the ActivityStreams vocabulary.
2019-03-13 21:09:51 +01:00
Cory Slep ee16417bb3 Add PostOutbox tests and bugfixes for side effect actor. 2019-03-04 00:06:37 +01:00
Cory Slep 305e8100f1 Complete inbox forwarding tests 2019-03-03 23:17:00 +01:00
Cory Slep b4b18b96fa Two more inbox forwarding tests now pass. 2019-03-02 19:13:02 +01:00
Cory Slep d58887e7b3 Inbox forwarding bug fixes.
Properly use nil guards on inbox forwarding properties. Correct the
recursion when examining these values so that it no longer inspects
ownership of the activitiy, and no longer double-checks an unowned IRI.
Finally, have deliver use the batch deliver call.
2019-03-02 19:11:26 +01:00
Cory Slep e1c2c8868a Fix go.mod 2019-03-01 22:28:13 +01:00
Cory Slep 48e15c99a2 Remove workaround for go v1.11 compiler bug 2019-02-28 23:19:53 +01:00
Cory Slep 823ab4cc5a Update module files BUT enabling modules breaks the build.
The new updates to golang modules ensure that this code generation
solution just got a lot more painful, as now each generated subpackage
needs its own go-mod file autogenerated, it seems.
2019-02-28 23:12:50 +01:00
Cory Slep 6e4cff677a Fix formatting. 2019-02-28 22:43:35 +01:00
Cory Slep 5d598046c2 Disallow destination to have '..' in path 2019-02-28 22:13:49 +01:00
Cory Slep 87e56d8d96 Require destination directory to be specified.
Permit generation to a subdirectory and the current working directory.
2019-02-28 22:11:24 +01:00
Cory Slep 1fac7a8c8d Fix README typo 2019-02-28 21:31:35 +01:00
Cory Slep 3dfe12a7a1 Update CHANGELOG 2019-02-24 16:51:35 +01:00
Cory Slep 19c351e729 Minor fixes in the README 2019-02-24 16:45:54 +01:00
Cory Slep 4a8f7314a7 Update READMEs 2019-02-24 16:27:02 +01:00
Cory Slep 9417306aa3 Outline the needed federating side effects tests. 2019-02-24 13:05:00 +01:00
Cory Slep 5f6b12a76d More unit tests and bug fixes. 2019-02-23 18:42:40 +01:00
Cory Slep 36a38b74b0 Increase side effect actor test coverage.
Introduce DefaultCallback to the protocol interfaces.
2019-02-20 21:31:11 +01:00
Cory Slep 1e5f5f9c86 Update authn and authz functions to have intuitive return values 2019-02-19 20:40:26 +01:00
Cory Slep b41491e2c2 Add missing Unlock calls. 2019-02-19 20:33:42 +01:00
Cory Slep 82851801ed Some tests for the side_effect_actor.go 2019-02-19 20:33:22 +01:00
Cory Slep 71b853d708 Update base actor tests to accurate reflect interfaces. 2019-02-19 20:07:26 +01:00
Cory Slep 52bc5fb9a5 Add tests for the base actor.
Removed old pub tests.
2019-02-16 20:30:18 +01:00
Cory Slep dc16296d74 Remove TODO that is now documented as an issue. 2019-02-15 23:56:49 +01:00
Cory Slep 674fa0245d Use AppendType instead of custom resolver 2019-02-15 23:54:26 +01:00
Cory Slep 3180eaf568 Add AppendType and PrependType 2019-02-15 23:54:09 +01:00
Cory Slep 4b87e634a0 Add SetType to properties 2019-02-15 23:29:38 +01:00
Cory Slep 8a333d6192 gofmt the new constants.go file 2019-02-15 23:11:33 +01:00
Cory Slep 412c6737c3 Generate constants for type and property names 2019-02-15 23:11:08 +01:00
Cory Slep 272d7239f3 Generate generic ToType function. 2019-02-15 22:47:59 +01:00
Cory Slep 2d8651d95c Remove obsolete deliverer package 2019-02-15 22:16:11 +01:00
Cory Slep 1227ec9a5a Change GetName to GetTypeName 2019-02-15 22:15:25 +01:00
Cory Slep c4a7b0524e Always acquire db lock before calling Owns 2019-02-15 22:04:08 +01:00
Cory Slep 2099e89851 Address several TODOs in side_effect_actor 2019-02-15 21:59:02 +01:00
Cory Slep b625914c1a Send BadRequest if library can't handle the type 2019-02-15 21:45:48 +01:00
Cory Slep 7beac61f97 Address IRI dereferencing in util 2019-02-15 21:42:56 +01:00
Cory Slep 6ead50c643 Address TODOs in federating_wrapped_callbacks 2019-02-15 21:33:48 +01:00
Cory Slep cc83b751a1 Update interface definitions and wrapper improvements.
Wrapping default behavior can now be overridden.
2019-02-15 21:03:43 +01:00
Cory Slep bd8220a56c Update comments 2019-02-15 18:34:58 +01:00
Cory Slep 8efdb62ad9 Port ActivityStreams http handler function.
Also removed the 'old' pub code.

Moved the old tests back into the new pub, which means no tests pass
because the test build fails. So test porting needs to happen.
2019-02-14 22:28:46 +01:00
Cory Slep d3b0afef5e Finish porting the core of pub. 2019-02-14 21:51:57 +01:00
Cory Slep 0f7dce6839 Begin porting C2S side effect behaviors. 2019-02-13 23:56:36 +01:00
Cory Slep 1b7cec220b Get most federating callbacks ported. 2019-02-13 22:37:29 +01:00
Cory Slep c194883a93 More pub progress.
- Constructors for various Actor types.
- Continue porting the default implementations.
- Rethinking life.
2019-02-12 22:43:01 +01:00
Cory Slep d85ac83249 Remove v0 codegen'd vocabulary 2019-02-12 00:22:21 +01:00
Cory Slep 16af404462 First pass converting to new pub library.
Still a lot to do:
- Delete old deliverer folder
- Revisit handler funcs
- Constructors
- Side effects for the wrapped callback functions
- Any other TODOs
2019-02-12 00:16:33 +01:00
Cory Slep 293cf3e752 Add more methods to Type interface 2019-02-10 15:02:32 +01:00
Cory Slep 546659ed10 Add method getting property values as a generic Type. 2019-02-10 14:09:41 +01:00
Cory Slep 6d618cb630 Regenerate code that is collision resistant to extensions. 2019-02-09 21:56:00 +01:00
Cory Slep f769e201b2 astool can handle colliding type and property names. 2019-02-09 21:51:16 +01:00
Cory Slep a5ea850465 Run fmt on astool 2019-02-09 13:48:35 +01:00
Cory Slep c973eec5e0 Deduplicate tests, increase coverage in the process. 2019-02-09 13:47:51 +01:00
Cory Slep b7b5c3d2c1 Update table test logging to be consistent. 2019-02-09 13:28:35 +01:00
Cory Slep c8f384583c Port vocabulary tests.
- Need to deduplicate the manual examples.
2019-02-09 13:27:04 +01:00
Cory Slep 17ac2d4df2 Regenerate with natural language map changes. 2019-02-09 13:26:47 +01:00
Cory Slep 8a7ec8c3a9 Properly handle natural language map.
- Deserialization now happens correctly into the rdf:langString
  property.
- Kluges to make sure the member is being set and referred to in the
  generation code.
- No longer generate special member for natural language map nor a
  special Is method. Use the rdf:langString generated proeprties.
- Keep the Set/Get special language members for handling individual
  languages.
2019-02-09 13:22:20 +01:00
Cory Slep aca6f4e857 Migrate manual data from vocab.
Next, need to use them in tests migrated from vocab.
2019-02-08 00:59:10 +01:00
Cory Slep 54dccc70c9 Add Summary to Link.
- Other minor spec tweaks to match examples and for type property
  convenience.
2019-02-08 00:58:34 +01:00
Cory Slep 436def09b4 Add orderedItems property and properly propagate it.
- Fix bug where non-inherited properties didn't apply to a type, only
  the type's children.
- Added orderedItems.
- Excluded items property from OrderedCollectionPage.
- Fixed OrderedCollectionPage extending from two types.
2019-02-06 22:18:08 +01:00
Cory Slep 4ca4b8235f Remove interface based Resolver types.
They do not work as intended, due to differing signature for the
LessThan generated methods.
2019-02-06 20:39:30 +01:00
Cory Slep 9fc75b87b8 Regen code with dependency injected type property constructor. 2019-02-06 20:28:32 +01:00
Cory Slep a69135b238 Inject the type property constructor at init time.
Undoes the static linking done two commits ago.
2019-02-06 20:27:19 +01:00
Cory Slep 32beb6881e Regenerate types from previous commit, passing all streams_old tests.
See previous commit message about improvements needed to reduce the
linking hell that is introduced in this code generation. Everything is
now linking against the 'type' property's concrete constructor.
2019-02-06 00:55:41 +01:00
Cory Slep bc058086d2 Type properties automatically set at creation.
All tests pass. However:

- Need to inject the type property constructor at init time, much like
  the manager. Statically linking sucks and really slowed it down with
  the direct linking method currently being used. I suspect this makes
  it no longer compile on a Raspberry Pi 3, so even more of a reason to
  do it at init time.
- Having the type property automatically set at construction time is an
  extremely nice feature lacking in v0.
- Unfortunately, right now this introduces a hack in the convert
  package.
- Modified the 'type' property definition to also be a string for major
  convenience; it has no semantic significance as xsd:string.
2019-02-06 00:51:35 +01:00
Cory Slep 9f79250293 Regenerate streams package with previous commit fixes. 2019-02-06 00:26:21 +01:00
Cory Slep f90d161ed1 Bugfixes as part of testing.
- Add missing ActivityPub actor and object properties
- Introspect type when deserializing
- Unknown on properties is now an interface
- Can now generate code when an extends hierarchy is specified multiple
  times as a range.
2019-02-06 00:23:47 +01:00
Cory Slep 7beafb477f All streams_old tests passing.
- Type resolution can handle multiple string types now.
- Stability in Disjoint/Extends/ExtendedBy literal generation, reducing
  code generation noise.
- Stability in init function package code generation, reducing the noise
  further.
2019-02-04 23:04:19 +01:00
Cory Slep ff885a86b3 Update generated code for tests.
All but 1 of the tests migrated from streams_old passes.
2019-02-04 22:29:04 +01:00
Cory Slep ff02890ddf Add streams_old migrated unit tests for the new generated code. 2019-02-04 22:27:22 +01:00
Cory Slep 9e1159dfd2 xsd:float is now float64 precision for a float32 number.
This is because Go's JSON package usees float64 under the hood, even
though xsd:float specifically specifies float32, I don't feel it is
worth getting that level of compatibility down, we will just be extra
precise.
2019-02-04 22:25:19 +01:00
Cory Slep e0e809134f Various Improvements.
- Fixed embarassing bug where a != should have been a ==
- Shortcut an len-1-array serialization to be a single value
2019-02-04 22:19:43 +01:00
Cory Slep 0f6ba298b3 Various improvements for tests.
- Sorting serialization and deserialization functions to reduce random
  noise when regenerating code.
- IRI methods can now short-cut to a URI equivalent value.
- Kind constructors.
- Extended types now can also be attached to properties (bleh). Blows up
  the API size, really quickly.
- Minor bugfixes.
2019-02-04 22:06:50 +01:00
Cory Slep 161ebc62e3 Add new ActivityStreams generated vocabulary. 2019-01-29 22:28:40 +01:00
Cory Slep 9240da5763 Don't output empty root files. 2019-01-29 22:28:20 +01:00
Cory Slep 8fa479da40 Move `streams` to `streams_old` in preparation for the new vocabulary. 2019-01-29 22:16:19 +01:00
Cory Slep ba00f3f875 Add JSONResolver.
Moved resolvers to root package due to JSONResolver's reliance on the
manager.
2019-01-29 22:00:33 +01:00
Cory Slep def4e9f167 Don't need to escape underscore in README 2019-01-28 21:52:37 +01:00
Cory Slep 63d04c78b7 Remove 'vocab' and 'streams' tools, add 'astool' 2019-01-28 21:51:12 +01:00
Cory Slep 9cf6568637 Rename spec.json and custom_spec.json.
This reflects the newly-added usage text in the experimental tool as
well.
2019-01-27 16:24:53 +01:00
Cory Slep db98736f28 Add Usage help text to the experimental tool. 2019-01-27 16:20:45 +01:00
Cory Slep ed1a715571 Remove 'prefix' and create autodetecting 'path' flag. 2019-01-27 15:25:49 +01:00
Cory Slep 1f7f42e4cc Remove 'individual' flag.
Clean up and comment the main file for the exp tool.

go fmt also rearranged the imports in a lot of files.
2019-01-27 12:16:56 +01:00
Cory Slep 5ac1ebd79e Remove old TODOs 2019-01-26 22:09:23 +01:00
Cory Slep 9c2c99588f Switch from cjslep fork to go-fed in exp tool 2019-01-26 21:36:04 +01:00
Cory Slep 5d21a35f8f Remove as directory 2019-01-26 21:30:03 +01:00
Cory J Slep e00b7d8d58
Merge pull request #80 from cjslep/dev
Experimental tool code-generation complete.
2019-01-26 19:49:59 +01:00
Cory Slep 248a464c02 Add property constructors. 2019-01-26 19:46:25 +01:00
Cory Slep da710a6bfd Add resolver generated comments 2019-01-26 19:00:06 +01:00
Cory Slep 929831f6e2 Code generate all kind of resolvers. 2019-01-26 12:48:06 +01:00
Cory Slep 355e2bd106 Initial resolver outline.
Need to:
- Hook into convert package
- Add comments everywhere to generated code
- Add version that supports navigating AS hierarchy (or applies
  interface-first instead of type-first).
2019-01-20 00:01:54 +01:00
Cory Slep 694c0e6898 Add TODOs for alias normalization when adding types and properties. 2019-01-19 17:59:11 +01:00
Cory Slep 8942794712 Vocab alias autodetected at generation time.
Default aliases no longer used at deserialization time.
2019-01-19 17:57:20 +01:00
Cory Slep ac87264b54 @context aliasing handled in generated serialization code. 2019-01-19 16:56:19 +01:00
Cory Slep a2a8775265 First pass at @context management.
Needs several improvements:
- private alias member for each type/property
- New constructor function sets this to be code-generated default
- Modify deserialize to also accept an @context alias map to override
  default if needed.
- Add utility function in root package to turn @context into an alias
  map, which will be needed by resolvers.
2019-01-19 15:17:39 +01:00
Cory Slep b349761fbc Multiple specs now build successfully after being generated. 2019-01-18 23:35:23 +01:00
Cory Slep bb777571c8 Put interfaces from different vocabularies together. 2019-01-18 21:49:55 +01:00
Cory Slep c3b9686d1f Fix init generation for multiple vocabularies. 2019-01-18 21:22:07 +01:00
Cory Slep a40bd3129f Also generate refrenced vocabularies. 2019-01-18 20:45:52 +01:00
Cory Slep ceb542ffd3 Multiple specs now generates, but generated code is borked.
The interfaces need to all be generated together, or else vocabularies
could have cyclic dependencies.

Package documentation will need to reflect each of the vocabularies'
documentation.

References' types and properties need to be generated.
2019-01-17 22:40:52 +01:00
Cory Slep c12e854487 Slightly saner internal function 2019-01-16 21:53:36 +01:00
Cory Slep 920144aba6 Multiple specs supported by RDF parser.
However, the converter still cannot handle converting it to generators
and then files.
2019-01-16 21:43:36 +01:00
Cory Slep 4582f88848 Add custom_spec.json. Populate Vocab name from spec document. 2019-01-14 22:06:03 +01:00
Cory Slep e2eed869af Code generate the comments in the type and property GoDoc. 2019-01-13 23:04:24 +01:00
Cory Slep d3a4507a2a Newline after main program says done. 2019-01-13 22:34:29 +01:00
Cory Slep ad06a07dfa Improve comments and add iterator methods.
Begin, End, Empty, Next, Prev were added. Clarified about concurrent
access not being a thing. Also added "should not use" or "do not use"
comments to the methods that needed it.
2019-01-13 22:17:43 +01:00
Cory Slep 648d7fb7b3 Add Set functions to non-functional property. 2019-01-13 18:17:54 +01:00
Cory Slep d14128bc0b Add package-level documentation, code generated. 2019-01-13 18:01:50 +01:00
Cory Slep 2527b47493 Generate root-level package documentation. 2019-01-12 22:34:27 +01:00
Cory Slep 730135b307 Lower case filenames for consistency. 2019-01-12 21:41:09 +01:00
Cory Slep 7b4fadd871 Use upper casing in function name for vocabularies. 2019-01-12 21:28:58 +01:00
Cory Slep e6160858f0 Remove ValueRoot and comment all direct code.
Still need to review the comments on all code-generated code.
2019-01-12 20:53:00 +01:00
Cory Slep 1f12842eeb Rename 'props' package to 'gen'. 2019-01-12 15:28:21 +01:00
Cory Slep ffd18e29d0 Individual package names prefixed with type or property 2019-01-12 15:21:40 +01:00
Cory Slep 3dbad22900 Add flag for generating with individual package policy. 2019-01-12 14:37:37 +01:00
Cory Slep 6d12549b18 Clean up duplicated code in package generators. 2019-01-12 14:34:19 +01:00
Cory Slep 884cbd8693 Expose extends, disjoint, and extendedBy in root package. 2019-01-12 14:02:13 +01:00
Cory Slep caf1e8943f Hook up managers at init time. 2019-01-12 12:52:48 +01:00
Cory Slep a93c6aa678 Add constructors in pkg and for types. 2019-01-12 12:30:08 +01:00
Cory Slep c8cec42c57 Make property members private. 2019-01-08 20:47:55 +01:00
Cory Slep a4f90ff8e0 Address two TODOs 2019-01-08 20:37:04 +01:00
Cory Slep f2c70191ab Fix bad formatting in comments. 2019-01-07 22:50:07 +01:00
Cory Slep 1a6a1213a5 Make comments look slick. 2019-01-07 22:39:30 +01:00
Cory Slep 9df52c8c45 Prepare comments to be auto-truncated at write time.
This changes comments to only be strings passed around between the
codegen package and its clients. This lets codegen in the future limit
how long a comment line is when generating the code.
2019-01-07 22:06:32 +01:00
Cory Slep 9369a8ad79 Ensure all interfaces have comments from the spec. 2019-01-07 21:31:36 +01:00
Cory Slep b7ec140c66 Non-functional property interfaces now have breathing room. 2019-01-07 21:26:04 +01:00
Cory Slep e8fb31437e Print interface, struct, and typedef methods and functions in alphabetical order 2019-01-07 21:22:01 +01:00
Cory Slep 1b0ceb8344 Add IRI methods to properties. 2019-01-07 20:33:03 +01:00
Cory Slep 7ac133d101 Add IRI support and fix all build errors.
Generated code now will also compile, for the first time in forever!
2019-01-06 22:15:07 +01:00
Cory Slep ea8af5c968 Prep for IRIs, no more circular deps, added getters/setters.
A lot of stuff just happened for the better.
2019-01-06 19:44:24 +01:00
Cory Slep 7eb1755c96 Add support for link relation values. 2019-01-06 12:41:47 +01:00
Cory Slep 1bdb66aa98 Add support for MIME media type. 2019-01-06 12:36:11 +01:00
Cory Slep 26988b6cd2 Add BCP47 to known values 2019-01-06 12:27:18 +01:00
Cory Slep 4bc51a9f97 Fix deserialize signatures for types and values 2019-01-06 12:26:58 +01:00
Cory Slep be110cf688 Add per-package files for property-based packages. 2019-01-06 00:13:24 +01:00
Cory Slep 7e96603df9 Use At method instead of indexing into other non-functional property 2019-01-05 23:54:17 +01:00
Cory Slep 2aefaeb873 Fix old "handled" deserialization and iterator interface qualifiers. 2019-01-05 23:49:18 +01:00
Cory Slep 73d677460b Make manager interface for type packages have unique methods 2019-01-05 23:19:50 +01:00
Cory Slep 99343f540a Stop method and member identifier name clash on types 2019-01-05 23:14:49 +01:00
Cory Slep 8b8cc2af27 Fix value qualified statements in interfaces.
Also fix the qualified interface name for non-functional properties in
the LessThan method.
2019-01-05 23:12:42 +01:00
Cory Slep 5c5fcd22e8 Fix functional property qualified interface bug. 2019-01-05 22:49:31 +01:00
Cory Slep 58e3d21e19 Add per-package files for type-based packages.
This takes care of abstracting away the manager for the generated types,
and also provides the hooks for the manager to inject itself at init
time.
2019-01-05 22:46:58 +01:00
Cory Slep ec7091be51 Add serialization and comparison for rdf:langString 2019-01-05 21:54:00 +01:00
Cory Slep 6b3d676552 Add serialization and comparison for xsd:duration 2019-01-05 18:13:15 +01:00
Cory Slep 6ece169417 Add serialization and comparison for xsd:nonNegativeInteger 2019-01-05 17:17:12 +01:00
Cory Slep 7a99c1587e Add serialization and comparison for xsd:boolean 2019-01-05 17:11:24 +01:00
Cory Slep 3a49fff6bb Add serialization and comparison for xsd:string 2019-01-05 17:02:03 +01:00
Cory Slep 79e3cee633 Add serialization and comparison for xsd:float 2019-01-05 16:58:56 +01:00
Cory Slep 8b8232f1ed Add serialization and comparison for xsd:anyURI 2019-01-05 16:52:47 +01:00
Cory Slep 338fe8d347 Add serialization and comparison for xsd:dateTime. 2019-01-05 16:43:49 +01:00
Cory Slep 4f47e7fdfa Clean up Manager generation.
- Organize manager function generation into one helper method
- Vocabulary name is passed into the type & property generators
- Use interface only in the manager
- Remove unused flags in the main program
2019-01-05 16:22:37 +01:00
Cory Slep c36c529c5f Add unknown property support 2019-01-05 13:05:11 +01:00
Cory Slep 87064de883 Simplify type getting parent types uniquely 2019-01-05 12:19:56 +01:00
Cory Slep 54f8549b10 Add generation of referenced values.
Fix the package qualified naming for value types and also correctly
reference the net/url package in the owl ontology.
2019-01-05 00:00:51 +01:00
Cory Slep df9ff825c2 Remove redundant interface code 2019-01-04 21:59:52 +01:00
Cory Slep 85ff299cf3 Cleanup kind's lessThan code generation 2019-01-04 21:56:29 +01:00
Cory Slep d087200e02 Implement type's Serialize function 2019-01-03 22:56:19 +01:00
Cory Slep 2045f2602a Fix name methods for non-functional properties 2019-01-03 22:35:24 +01:00
Cory Slep 48df99f07f Fix double-calling value Kind functions 2019-01-03 22:27:14 +01:00
Cory Slep 0005e23011 Hook up LessThan for properties. 2019-01-03 21:36:10 +01:00
Cory Slep 24265690f8 Add LessThan implementation for types 2019-01-03 20:49:48 +01:00
Cory Slep 133c2fd477 Fix extendedBy and disjointWith methods/funcs 2019-01-03 20:24:37 +01:00
Cory Slep ee9aade57f Add support for prefixing generated code paths. 2019-01-03 00:29:24 +01:00
Cory Slep 625e93d412 Pass package manager to properties. 2019-01-03 00:16:54 +01:00
Cory Slep aeda61d2f1 Clean up qualified names between implementations.
Implementations are relying more on each others' interfaces, which
allows for better code isolation and a better chance at pruning down
binaries when needed. Still plenty of TODO items left to tackle.
2018-12-31 18:42:39 +01:00
Cory Slep 5db3a68a8d Fix concrete types to be interfaces in properties.
Fix bug in method calling code generation.
2018-12-31 16:49:25 +01:00
Cory Slep b79b381a62 Only put exported methods in interfaces 2018-12-31 00:45:25 +01:00
Cory Slep 3da311641b Fix file writing locations.
Also write interfaces into public sections.

Lots of TODOs and more work to add.

Will need to look into how to simplify this logic -- there is a lot of
redundancy and kludgy-feeling things. Will definitely need to address
the converter part as it is very redundant.
2018-12-30 16:54:16 +01:00
Cory Slep ce699464bf Overhaul package management, add manager.
The manager class will be responsible for allowing the generated code to
be compilable while also permitting types and properties to be isolated,
such that binaries can be pruned to smaller sizes and not require the
entire gambit be built into the resulting executable.

This state will successfully generate code, but the generated code is
completely uncompilable. It will also trash the props/ directory.
2018-12-30 16:09:14 +01:00
Cory J Slep 3ca0b5182d
Merge pull request #79 from cjslep/dev
Update the experimental tool and added Code of Conduct from dev fork.
2018-12-24 07:56:07 +01:00
Cory Slep 0a7539cdf7 Add CODE_OF_CONDUCT.md.
It stinks that I'm the only team member, so complaints about me go
directly to me. Which doesn't seem fair for victims.
2018-12-24 07:51:51 +01:00
Cory Slep b0937b7dec Add TODOs for more type improvements, add IsExtending convenience. 2018-12-20 23:19:51 +01:00
Cory Slep 43ab7d319c Alphabetical order of type members and better extends API. 2018-12-20 23:03:13 +01:00
Cory Slep b5d927c49f Properties now have serialization references to Types.
This setup allows properties to recur deserializing into types as
necessary, and sets the groundwork for successfully handling all kinds
of JSON-LD input.
2018-12-19 09:44:57 +01:00
Cory Slep a3c3a7b5fc Prepare TypeGenerator being a Kind for Properties.
Right now the two-pass tooling system has issues with establishing
doubly-linked data between Functiona/NonFunctional Properties (which
have Kinds abstractions) and Types (which have Property abstraction).

While the experimental tool compiles, it panics at runtime currently
because the TypeGenerator needs to look at properties with Range of
itself, but it is applying itself to properties with Domain of its type.
Which is wrong.

Will need to stew on this and think of how to avoid making even more
shortcuts and hacky solutions in the name of progress.
2018-12-17 23:11:55 +01:00
Cory Slep 222074b503 Put properties on the vocabulary types. 2018-12-17 21:43:33 +01:00
Cory Slep 73f7e3cf36 Experimental codegen works end to end.
Still plenty of missing features, and missing implementations in the
generated code. Also missing some functionality and flags for generating
references and/or well-known references (ex: XML, RDF values).
2018-12-09 21:23:32 +01:00
Cory Slep 9a0f688c63 Fix build, hook up type serialization.
- Prepared Types to be Kinds.
- Need to handle DoesNotApplyTo (to remove Intransitive properties from
  parent)
- Need to handle crafting files in the appropriate structure.
2018-12-09 12:45:08 +01:00
Cory Slep fe8b4e7261 Add type conversion (still broken at this commit) 2018-12-09 11:40:59 +01:00
Cory Slep 069b8de820 Add initial convert (exp is broken at this commit)
Still need to flesh out the types for conversion. Also still need to add
the serialize and deserialize calls for individual types. Finally, will
need to put the finishing touches on writing the output files in the
desired directories. Then the experimental tool will be ready for end to
end testing.
2018-12-08 23:05:02 +01:00
Cory Slep dc2ce18fd7 Finish adding values and nat lang maps. 2018-12-08 18:57:40 +01:00
Cory Slep beb44b1bde Fix more items in the specification 2018-12-08 18:57:20 +01:00
Cory Slep 139dc3c5ea Hack for "without property", begin resolving references.
Also begin populating values in the intermediate definition.

TODO: Replace the hack in the spec definition with something applicable
RDF-wise (is there anything that permits this RDF wise?).
2018-12-08 17:50:26 +01:00
Cory Slep 8589b0f14f Cleaned up the spec definition file 2018-12-08 17:50:02 +01:00
Cory J Slep 9c3005b17f
Merge pull request #78 from cjslep/dev
Update next generation experimental tool
2018-12-05 23:55:14 +01:00
Cory Slep d2182dc3b9 Tool can now print its intermediate state 2018-12-05 23:50:20 +01:00
Cory Slep e5693c2eb9 Finish implementing ontologies.
- ActivityStreams specification can now be entirely parsed.
- Removed print statements
- Added missing ontology items to rdfs
2018-12-05 23:36:27 +01:00
Cory Slep dae9d884ab Remove duplicate entries 2018-12-05 23:35:53 +01:00
Cory Slep 41be1062fe Improve spec.json
- Remove reference to ActivityStreams (as its defining ActivityStreams!)
- Change type Link to type owl:Class
- Change https to http for parsing
2018-12-05 22:54:35 +01:00
Cory Slep 0f98307f07 Bug fixes and ontology improvements.
- Fixed the way indexing nodes were being applied
- Implemented property types in ontologies
- Improved class types in ontologies
- Lots of other stuff
2018-12-05 22:53:26 +01:00
Cory Slep 864616542c Prepare for @type to contextually resolve.
- Remove unused 'as' ontology
- Outline new GetByName for ontologies
- Fix bugs
- Prepare the type to use the RDFRegistry
2018-12-02 23:48:54 +01:00
Cory Slep 0530121039 Progress on implementing the schema ontology.
- can process name
- can process url
- properties can have examples
- id no longer requires a thing to be set on (may need to be revisited)
2018-12-02 22:37:43 +01:00
Cory Slep e508425ddb Remove leftover print call 2018-12-02 22:36:43 +01:00
Cory Slep 676dc64dbb Allow a node to hijack the rest of the recursive descent.
Also, fixed a bug where errors were not getting propagated properly from
RDFNodes back to the apply process.
2018-12-02 22:35:46 +01:00
Cory Slep e13d837405 Add alias to name in spec 2018-12-02 22:35:23 +01:00
Cory Slep 503e95e52e Add alias for url 2018-12-02 20:07:01 +01:00
Cory Slep 30f9f6a16b Fix aliasing specific elements.
Also adds some placeholders for the schema ontology.

Eliminated some dead code in the RDF manager thing.

Next step is to get mainEntity parsing to ignore the rest of the values,
which should be easier to do, maybe.
2018-12-02 20:04:56 +01:00
Cory Slep caa2e4d2fa Expand the as alias to the ActivityStreams URI 2018-12-02 13:56:36 +01:00
Cory Slep 90b12d43aa Fix bugs for handling aliased nodes and containers
Implemented the beginning of the container and index handling, but have
not yet completed it.
2018-12-02 13:55:29 +01:00
Cory Slep b657bd2307 Add some OWL ontology implementations 2018-12-01 18:31:35 +01:00
Cory Slep c61bd2d129 Add rdf ontology nodes, make processing mandatory. 2018-12-01 16:36:33 +01:00
Cory Slep 94569ca549 Setup applying RDF node understanding.
Next, the actual nodes need to be created in order to construct the
proper intermediate form and translate the parsed data into a meaningful
structure that can be used to generate code.

Ideally, this could also potentially allow code generation in other
languages too. And different ways to read in ActivityStreams
specifications and extensions. But that would be way off in the future.
2018-11-29 22:53:48 +01:00
Cory Slep 68f6602ebb Fix fork imports and rearrange parsing functions 2018-11-29 01:22:56 +01:00
Cory Slep 0470f8e603 Add internal parsed RDF definition data types.
This is an evolution of the old tools/defs/types.go file which
essentially had these data structs manually defined in a static go file.
Now, the parser should be able to construct this data structure on the
fly from input files and reference files.
2018-11-28 23:50:07 +01:00
Cory Slep 8b4f9fc81c Prepare tool for ingesting JSON LD context defs. 2018-11-28 21:40:11 +01:00
Cory Slep 2709d26229 Ran gofmt in props dir 2018-11-28 21:39:11 +01:00
Cory Slep 9cae676dd9 Add activitystreams specification definition.
Credit for the file goes to:
https://github.com/gobengo/activitystreams2-spec-scraped/blob/master/data/activitystreams-vocabulary/1528589057.json
2018-11-28 21:38:40 +01:00
Cory J Slep c2325ed7ea
Merge pull request #77 from BenLubar-PR/gosum
Update go.modverify to go.sum.
2018-11-20 08:14:50 +01:00
Ben Lubar d45efd665a
Add go.mod hashes to go.sum via "go get". 2018-11-19 13:56:06 -06:00
Ben Lubar 468b81f61f
Rename go.modverify to go.sum. 2018-11-19 13:55:34 -06:00
Cory Slep ff0fcb60f3 Update README and CHANGELOG in preparation for v0.4.0 release 2018-11-17 17:17:38 +01:00
Cory Slep 31482f867e Add constructors for the streams package. 2018-11-17 16:59:49 +01:00
Cory Slep 2d4695e969 Update Write.as to WriteFreely in README 2018-11-17 16:43:32 +01:00
Cory Slep 8795587007 Experimental: Add RDF package for JSONLD context definitions.
This package will be the frontend for reading the JSONLD context
descriptions that specify ActivityStreams vocabularies. This will allow
ingesting publicly hosted or manually-created vocabularies and
generating an internal representation for later code generation.
2018-11-17 16:33:42 +01:00
Cory Slep 137c1d51fb Remove unnecessary whitespace 2018-11-17 16:33:21 +01:00
Cory Slep 7f8b7a6723 experimental: Add disjoint type function
Also fix a bug where the Name() method was being called on a string.
2018-11-04 19:20:42 +01:00
Cory Slep 2ebd905f76 experimental: More type improvements.
- Fixed extendedBy generated method behaving like extends
- Add the extends generated method
- Extends / extendedBy examine the parent / children as well
- Properties and types cache their generated structs, only creating the
  codegen types once
- Create convenience constructor methods
2018-11-04 19:05:56 +01:00
Cory Slep d08cc46275 experimental: Add ActivityPub types.
This begins adding types as standalone compositions of properties, along
with helper functions to manage the hierarchy better than the current v0
implementation.

I think it will still need to be focused on flexibility at compile time
over runtime; but this will still allow extensions to be generated
easily from existing code.

This is a natural extension of the v0 philosophy: many folks still
cannot understand the similarity that to deploy new meaningful behaviors
with interpreted javascript/python/etc then code still needs to be
written and deployed, just as this go code will need to be regenerated,
written against, and deployed.

Code generation plus type system means a lot of the heavy lifting and
potential errors are already thought through for an ActivityPub
developer.
2018-11-03 16:56:09 +01:00
Cory Slep e834879207 Experimental: restructure directories 2018-10-19 22:44:13 +02:00
Cory Slep c4425ee50e experimental: Keep single-type special-case API
After deliberating on the APIs for properties that can have a single
type vs multiple types, I've decided to keep the distinctly separate
APIs for the single-type properties. This means humans reading the APIs
will use simpler and more reasonable getters/setters, etc. However, by
default the two kinds of properties will not be able to satisfy the same
interface.

If this is needed, in the future we can auto-generate thin-wrapper types
around single-type properties that cause them to have a shared API with
the multi-type properties. But that won't be tackled for now, as its
expected use case is small.
2018-10-16 22:08:10 +02:00
Cory Slep 4f628f1b7e Support natual language maps for properties. 2018-10-16 22:00:18 +02:00
Cory Slep c91db1e4ae Support unknown property types when serializing and deserializing 2018-10-11 12:15:55 +02:00
Cory Slep 3a9b9812b5 Fix spacing in generated comment 2018-10-11 11:38:46 +02:00
Cory Slep 4a35079fd0 Add comments, clean up code 2018-10-10 00:32:37 +02:00
Cory Slep eb09223588 Make code generation more robust 2018-10-08 22:19:10 +02:00
Cory Slep c8431878da Now that core fundamentals are working, begin refactoring 2018-09-25 23:21:07 +02:00
Cory Slep 1426f30be1 Add comments to nonfunctional properties. 2018-09-20 19:24:20 +02:00
Cory Slep d0c1e6a3e3 Add comments to functional properties.
Also, clean up the spacing for generated code so it is pleasing to look at.
2018-09-18 23:00:08 +02:00
Cory Slep 87032d3af9 Add serialization/deserialization generated code. 2018-09-16 00:02:11 +02:00
Cory Slep c3ec4d5344 More exprimentation with property-oriented code generation 2018-09-12 22:45:59 +02:00
Cory Slep 1a1c50acfb Experimental directory for v1 code generation. 2018-09-04 23:44:16 +02:00
Cory Slep 3076a5410a Add nascent idea to static + dynamic plugin system
This is on the road for v1: enabling plugins for vocabulary extensions
and either supporting them in static or dynamic scenarios.

This new as/ folder is temporary to play around with, it is expected to
fold back into the pub area once I am making more sweeping changes for v1.
2018-09-01 20:03:51 +02:00
Cory Slep 806c037e3b Add note to self to check Follow edge case 2018-09-01 20:03:40 +02:00
Cory Slep 3b3c8efdd1 Add Read.as to the list of client applications 2018-08-28 20:24:53 +02:00
Cory Slep a7d2d52351 Add multiply-typed unknown property methods to vocab interfaces. 2018-08-21 22:26:13 +02:00
Cory Slep 9e2efad733 Update README and CHANGELOG for v0.3.0 2018-08-21 20:56:34 +02:00
Cory Slep 012f23e435 Update CHANGELOG 2018-08-21 20:53:18 +02:00
Cory Slep 83277d57c9 Announce activities now add to shares collections on objects. 2018-08-21 20:49:24 +02:00
Cory Slep 2071460bc1 Update CHANGELOG 2018-08-21 19:07:14 +02:00
Cory Slep bd6369d284 Add the missing 'shares' property to streams.Object 2018-08-21 19:01:16 +02:00
Cory Slep befab36c29 Add the missing 'shares' property to vocab.Object 2018-08-21 18:59:18 +02:00
Cory Slep c430c8af1d Update CHANGELOG 2018-08-20 23:19:43 +02:00
Cory Slep 1252727ca8 Add tests for Callbacker extensions 2018-08-20 23:11:33 +02:00
Cory Slep 1c057922ca Fix intransitive activity generated code in vocab.
The sub-types of Intransitive Activity (Arrive, etc) would not satisfy
the vocab.IntransitiveActivityType interface, due to not accounting for
the parent WithoutProperties definition.

This fixes that code generation, so that all Activity subtypes will be
able to be properly converted to vocab.ActivityType or
vocab.IntransitiveActivityType.

Updated the PostOutbox code path to properly handle this distinction
when receiving a C2S IntransitiveActivity.
2018-08-20 23:08:02 +02:00
Cory Slep 797b2f79a8 Support other activity types 2018-08-20 00:35:31 +02:00
Cory Slep d0812c9471 Support unknown (extended) properties in vocab 2018-08-19 23:37:02 +02:00
Cory Slep 387ed4a775 Fix body delivery bytes being incorrectly copied 2018-08-19 23:25:14 +02:00
Cory Slep 17fce2255a Update README and CHANGELOG for v0.2.1 release 2018-08-19 19:00:48 +02:00
Cory Slep 2f2858f92b Include more links to go-fed.org and official reports 2018-08-19 18:39:54 +02:00
Cory Slep 46d55732d5 Use minimum sized types for enumerated values 2018-08-19 18:31:45 +02:00
Cory Slep 493b48b665 Update README with official implementation report links. 2018-08-19 18:26:02 +02:00
Cory Slep 3b0d31022c Update README with applications using the library 2018-08-19 18:20:58 +02:00
Cory Slep 8b0590a0cd Correctly test for root cause in issue 75. 2018-08-19 18:03:56 +02:00
Cory Slep 3740d4a418 Remove TODO 2018-08-19 11:06:52 +02:00
Cory Slep 452d6f1af7 Add unit test for issue 75: C2S not forwarding 2018-08-19 01:05:02 +02:00
Cory Slep a5757b4382 Update README and CHANGELOG for v0.2.0 release 2018-08-04 15:36:53 +02:00
Cory Slep 377455aa70 Add IsPublic to streams types 2018-08-04 15:30:31 +02:00
Cory Slep e372d2781e Update CHANGELOG 2018-08-04 15:28:00 +02:00
Cory Slep c655c635ff Add IsPublic to vocab.Object types & subtypes.
The IsPublic method will return 'true' if the special public collection
defined in the ActivityPub spec is addressed in the 'to', 'bto' 'cc', or
'bcc' properties of the Object or any of the types extending from them.
2018-08-04 15:20:34 +02:00
Cory Slep f8b24eef88 Use OrderedCollection by default for collections.
The 'liked', 'likes', 'followers', and 'following' collections now
default to the 'OrderedCollection' type instead of throwing an error if
the client application does not supply an IRI, 'Collection', or
'OrderedCollection' type on the actor or object.
2018-08-04 13:53:33 +02:00
Cory Slep dfffcd7eb2 Update CHANGELOG 2018-08-04 13:36:43 +02:00
Cory Slep be4644fbca Examine IRI when origin checking Update/Delete
Before we just always threw an error. Also update tests to check for
both cases.
2018-08-03 22:54:12 +02:00
Cory Slep f82653f709 Don't do an Activity's side effects more than once
If a federated Activity has been seen before, skip handling its side
effects.
2018-08-03 22:30:53 +02:00
Cory Slep 8fa0f2395c Add Like activities to the likes collection.
Previously, it was incorrectly adding the actors to the likes collection.
2018-08-02 22:29:03 +02:00
Cory Slep 107468d8e9 Fix GetInbox failure due to unavailable messages.
When deduplicating IRIs in a user's inbox, the library tries to
dereference the IRI to... obtain the ID. I must have added this when I
had no idea what I was doing. Oh my god, everyone's going to know I am
dumb! Quick, don't read this commit message, I'll just finish typing
this sentence over here.

Well, it's fixed now.
2018-08-01 23:30:20 +02:00
Cory Slep 10e099bd8a Remove unused test method from previous interface. 2018-08-01 23:22:46 +02:00
Cory Slep b0c125a7ba Improve media type detection for ActivityPub types 2018-08-01 23:18:41 +02:00
Cory Slep 1ed478582e To immediately be deleted.
I don't need this level of detail for headers. This is beyond overkill
for content negotiation. Just do something simpler.
2018-08-01 19:27:06 +02:00
Cory Slep cfdfddfda4 Update changelog and contributing 2018-07-28 13:26:55 +02:00
Cory J Slep 50f3ddbfc5
Merge pull request #52 from 21stio/patch-1
Update README.md
2018-07-01 13:15:50 +02:00
Cory J Slep 4870298a37
Merge pull request #53 from 21stio/patch-2
Update README.md
2018-07-01 13:15:34 +02:00
Cory J Slep c6b55cc0f2
Merge pull request #54 from 21stio/patch-3
Update README.md
2018-07-01 13:15:05 +02:00
21stio 6bea627342
Update README.md
added syntax highlighting
2018-06-25 09:45:09 +02:00
21stio 3c9368c902
Update README.md
added syntax highlighting
2018-06-25 09:44:24 +02:00
21stio 7b788cf596
Update README.md
added syntax highlighting
2018-06-25 09:43:15 +02:00
Cory Slep af5ed41bad Remove duplicate intermediate types.
This condenses the N types and M properties from N*M intermediate type
definitions to just M intermediate type definitions. These intermediate
types are no longer generated in the gen_<TYPE>.go files, but are all
within the separate gen_intermediate.go file.

This will hopefully reduce resource consumption during compilation of
the vocab package.
2018-06-17 18:51:30 +02:00
931 changed files with 513371 additions and 747175 deletions

29
.github/workflows/build.yml vendored Normal file
View File

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

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

72
CODE_OF_CONDUCT.md Normal file
View File

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

View File

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

View File

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

133
astool/README.md Normal file
View File

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

View File

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

247
astool/codegen/method.go Normal file
View File

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

105
astool/codegen/struct.go Normal file
View File

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

95
astool/codegen/typedef.go Normal file
View File

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

68
astool/codegen/utils.go Normal file
View File

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

1565
astool/convert/convert.go Normal file

File diff suppressed because it is too large Load Diff

84
astool/convert/sort.go Normal file
View File

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

View File

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

881
astool/forgefed.jsonld Normal file
View File

@ -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 repos 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 collections 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 doesnt 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 cant 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 cant 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 arent 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 commits 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 didnt 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 youd 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 youd 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"
}
]
}

38
astool/gen/constants.go Normal file
View File

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

197
astool/gen/docs.go Normal file
View File

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

1240
astool/gen/funcprop.go Normal file

File diff suppressed because it is too large Load Diff

46
astool/gen/jsonld.go Normal file
View File

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

233
astool/gen/manager.go Normal file
View File

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

1077
astool/gen/nonfuncprop.go Normal file

File diff suppressed because it is too large Load Diff

497
astool/gen/pkg.go Normal file
View File

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

476
astool/gen/property.go Normal file
View File

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

959
astool/gen/resolver.go Normal file
View File

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

1203
astool/gen/type.go Normal file

File diff suppressed because it is too large Load Diff

423
astool/main.go Normal file
View File

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

428
astool/rdf/data.go Normal file
View File

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

264
astool/rdf/jsonld.go Normal file
View File

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

36
astool/rdf/nodes.go Normal file
View File

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

338
astool/rdf/ontology.go Normal file
View File

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

539
astool/rdf/owl/ontology.go Normal file
View File

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

619
astool/rdf/parse.go Normal file
View File

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

325
astool/rdf/rdf.go Normal file
View File

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

367
astool/rdf/rdfs/ontology.go Normal file
View File

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

212
astool/rdf/referencing.go Normal file
View File

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

376
astool/rdf/rfc/ontology.go Normal file
View File

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

View File

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

1250
astool/rdf/xsd/ontology.go Normal file

File diff suppressed because it is too large Load Diff

143
astool/security-v1.jsonld Normal file
View File

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

270
astool/toot.jsonld Normal file
View File

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

View File

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

View File

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

View File

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

4
gen.go Normal file
View File

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

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

View File

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

12
go.sum Normal file
View File

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

View File

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

49
pub/activity.go Normal file
View File

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

127
pub/actor.go Normal file
View File

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

494
pub/base_actor.go Normal file
View File

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

756
pub/base_actor_test.go Normal file
View File

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

11
pub/clock.go Normal file
View File

@ -0,0 +1,11 @@
package pub
import (
"time"
)
// Clock determines the time.
type Clock interface {
// Now returns the current time.
Now() time.Time
}

89
pub/common_behavior.go Normal file
View File

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

146
pub/database.go Normal file
View File

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

248
pub/delegate_actor.go Normal file
View File

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

9
pub/doc.go Normal file
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

124
pub/federating_protocol.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

47
pub/mock_clock_test.go Normal file
View File

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

View File

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

345
pub/mock_database_test.go Normal file
View File

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

View File

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

View File

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

166
pub/mock_httpsig_test.go Normal file
View File

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

View File

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

117
pub/mock_transport_test.go Normal file
View File

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

117
pub/property_interfaces.go Normal file
View File

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

View File

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

707
pub/pub_test.go Normal file
View File

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

View File

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

857
pub/side_effect_actor.go Normal file
View File

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

82
pub/social_protocol.go Normal file
View File

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

View File

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

207
pub/transport.go Normal file
View File

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

159
pub/transport_test.go Normal file
View File

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

1005
pub/util.go Normal file

File diff suppressed because it is too large Load Diff

76
pub/util_test.go Normal file
View File

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

15
pub/version.go Normal file
View File

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

View File

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

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

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

Some files were not shown because too many files have changed in this diff Show More