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