37

Jeison: An Emacs library for declarative JSON parsing

 4 years ago
source link: https://www.tuicool.com/articles/UvmMnyF
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

jeison

Jeison is a library for transforming JSON objects (or alist s) into EIEIO objects.

Installation

TODO: add to MELPA

Main idea

Main idea behind jeison is to create an easier way of dealing with deep JSON objects that contain lots and lots of information. Converting those into your own internal structures (or simply working with them) might become a burden pretty fast. Jeison helps to avoid writing tons of boilerplate code and simply declare how to find your data inside of JSON objects.

In order to use jeison one must first define a class using jeison-defclass . It works absolutely like defclass from EIEIO, but allows you to add another property to the class slots: :path . Here is an example:

(jeison-defclass my-first-jeison-class nil
                 ((name :initarg :name :path (personal name last))
                  (job :initarg :job :path (job title))))

Defining class like this we tell jeison where it should fetch values for name and job slots. In JavaScript syntax, it would look similar to the following code:

name = json['personal']['name']['last'];
job = json['job']['title'];

The next step would be transforming actual JSON object into EIEIO object. This is done by function jeison-read . Let's assume that we have the following JSON object in the Emacs Lisp variable json-person :

// json-person
{
  "personal": {
    "address": { },
    "name": {
      "first": "John",
      "last": "Johnson"
    }
  },
  "job": {
    "company": "Good Inc.",
    "title": "CEO"
  },
  "skills": [ ]
}

Calling jeison-read will produce the following results:

(setq person (jeison-read my-first-jeison-class json-person))
(oref person name) ;; => "Johnson"
(oref person job)  ;; => "CEO"

jeison-read also accepts optional third argument that contains a path to sub-object that we should read. For example, we can use jeison just to get one value from JSON object:

(jeison-read 'string json-person '(personal name last)) ;; => "Johnson"

Features

Default paths

In many cases, classes that we use in code significantly resemble the structure of the source JSON object. This means that :path will have same value as the corresponding slot's name. In order to avoid this duplication, jeison allows to omit :path property and use slot's name as a default:

(jeison-defclass default-path-class nil
                 ((first) (last) (full)))
// json-name
{
  "name": {
    "first": "John",
    "last": "Johnson",
    "full": "John Johnson"
  }
}
(setq name (jeison-read default-path-class json-name '(name)))
(oref name first) ;; => "John"
(oref name last)  ;; => "Johnson"
(oref name full)  ;; => "John Johnson"

Type checks

Jeison checks that type that user wants to read from JSON object matches the one that was actually found:

(jeison-read 'string '((x . 1)) 'x) ;; => (jeison-wrong-parsed-type string 1)

Nested objects

EIEIO allows annotating class slots with types . Besides checking the type of the found object, Jeison uses this information for constructing nested objects .

Let's consider the following example:

(jeison-defclass jeison-person nil
                 ((name :initarg :name :path (personal name last))
                  (job :initarg :job :type jeison-job)))

(jeison-defclass jeison-job nil
                 ((company :initarg :company)
                  (position :initarg :position :path title)
                  (location :initarg :location :path (location city))))

In this example, jeison-person has a slot that has a type of another jeison class: jeison-job . As the result of this hierarchy, for the next JSON object:

// json-person
{
  "personal": {
    "address": { },
    "name": {
      "first": "John",
      "last": "Johnson"
    }
  },
  "job": {
    "company": "Good Inc.",
    "title": "CEO",
    "location": {
      "country": "Norway",
      "city": "Oslo"
    }
  },
  "skills": [ ]
}

We have these results:

(setq person (jeison-read jeison-person json-person))
(oref person name) ;; => "Johnson"
(setq persons-job (oref person job))
(oref persons-job company) ;; => "Good Inc."
(oref persons-job position) ;; => "CEO"
(oref persons-job location) ;; => "Oslo"

Lists

Another JSON's basic data type is array . Jeison can deal with arrays in two ways:

  • ignore : jeison can behave like it is a normal value and do nothing about it
(jeison-read
 t "{\"numbers\": [1, 2, 10, 40, 100]}" 'numbers) ;; => [1 2 10 40 100]

It is a vector by the given path and jeison can simply return it.

However, sometimes we might want to have a list or check that all elements have certain type.

  • type-specific processing : jeison can process JSON arrays based on the expected type
(jeison-read '(list-of integer)
             "{\"numbers\": [1, 2, 10, 40, 100]}"
             'numbers) ;; => (1 2 10 40 100)

EIEIO defines a very handy type list-of that we can use for processing array elements and checking that they match the corresponding type.

This mechanism also allows jeison to parse lists of nested objects . Let's continue our "John Johnson" example and add skill processing:

(jeison-defclass jeison-person nil
                 ((name :initarg :name :path (personal name last))
                  (job :initarg :job :type jeison-job)
                  (skills :initarg :skills :type (list-of jeison-skill))))

(jeison-defclass jeison-job nil
                 ((company :initarg :company)
                  (position :initarg :position :path title)
                  (location :initarg :location :path (location city))))
                  
(jeison-defclass jeison-skill nil
                 ((name :initarg :name :type string)
                  (level :initarg :level :type integer)))

For the following JSON object:

// json-person
{
  "personal": {
    "address": { },
    "name": {
      "first": "John",
      "last": "Johnson"
    }
  },
  "job": {
    "company": "Good Inc.",
    "title": "CEO",
    "location": {
      "country": "Norway",
      "city": "Oslo"
    }
  },
  "skills": [
    {
      "name": "Programming",
      "level": 9
    },
    {
      "name": "Design",
      "level": 4
    },
    {
      "name": "Communication",
      "level": 1
    }
  ]
}

jeison produces these results:

(setq person (jeison-read jeison-person json-person))
(oref person name) ;; => "Johnson"
(mapcar
 (lambda (skill)
   (format "%s: %i" (oref skill name) (oref skill level)))
 (oref person skills)) ;; => ("Programming: 9" "Design: 4" "Communication: 1")

Indexed elements

Sometimes, though, it is not required to process the whole list. Sometimes we want just one element, especially in case of heterogeneous arrays. For this use case, jeison supports indices in its :path properties.

Here is an example:

// json-company
{
  "company": {
    "name": "Good Inc.",
    "CEOs": [
      {
        "name": {
          "first": "Gunnar",
          "last": "Olafson"
        },
        "founder": true
      },
      {
        "name": "TJ-B",
        "cool": true
      },
      {
        "name": {
          "first": "John",
          "last": "Johnson"
        }
      }
    ]
  }
}
(jeison-read 'boolean json-company '(company CEOs 0 founder)) ;; => t

Unlike arguments to elt function, indices can be negative . Negative indices have the same semantics as in Python language (enumeration from the end):

(jeison-read 'string json-company '(company CEOs -1 name last)) ;; => "Johnson"
(jeison-read 'boolean json-company '(company CEOs -2 cool)) ;; => t

Turning regular classes into jeison classes

Additionally, jeison has a feature of transforming existing classes declared with defclass macro into jeison classes:

(defclass usual-class nil ((x) (y)))
(jeisonify usual-class)

(setq parsed (jeison-read usual-class "{\"x\": 10, \"y\": 20}"))
(oref parsed x) ;; => 10
(oref parsed y) ;; => 20

NOTE 1even if defclass included :path properties, jeison will still use default paths.

NOTE 2 jeison hacks into the structure of EIEIO classes and their slots. If the modified class relies on the purity of slot properties or class options, don't use jeisonify functionality and create a new class instead.

Development

Jeison uses cask . After the installation of cask , install all dependencies and run tests:

$ cask install
$ cask exec ert-runner

Contribute

All contributions are most welcome!

It might include any help: bug reports, questions on how to use it, feature suggestions, and documentation updates.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK