D言語erの方もそうでない方も、おはようございます!
D言語 Advent Calendar 2012の9日目の記事です。
前回の記事で、ctpgを紹介したので、
今回の記事で、ctpgを使った例を紹介します。
タイトルにもある通り、ctpgを使ってJSONパーサを作っていきます。
パーサを作る
ctpgでパーサを作るので、とりあえず外枠を書きます。
import ctpg, std.json, std.array;
import std.conv: to;
mixin(generateParser(q{
@default_skip(defaultSkip)
}));
こんな感じですね。
@default_skip
で、全体的なスキップパーサを指定しています。
defaultSkip
は、主な空白にマッチするパーサです。ctpg内で定義されてます。
以下のソースコードは、基本的にこの枠の中に書かれていると思って下さい。
json
ルートの規則名は、とりあえずjson
にしましょう。
JSONValue json = _object / array / str / integer / _null;
それぞれの規則を/
でつなげています。
/
で繋げられた規則は、まず左側の規則が評価され、マッチしなかった場合、右側の規則が評価されるというものです。
このあたりは、特に難しくは無いですね。
今回は、話を簡単にするために、負の整数と浮動小数点数には対応しないことにします。
ところで、唐突にJSONValue
なる構造体が出てきましたが、これはstd.json内で定義されている、JSONの内容を表す構造体です。
_object
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
JSONValue array = !"[" json*<","> !"]" >> (JSONValue[] jsons){
JSONValue val;
val.type = JSON_TYPE.ARRAY;
val.array = jsons;
return val;
};
あまり_object
と変わりません。
解説は大丈夫ですね。
str
JSONValue str = strLit >> (string str){
JSONValue val;
val.type =JSON_TYPE.STRING;
val.str = str;
return val;
};
strLitは、上でも書きましたが、最後辺りに定義します。
integer
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
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
JSONValue _null = !"null" >> { JSONValue val; val.type = JSON_TYPE.NULL; return val; };
これもboolean
とあまり変わりません。
strLit
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
の結果は"
などを含んでしまうので、変換でそれを取り除いています。
書けた!
書けました。上で書いた規則を全てあわせると、
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を用意します。
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
]
`;
これを、上で定義した規則でパースして、確かめてみます。
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で作ったパーサは、コンパイル時にも動作するので、確かめてみます。
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さんです。二回目ですね!