emacsでjqをJSONファイルのformatterとして使う

emacsでjqをJSONファイルのformatterとして使う。方法は2つ。

  • shell-command-on-regionを使う
  • formatter専用の関数を作る

shell-command-on-regionを使う

M-|shell-command-on-regionという関数がbindされている。これに引数を与えてあげると、現在選択されているリージョンを上書きしてくれる。

shell-command-on-region の定義は以下の様になっている(引数の部分のみ)。

(defun shell-command-on-region (start end command
                      &optional output-buffer replace
                      error-buffer display-error-buffer
                      region-noncontiguous-p)

  (interactive (let (string)
         (unless (mark)
           (user-error "The mark is not set now, so there is no region"))
         ;; Do this before calling region-beginning
         ;; and region-end, in case subprocess output
         ;; relocates them while we are in the minibuffer.
         (setq string (read-shell-command "Shell command on region: "))
         ;; call-interactively recognizes region-beginning and
         ;; region-end specially, leaving them in the history.
         (list (region-beginning) (region-end)
               string
               current-prefix-arg
               current-prefix-arg
               shell-command-default-error-buffer
               t
               (region-noncontiguous-p))))
...
)

ここでreplaceの引数にnon-nilな値を与えてあげれば良い。例えば以下の様な形で使う。

C-u - 1 M-|
# minibufferで jq . -S

formatter専用の関数を作る

shell-command-on-region を使うのも便利なんだけれど。呼び出しが頻繁になってきた場合に以下の様な点で困る。

  • エラーのときにもエラーメッセージで置換されてしまう
  • そもそもjqとコマンドを打つのがめんどくさい

例えば、javascript-modeを拡張したjson-modeを自分で作ってあげて、拡張子が.jsonのファイルを開いた場合には、この自作のjson-modeで開くようにする。 そして、この自作したmodeのkey-mapにテキトウなformmatter専用の関数を割り当てる。

(require 'derived)
(define-derived-mode my:json-mode javascript-mode "json mode")

(defun my:json-mode-setup ()
  ;; 個人的にはバッファの保存は自動で行っているので`C-x C-s`に割り当ててしまっている
  (define-key my:json-mode-map (kbd "C-x C-s") 'my:jsonfmt)
  )

(add-to-list 'auto-mode-alist '("\\.json$" . my:json-mode))
(add-hook 'my:json-mode-hook 'my:json-mode-setup)

ここで、my:jsonfmtが自作したformatter関数。定義は以下の様な感じ。

(defun my:jsonfmt (beg end)
  (interactive "r")
  (unless (region-active-p)
    (setq beg (point-min))
    (setq end (point-max))
    )
  (my:execute-formatter-command "jq" "jq . -S -e" beg end))

(defun my:get-fresh-buffer-create (name)
  (let ((buf (get-buffer-create name)))
    (with-current-buffer buf
      (setf (buffer-string) ""))
    buf
    ))

(defun my:execute-formatter-command (cmd-name cmd beg end)
  (cond ((executable-find cmd-name)
         (save-excursion
           (save-restriction
             (narrow-to-region beg end)
             (let ((buf (my:get-fresh-buffer-create (format "*%s*" cmd-name)))
                   (err-buf (my:get-fresh-buffer-create (format "*%s error*" cmd-name))))
               (let ((status
                      ;; xxx
                      (flet ((display-message-or-buffer (&rest args) nil))
                        (shell-command-on-region (point-min) (point-max) cmd buf nil err-buf)
                        )))
                 (cond ((= 0 status)
                        (let ((replaced (with-current-buffer buf (buffer-string))))
                          (cond ((string= replaced "")
                                 (message "succeeded with no output"))
                                (t
                                 (delete-region (point-min) (point-max))
                                 (insert replaced)))))
                       (t (message (with-current-buffer err-buf (buffer-string))))))))))
        (t (message (format "%s is not found" cmd-name)))))

(メッセージを抑制するためにfletを使っているのだけれど。お行儀が良くない)

marshmallow-polyfieldを使ってoneOf的な構造のdataを扱う

はじめに

例えば、以下のよう1つのfieldに複数の形状の値が入ることがある。そして、その形状を決めるためにtypeなどfieldを含まれているJSONがあるとする。 以下の様な感じ(下の例では、personとgroupという2つの形状がobに入る可能性がある)。

{
  "ob": {
    "age": 20,
    "name": "foo"
  },
  "type": "person"
}

もしくはこう。

{
  "ob": {
    "name": "A",
    "members": [
      {
        "age": 20,
        "name": "foo"
      }
    ]
  },
  "type": "group"
}

それぞれ、typeで判別できるけれど。これを良い感じにmarshmallowでserialize,deserializeしたいという話し。

準備

事前に以下が必要。marshmallow-polyfieldを使う。

$ pip install marshmallow-polyfield

方法

以下の様な感じ。

from marshmallow import Schema, fields
from marshmallow_polyfield import PolyField


class Person(Schema):
    name = fields.String(required=True)
    age = fields.Integer(required=True)


class Group(Schema):
    name = fields.String(required=True)
    members = fields.List(fields.Nested(Person()), required=True)


def selector_for_deserialize(d, parent):
    if parent.get("type") == "group":
        return Group()
    else:
        return Person()


def selector_for_serialize(ob, parent):
    if "members" in ob:
        parent["type"] = "group"
        return Group()
    else:
        parent["type"] = "person"
        return Person()


class S(Schema):
    type = fields.String(required=True)
    ob = PolyField(
        serialization_schema_selector=selector_for_serialize,
        deserialization_schema_selector=selector_for_deserialize,
        required=True
    )


print(S().load({"ob": {"name": "foo", "age": 20}, "type": "person"}))
print(S().load({"ob": {"name": "A", "members": [{"name": "foo", "age": 20}]}, "type": "group"}))
print(S().dump({"ob": {"name": "foo", "age": 20}}))
print(S().dump({"ob": {"name": "A", "members": [{"name": "foo", "age": 20}]}}))

# UnmarshalResult(data={'ob': {'name': 'foo', 'age': 20}, 'type': 'person'}, errors={})
# UnmarshalResult(data={'ob': {'name': 'A', 'members': [{'name': 'foo', 'age': 20}]}, 'type': 'group'}, errors={})
# MarshalResult(data={'ob': {'name': 'foo', 'age': 20}, 'type': 'person'}, errors={})
# MarshalResult(data={'ob': {'name': 'A', 'members': [{'name': 'foo', 'age': 20}]}, 'type': 'group'}, errors={})