Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

D言語erの方もそうでない方も、おはようございます!
D言語 Advent Calendar 2012の9日目の記事です。

前回の記事で、ctpgを紹介したので、
今回の記事で、ctpgを使った例を紹介します。
タイトルにもある通り、ctpgを使ってJSONパーサを作っていきます。

パーサを作る

ctpgでパーサを作るので、とりあえず外枠を書きます。

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

mixin(generateParser(q{
    @default_skip(defaultSkip)

}));

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

json

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

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

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

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

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

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

_object

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

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

str

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

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

integer

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
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
JSONValue _null = !"null" >> { JSONValue val; val.type = JSON_TYPE.NULL; return val; };

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

strLit

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の結果は"などを含んでしまうので、変換でそれを取り除いています。

書けた!

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

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を用意します。

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
]
`;

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

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();
}

これを実行すると、

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

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

コンパイル時に動かしてみる

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

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(){}

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

$ 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さんです。二回目ですね!

youxkei
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした