Как преобразовать json-строку в «сложный» объект CLOS с помощью библиотеки cl-json? - PullRequest
1 голос
/ 27 сентября 2019

Я исхожу из этого вопроса Как преобразовать json-строку в объект CLOS с помощью библиотеки cl-json? , в которой ответ предоставляет способ создания экземпляра объекта указанного класса с использованием входного json.

К сожалению, в ответе пропущена рекурсивность, поскольку слоты объекта верхнего класса (которые типизируются для другого класса с помощью: type внутри defclass) не получают экземпляр этого класса.

Какможно ли изменить ответ, чтобы добиться этого (мне пока не удобно работать с понятиями mop).
В качестве альтернативы, пока не существует библиотеки, которая могла бы выполнять такую ​​реализацию моих классов из данных JSON в общем и автоматическом режиме.Кстати?

Я думал, что это будет очень распространенное использование JSON.

РЕДАКТИРОВАТЬ: Эта библиотека https://github.com/gschjetne/json-mop, кажется, делает свою работу, но она использует метакласс и специальныеключевые слова опций слотов, которые делают defclass нестандартным для сторонних классов.

Ответы [ 2 ]

1 голос
/ 27 сентября 2019

Библиотека sanity-clause позволяет взять json с вложенными объектами (я не пробовал больше уровней, чем примеры readme) и превращать их в объекты.Кроме того, он выполняет проверку данных.

Его пример немного отличается от вашего.Вы создали объекты, закодировали их в json и попытались снова их декодировать.

Пример предложения sanity начинается с json:

{
  "title": "Swagger Sample App",
  "description": "This is a sample server Petstore server.",
  "termsOfService": "http://swagger.io/terms/",
  "contact": {
    "name": "API Support",
    "url": "http://www.swagger.io/support",
    "email": "support@swagger.io"
  },
  "license": {
    "name": "Apache 2.0",
    "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
  },
  "version": "1.0.1"
}

Он имеет три соответствующих класса: contact, license иинформация (верхний уровень).В конце мы получаем информационный объект, где его контактные и лицензионные слоты имеют соответствующие типы:

(describe #<INFO-OBJECT {1006003ED3}>)
#<INFO-OBJECT {1006003ED3}>
  [standard-object]

Slots with :INSTANCE allocation:
  TITLE                          = "Swagger Sample App"
  DESCRIPTION                    = "This is a sample server Petstore server."
  TERMS-OF-SERVICE               = "http://swagger.io/terms/"
  CONTACT                        = #<CONTACT-OBJECT {1005FFDB43}>
  LICENSE                        = #<LICENSE-OBJECT {10060021F3}>
  VERSION                        = "1.0.1"

После объявления класса этот объект был загружен с

(let ((v2-info (alexandria:read-file-into-string "v2-info.json")))
  (sanity-clause:load (find-class 'info-object) (jojo:parse v2-info :as :alist)))
0 голосов
/ 30 сентября 2019

Круговые структуры

Прежде всего, в примере, который вы связали, у меня возникает ошибка, когда я пытаюсь создать экземпляр "Алиса", так как его слот :partner имеет значение nil, но объявленный типэто person.Либо вам нужно разрешить для person быть ноль с типом (or null person), либо вы должны обеспечить, чтобы все :partner эффективно указывали на экземпляры person.

Но даже если person может быть nil, могут быть случаи, когда Алиса и Боб оба являются партнерами друг друга;это легко настроить, если человек может быть nil, но в случае, если вы хотите принудительно назначить не ноль человека, вам нужно будет создать его экземпляр следующим образом: сначала вы выделяете оба экземпляра, а затем инициализируете их как обычно:

(flet ((person () (allocate-instance (find-class 'person))))
  (let ((alice (person)) (bob (person)))
    (setf *mypartner* (initialize-instance alice
                                           :name "Alice"
                                           :address "Regent Street, London"
                                           :phone "555-99999"
                                           :color "blue"
                                           :partner bob
                                           :employed-by *mycompany*))
    (setf *myperson* (initialize-instance bob
                                          :name "Bob"
                                          :address "Broadway, NYC"
                                          :phone "555-123456"
                                          :color "orange"
                                          :partner alice
                                          :employed-by *mycompany*))))

В любом случае, если у вас круговые структуры данных, экспорт завершится неудачно с переполнением стека (бесконечная рекурсия).Если вы подозреваете, что у вас могут быть циклические структуры данных, вам нужно хэшировать их с идентификатором при первом посещении и кодировать ссылку на соответствующий идентификатор при следующем посещении.

Например,Возможный способ закодировать эти перекрестные ссылки - добавить "id" ко всем объектам, на которые есть перекрестные ссылки из других источников, и разрешить { "ref" : <id> } вместо фактических значений:

[ {"id" : 0,
   "name" : "Alice",
   "partner" : { "id" : 1,
                 "name" : "Bob",
                 "partner" : { "ref" : 0 }},
   { "ref" : 1 } ]

Это, однако, должно бытьсделано на промежуточном уровне, не жестко закодировано для всех классов.

Самоанализ

Если вы хотите использовать MOP для автоматического сопоставления имен слотов с ключами json, вы можете определить следующие вспомогательные функции:

(defun decompose-class (class)
  (mapcar (lambda (slot)
            (let* ((slot-name (closer-mop:slot-definition-name slot)))
              (list slot-name
                    (string-downcase slot-name)
                    (closer-mop:slot-definition-type slot))))
          (closer-mop:class-direct-slots (find-class class))))

(defun decompose-value (value)
  (loop
     for (name str-name type) in (decompose-class (class-of value))
     for boundp = (slot-boundp value name)
     collect (list name
                   str-name
                   type
                   boundp
                   (and boundp (slot-value value name)))))

Они зависят от closer-mop и извлекают интересную информацию из класса или данного объекта.Например, вы можете извлечь имена и типы для класса, что полезно для знания того, как кодировать или декодировать значение определенного класса:

(decompose-class 'person)
=> (("name" T)
    ("address" T)
    ("phone-number" T)
    ("favorite-color" T)
    ("partner" PERSON)
    ("employed-by" COMPANY))

Аналогично, вы можете захотеть иметь ту же информациюдля объекта вместе с конкретными значениями, связанными со слотом, когда он связан со слотом:

(decompose-value *myperson*)
=> (("name" T T "Bob")
    ("address" T T "Broadway, NYC")
    ("phone-number" T T "555-123456")
    ("favorite-color" T T "orange")
    ("partner" PERSON T #<PERSON {100E12CB53}>)
    ("employed-by" COMPANY T #<COMPANY {100D6B3533}>))

Один из возможных способов декодирования объектов заключается в следующем, где "key/values" - это либо необработанный json, либоуже декодированный список ассоциаций в Лиспе, а finder - это функция, которая знает, как получить доступ к key/values, в частности, перекрестные ссылки:

;; finder is responsible for resolving cross-references
(defun decode-as-object (class key/values finder)
  (flet ((resolve (name) (funcall finder name key/values)))
    (let ((object (allocate-instance (find-class class))))
      (dolist (tuple (decompose-class class) (shared-initialize object ()))
        (destructuring-bind (name strname class) tuple
          (multiple-value-bind (value boundp) (resolve strname)
            (when boundp
              (setf (slot-value object name)
                    (if (eql class t)
                        value
                        (decode-as-object class value finder))))))))))
...