Compare commits
No commits in common. "4df575610e3b3a8939feb27d10fb33ca4105b651" and "6fab9f42d32f895e7560a6b0153b5ea220f990a0" have entirely different histories.
4df575610e
...
6fab9f42d3
|
@ -3,129 +3,315 @@
|
||||||
;; Copyright © 2024 Jaidyn Ann <jadedctrl@posteo.at>
|
;; Copyright © 2024 Jaidyn Ann <jadedctrl@posteo.at>
|
||||||
;;
|
;;
|
||||||
;; This program is free software: you can redistribute it and/or
|
;; This program is free software: you can redistribute it and/or
|
||||||
;; modify it under the terms of the GNU Affero General Public License
|
;; modify it under the terms of the GNU General Public License as
|
||||||
;; as published by the Free Software Foundation, either version 3 of
|
;; published by the Free Software Foundation, either version 3 of
|
||||||
;; the License, or (at your option) any later version.
|
;; the License, or (at your option) any later version.
|
||||||
;;
|
;;
|
||||||
;; This program is distributed in the hope that it will be useful,
|
;; This program is distributed in the hope that it will be useful,
|
||||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
;; GNU Affero General Public License for more details.
|
;; GNU General Public License for more details.
|
||||||
;;
|
;;
|
||||||
;; You should have received a copy of the GNU Affero General Public License
|
;; You should have received a copy of the GNU General Public License
|
||||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
(defpackage #:activity-servist/activity-streams
|
(defpackage #:activity-servist/activity-streams
|
||||||
(:use #:cl)
|
(:use #:cl)
|
||||||
(:nicknames "AS/AS" "ACTIVITY-STREAMS")
|
(:nicknames "AS/AS" "ACTIVITY-STREAMS")
|
||||||
(:export
|
(:export
|
||||||
|
;; Functions
|
||||||
|
:parse :encode
|
||||||
|
:define-class-encoders
|
||||||
|
;; Globals
|
||||||
|
:*ap-packages* :*default-class*
|
||||||
;; Classes
|
;; Classes
|
||||||
:object
|
:object
|
||||||
;; Accessors
|
;; Accessors
|
||||||
#'object-actor #'object-attachment #'object-attributed-to #'object-audience
|
:object-@context :object-unsupported
|
||||||
#'object-content #'object-context #'object-name #'object-end-time
|
|
||||||
#'object-generator #'object-icon #'object-image #'object-in-reply-to
|
|
||||||
#'object-location #'object-preview #'object-published #'object-replies
|
|
||||||
#'object-start-time #'object-summary #'object-tag #'object-updated
|
|
||||||
#'object-url #'object-to #'object-bto #'object-cc #'object-bcc
|
|
||||||
#'object-media-type #'object-duration
|
|
||||||
;; Slots
|
;; Slots
|
||||||
:attachment :attributed-to :audience :content :context :name :end-time
|
:@context :@type :type :@id :id :unsupported))
|
||||||
:generator :icon :image :in-reply-to :location :preview :published :replies
|
|
||||||
:start-time :summary :tag :updated :url :to :bto :cc :bcc :media-type :duration))
|
|
||||||
|
|
||||||
(in-package #:activity-servist/activity-streams)
|
(in-package #:activity-servist/activity-streams)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;;; Globals
|
||||||
|
;;; ————————————————————————————————————————
|
||||||
|
(defparameter *ap-packages* (list :activity-servist/activity-vocabulary *package*)
|
||||||
|
"A list of packages in which we should search for AP classes and slot-symbols
|
||||||
|
during JSON parsing. The class-name searched for is simply the value of the JSON
|
||||||
|
object’s “type” key. The package first in the list to export such a symbol
|
||||||
|
is the winner.")
|
||||||
|
|
||||||
|
(defparameter *default-class* 'activity-servist/activity-streams:object
|
||||||
|
"The class used for ActivityStreams objects found during parsing that don’t
|
||||||
|
have a corresponding class defined. Notably, all keys and values without
|
||||||
|
corresponding slots are placed in the UNSUPPORTED slot.
|
||||||
|
The class you choose should inherit ACTIVITY-SERVIST/ACTIVITY-STREAMS:OBJECT.")
|
||||||
|
|
||||||
|
;; Private, internal variable.
|
||||||
|
(defparameter *@context* nil
|
||||||
|
"Used in YASON:ENCODE to ensure that a single top-level @context can be
|
||||||
|
created where AP objects contain other AP objects in their slots.
|
||||||
|
This variable is overridden locally during encoding (LET), and should never be
|
||||||
|
modified globally (as we expect it to be nil in top-level objects.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;;; Macros
|
||||||
|
;;; ————————————————————————————————————————
|
||||||
|
;; This macro and the following function are related to JSON serialization; see
|
||||||
|
;; the below “JSON serialization” section for other related functions.
|
||||||
|
(defmacro define-yason-encode-slots (class)
|
||||||
|
"Define a YASON:ENCODE-SLOTS method for a CLASS, which simply encodes all of
|
||||||
|
CLASS’es slots with JSON keys based on the camel-cased slot name."
|
||||||
|
(append
|
||||||
|
`(defmethod yason:encode-slots progn ((obj ,class)))
|
||||||
|
(mapcar (yason-encode-slot-function)
|
||||||
|
(class-slots-activity-alist class))))
|
||||||
|
|
||||||
|
(defun yason-encode-slot-function ()
|
||||||
|
"Helper-function for the DEFINE-YASON-ENCODE-SLOTS macro.
|
||||||
|
This returns a function to create a quoted function that should be called for each slot,
|
||||||
|
again and again, by YASON:ENCODE-SLOTS."
|
||||||
|
(lambda (slot-key-pair)
|
||||||
|
`(let ((key ',(car slot-key-pair))
|
||||||
|
(value (ignore-errors (slot-value obj ',(car slot-key-pair)))))
|
||||||
|
(cond ((eq key '@context) ; Actually in YASON:ENCODE-OBJECT using *@context*
|
||||||
|
(setq *@context* (merge-@contexts *@context* (object-@context obj))))
|
||||||
|
((eq key 'unsupported)
|
||||||
|
;; Keys/values without a slot are stored in this UNSUPPORTED alist.
|
||||||
|
(mapcar (lambda (cell)
|
||||||
|
(yason:encode-object-element
|
||||||
|
(camel-case (symbol-name (car cell)))
|
||||||
|
(cdr cell)))
|
||||||
|
value))
|
||||||
|
((eq key 'type) ; Encode type based on class-name or TYPE slot
|
||||||
|
(yason:encode-object-element
|
||||||
|
"type" (or value
|
||||||
|
(class-pretty-name (class-of obj)))))
|
||||||
|
(value
|
||||||
|
(yason:encode-object-element ,(cdr slot-key-pair) value))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;;; Core class
|
;;; Core class
|
||||||
;;; ————————————————————————————————————————
|
;;; ————————————————————————————————————————
|
||||||
(json-ld::define-json-type (object "Object") () "https://www.w3.org/ns/activitystreams"
|
(defclass object ()
|
||||||
((actor
|
((@context :initform nil)
|
||||||
"actor"
|
(as/as:@type :initform nil)
|
||||||
:documentation "Describes one or more entities that either performed or are expected to perform the activity. Any single activity can have multiple actors. The actor MAY be specified using an indirect Link.")
|
(as/as:type :initform nil)
|
||||||
(attachment
|
(as/as:@id :initform nil)
|
||||||
"attachment"
|
(as/as:id :initform nil)
|
||||||
:documentation "Identifies a resource attached or related to an object that potentially requires special handling. The intent is to provide a model that is at least semantically similar to attachments in email.")
|
(as/as:unsupported :initform nil :accessor object-unsupported)))
|
||||||
(attributed-to
|
|
||||||
"attributedTo"
|
|
||||||
:documentation "Identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. For instance, an object might be attributed to the completion of another activity.")
|
|
||||||
(audience
|
;;; Accessors
|
||||||
"audience"
|
;;; ————————————————————————————————————————
|
||||||
:documentation "Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.")
|
(defgeneric object-@context (obj)
|
||||||
(content
|
(:documentation "Accessor for an object’s associated JSON-LD @context.
|
||||||
"content"
|
As @context can sometimes vary on an object’s contents, on-the-fly, this
|
||||||
:documentation "The content or textual representation of the Object encoded as a JSON string. By default, the value of content is HTML. The media-type property can be used in the object to indicate a different content type.
|
method is invoked during JSON encoding of an object. The @CONTEXT
|
||||||
The content MAY be expressed using multiple language-tagged values. ")
|
slot-value should be prioritized over the @CONTEXT value is calculated
|
||||||
(context
|
by this method."))
|
||||||
"context"
|
|
||||||
:documentation "Identifies the context within which the object exists or an activity was performed.
|
(defmethod object-@context ((obj object))
|
||||||
The notion of “context” used is intentionally vague. The intended function is to serve as a means of grouping objects and activities that share a common originating context or purpose. An example could be all activities relating to a common project or event.")
|
(or (slot-value obj '@context)
|
||||||
(name
|
"https://www.w3.org/ns/activitystreams"))
|
||||||
"name"
|
|
||||||
:documentation "A simple, human-readable, plain-text name for the object. HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.")
|
(defmethod (setf object-@context) (context (obj object))
|
||||||
(end-time
|
(setf (slot-value obj '@context) context))
|
||||||
"endTime"
|
|
||||||
:documentation "The date and time describing the actual or expected ending time of the object. When used with an Activity object, for instance, the end-time property specifies the moment the activity concluded or is expected to conclude.")
|
|
||||||
(generator
|
|
||||||
"generator"
|
;;; JSON parsing
|
||||||
:documentation "Identifies the entity (e.g. an application) that generated the object.")
|
;;; ————————————————————————————————————————
|
||||||
(icon
|
(defun parse (string)
|
||||||
"icon"
|
"Parse a string containing JSON into an ActivityPub object."
|
||||||
:documentation "Indicates an entity that describes an icon for this object. The image should have an aspect ratio of one (horizontal) to one (vertical) and should be suitable for presentation at a small size.")
|
(parse-table (yason:parse string)))
|
||||||
(image
|
|
||||||
"image"
|
(defun parse-table (table)
|
||||||
:documentation "Indicates an entity that describes an image for this object. Unlike the icon property, there are no aspect ratio or display size limitations assumed.")
|
"Parse a hash-table corresponding to YASON-parsed JSON into an ActivityPub object."
|
||||||
(in-reply-to
|
(let* ((found-class (car (find-registered-classes (param-case (gethash "type" table)))))
|
||||||
"inReplyTo"
|
(class (or found-class (find-class *default-class*)))
|
||||||
:documentation "Indicates one or more entities for which this object is considered a response.")
|
(obj (make-instance class)))
|
||||||
(location
|
(loop for key being each hash-key of table
|
||||||
"location"
|
for val being each hash-value of table
|
||||||
:documentation "Indicates one or more physical or logical locations associated with the object.")
|
do (let* ((slot-name (string-upcase (param-case key)))
|
||||||
(preview
|
(slot-sym (car (find-registered-symbols slot-name)))
|
||||||
"preview"
|
(val (parse-value val)))
|
||||||
:documentation "Identifies an entity that provides a preview of this object.")
|
(handler-case
|
||||||
(published
|
(setf (slot-value obj slot-sym) val)
|
||||||
"published"
|
(error nil
|
||||||
:documentation "The date and time describing the actual or expected starting time of the object. When used with an Activity object, for instance, the start-time property specifies the moment the activity began or is scheduled to begin.")
|
(setf (slot-value obj 'unsupported)
|
||||||
(replies
|
(append (ignore-errors (slot-value obj 'unsupported))
|
||||||
"replies"
|
(list (cons (intern slot-name) val))))))))
|
||||||
:documentation "Identifies a Collection containing objects considered to be responses to this object.")
|
obj))
|
||||||
(start-time
|
|
||||||
"startTime"
|
(defun parse-value (val)
|
||||||
:documentation "The date and time describing the actual or expected starting time of the object. When used with an Activity object, for instance, the start-time property specifies the moment the activity began or is scheduled to begin.")
|
"Parse the value of a key found in YASON-parsed JSON.
|
||||||
(summary
|
All ActivityPub objects (hash-tables containing “type”) will be parsed into
|
||||||
"summary"
|
ActivityPub objects; all others will parsed into associative lists."
|
||||||
:documentation "A natural language summarization of the object encoded as HTML. Multiple language tagged summaries MAY be provided.")
|
(typecase val
|
||||||
(tag
|
(hash-table (maybe-parse-table val))
|
||||||
"tag"
|
(list (mapcar (lambda (a)
|
||||||
:documentation "One or more “tags” that have been associated with an objects. A tag can be any kind of Object. The key difference between attachment and tag is that the former implies association by inclusion, while the latter implies associated by reference.")
|
(if (hash-table-p a)
|
||||||
(updated
|
(maybe-parse-table a)
|
||||||
"updated"
|
a))
|
||||||
:documentation "The date and time at which the object was updated.")
|
val))
|
||||||
(url
|
(t val)))
|
||||||
"url"
|
|
||||||
:documentation "Identifies one or more links to representations of the object.")
|
(defun maybe-parse-table (table)
|
||||||
(to
|
"If a hash-table seems to be a valid ActivityPub object, attempt parsing it
|
||||||
"to"
|
into one. Otherwise, parse it into an associative list."
|
||||||
:documentation "Identifies an entity considered to be part of the public primary audience of an Object.")
|
(if (gethash "type" table)
|
||||||
(bto
|
(parse-table table)
|
||||||
"bto"
|
(alexandria:hash-table-alist table)))
|
||||||
:documentation "Identifies an Object that is part of the private primary audience of this Object.")
|
|
||||||
(cc
|
|
||||||
"cc"
|
|
||||||
:documentation "Identifies an Object that is part of the public secondary audience of this Object.")
|
;;; JSON serialization
|
||||||
(bcc
|
;;; ————————————————————————————————————————
|
||||||
"bcc"
|
(defun define-class-encoders (classes)
|
||||||
:documentation "Identifies one or more Objects that are part of the private secondary audience of this Object.")
|
"For each class in CLASSES, define an YASON:ENCODE-SLOTS method for that class,
|
||||||
(media-type
|
which only encodes slots unique to that class (as ENCODE-SLOTS is called for each
|
||||||
"mediaType"
|
inherited class). Each slot’s name is converted to camel-case, as per convention."
|
||||||
:documentation "When used on a Link, identifies the MIME media type of the referenced resource.
|
(mapcar (lambda (class)
|
||||||
When used on an Object, identifies the MIME media type of the value of the content property. If not specified, the content property is assumed to contain text/html content.")
|
(closer-mop:finalize-inheritance class)
|
||||||
(duration
|
(eval `(define-yason-encode-slots ,class)))
|
||||||
"duration"
|
classes))
|
||||||
:documentation "When the object describes a time-bound resource, such as an audio or video, a meeting, etc, the duration property indicates the object's approximate duration. The value MUST be expressed as an xsd:duration as defined by [ xmlschema11-2], section 3.3.6 (e.g. a period of 5 seconds is represented as “PT5S”)."))
|
|
||||||
(:documentation
|
(defun encode (obj &optional (stream *standard-output*))
|
||||||
"Describes an object of any kind. The Object type serves as the base type for
|
"Encode OBJ into JSON. A mere wrapper around YASON:ENCODE."
|
||||||
most of the other kinds of objects defined in the Activity Vocabulary,
|
(yason:encode obj stream))
|
||||||
including other Core types such as Activity, IntransitiveActivity, Collection
|
|
||||||
and OrderedCollection."))
|
(defmethod yason:encode ((obj object) &optional (stream *standard-output*))
|
||||||
|
(yason:with-output (stream)
|
||||||
|
(yason:encode-object obj)))
|
||||||
|
|
||||||
|
(defmethod yason:encode-object ((obj object))
|
||||||
|
(typecase *@context*
|
||||||
|
(null ; If this is the top-level (non-nested) object, establish a @context.
|
||||||
|
(let ((*@context* 'top-level))
|
||||||
|
(yason:encode-object obj)))
|
||||||
|
(symbol ; In the top-level, encode slots and then @context.
|
||||||
|
(setq *@context* (object-@context obj))
|
||||||
|
(yason:with-object ()
|
||||||
|
(yason:encode-slots obj)
|
||||||
|
(yason:encode-object-element "@context" *@context*)))
|
||||||
|
(T ; In nested objects, only encode slots — not *@context*.
|
||||||
|
(yason:with-object ()
|
||||||
|
(yason:encode-slots obj)))))
|
||||||
|
|
||||||
|
(defun class-slots-activity-alist (class)
|
||||||
|
"Return an associative list containing CLASSes slots’ symbols consed with
|
||||||
|
their sanitized string keys appropriate for ActivityVocabular custom.
|
||||||
|
A class with slots MAP-AWAY and COLLECTION-AGAIN would return
|
||||||
|
((MAP-AWAY . “mapAway”)(COLLECTION-AGAIN . “collectionAgain”))"
|
||||||
|
(alist-mapcdr #'camel-case
|
||||||
|
(class-slots-alist class)))
|
||||||
|
|
||||||
|
(defun merge-@contexts (a b)
|
||||||
|
"Given two @context lists, A and B, merge them into one JSON-LD @context list
|
||||||
|
containing both of their elements."
|
||||||
|
(cond ((equal a b) a)
|
||||||
|
((not b) a)
|
||||||
|
((not a) b)
|
||||||
|
((and (listp a)
|
||||||
|
(find b a :test #'equal))
|
||||||
|
a)
|
||||||
|
(T
|
||||||
|
(merge-lists
|
||||||
|
(if (listp a) a (list a))
|
||||||
|
(if (listp b) b (list b))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;;; Util
|
||||||
|
;;; ————————————————————————————————————————
|
||||||
|
(defun camel-case (string)
|
||||||
|
"Convert a STRING to camel-casing. That is, casingLikeThis.
|
||||||
|
Wrapper around STR:CAMEL-CASE, working around a bug that a non-alphanumeric
|
||||||
|
character at the start of the string gets erroneously (or at least undesireably,
|
||||||
|
to us) removed."
|
||||||
|
(keep-nonalphanumeric-prefix string
|
||||||
|
(str:camel-case string)))
|
||||||
|
|
||||||
|
(defun param-case (string)
|
||||||
|
"Convert a STRING to param-casing. That is, casing-like-this.
|
||||||
|
Wrapper around STR:PARAM-CASE, working around a bug that a non-alphanumeric
|
||||||
|
character at the start of the string gets erroneously (or at least undesireably,
|
||||||
|
to us) removed."
|
||||||
|
(keep-nonalphanumeric-prefix string
|
||||||
|
(str:param-case string)))
|
||||||
|
(defun camel-case (str)
|
||||||
|
"Convert a STRING to camel-casing.
|
||||||
|
Wrapper around STR:CAMEL-CASE, working around a bug that a non-alphanumeric
|
||||||
|
character at the start of the string gets erroneously (or at least undesireably,
|
||||||
|
to us) removed."
|
||||||
|
(keep-nonalphanumeric-prefix str (str:camel-case str)))
|
||||||
|
|
||||||
|
(defun keep-nonalphanumeric-prefix (str child-str)
|
||||||
|
"This ensures that a CHILD-STR derived from STR has the same nonalphanumeric
|
||||||
|
prefix as STR, as some functions like to remove such prefixes."
|
||||||
|
(if (not (alphanumericp (aref str 0)))
|
||||||
|
(concatenate 'string
|
||||||
|
(string (aref str 0))
|
||||||
|
child-str)
|
||||||
|
child-str))
|
||||||
|
|
||||||
|
(defun class-pretty-name (class)
|
||||||
|
"Return a CLASS’es name in a “pretty” (sentence-capitalized) string."
|
||||||
|
(string-capitalize (symbol-name (class-name class))))
|
||||||
|
|
||||||
|
(defun merge-lists (a b)
|
||||||
|
"Given lists A and B, merge them into one list non-redundantly — all unique
|
||||||
|
items in each will be contained in the resultant list."
|
||||||
|
(append a (remove-if (lambda (item) (find item a :test #'equal)) b)))
|
||||||
|
|
||||||
|
(defun find-registered-symbols (str)
|
||||||
|
"Find all symbols identified by string STR within packages in the
|
||||||
|
*ap-packages* list."
|
||||||
|
(remove-if
|
||||||
|
#'not
|
||||||
|
(mapcar (lambda (package)
|
||||||
|
(multiple-value-bind (sym context)
|
||||||
|
(find-symbol (string-upcase str) package)
|
||||||
|
(unless (eq context :inherited)
|
||||||
|
sym)))
|
||||||
|
*ap-packages*)))
|
||||||
|
|
||||||
|
(defun find-registered-classes (str)
|
||||||
|
"Find all classes identified by string STR within pacakges in the
|
||||||
|
*ap-packages* list."
|
||||||
|
(remove-if
|
||||||
|
#'not
|
||||||
|
(mapcar (lambda (sym) (find-class sym))
|
||||||
|
(find-registered-symbols str))))
|
||||||
|
|
||||||
|
(defun alist-mapcdr (function alist)
|
||||||
|
"Apply a FUNCTION to all values (cdrs) of an ALIST’s pairs. Returns a new ALIST
|
||||||
|
of the same keys, whose values are the results of FUNCTION."
|
||||||
|
(mapcar
|
||||||
|
(lambda (cell)
|
||||||
|
(cons (car cell)
|
||||||
|
(funcall function (cdr cell))))
|
||||||
|
alist))
|
||||||
|
|
||||||
|
(defun class-slots-alist (class)
|
||||||
|
"Return an associative list of a CLASS’es direct slots (by symbol) matched with
|
||||||
|
their names as strings. For instance, a class with slots MAP-AWAY and
|
||||||
|
COLLECTION-AGAIN would return:
|
||||||
|
((MAP-AWAY . “MAP-AWAY”)(COLLECTION-AGAIN . “COLLECTION-AGAIN”)"
|
||||||
|
(mapcar
|
||||||
|
(lambda (slot)
|
||||||
|
(let ((name (closer-mop:slot-definition-name slot)))
|
||||||
|
(cons name (symbol-name name))))
|
||||||
|
(closer-mop:class-direct-slots class)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;;; Defining YASON:ENCODE-SLOTS
|
||||||
|
;;; ————————————————————————————————————————
|
||||||
|
(as/as:define-class-encoders (list (find-class 'object)))
|
||||||
|
|
145
src/json-ld.lisp
145
src/json-ld.lisp
|
@ -19,11 +19,7 @@
|
||||||
(:use #:cl)
|
(:use #:cl)
|
||||||
(:nicknames "AS/JSON-LD" "JSON-LD")
|
(:nicknames "AS/JSON-LD" "JSON-LD")
|
||||||
(:export
|
(:export
|
||||||
#:define-json-type
|
#:define-json-type #:json-ld-context))
|
||||||
;; Accessors
|
|
||||||
#:json-ld-context #:json-ld-etc #:json-ld-id #:json-ld-type
|
|
||||||
;; Slots
|
|
||||||
:@context :@id :@type :.etc))
|
|
||||||
|
|
||||||
(in-package #:activity-servist/json-ld)
|
(in-package #:activity-servist/json-ld)
|
||||||
|
|
||||||
|
@ -37,7 +33,7 @@
|
||||||
|
|
||||||
;;; Base class
|
;;; Base class
|
||||||
;;; ————————————————————————————————————————
|
;;; ————————————————————————————————————————
|
||||||
(defclass json-ld-object ()
|
(defclass json-ld-type ()
|
||||||
((@context
|
((@context
|
||||||
:initform nil
|
:initform nil
|
||||||
:documentation
|
:documentation
|
||||||
|
@ -45,57 +41,28 @@
|
||||||
The method JSON-LD-CONTEXT is how the contents of encoded @context is
|
The method JSON-LD-CONTEXT is how the contents of encoded @context is
|
||||||
determined; to change a class’es default/calculated @context, override that
|
determined; to change a class’es default/calculated @context, override that
|
||||||
method. This slot is for changing a specific object’s @context.")
|
method. This slot is for changing a specific object’s @context.")
|
||||||
(@id
|
(etc
|
||||||
:initform nil
|
:initform nil
|
||||||
:accessor json-ld-id
|
|
||||||
:documentation
|
|
||||||
"Provides the globally unique identifier for an object.")
|
|
||||||
(@type
|
|
||||||
:initform nil
|
|
||||||
:accessor json-ld-type
|
|
||||||
:documentation
|
|
||||||
"Identifies the type of an object. Used to determine the corresponding CLOS-object.")
|
|
||||||
(.etc
|
|
||||||
:initform nil
|
|
||||||
:accessor json-ld-etc
|
|
||||||
:documentation
|
:documentation
|
||||||
"Components of the JSON object which, during parsing, did not match any specific
|
"Components of the JSON object which, during parsing, did not match any specific
|
||||||
slot. This is often filled up in the case of undefined node-types or non-adherent
|
slot. This is often filled up in the case of undefined node-types or non-adherent
|
||||||
object definitions.")))
|
object definitions.")))
|
||||||
|
|
||||||
(setf (gethash "*" *json-types*)
|
|
||||||
'((json-ld-object)
|
|
||||||
("@context" @context . "@context")
|
|
||||||
("@id" @id . "@id")
|
|
||||||
("@type" @type . "@type")))
|
|
||||||
|
|
||||||
(defgeneric json-ld-context (obj)
|
(defgeneric json-ld-context (obj)
|
||||||
(:documentation
|
(:documentation
|
||||||
"Returns a JSON-LD CLOS object’s @context, for use in JSON-encoding of the
|
"Returns a JSON-LD CLOS object’s @context, for use in JSON-encoding of the
|
||||||
object.
|
object.
|
||||||
The implementation for the JSON-LD-OBJECT class simply returns the activitystreams
|
The implementation for the JSON-LD-TYPE class simply returns the activitystreams
|
||||||
URL.
|
URL.
|
||||||
If you would like to change @context on a class-level, override this method.
|
If you would like to change @context on a class-level, override this method.
|
||||||
If you would like to change it on an object-level, set the @CONTEXT slot."))
|
If you would like to change it on an object-level, set the @CONTEXT slot."))
|
||||||
|
|
||||||
(defmethod json-ld-context ((obj json-ld-object))
|
(defmethod json-ld-context ((obj json-ld-type))
|
||||||
(or (slot-value obj '@context)
|
(or (slot-value obj '@context)
|
||||||
"https://www.w3.org/ns/activitystreams"))
|
"https://www.w3.org/ns/activitystreams"))
|
||||||
|
|
||||||
(defmethod yason:encode-slots progn ((obj json-ld-object))
|
(defmethod yason:encode-slots progn ((obj json-ld-type))
|
||||||
(let ((context (json-ld-context obj))
|
(yason:encode-object-element "@context" (json-ld-context obj)))
|
||||||
(id (json-ld-id obj))
|
|
||||||
(type (json-ld-type obj)))
|
|
||||||
(when context
|
|
||||||
(yason:encode-object-element "@context" (json-ld-context obj)))
|
|
||||||
(when id
|
|
||||||
(yason:encode-object-element "@id" (json-ld-id obj)))
|
|
||||||
(when type
|
|
||||||
(yason:encode-object-element "@type" (json-ld-type obj))))
|
|
||||||
(mapcar (lambda (alist-cell)
|
|
||||||
(yason:encode-object-element (car alist-cell)
|
|
||||||
(cdr alist-cell)))
|
|
||||||
(json-ld-etc obj)))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,9 +78,8 @@ encoding as @type’s value. If only the CLOS class-name is provided, @type will
|
||||||
not be encoded for this object.
|
not be encoded for this object.
|
||||||
|
|
||||||
DIRECT-SUPERCLASSES is a list of JSON-LD CLOS classes, whose slots and context
|
DIRECT-SUPERCLASSES is a list of JSON-LD CLOS classes, whose slots and context
|
||||||
this should inherit. JSON-LD-OBJECT should be somewhere in the hierarchy, in order
|
this should inherit. JSON-LD-TYPE should be somewhere in the hierarchy, in order
|
||||||
to provide “@context”, “@id”, and “@type”; if no superclasses are provided,
|
to provide “@context”; if no superclasses are provided, JSON-LD-TYPE is default.
|
||||||
JSON-LD-OBJECT is default.
|
|
||||||
|
|
||||||
CONTEXT is a context hash-table, parsed from a JSON-LD context by JSON-LD:PARSE
|
CONTEXT is a context hash-table, parsed from a JSON-LD context by JSON-LD:PARSE
|
||||||
or JSON-LD:PARSE-CONTEXT. Any terms defined in this context will be used to
|
or JSON-LD:PARSE-CONTEXT. Any terms defined in this context will be used to
|
||||||
|
@ -128,7 +94,13 @@ encoded nor decoded in JSON.
|
||||||
|
|
||||||
SLOT-OPTIONS are key/value pairs in the format of DEFCLASS’es slot-options.
|
SLOT-OPTIONS are key/value pairs in the format of DEFCLASS’es slot-options.
|
||||||
Keywords such as :INITFORM, :DOCUMENTATION, etc. can be used.
|
Keywords such as :INITFORM, :DOCUMENTATION, etc. can be used.
|
||||||
There is one keyword with behavior unlike DEFCLASS, however — :ACCESSOR.
|
There are two keywords with behavior unlike DEFCLASS, however:
|
||||||
|
:REQUIRED and :ACCESSOR.
|
||||||
|
|
||||||
|
By default, a slot will have an init-form of NIL; this can of course be
|
||||||
|
overridden by putting :INITFORM yourself in the slot definition.
|
||||||
|
Set :REQUIRED to T to not set :INITFORM at all, effectively making the slot
|
||||||
|
“required.”
|
||||||
|
|
||||||
By default, a slot will have an accessor named after the class and slot, like
|
By default, a slot will have an accessor named after the class and slot, like
|
||||||
PLACE-RADIUS for the class PLACE and the slot RADIUS.
|
PLACE-RADIUS for the class PLACE and the slot RADIUS.
|
||||||
|
@ -143,15 +115,17 @@ Here is a brief example partially defining the “Place” type from ActivityStr
|
||||||
((altitude “altitude”
|
((altitude “altitude”
|
||||||
:documentation “Indicates the altitude of a place.”)
|
:documentation “Indicates the altitude of a place.”)
|
||||||
(latitude “latitude”
|
(latitude “latitude”
|
||||||
|
:required T
|
||||||
:documentation “The latitude of a place.”)
|
:documentation “The latitude of a place.”)
|
||||||
(longitude “longitude”
|
(longitude “longitude”
|
||||||
|
:required T
|
||||||
:documentation “The longitude of a place.”)))"
|
:documentation “The longitude of a place.”)))"
|
||||||
`(let ((json-class
|
`(let ((json-class
|
||||||
(define-json-clos-class ,names
|
(define-json-clos-class ,names
|
||||||
,(or direct-superclasses `(json-ld-object))
|
,(or direct-superclasses `(json-ld-type))
|
||||||
,direct-slots ,options)))
|
,direct-slots ,options)))
|
||||||
(define-json-type-encoder ,(car names) ,direct-slots)
|
(define-json-type-encoder ,(car names) ,direct-slots)
|
||||||
(register-json-type ',names (or ',direct-superclasses '(json-ld-object)) ',direct-slots ,context)
|
(register-json-type ',names ',direct-slots ,context)
|
||||||
json-class))
|
json-class))
|
||||||
|
|
||||||
(defmacro define-json-clos-class (names direct-superclasses direct-slots options)
|
(defmacro define-json-clos-class (names direct-superclasses direct-slots options)
|
||||||
|
@ -179,15 +153,21 @@ applying default slot-options, etc."
|
||||||
|
|
||||||
(defun json-type-implicit-slot-options (class-name slot-name)
|
(defun json-type-implicit-slot-options (class-name slot-name)
|
||||||
"Return default property-list slot options for a json-type CLOS class."
|
"Return default property-list slot options for a json-type CLOS class."
|
||||||
(list :accessor (intern (format nil "~A-~A" class-name slot-name))))
|
(list :initform nil
|
||||||
|
:accessor (intern (format nil "~A-~A" class-name slot-name))))
|
||||||
|
|
||||||
(defun json-type-normalize-slot-options (slot-opts)
|
(defun json-type-normalize-slot-options (slot-opts)
|
||||||
"Take property-list slot options from a DEFINE-JSON-TYPE format and massage it
|
"Take property-list slot options from a DEFINE-JSON-TYPE format and massage it
|
||||||
into a DEFCLASS format."
|
into a DEFCLASS format."
|
||||||
(let* ((sans-accessor-maybe (if (and (find :accessor slot-opts)
|
(let* ((required (getf slot-opts :required))
|
||||||
|
(sans-required (alexandria:remove-from-plist slot-opts :required))
|
||||||
|
(sans-initform-maybe (if required
|
||||||
|
(alexandria:remove-from-plist sans-required :initform)
|
||||||
|
sans-required))
|
||||||
|
(sans-accessor-maybe (if (and (find :accessor slot-opts)
|
||||||
(not (getf slot-opts :accessor)))
|
(not (getf slot-opts :accessor)))
|
||||||
(alexandria:remove-from-plist slot-opts :accessor)
|
(alexandria:remove-from-plist sans-initform-maybe :accessor)
|
||||||
slot-opts)))
|
sans-initform-maybe)))
|
||||||
sans-accessor-maybe))
|
sans-accessor-maybe))
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,51 +181,38 @@ CLASS is the class-name; see DEFINE-JSON-TYPE’s docstring about DIRECT-SLOTS."
|
||||||
(append
|
(append
|
||||||
`(defmethod yason:encode-slots progn ((obj ,class)))
|
`(defmethod yason:encode-slots progn ((obj ,class)))
|
||||||
(mapcar (lambda (slot)
|
(mapcar (lambda (slot)
|
||||||
`(when (slot-boundp obj ',(car slot))
|
`(yason:encode-object-element ,(cadr slot) (slot-value obj ',(car slot))))
|
||||||
(yason:encode-object-element ,(cadr slot) (slot-value obj ',(car slot)))))
|
|
||||||
direct-slots)))
|
direct-slots)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;;; Parsing
|
;;; Parsing
|
||||||
;;; ————————————————————————————————————————
|
;;; ————————————————————————————————————————
|
||||||
(defun register-json-type (names direct-superclasses direct-slots context)
|
(defun register-json-type (names direct-slots context)
|
||||||
"Register a JSON node-type. This allows PARSE to recognize the type (and
|
"Register a JSON node-type. This allows PARSE to recognize the type (and
|
||||||
corresponding CLOS class) of a node."
|
corresponding CLOS class) of a node."
|
||||||
(let* ((ctx (parse-context context))
|
(let* ((ctx (parse-context context))
|
||||||
(type-iri (getf (gethash (cadr names) ctx) :id))
|
(type-iri (getf (gethash (cadr names) ctx) :id))
|
||||||
(type-name (or type-iri (cadr names))))
|
(type-name (or type-iri (cadr names))))
|
||||||
(setf (gethash type-name *json-types*)
|
(setf (gethash type-name *json-types*)
|
||||||
(json-type-registry-list names direct-superclasses ctx direct-slots))))
|
(json-type-registry-list names ctx direct-slots))))
|
||||||
|
|
||||||
(defun json-type-registry-list (names direct-superclasses parsed-context direct-slots)
|
(defun json-type-registry-list (names parsed-context direct-slots)
|
||||||
"Return a REGISTER-JSON-TYPE-formed registry entry, a simple list of the form:
|
"Return a REGISTER-JSON-TYPE-formed registry entry, a simple list of the form:
|
||||||
(TYPE-IRI (PROPERTY-NAME SLOT-NAME) ⋯ (PROPERTY-NAME SLOT-NAME))
|
(TYPE-IRI (PROPERTY-NAME SLOT-NAME) ⋯ (PROPERTY-NAME SLOT-NAME))
|
||||||
… where TYPE-IRI is the (hopefully) fully-resolved IRI form of the node-type’s
|
… where TYPE-IRI is the (hopefully) fully-resolved IRI form of the node-type’s
|
||||||
name, though it might be unresolved if context was unprovided or lacking."
|
name, though it might be unresolved if context was unprovided or lacking."
|
||||||
(append
|
(append (list (cons (car names) (cadr names)))
|
||||||
;; The class-name and type-name.
|
(mapcar
|
||||||
(list (cons (car names) (cadr names)))
|
(lambda (slot)
|
||||||
;; Add the class’es direct slots.
|
(when (cadr slot)
|
||||||
(mapcar
|
(let* ((property-name (cadr slot))
|
||||||
(lambda (slot)
|
(slot-name (car slot))
|
||||||
(when (cadr slot)
|
(ctx-item (gethash property-name parsed-context))
|
||||||
(let* ((property-name (cadr slot))
|
(url (or (getf ctx-item :id)
|
||||||
(slot-name (car slot))
|
property-name)))
|
||||||
(ctx-item (gethash property-name parsed-context))
|
(cons url (cons slot-name property-name)))))
|
||||||
(url (or (getf ctx-item :id)
|
direct-slots)))
|
||||||
property-name)))
|
|
||||||
(cons url (cons slot-name property-name)))))
|
|
||||||
direct-slots)
|
|
||||||
;; Add the slots of parent-classes.
|
|
||||||
(reduce (lambda (slots-a slots-b)
|
|
||||||
(append slots-a slots-b))
|
|
||||||
(mapcar (lambda (class-name)
|
|
||||||
(let* ((type-name (class-json-type-name class-name))
|
|
||||||
(type-registry (gethash type-name *json-types*)))
|
|
||||||
(if type-registry
|
|
||||||
type-registry)))
|
|
||||||
direct-superclasses))))
|
|
||||||
|
|
||||||
(defun parse (str)
|
(defun parse (str)
|
||||||
"Parse the JSON-LD document contained in STR."
|
"Parse the JSON-LD document contained in STR."
|
||||||
|
@ -273,10 +240,11 @@ name, though it might be unresolved if context was unprovided or lacking."
|
||||||
(getf val :id))))
|
(getf val :id))))
|
||||||
;; Now, actually parse.
|
;; Now, actually parse.
|
||||||
(let* ((parsed-table (parse-table-inplace table ctx))
|
(let* ((parsed-table (parse-table-inplace table ctx))
|
||||||
(type (identify-json-type table ctx))
|
(type (identify-json-type table ctx rev-ctx))
|
||||||
(type-def (or (gethash type *json-types*)
|
(typedef (gethash type *json-types*)))
|
||||||
(gethash "*" *json-types*))))
|
(if typedef
|
||||||
(parse-table-into-object parsed-table type-def ctx rev-ctx))))
|
(parse-table-into-object parsed-table typedef ctx rev-ctx) ; We prefer this!
|
||||||
|
parsed-table)))) ; … but just in case you wanna use an undefined type…
|
||||||
|
|
||||||
(defun parse-table-inplace (table ctx)
|
(defun parse-table-inplace (table ctx)
|
||||||
"Expand a YASON-parsed JSON-LD node-object in TABLE. That is, replace all
|
"Expand a YASON-parsed JSON-LD node-object in TABLE. That is, replace all
|
||||||
|
@ -311,17 +279,17 @@ CTX is the according parsed-context, and REV-CTX is the reversed
|
||||||
(lambda (property value)
|
(lambda (property value)
|
||||||
(let* ((property-def (assoc property type-def :test #'equal))
|
(let* ((property-def (assoc property type-def :test #'equal))
|
||||||
(slot-name (second property-def))
|
(slot-name (second property-def))
|
||||||
(etc-value (slot-value obj '.etc)))
|
(etc-value (slot-value obj 'etc)))
|
||||||
(if property-def
|
(if property-def
|
||||||
(setf (slot-value obj slot-name) value)
|
(setf (slot-value obj slot-name) value)
|
||||||
(setf (slot-value obj '.etc)
|
(setf (slot-value obj 'etc)
|
||||||
(append etc-value
|
(append etc-value
|
||||||
(list (cons property value)))))))
|
(list (cons property value)))))))
|
||||||
table)
|
table)
|
||||||
(setf (slot-value obj '@context) (gethash "@context" table))
|
(setf (slot-value obj '@context) (gethash "@context" table))
|
||||||
obj))
|
obj))
|
||||||
|
|
||||||
(defun identify-json-type (table ctx)
|
(defun identify-json-type (table ctx rev-ctx)
|
||||||
"Given an parsed JSON-LD object’s hash-TABLE, return the name/IRI of the
|
"Given an parsed JSON-LD object’s hash-TABLE, return the name/IRI of the
|
||||||
JSON-type that best suits the object — using the types registered into
|
JSON-type that best suits the object — using the types registered into
|
||||||
*JSON-TYPES* with REGISTER-JSON-TYPE."
|
*JSON-TYPES* with REGISTER-JSON-TYPE."
|
||||||
|
@ -329,13 +297,6 @@ JSON-type that best suits the object — using the types registered into
|
||||||
(or (getf (gethash type ctx) :id)
|
(or (getf (gethash type ctx) :id)
|
||||||
type)))
|
type)))
|
||||||
|
|
||||||
(defun class-json-type-name (class-name)
|
|
||||||
"Return the name (IRI) of a registered JSON-type its CLOS class’es name."
|
|
||||||
(loop for iri being the hash-keys in *json-types*
|
|
||||||
for registry being the hash-values in *json-types*
|
|
||||||
if (eq class-name (caar registry))
|
|
||||||
return iri))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;;; Context-parsing
|
;;; Context-parsing
|
||||||
|
|
Ŝarĝante…
Reference in New Issue