File size: 15,166 Bytes
43203b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | ;;;; -*- Mode: LISP; Syntax: ANSI-COMMON-LISP; Base: 10; Package: JSON -*-
;;;; Copyright (c) 2006-2008 Henrik Hjelte
;;;; All rights reserved.
;;;; See the file LICENSE for terms of use and distribution.
(in-package :json)
(defvar *class-registry* nil
"A list of anonymous fluid classes, one member for every distinct
combination of direct superclasses.")
(defmacro with-local-class-registry ((&key inherit) &body body)
"Run BODY in a dynamic environment where *CLASS-REGISTRY* is a
temporary local list. If :INHERIT is non-null, the local registry
shall initially have the same content as the exterior *CLASS-REGISTRY*,
otherwise it shall be NIL."
`(let ((*class-registry*
,(if inherit
`(copy-alist
,(if (eql inherit t) '*class-registry* inherit)))))
,@body))
(defun clear-class-registry ()
"Reset the *CLASS-REGISTRY* to NIL."
(setq *class-registry* nil))
(defun find-class* (class-designator)
"Like FIND-CLASS, but allow self-designating classes for the
argument, and assert that the resulting class is a STANDARD-CLASS."
(let ((class
(if (typep class-designator 'class)
class-designator
(or (find-class class-designator nil)
(error 'cell-error :name 'class-designator)))))
(check-type class standard-class)
class))
(eval-when (:load-toplevel :compile-toplevel :execute)
(defclass fluid-class (standard-class) ()
(:documentation "A class to whose instances arbitrary new slots may
be added on the fly.")))
(defmethod add-direct-subclass ((superclass class)
(subclass fluid-class))
"Fluid classes are thought to be anonymous, and so should not be
registered in the superclass."
(declare (ignore superclass subclass))
(values))
(defmethod remove-direct-subclass ((superclass class)
(subclass fluid-class))
"Fluid classes are thought to be anonymous, and so should not be
registered in the superclass."
(declare (ignore superclass subclass))
(values))
(eval-when (:compile-toplevel :load-toplevel :execute)
(defmethod validate-superclass ((class fluid-class)
(superclass standard-class))
"Any fluid class is also a standard class."
t))
(eval-when (:compile-toplevel :load-toplevel :execute)
(defclass fluid-object (standard-object) ()
(:documentation "Any instance of a fluid class.")
(:metaclass fluid-class)))
(defmethod compute-class-precedence-list ((class fluid-class))
"Objects of fluid classes are fluid objects."
(loop for c in (call-next-method)
with standard = (find-class 'standard-object)
and fluid = (find-class 'fluid-object)
if (eq c fluid) do (setq fluid nil)
else if (and (eq c standard) fluid) collect fluid
collect c))
(defmethod slot-missing ((class fluid-class) (object fluid-object) name
(op (eql 'slot-boundp)) &optional new-value)
"A missing slot in a fluid class is considered unbound."
(declare (ignore class object name op new-value))
nil)
(defmethod slot-missing ((class fluid-class) (object fluid-object) name
(op (eql 'slot-makunbound)) &optional new-value)
"A missing slot in a fluid class is considered unbound."
(declare (ignore class name op new-value))
object)
(defmethod slot-missing ((class fluid-class) (object fluid-object) name
(op (eql 'slot-value)) &optional new-value)
"On attempting to get the value of a missing slot, raise a
slot-unbound error."
(declare (ignore op new-value))
(slot-unbound class object name))
(defmethod slot-missing ((class fluid-class) (object fluid-object) name
(op (eql 'setf)) &optional new-value)
"On attempting to set a missing slot, add the slot to the class,
then repeat SETF."
(reinitialize-instance class
:direct-superclasses
(class-direct-superclasses class)
:direct-slots
(let ((extant-slots (class-direct-slots class)))
(if (null extant-slots)
`((:name ,name))
(loop for slots on extant-slots
for slot-name = (slot-definition-name (car slots))
if (endp (cdr slots))
collect `(:name ,slot-name)
and collect `(:name ,name)
else
collect `(:name ,slot-name)))))
(make-instances-obsolete class)
(setf (slot-value object name) new-value))
(defun ensure-fluid-class-with-slots (slots superclasses
&optional extant-class)
"Create or update a fluid class, ensuring that it has (at least) all
the given SLOTS and SUPERCLASSES."
(flet ((extant-slot-p (name)
(lambda (class)
(loop for slot in (class-slots class)
thereis (eq (slot-definition-name slot) name))))
(slot-init (name) `(:name ,name)))
(if extant-class
(let* ((extant-superclasses
(class-direct-superclasses extant-class))
(new-superclasses
(remove-if (lambda (class)
(find class extant-superclasses))
superclasses))
(extant-slots
(mapcar #'slot-definition-name
(class-direct-slots extant-class)))
(new-slots
(remove-if (lambda (name)
(let ((containing (extant-slot-p name)))
(or (funcall containing extant-class)
(some containing superclasses))))
slots)))
(if (or new-superclasses new-slots)
(make-instances-obsolete
(reinitialize-instance extant-class
:direct-superclasses
(append extant-superclasses new-superclasses)
:direct-slots
(mapcar #'slot-init (nconc extant-slots new-slots)))))
extant-class)
(make-instance 'fluid-class
;; CMUCL's PCL implementation tries to perform (SETF
;; KERNEL::FIND-CLASS) on dynamically created fluid classes
;; which leads to an error if the class has a NIL name.
:name #-cmu nil #+cmu (gensym "FLUID")
:direct-superclasses superclasses
:direct-slots
(loop for slot in slots
unless (some (extant-slot-p slot) superclasses)
collect (slot-init slot))))))
(defun make-and-populate-instance (class bindings)
"Make an instance of the given CLASS, and set its slots to given
values. BINDINGS must be a list of pairs whose CARs are slot names
and CDRs are the respective values. If no slot of a given name is
defined in the CLASS, the corresponding value is discarded."
(let ((object (make-instance class)))
(if (typep class 'fluid-class)
(loop for slot in (class-direct-slots class)
for slot-name = (slot-definition-name slot)
if (and (slot-boundp object slot-name)
(null (slot-value object slot-name)))
do (slot-makunbound object slot-name)))
(loop for (slot . value) in bindings
if (slot-exists-p object slot)
do (setf (slot-value object slot) value))
object))
(defgeneric make-object (bindings class &optional superclasses)
(:documentation "If CLASS is not NIL, create an instance of that
class. Otherwise, create a fluid object whose class has the given
SUPERCLASSES (null list by default). In either case, populate the
resulting object using BINDINGS (an alist of slot names and
values)."))
(defmethod make-object (bindings (class (eql nil))
&optional (superclasses nil))
"Create a FLUID-OBJECT with the slots given by BINDINGS and whose
class has all the given SUPERCLASSES. If the current *CLASS-REGISTRY*
has a member with exactly the same direct superclasses, it is updated
to include all the given slots. Otherwise, a new FLUID-CLASS is
allocated and added to the *CLASS-REGISTRY*."
(let* ((superclasses
(mapcar #'find-class*
(if (find-if (lambda (c) (subtypep c 'fluid-object))
superclasses)
superclasses
(append superclasses (list 'fluid-object)))))
(extant-class-etc
(member superclasses *class-registry*
:test #'equal :key #'class-direct-superclasses))
(extant-class (car extant-class-etc))
(updated-class
(ensure-fluid-class-with-slots
(mapcar #'car bindings) superclasses extant-class)))
(if extant-class
(if (not (eq extant-class updated-class))
(setf (car extant-class-etc) updated-class))
(push updated-class *class-registry*))
(make-and-populate-instance updated-class bindings)))
(defmethod make-object (bindings class &optional superclasses)
"If the CLASS is explicitly specified, just create and populate an
instance, discarding any of the BINDINGS which do not correspond to
the slots of that CLASS."
(declare (ignore superclasses))
(let ((class (find-class* class)))
(make-and-populate-instance class bindings)))
(defmethod make-object (bindings (class (eql (find-class 'cons)))
&optional superclasses)
"If the CLASS is given as 'CONS, return the BINDINGS as alist."
(declare (ignore superclasses))
(copy-seq bindings))
(defmethod make-object (bindings (class (eql (find-class 'list)))
&optional superclasses)
"If the CLASS is given as 'LIST, return the BINDINGS as plist."
(declare (ignore superclasses))
(loop for (key . value) in bindings
collect key collect value))
(defmethod make-object (bindings (class (eql (find-class 'hash-table)))
&optional superclasses)
"If the CLASS is given as 'HASH-TABLE, return the BINDINGS as hash
table."
(declare (ignore superclasses))
(let ((table (make-hash-table :test #'equal)))
(loop for (key . value) in bindings
do (setf (gethash key table) value))
table))
(defmethod make-object (bindings (class symbol) &optional superclasses)
"If the CLASS is given as a symbol, find it and resort to the usual
procedure."
(declare (ignore superclasses))
(make-object bindings (find-class class)))
(defun max-package (symbols &key ((:initial-value package)
(find-package '#:common-lisp)))
"Try to find a package P such that the names of the given SYMBOLS,
when interned in P, yield the same symbols. If no such package
exists, return an unspecific value and issue a warning."
(labels ((symbol-in-package-p (symbol)
(eq (find-symbol (symbol-name symbol) package) symbol))
(update-package-for-symbol (symbol)
(if (not (symbol-in-package-p symbol))
(setq package (symbol-package symbol))))
(check-symbol (symbol)
(if (not (symbol-in-package-p symbol))
(warn "Symbol ~S cannot be found in apparent package ~S."
symbol package))))
(mapc #'update-package-for-symbol symbols)
(mapc #'check-symbol symbols)
package))
(defun package-name* (package)
"Same as PACKAGE-NAME, but ensure that the result is a symbol."
(let ((name (package-name package)))
(if (stringp name) (make-symbol name) name)))
(defvar *prototype-name* 'prototype
"The key of the prototype field in a JSON Object, and the name of a
slot in a Lisp object which accepts its prototype.")
(defclass prototype ()
((lisp-class :initarg :lisp-class :reader lisp-class)
(lisp-superclasses :initarg :lisp-superclasses :reader lisp-superclasses)
(lisp-package :initarg :lisp-package :reader lisp-package))
(:default-initargs :lisp-class nil :lisp-superclasses nil
:lisp-package nil)
(:documentation "A PROTOTYPE contains metadata for an object's class
in a format easily serializable to JSON: either the name of the class
as a string or (if it is anonymous) the names of the superclasses as a
list of strings; and the name of the Lisp package into which the names
of the class's slots and the name of the class / superclasses are to
be interned."))
(defmethod make-load-form ((prototype prototype) &optional environment)
(declare (ignore environment))
`(make-instance 'prototype
,@(if (slot-boundp prototype 'lisp-class)
`(:lisp-class ,(lisp-class prototype)))
,@(if (slot-boundp prototype 'lisp-superclasses)
`(:lisp-superclasses ,(lisp-superclasses prototype)))
,@(if (slot-boundp prototype 'lisp-package)
`(:lisp-package ,(lisp-package prototype)))))
(defgeneric make-object-prototype (object &optional slot-names)
(:documentation "Return a PROTOTYPE describing the OBJECT's class or
superclasses, and the package into which the names of the class /
superclasses and of the OBJECT's slots are to be interned."))
(defmethod make-object-prototype (object &optional slot-names)
"Return a PROTOTYPE describing the OBJECT's class or superclasses,
and the package into which the names of the class / superclasses and
of the OBJECT's slots are to be interned."
(let* ((class (class-of object))
(class-name (if class (class-name class)))
(superclass-names
(if (not class-name)
(set-difference
(mapcar #'class-name (class-direct-superclasses class))
'(standard-object fluid-object))))
(package
(max-package (append superclass-names slot-names)
:initial-value (if class-name
(symbol-package class-name)
(find-package '#:common-lisp)))))
(make-instance 'prototype
:lisp-class class-name
:lisp-superclasses superclass-names
:lisp-package (package-name* package))))
(defmethod make-object-prototype ((class-name symbol) &optional slot-names)
"Return a PROTOTYPE of an object of the class named by CLASS-NAME."
(let ((package
(max-package slot-names
:initial-value (symbol-package class-name))))
(make-instance 'prototype
:lisp-class class-name
:lisp-package (package-name* package))))
(defmethod make-object-prototype ((object prototype) &optional slot-names)
"Prototypes are not to be given their own prototypes, otherwise we
would proceed ad malinfinitum."
(declare (ignore object slot-names))
nil)
(defun maybe-add-prototype (object prototype)
"If the PROTOTYPE is not NIL, and the OBJECT has a slot to accept it,
do set it. Return OBJECT."
(if (and prototype (slot-exists-p object *prototype-name*))
(setf (slot-value object *prototype-name*) prototype))
object)
(defun map-slots (function object)
"Call FUNCTION on the name and value of every bound slot in OBJECT."
(loop for slot in (class-slots (class-of object))
for slot-name = (slot-definition-name slot)
if (slot-boundp object slot-name)
do (funcall function slot-name (slot-value object slot-name))))
|