Much more work on duck-typing.

This commit is contained in:
Simon Brooke 2022-12-19 21:38:12 +00:00
parent 032dcb7536
commit b2ff133e4a
3 changed files with 275 additions and 20 deletions

View file

@ -12,7 +12,11 @@ Good.
Let us proceed. Let us proceed.
**The Old Dog and Duck** is intended to be a set of libraries to enable people to build stuff which interacts with ActivityPub. It isn't intended to be a replacement for, or clone of, Mastodon. I do think I might implement my own ActivityPub server on top of The Old Dog and Duck, that specifically might allow for user-pluggable feed-sorting algorithms and with my own user interface/user experience take, but that project is not this project. **The Old Dog and Duck** is intended to be a set of libraries to enable people to build stuff which interacts with ActivityPub. It isn't intended to be a replacement for, or clone of, Mastodon. I do think I might implement my own ActivityPub server on top of The Old Dog and Duck, that specifically might allow for user-pluggable feed-sorting algorithms and with my own user interface/user experience take, but that project is not (yet, at any rate) this project.
## Status
This is a long way pre-alpha. Everything will change. Feel free to play, but do so at your own risk. Contributions welcome.
## Architecture ## Architecture
@ -47,17 +51,31 @@ Where deliveries are ordered and arrive; and from where deliveries onwards are d
Duck-typing for ActivityStreams objects. Duck-typing for ActivityStreams objects.
As of version 0.1.0, this is substantially the only part that is yet at all useful, and it is still a long way from finished or robust.
### Scratch ### Scratch
What the dog does when bored. Essentially, a place where I can learn how to make this stuff work, but perhaps eventually an ActivityPub server in its own right. What the dog does when bored. Essentially, a place where I can learn how to make this stuff work, but perhaps eventually an ActivityPub server in its own right.
## Usage ## Usage
FIXME At present, only the duck-typing functions work. To play with them, use
```clojure
(require '[dog-and-duck.quack.quack :as q])
```
## Testing
Prior to testing, you should clone [activitystreams-test-documents](https://github.com/w3c-social/activitystreams-test-documents) into the `resources` directory. You can then test with
```bash
lein test
```
## License ## License
Copyright © Simon Brooke, 2022 Copyright © Simon Brooke, 2022.
This program and the accompanying materials are made available under the This program and the accompanying materials are made available under the
terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your

View file

@ -20,7 +20,7 @@
;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
(defn object? (defn object?
"Return `true` iff `x` is recognisably an ActivityStreams object. "Returns `true` iff `x` is recognisably an ActivityStreams object.
**NOTE THAT** The ActivityStreams spec **NOTE THAT** The ActivityStreams spec
[says](https://www.w3.org/TR/activitystreams-core/#object): [says](https://www.w3.org/TR/activitystreams-core/#object):
@ -34,9 +34,6 @@
[x] [x]
(and (map? x) (:type x) true)) (and (map? x) (:type x) true))
(object? nil)
(object? {:type "test"})
(defn persistent-object? (defn persistent-object?
"`true` iff `x` is a persistent object. "`true` iff `x` is a persistent object.
@ -49,31 +46,142 @@
(persistent-object? {:type "test" :id "https://mastodon.scot/@barfilfarm"}) (persistent-object? {:type "test" :id "https://mastodon.scot/@barfilfarm"})
(defn actor? (def ^:const actor-types
"TODO!" "The set of types we will accept as actors.
[x]
true) There's an [explicit set of allowed actor types]
(https://www.w3.org/TR/activitystreams-vocabulary/#actor-types)."
#{"Application"
"Group"
"Organization"
"Person"
"Service"})
(def verb? (defn actor-type?
;; TODO: better as a macro
[x]
(if (actor-types x) true false))
(def ^:const verb-types
"The set of types we will accept as verbs. "The set of types we will accept as verbs.
There's an [explicit set of allowed verbs] There's an [explicit set of allowed verb types]
(https://www.w3.org/TR/activitystreams-vocabulary/#activity-types)." (https://www.w3.org/TR/activitystreams-vocabulary/#activity-types)."
#{"Accept" "Add" "Announce" "Arrive" "Block" "Create" "Delete" "Dislike" #{"Accept" "Add" "Announce" "Arrive" "Block" "Create" "Delete" "Dislike"
"Flag" "Follow" "Ignore" "Invite" "Join" "Leave" "Like" "Listen" "Move" "Flag" "Follow" "Ignore" "Invite" "Join" "Leave" "Like" "Listen" "Move"
"Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept" "Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept"
"TentativeReject" "Travel" "Undo" "Update" "View"}) "TentativeReject" "Travel" "Undo" "Update" "View"})
(defn activity? (defn verb-type?
"`true` iff `x` is an activity, else false. ;; TODO: better as a macro
[x]
(if (verb-types x) true false))
see " (def ^:const activitystreams-context-uri
"The URI of the context of an ActivityStreams object is expected to be this
literal string."
"https://www.w3.org/ns/activitystreams")
(defn context?
"Returns `true` iff `x` quacks like an ActivityStreams context, else false.
A context is either
1. the URI (actually an IRI) `activitystreams-context-uri`, or
2. a collection comprising that URI and a map."
[x]
(cond
(nil? x) false
(string? x) (and (= x activitystreams-context-uri) true)
(coll? x) (and (context? (first (remove map? x)))
(= (count x) 2)
true)
:else false))
(defmacro has-context? [x]
`(context? ((keyword "@context") ~x)))
(defn actor?
"Returns `true` if `x` quacks like an actor, else false."
[x]
(and
(object? x)
(has-context? x)
(uri? (URI. (:inbox x)))
(uri? (URI. (:outbox x)))
(actor-type? (:type x))
true))
(defn activity?
"`true` iff `x` quacks like an activity, else false."
[x] [x]
(try (try
(and (object? x) (and (object? x)
(uri? (URI. ((keyword "@context") x))) (has-context? x)
(string? (:summary x)) (string? (:summary x))
(actor? (:actor x)) (actor? (:actor x))
(verb? (:type x)) (verb-type? (:type x))
(or (object? (:object x)) (uri? (URI. x)))) (or (object? (:object x)) (uri? (URI. (:object x))))
(catch URISyntaxException _ false))) true)
(catch URISyntaxException _ false)))
(defn link?
"`true` iff `x` quacks like a link, else false."
[x]
(and (object? x)
(= (:type x) "Link")
(uri? (URI. (:href x)))
true))
(defn link-or-uri?
"`true` iff `x` is either a URI or a link, else false.
There are several points in the specification where e.g. the `:image`
property (if present) may be either a link or a URI."
[x]
(and
(cond (string? x) (uri? (URI. x))
:else (link? x))
true))
(defn collection?
"`true` iff `x` quacks like a collection of type `type`, else `false`.
With one argument, will recognise plain collections and ordered collections,
but (currently) not collection pages."
([x type]
(let [items (or (:items x) (:orderedItems x))]
(and
(cond
(:items x) (nil? (:orderedItems x))
(:orderedItems x) (nil? (:items x))) ;; can't have both properties
(object? x)
(= (:type x) type)
(coll? items)
(every? object? items)
(integer? (:totalItems x))
true)))
([x]
(or (collection? x "Collection")
(collection? x "OrderedCollection"))))
(defn unordered-collection?
"`true` iff `x` quacks like an unordered collection, else `false`."
[x]
(collection? x "Collection"))
(defn ordered-collection?
"`true` iff `x` quacks like an ordered collection, else `false`."
[x]
(collection? x "OrderedCollection"))
(defn collection-page?
"`true` iff `x` quacks like a page in a paged collection, else `false`."
[x]
(collection? x "CollectionPage"))
(defn ordered-collection-page?
"`true` iff `x` quacks like a page in an ordered paged collection, else `false`."
[x]
(collection? x "OrderedCollectionPage"))

View file

@ -0,0 +1,129 @@
(ns dog-and-duck.quack.quack-test
(:require [clojure.test :refer [deftest is testing]]
[dog-and-duck.quack.quack :refer [activitystreams-context-uri
actor? actor-type? context?
object? persistent-object?
verb-type?]]
[dog-and-duck.scratch.parser :refer [clean]]))
;;; Copyright (C) Simon Brooke, 2022
;;; This program is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU General Public License
;;; as published by the Free Software Foundation; either version 2
;;; of the License, or (at your option) any later version.
;;; This program is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;; You should have received a copy of the GNU General Public License
;;; along with this program; if not, write to the Free Software
;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
(deftest object-test
(testing "object recognition"
(let [expected false
actual (object? nil)]
(is (= actual expected)))
(let [expected true
actual (object? {:type "Test"})]
(is (= actual expected)))
(let [expected false
actual (object?
(first
(clean
(slurp "resources/activitystreams-test-documents/empty.json"))))]
(is (= actual expected)))
(let [expected true
actual (object?
(first
(clean
(slurp "resources/activitystreams-test-documents/core-ex1-jsonld.json"))))]
(is (= actual expected)))))
(deftest persistent-object-test
(testing "persistent object recognition"
(let [expected false
actual (persistent-object? nil)]
(is (= actual expected) "Not persistent: not an object."))
(let [expected true
actual (persistent-object? {:type "Test" :id "https://foo.bar/@ban"})]
(is (= actual expected) "Is persistent: has both id and type."))
(let [expected false
actual (persistent-object?
(first
(clean
(slurp "resources/activitystreams-test-documents/simple0001.json"))))]
(is (= actual expected) "Not persistent: has no id."))
(let [expected true
actual (persistent-object?
(first
(clean
(slurp "resources/activitystreams-test-documents/simple0008.json"))))]
(is (= actual expected) "Is persistent: has both id and type."))))
(deftest actor-type-test
(testing "identification of actor types"
(let [expected false
actual (actor-type? nil)]
(is (= actual expected) "nil is not an actor"))
(let [expected false
actual (actor-type? "Duck")]
(is (= actual expected) "A duck is not an actor"))
(let [expected true
actual (actor-type? "Person")]
(is (= actual expected) "A person is an actor"))
(let [expected true
actual (actor-type? "Service")]
(is (= actual expected) "A service is an actor"))))
(deftest verb-type-test
(testing "identification of verb types"
(let [expected false
actual (verb-type? nil)]
(is (= actual expected) "nil is not a verb"))
(let [expected false
actual (verb-type? "Quack")]
(is (= actual expected) "Quack is not a verb"))
(let [expected true
actual (verb-type? "Create")]
(is (= actual expected) "Create is a verb"))
(let [expected true
actual (verb-type? "Reject")]
(is (= actual expected) "Reject is a verb"))))
(deftest context-test
(testing "identification of valid contexts"
(let [expected false
actual (context? "https://foo.bar/ban/")]
(is (= actual expected)
"Only `activitystreams-context-uri` is valid as a context on its own"))
(let [expected true
actual (context? activitystreams-context-uri)]
(is (= actual expected)
"`activitystreams-context-uri` is valid as a context on its own"))
(let [expected false
actual (context? [{:foo "bar"} "https://foo.bar/ban/"])]
(is (= actual expected)
"Only `activitystreams-context-uri` is valid as a context uri"))
(let [expected true
actual (context? [{:foo "bar"} activitystreams-context-uri])]
(is (= actual expected)
"`activitystreams-context-uri` is valid as a context uri"))
(let [expected true
actual (context? [activitystreams-context-uri {:foo "bar"}])]
(is (= actual expected)
"order of elements within a context should not matter"))
))
(deftest actor-test
(testing "identification of actors"
(let [expected false
actual (actor? (-> "resources/activitystreams-test-documents/simple0008.json" slurp clean first))]
(is (= actual expected) "A Note is not an actor"))
(let [expected true
actual (actor? (-> "resources/activitystreams-test-documents/simple0020.json" slurp clean first :actor))]
(is (= actual expected) "A Person is an actor"))
))