001 (ns dog-and-duck.quack.picky "Fault-finder for ActivityPub documents.
002
003 Generally, each `-faults` function will return:
004 1. `nil` if no faults were found;
005 2. a sequence of fault objects if faults were found.
006
007 Each fault object shall have the properties:
008 1. `:@context` whose value shall be the URL of a
009 document specifying this vocabulary;
010 2. `:type` whose value shall be `Fault`;
011 3. `:severity` whose value shall be one of
012 `minor`, `should`, `must` or `critical`;
013 4. `:fault` whose value shall be a unique token
014 representing the particular fault type;
015 5. `:narrative` whose value shall be a natural
016 language description of the fault type.
017
018 Note that the reason for the `:fault` property is
019 to be able to have a well known place, linked to
020 from the @context URL, which allows narratives
021 for each fault type to be served in as many
022 natural languages as possible.
023
024 The idea further is that it should ultimately be
025 possible to serialise a fault report as a
026 document which in its own right conforms to the
027 ActivityStreams spec."
028 (:require [dog-and-duck.utils.process :refer [pid]]))
029
030 (def ^:const severity
031 "Severity of faults found, as follows:
032
033 1. `:minor` things which I consider to be faults, but which
034 don't actually breach the spec;
035 2. `:should` instances where the spec says something SHOULD
036 be done, which isn't;
037 3. `:must` instances where the spec says something MUST
038 be done, which isn't;
039 4. `:critical` instances where I believe the fault means that
040 the object cannot be meaningfully processed."
041 #{:minor :should :must :critical})
042
043 (def ^:const severity-filters
044 "Hack for implementing a severity hierarchy"
045 {:all #{}
046 :minor #{:minor}
047 :should #{:minor :should}
048 :must #{:minor :should :must}
049 :critical severity})
050
051 (defn filter-severity
052 "Return a list of reports taken from these `reports` where the severity
053 of the report is greater than this `severity`."
054 [reports severity]
055 (assert
056 (and
057 (coll? reports)
058 (every? map? reports)
059 (every? :severity reports)))
060 (remove
061 #((severity-filters severity) (:severity %))
062 reports))
063
064 (def ^:const activitystreams-context-uri
065 "The URI of the context of an ActivityStreams object is expected to be this
066 literal string."
067 "https://www.w3.org/ns/activitystreams")
068
069 (def ^:const validation-fault-context-uri
070 "The URI of the context of a validation fault report object shall be this
071 literal string."
072 "https://simon-brooke.github.io/dog-and-duck/codox/Validation_Faults.html")
073
074 (defn context?
075 "Returns `true` iff `x` quacks like an ActivityStreams context, else false.
076
077 A context is either
078 1. the URI (actually an IRI) `activitystreams-context-uri`, or
079 2. a collection comprising that URI and a map."
080 [x]
081 (cond
082 (nil? x) false
083 (string? x) (and (= x activitystreams-context-uri) true)
084 (coll? x) (and (context? (first (remove map? x)))
085 (= (count x) 2)
086 true)
087 :else false))
088
089 (defmacro has-context?
090 "True if `x` is an ActivityStreams object with a valid context, else `false`."
091 [x]
092 `(context? ((keyword "@context") ~x)))
093
094
095
096 (defn make-fault-object
097 "Return a fault object with these `severity`, `fault` and `narrative` values.
098
099 An ActivityPub object MUST have a globally unique ID. Whether this is
100 meaningful depends on whether we persist fault report objects and serve
101 them, which at present I have no plans to do."
102 [severity fault narrative]
103 (assoc {}
104 (keyword "@context") validation-fault-context-uri
105 :id (str "https://"
106 (.. java.net.InetAddress getLocalHost getHostName)
107 "/fault/"
108 pid
109 ":"
110 (inst-ms (java.util.Date.)))
111 :type "Fault"
112 :severity severity
113 :fault fault
114 :narrative narrative))
115
116 (defn object-faults
117 [x]
118 (remove
119 empty?
120 (list
121 (when-not
122 (has-context? x)
123 (make-fault-object
124 :should
125 :no-context
126 "Section 3 of the ActivityPub specification states
127 `Implementers SHOULD include the ActivityPub context in
128 their object definitions`.")
129 (when-not (:type x)
130 (make-fault-object
131 :minor
132 :no-type
133 "The ActivityPub specification states that the `type` field is
134 optional, but it is hard to process objects with no known type."))
135 (when-not (contains? x :id)
136 (make-fault-object
137 :minor
138 :no-id-transient
139 "The ActivityPub specification allows objects without `id` fields
140 only if they are intentionally transient; even so it is preferred
141 that the object should have an explicit null id."
142 ))
143 ))))