コンパイル時にも動くJSONパーサをサクっと作ってみる

D言語erの方もそうでない方も、おはようございます！
[D言語 Advent Calendar 2012](http://qiita.com/advent-calendar/2012/d)の9日目の記事です。

[前回の記事](http://qiita.com/items/5ccc36a9a0b0c50ee6ee)で、[ctpg](https://github.com/youkei/ctpg)を紹介したので、
今回の記事で、ctpgを使った例を紹介します。
タイトルにもある通り、ctpgを使ってJSONパーサを作っていきます。
## パーサを作る
ctpgでパーサを作るので、とりあえず外枠を書きます。

```d:D
import ctpg, std.json, std.array;
import std.conv: to;

mixin(generateParser(q{
    @default_skip(defaultSkip)

}));
```

こんな感じですね。
`@default_skip`で、全体的なスキップパーサを指定しています。
`defaultSkip`は、主な空白にマッチするパーサです。ctpg内で定義されてます。
以下のソースコードは、基本的にこの枠の中に書かれていると思って下さい。

### json
ルートの規則名は、とりあえず`json`にしましょう。

```d:D
JSONValue json = _object / array / str / integer / _null;
```

それぞれの規則を`/`でつなげています。
`/`で繋げられた規則は、まず左側の規則が評価され、マッチしなかった場合、右側の規則が評価されるというものです。

このあたりは、特に難しくは無いですね。

今回は、話を簡単にするために、負の整数と浮動小数点数には対応しないことにします。

ところで、唐突に`JSONValue`なる構造体が出てきましたが、これはstd.json内で定義されている、JSONの内容を表す構造体です。

### _object
```d:D
JSONValue _object = !"{" ( strLit !":" json )*<","> !"}" >> (Tuple!(string, JSONValue)[] pairs){
    JSONValue val;
    val.type = JSON_TYPE.OBJECT;
    foreach(pair; pairs){
        val.object[pair[0]] = pair[1];
    }
    return val;
};
```

`_object`は変換を含んでいるので、複雑ですね・・・

規則の定義の中で使われている演算子の`!`ですが、これは、結果を破棄するというものです。
例えば、`"hoge" "piyo"`の型は`Tuple!(string, string)`ですが、
`!"hoge" "piyo"`とすると、`Tuple!(string)`となります。
更に、`Tuple!(string)`は`string`に自動的に変換されます。

それをふまえて、とりあえず括弧内を見てみると、
`( strLit !":" json )`となっています。

strLitは、文字列リテラルにマッチする規則です。あとで定義します。型は`string`です。
jsonは、上で定義した規則です。

すると、括弧内の型は、`Tuple!(string, JSONValue)`になりそうですね。

括弧のすぐ右に、`*<",">`とありますが、これは、`","`を区切りとして、0個以上にマッチするというものです。型は、演算子がかかっている規則の型の動的配列になります。

すると、`( strLit !":" json )*<",">`の型は`Tuple!(string, JSONValue)[]`になりますね。

左右にある`!"{"`や`!"}"`は無視されるので、結果として型は`Tuple!(string, JSONValue)[]`になり、それを`>>`を使って`_object`の型である`JSONValue`に変換しています。

変換の内容は・・・特に難しくないですね。

### array
```d:D
JSONValue array = !"[" json*<","> !"]" >> (JSONValue[] jsons){
    JSONValue val;
    val.type = JSON_TYPE.ARRAY;
    val.array = jsons;
    return val;
};
```

あまり`_object`と変わりません。
解説は大丈夫ですね。

### str
```d:D
JSONValue str = strLit >> (string str){
    JSONValue val;
    val.type =JSON_TYPE.STRING;
    val.str = str;
    return val;
};
```

strLitは、上でも書きましたが、最後辺りに定義します。

### integer
```d:D
JSONValue integer = [0-9]+ >> join >> to!ulong >> (ulong num){
    JSONValue val;
    val.type = JSON_TYPE.UINTEGER;
    val.uinteger = num;
    return val;
};
```

`>>`がたくさん連なってて楽しいですね！

`[0-9]`ですが、正規表現と同じく、`'0'`から`'9'`までの文字にマッチします。型は、不思議なことに、`string`です。
その右にある`+`ですが、`*`の1個以上版です。`<〜〜>`を省略出来て、その場合、区切りはなしとなります。

そんな訳で、`[0-9]+`の型は`string[]`となり、これをstd.array.joinに渡して`string`に変換し、`to!ulong`に渡して`ulong`に変換します。
最後に、自分でJSONValueに変換していますね。

### boolean
```d:D
JSONValue boolean
    = !"true"  >> { JSONValue val; val.type = JSON_TYPE.TRUE;  return val; }
    / !"false" >> { JSONValue val; val.type = JSON_TYPE.FALSE; return val; }
;
```

少し変わった書き方をしています。
`!"true"`のような場合、型は`Tuple!()`になります。
規則の型が`Tuple!()`で、変換を自分で書く場合、`>> (){ 〜〜 }`のようになります。
この時、`()`が省略できて、結果的に上のようになっています。

もうひとつ、`/`の優先度は`>>`よりも低いので、上のように書けます。

### _null
```d:D
JSONValue _null = !"null" >> { JSONValue val; val.type = JSON_TYPE.NULL; return val; };
```

これも`boolean`とあまり変わりません。

### strLit
```d:D
string strLit = strLit_p >> (string str){
    if(str[0] == '"' || str[0] == '`'){
        str = str[1..$-1];
    }else if(str[1] == 'r'){
        str = str[2..$-1];
    }
    return str;
};
```

またまた唐突に出てきた`strLit_p`ですが、これは、ctpg内に定義されている文字列リテラルにマッチするパーサです。
`strLit_p`の結果は`"`などを含んでしまうので、変換でそれを取り除いています。

## 書けた！
書けました。上で書いた規則を全てあわせると、

```d:parser.d
import ctpg, std.json, std.array;
import std.conv: to;

mixin(generateParsers(q{
    @default_skip(defaultSkip)

    JSONValue json = _object / array / str / integer / _null;

    JSONValue _object = !"{" ( strLit !":" json )*<","> !"}" >> (Tuple!(string, JSONValue)[] pairs){
        JSONValue val;
        val.type = JSON_TYPE.OBJECT;
        foreach(pair; pairs){
            val.object[pair[0]] = pair[1];
        }
        return val;
    };

    JSONValue array = !"[" json*<","> !"]" >> (JSONValue[] jsons){
        JSONValue val;
        val.type = JSON_TYPE.ARRAY;
        val.array = jsons;
        return val;
    };

    JSONValue str = strLit >> (string str){
        JSONValue val;
        val.type =JSON_TYPE.STRING;
        val.str = str;
        return val;
    };

    JSONValue integer = [0-9]+ >> join >> to!ulong >> (ulong num){
        JSONValue val;
        val.type = JSON_TYPE.UINTEGER;
        val.uinteger = num;
        return val;
    };

    JSONValue boolean
        = !"true"  >> { JSONValue val; val.type = JSON_TYPE.TRUE;  return val; }
        / !"false" >> { JSONValue val; val.type = JSON_TYPE.FALSE; return val; }
    ;

    JSONValue _null = !"null" >> { JSONValue val; val.type = JSON_TYPE.NULL; return val; };

    string strLit = strLit_p >> (string str){
        if(str[0] == '"' || str[0] == '`'){
            str = str[1..$-1];
        }else if(str[1] == 'r'){
            str = str[2..$-1];
        }
        return str;
    };
}));
```

となります。
パーサなので、相当な行数になるかと思いきや、そうでもありませんね。
大体50行くらいになっています。

## 使ってみる
まず、適当なJSONを用意します。

```d:parser.d続き
enum src = `
[
    {
        "name":"ctpg",
        "author":"Hisayuki Mima",
        "place":"https://github.com/youkei/ctpg",
        "stars":17,
        "pull requests":0
    },
    {
        "name":"ytl",
        "author":"Yutopp",
        "place":"https://github.com/yutopp/ytl",
        "stars":4,
        "pull requests":0
    },
    null
]
`;
```

これを、上で定義した規則でパースして、確かめてみます。

```d:sample.d
import std.stdio, std.json, parser, ctpg;

void main(){
    auto parsed = parse!json(src);
    assert(parsed.match);
    JSONValue val = parsed.value;
    val.array[0].object["name"].str.writeln();
    val.array[1].object["author"].str.writeln();
}
```

これを実行すると、

```bash
$ dmd -ofrun -Ictpg/src ctpg/src/ctpg.d parser.d -run sample.d
ctpg
Yutopp
```

となります。
無事、パース出来てますね！

## コンパイル時に動かしてみる
ctpgで作ったパーサは、コンパイル時にも動作するので、確かめてみます。

```d:sample_ct.d
import std.json, ctpg, parser;

pragma(msg, {
    JSONValue val = parse!json(src).value;
    assert(val.array[0].object["name"].str == "ctpg");
    assert(val.array[0].object["author"].str == "Hisayuki Mima");
    assert(val.array[0].object["place"].str == "https://github.com/youkei/ctpg");
    assert(val.array[0].object["stars"].uinteger == 17);
    assert(val.array[0].object["pull requests"].uinteger == 0);

    assert(val.array[1].object["name"].str == "ytl");
    assert(val.array[1].object["author"].str == "Yutopp");
    assert(val.array[1].object["place"].str == "https://github.com/yutopp/ytl");
    assert(val.array[1].object["stars"].uinteger == 4);
    assert(val.array[1].object["pull requests"].uinteger == 0);

    assert(val.array[2].type == JSON_TYPE.NULL);
    return val;
}());

void main(){}
```

コンパイル時に、パースしたデータを全てチェックして、表示するコードです。
実行してみると、

```bash
$ dmd -ofrun -Ictpg/src ctpg/src/ctpg.d parser.d -run sample_ct.d
JSONValue(, 0L, 0LU, nanL, null, [JSONValue(, , 0LU, nanL, [['n', 'a', 'm', 'e']:JSONValue(['c', 't', 'p', 'g'], , , , , , cast(JSON_TYPE)cast(byte)0),['a', 'u', 't', 'h', 'o', 'r']:JSONValue(['H', 'i', 's', 'a', 'y', 'u', 'k', 'i', ' ', 'M', 'i', 'm', 'a'], , , , , , cast(JSON_TYPE)cast(byte)0),['p', 'l', 'a', 'c', 'e']:JSONValue(['h', 't', 't', 'p', 's', ':', '/', '/', 'g', 'i', 't', 'h', 'u', 'b', '.', 'c', 'o', 'm', '/', 'y', 'o', 'u', 'k', 'e', 'i', '/', 'c', 't', 'p', 'g'], , , , , , cast(JSON_TYPE)cast(byte)0),['s', 't', 'a', 'r', 's']:JSONValue(, , 17LU, , null, null, cast(JSON_TYPE)cast(byte)2),['p', 'u', 'l', 'l', ' ', 'r', 'e', 'q', 'u', 'e', 's', 't', 's']:JSONValue(, , 0LU, , null, null, cast(JSON_TYPE)cast(byte)2)], null, cast(JSON_TYPE)cast(byte)4), JSONValue(, , 0LU, nanL, [['n', 'a', 'm', 'e']:JSONValue(['y', 't', 'l'], , , , , , cast(JSON_TYPE)cast(byte)0),['a', 'u', 't', 'h', 'o', 'r']:JSONValue(['Y', 'u', 't', 'o', 'p', 'p'], , , , , , cast(JSON_TYPE)cast(byte)0),['p', 'l', 'a', 'c', 'e']:JSONValue(['h', 't', 't', 'p', 's', ':', '/', '/', 'g', 'i', 't', 'h', 'u', 'b', '.', 'c', 'o', 'm', '/', 'y', 'u', 't', 'o', 'p', 'p', '/', 'y', 't', 'l'], , , , , , cast(JSON_TYPE)cast(byte)0),['s', 't', 'a', 'r', 's']:JSONValue(, , 4LU, , null, null, cast(JSON_TYPE)cast(byte)2),['p', 'u', 'l', 'l', ' ', 'r', 'e', 'q', 'u', 'e', 's', 't', 's']:JSONValue(, , 0LU, , null, null, cast(JSON_TYPE)cast(byte)2)], null, cast(JSON_TYPE)cast(byte)4), JSONValue(null, 0L, 0LU, nanL, null, null, cast(JSON_TYPE)cast(byte)8)], cast(JSON_TYPE)cast(byte)5)
```

となります。
なんだか見づらいですが、チェックも通ってるので、コンパイル時にちゃんと動いてますね！！

## まとめ
この記事では、コンパイル時にも動作するJSONパーサを、ctpgを使って書いてきました。
負の整数や浮動小数点数を無視していますが、50行くらいでサクッと書けましたね。

ctpgの展望ですが、今後はParse TreeやASTを吐けるようにしたいです。
JSONのような単純なものは、上のようなすぐに変換する方法で何とかなりますが、複雑になってくると、一度ASTにしてからそれをいじる必要が出てくると思います。
そんな時に、ASTが吐けると便利ですよね。

10日目は、[@mono_shoo](http://qiita.com/users/mono_shoo)さんです。二回目ですね！