Racc で JSON スキーマを生成する DSL を書いてみた

Ruby には Yacc ならぬ Racc というものがあり、Yacc のようなパーサをつくることができます。 Yacc のことやプログラミング言語をつくることに詳しいわけではないのですが、ちょっとお試しをしてみました。 まがりなりにも独自の言語(といっても JSON schema をつくるためだけのもの)ができたことになります。

動機

JSON には、余分なカンマを打てないこと、コメントアウトができないことなど、独特の不便な点があります。また、フィールド(項目)名を " で囲まなければいけないことも、なんとなく私には億劫だったりもします。

もちろん、これらのことは、JSONYAML で書いて変換するようにしてしまえば問題はないとも言えます。すべての JSONYAML ですので、これは可能なことです。単に YAML を読み込んで、JSON で出力すればいいだけです。

わざわざ新しい言語を覚えなくてもいいのが JSONYAML の利点ですから、これで解決できるなら、そうしたほうがいいと言えそうです。

しかし、JSONYAML の形式で表現しなければいけないことからくる面倒くささがもしあるなら、そこには別言語を試してみる余地もあるのかもしれません。

文法

といった名目で(?)、JSON schema の内容を簡潔に表現する新しい DSL をつくってみました。その文法を簡単にご紹介します。

データ型の表現

データ型は <> で囲んで表現します。

<object> { }

こう書くと

{ "type": "object" }

と同等になります。

プロパティの表現

プロパティは、先頭に + もしくは - をつけて表現します。 +required を表します。 ですので、

<object> {
  + name <string>
  + price <number>
}

と書くと、

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "price": {
      "type": "number"
    }
  },
  "required": [
    "name",
    "price"
  ]
}

と同等になります。

ボキャブラリの表現

フィールド以外の JSON schema ボキャブラリは @ をつけて表します。 なお、何が定義されたボキャブラリであるかなどは、パーサはまったく関知しません。

<object> {
  + email <string> { @format: "email" }
  @additional-properties: false
}

たとえばこのように書くと

{
  "type": "object",
  "properties": {
    "email": {
      "type": "string",
      "format": "email"
    }
  },
  "required": [
    "email"
  ],
  "additionalProperties": false
}

こうなります。

キーワードの表現

$id$schema などのキーワードは、基本的に $ をつけてそのまま書きます。

<object> {
  $schema: "http://..."
}

と書けば

{
  "type": "object",
  "$schema": "http://..."
}

ということになります。

ただし、$id{} の外にも書けるようになっています。これは、id を名前のように目立たせたい場合があると思ったからです。

$id: "http://example.com/foo.schema.json"
<object> { }

これは

{
  "$id": "http://example.com/foo.schema.json",
  "type": "object"
}

となります。

$defs$ref の表現

$defs$ref は、若干わかりにくいかと思いました。ですので、こんなふうに書けるようにしてみました(もう少しこなれたものにしたいところです)。

<array> {
  @items: [
    { $ref: (ref employee) }
  ]

  def employee is <object> {
    + name <string>
    + email <string> { @format: "email" }
    - date-of-birth <string> { @format: "date" }
  }
}

こう書くと、

{
  "type": "array",
  "items": [
    {
      "$ref": "#/$defs/employee"
    }
  ],
  "$defs": {
    "employee": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "email": {
          "type": "string",
          "format": "email"
        },
        "dateOfBirth": {
          "type": "string",
          "format": "date"
        }
      },
      "required": [
        "name",
        "email"
      ]
    }
  }
}

こうなります。

リポジトリ

プログラムはこちらリポジトリから見ていただけます。 ただ、お試しの迷い書き状態なので、実装できていないことやバグも多いかなと思います(テスト書きます)。

「サブセット」WOFF をつくってみる(1)

フォント関係の記事を Qiita で見ていましたら、ウェブでの表示用に「サブセット化」するというニーズもあるそうです。

日本語Webフォントの流行の最適化「NotoSans」「サブセット化」 | Qiita

こうしたサブセットは、FontForgeスクリプトでもつくることはできそうです。 差し当たって、ここではまず常用漢字のリストを取得することを考えてみましょう。

Wikipedia から常用漢字を取得してみる

常用漢字一覧の入手元としては候補はいくつかありそうですが、ここでは こちらWikipedia のページから取得してみることにします。 ページのレスポンスを次のように取得できるので

import urllib.parse
from urllib.request import Request, urlopen

uri = urllib.parse.quote(u"//ja.wikipedia.org/wiki/常用漢字一覧")
response = urlopen(Request(f"https:{uri}"))

ここで得た response をつかって、XPath で希望の箇所をだけ抜き取ってみます。

from lxml import etree

tree = etree.parse(response, etree.HTMLParser())
hans = tree.xpath(u"//h2//span[text()='一覧']/following::table[1]//tr[not(@style)]/child::td[2]/a/text()")

for c in hans: print(c)

もちろん、スクレイピングは記事の書かれ方に依存してしまいますから、永続的にこれで期待の結果が得られる保証はありません。たとえば、ここでは「削除された漢字の列には style が当たっている」とみなしていますし、目的の漢字は td の直下の a に書かれているとみなしています。

ところで、これを実際に動かして wc -l で行数をみると、2137 返ってきてしまいます。Wikipedia の表は、行にインデックスが振られているので漢字の総数がわかりますが、漢字の数は 2136 のはずですから、1 つ多いです。 なぜだろうと思って、ためしに sort してみたりすると、ひとつ毛色のちがうものが見つかります。表をよくみると 830 個目に漢字が 2 つあるのですね。Wikipedia の著者、かなり知識が細かいです (^^;

次回もしできそうだったら、このリストをつかって WOFF をつくってみましょう(そんな記事は書かないかもしれませんが)。

CJK互換漢字について

昨日 Relaxed Typing Mono JP というフォントについて記事を書きました。

Github のプロジェクトページでフォント生成スクリプトを確認すると、compat_map.py というやたらと長い dictionary があります。これが何なのか、もしピンとくる方がいらっしゃるなら、かなり Unicode に詳しい方ではないでしょうか。

CJK 互換漢字のリストが必要なわけ

この長大な dictionary は「CJK 互換漢字(CJK Compatibility Ideographs)」のリストです。文字コードには、いろいろ思わぬ点があり、同じ字形に対して複数の文字コードが割り振られている場合があります。この互換漢字はそのひとつです。

Relaxed Typing Mono JP の合成元である Noto Sans JP は CID フォントですが、各グリフには Unicode 値も振られています。しかし、この値が互換漢字のほうの値で振られている場合もあるようです。CID フォントについて詳しいわけではないので、これが Noto Sans JP に固有のことなのかはわかりませんが、今回のケースでは、この変換マップが必要になりました(実質的には、ここまで長い dictionary である必要はなかったかもしれませんが、機械的につくったものですので)。

同じ字形に異なるコード値が割り振られているとは

CJK 互換漢字について、ざっとした説明を書きましたが、ざっとしすぎたきらいがあるので、具体例も紹介しておきたいと思います。 同じ字形に異なるコード値が振られているとはどういうことか、たとえば U+FA04 は、CJK 互換漢字のブロックに含まれる文字ですが、これは U+5B85 と互換関係にあるということです。つまり、

$ python
>>> print(u"\uFA04")
宅
>>> print(u"\u5B85")
宅

ということです。つまり、「宅」という字形を示すコード値は、ふつうは U+5B85 ですが、この字は互換漢字の対象なので U+FA04 のこともないわけではないということです。


同じ字形に異なるコード値の例
同じ字形に異なるコード値の例

Source Code Pro と Noto Sans JP を組み合わせてフォントをつくりました

Source Code ProNoto Sans JP を組み合わせて、等幅フォントをつくりました。Relaxed Typing Mono JP という名前で公開しています。ライセンス上、似た名前にするのが憚られましたので、関係ない新しい名前を考えました。

Source Code Pro と Noto Sans JP を組み合わせたフォントには、すでに Source Han Code JP があります。ただ、Source Han Code JP は、英数字と漢字(ひらがな等含む)の文字幅の比が 3:5 なのです。英字 2 文字が漢字 1 文字より幅が広くなってしまうので、期待したようには桁が揃わないのが難点でした。

そこで、英数字と漢字の文字幅が 1:2 になるように調整したものをつくったというわけです。

 

■ 表示例

screenshot

 

■ ダウンロード

ダウンロードはこちらからおこなえます。お試しください。