serde-jsonのjson!マクロ?
Rustでシリアライズ・デシリアライズをするためのライブラリ「serde」の子ライブラリで、JSONのシリアライズ・デシリアライズをする「serde-json」には、JSONを解釈してserde_json::Value
型にするためのマクロがついています。
use serde_json::json;
fn main() {
let my_json = json!{
{"status": 200,
"foo": {
"aaa": 123,
"bar": [1, 3, "str", ["a", "b", 2]]
},
"result": null,
"bool": [true, false],
"float": 1.23
}
};
println!("{}", my_json);
/* 結果
{"bool":[true,false],"float":1.23,"foo":{"aaa":123,"bar":[1,3,"str",["a","b",2]]},"result":null,"status":200}
*/
// デバッグ表示
println!("{:?}", my_json);
/* 結果
Object({"bool": Array([Bool(true), Bool(false)]), "float": Number(1.23), "foo": Object({"aaa": Number(123), "bar": Array([Number(1), Number(3), String("str"), Array([String("a"), String("b"), Number(2)])])}), "result": Null, "status": Number(200)})
*/
}
「構文解析とかがんばってるんだろうなー」と思いきや、案外、力技でなんとかしていたので、今回はそれを解説していきます。
Rustの基本的な文法や、Rustでのmacro_rules!
を使ったマクロ定義については、ある程度は理解している読者を想定しています。
マクロについて詳しくない方は、こちらなどが参考になります。
serde_json::Value
型
JavaScriptをはじめとする動的型付け言語では、普通にあることなのですが、同じ配列や連想配列に、整数や文字列など様々な型のものが混ざっているのは、Rustのような静的型付け言語では基本的にはご法度です。
けれど、実際そういうデータ形式になっているので、enum
型でなんとかしています。
#[derive(Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Number(Number),
String(String),
Array(Vec<Value>),
Object(Map<String, Value>),
}
(引用元: src/value/mod.rsより、docstringを削除)
Map
とは? Number
とは? とか思うかもしれませんが、あまり深く気にせずいきましょう。(featureによって切り替わりますが、デフォルトでは、Map
はBTreeMap
を使っていて、Number
はさらなるenum
型になっています)
json!
マクロは、Value
型を作るためのマクロです。
json_internal!
マクロ
json!
マクロ自体は実質なにもしておらず、json_internal!
マクロががんばっています。
macro_rules! json {
// Hide distracting implementation details from the generated rustdoc.
($($json:tt)+) => {
json_internal!($($json)+)
};
}
(引用元: src/macros.rs 以下も同じ)
なので、json_internal!
マクロを読みます。
長いですが、一旦全部引用します。
macro_rules! json_internal {
//////////////////////////////////////////////////////////////////////////
// TT muncher for parsing the inside of an array [...]. Produces a vec![...]
// of the elements.
//
// Must be invoked as: json_internal!(@array [] $($tt)*)
//////////////////////////////////////////////////////////////////////////
// Done with trailing comma.
(@array [$($elems:expr,)*]) => {
json_internal_vec![$($elems,)*]
};
// Done without trailing comma.
(@array [$($elems:expr),*]) => {
json_internal_vec![$($elems),*]
};
// Next element is `null`.
(@array [$($elems:expr,)*] null $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(null)] $($rest)*)
};
// Next element is `true`.
(@array [$($elems:expr,)*] true $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(true)] $($rest)*)
};
// Next element is `false`.
(@array [$($elems:expr,)*] false $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(false)] $($rest)*)
};
// Next element is an array.
(@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!([$($array)*])] $($rest)*)
};
// Next element is a map.
(@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!({$($map)*})] $($rest)*)
};
// Next element is an expression followed by comma.
(@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!($next),] $($rest)*)
};
// Last element is an expression with no trailing comma.
(@array [$($elems:expr,)*] $last:expr) => {
json_internal!(@array [$($elems,)* json_internal!($last)])
};
// Comma after the most recent element.
(@array [$($elems:expr),*] , $($rest:tt)*) => {
json_internal!(@array [$($elems,)*] $($rest)*)
};
// Unexpected token after most recent element.
(@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
json_unexpected!($unexpected)
};
//////////////////////////////////////////////////////////////////////////
// TT muncher for parsing the inside of an object {...}. Each entry is
// inserted into the given map variable.
//
// Must be invoked as: json_internal!(@object $map () ($($tt)*) ($($tt)*))
//
// We require two copies of the input tokens so that we can match on one
// copy and trigger errors on the other copy.
//////////////////////////////////////////////////////////////////////////
// Done.
(@object $object:ident () () ()) => {};
// Insert the current entry followed by trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
let _ = $object.insert(($($key)+).into(), $value);
json_internal!(@object $object () ($($rest)*) ($($rest)*));
};
// Current entry followed by unexpected token.
(@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => {
json_unexpected!($unexpected);
};
// Insert the last entry without trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr)) => {
let _ = $object.insert(($($key)+).into(), $value);
};
// Next value is `null`.
(@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(null)) $($rest)*);
};
// Next value is `true`.
(@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(true)) $($rest)*);
};
// Next value is `false`.
(@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(false)) $($rest)*);
};
// Next value is an array.
(@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!([$($array)*])) $($rest)*);
};
// Next value is a map.
(@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!({$($map)*})) $($rest)*);
};
// Next value is an expression followed by comma.
(@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!($value)) , $($rest)*);
};
// Last value is an expression with no trailing comma.
(@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!($value)));
};
// Missing value for last entry. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
// "unexpected end of macro invocation"
json_internal!();
};
// Missing colon and value for last entry. Trigger a reasonable error
// message.
(@object $object:ident ($($key:tt)+) () $copy:tt) => {
// "unexpected end of macro invocation"
json_internal!();
};
// Misplaced colon. Trigger a reasonable error message.
(@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `:`".
json_unexpected!($colon);
};
// Found a comma inside a key. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `,`".
json_unexpected!($comma);
};
// Key is fully parenthesized. This avoids clippy double_parens false
// positives because the parenthesization may be necessary here.
(@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
};
// Munch a token into the current key.
(@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
};
//////////////////////////////////////////////////////////////////////////
// The main implementation.
//
// Must be invoked as: json_internal!($($json)+)
//////////////////////////////////////////////////////////////////////////
(null) => {
$crate::Value::Null
};
(true) => {
$crate::Value::Bool(true)
};
(false) => {
$crate::Value::Bool(false)
};
([]) => {
$crate::Value::Array(json_internal_vec![])
};
([ $($tt:tt)+ ]) => {
$crate::Value::Array(json_internal!(@array [] $($tt)+))
};
({}) => {
$crate::Value::Object($crate::Map::new())
};
({ $($tt:tt)+ }) => {
$crate::Value::Object({
let mut object = $crate::Map::new();
json_internal!(@object object () ($($tt)+) ($($tt)+));
object
})
};
// Any Serialize type: numbers, strings, struct literals, variables etc.
// Must be below every other rule.
($other:expr) => {
$crate::to_value(&$other).unwrap()
};
}
The main implementation を読む
恐らく、ここから読むのが一番簡単なので、読んでいきます。
//////////////////////////////////////////////////////////////////////////
// The main implementation.
//
// Must be invoked as: json_internal!($($json)+)
//////////////////////////////////////////////////////////////////////////
(null) => {
$crate::Value::Null
};
(true) => {
$crate::Value::Bool(true)
};
(false) => {
$crate::Value::Bool(false)
};
([]) => {
$crate::Value::Array(json_internal_vec![])
};
([ $($tt:tt)+ ]) => {
$crate::Value::Array(json_internal!(@array [] $($tt)+))
};
({}) => {
$crate::Value::Object($crate::Map::new())
};
({ $($tt:tt)+ }) => {
$crate::Value::Object({
let mut object = $crate::Map::new();
json_internal!(@object object () ($($tt)+) ($($tt)+));
object
})
};
// Any Serialize type: numbers, strings, struct literals, variables etc.
// Must be below every other rule.
($other:expr) => {
$crate::to_value(&$other).unwrap()
};
null
, true
, false
, []
, {}
は直接値を作っています。また、その他の式(数字、文字列も含む)はValue
型に変換を試みてunwrap
しています。
蛇足ですが、to_value
は
pub fn to_value<T>(value: T) -> Result<Value, Error>
where
T: Serialize,
{
value.serialize(Serializer)
}
(引用元: src/value/mod.rs)
となっていますので、型自体が不適切な場合はコンパイル時にチェックできますが、serialize
が実装されているけれど実行時に変換にミスった場合はpanic
します。
本題に戻ります。
やや不穏なのは
([ $($tt:tt)+ ]) => {
$crate::Value::Array(json_internal!(@array [] $($tt)+))
};
// 略
({ $($tt:tt)+ }) => {
$crate::Value::Object({
let mut object = $crate::Map::new();
json_internal!(@object object () ($($tt)+) ($($tt)+));
object
})
};
の実装です。
つまり、
json_internal!( [1, 2, 3] )
は
Value::Array(json_internal!(@array [] 1, 2, 3))
として、
json_internal!( {"a": 1, "b": 2} )
は、
Value::Object({ let mut object = Map::new(); json_internal!(@object object () ("a": 1, "b": 2) ("a": 1, "b": 2)); object })
として再帰的に呼び出されます。
@array
や@object
は、Rustの文法的には特に意味がなく、単にマクロのパターンマッチのためのマーカーとして使われます。
@array
たちを読む
適宜、順番を入れ替えながら説明します。
Value::Array(json_internal!(@array [] 1 , 2 , 3))
の形に変換されるのでした。これがどう変換されていくのでしょうか。
// Next element is `null`.
(@array [$($elems:expr,)*] null $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(null)] $($rest)*)
};
// Next element is `true`.
(@array [$($elems:expr,)*] true $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(true)] $($rest)*)
};
// Next element is `false`.
(@array [$($elems:expr,)*] false $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!(false)] $($rest)*)
};
// Next element is an array.
(@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!([$($array)*])] $($rest)*)
};
// Next element is a map.
(@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!({$($map)*})] $($rest)*)
};
// Next element is an expression followed by comma.
(@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
json_internal!(@array [$($elems,)* json_internal!($next),] $($rest)*)
};
ここで挙げたものはすべて
(@array [$($elems)*] $next $($rest)*
または(@array [$($elems)*] $next, $($rest)*
の$next
をValue化しながら、再帰的に[$($elems)*]
の中に付け足していく働きをしています。
また、先頭カンマは、次のように取り除きます。
// Comma after the most recent element.
(@array [$($elems:expr),*] , $($rest:tt)*) => {
json_internal!(@array [$($elems,)*] $($rest)*)
};
このようにすると、[]
に入っていない要素がどんどん減っていき、最後の要素は
// Last element is an expression with no trailing comma.
(@array [$($elems:expr,)*] $last:expr) => {
json_internal!(@array [$($elems,)* json_internal!($last)])
};
のように処理されます。そして、
// Done with trailing comma.
(@array [$($elems:expr,)*]) => {
json_internal_vec![$($elems,)*]
};
// Done without trailing comma.
(@array [$($elems:expr),*]) => {
json_internal_vec![$($elems),*]
};
によって、Value::Array(Vec<Value>)
になります。
なお、この過程で、どれにもパターンマッチしない例が出たときは、ここに落ちるようです。(プログラマが直接@array
を書いたとき以外、こうなることはあるのか、よく分かりませんでした)
// Unexpected token after most recent element.
(@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
json_unexpected!($unexpected)
};
@object
たちを読む
json_internal!( {"a": 1, "b": 2} )
は、
Value::Object({ let mut object = Map::new(); json_internal!(@object object () ("a": 1, "b": 2) ("a": 1, "b": 2)); object })
となるのでした。
マクロでobject
に値を挿入していくんだろうな、と予想ができます。
なお、中味がなぜか2つある件については、エラー処理に使う、とのコメントが書いてあります。
:
の直前まで、@object object () ...
の、()
の中に入れていきます。
// Key is fully parenthesized. This avoids clippy double_parens false
// positives because the parenthesization may be necessary here.
(@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
};
// Munch a token into the current key.
(@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
};
通常は、上のパターンマッチが使われます。
下のパターンマッチのように、式ではないが連想配列のキーになるものが何なのか、私には分かりませんでした。
続いて、コロンが来て、値が来るはずです。
// Next value is `null`.
(@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(null)) $($rest)*);
};
// Next value is `true`.
(@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(true)) $($rest)*);
};
// Next value is `false`.
(@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!(false)) $($rest)*);
};
// Next value is an array.
(@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!([$($array)*])) $($rest)*);
};
// Next value is a map.
(@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!({$($map)*})) $($rest)*);
};
// Next value is an expression followed by comma.
(@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!($value)) , $($rest)*);
};
// Last value is an expression with no trailing comma.
(@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
json_internal!(@object $object [$($key)+] (json_internal!($value)));
};
面白いのは、(@object $object ($key) ...)
だったのが(@object $object [$key] ...)
と、角かっこに変わっています。
角かっこのパターンマッチを見ていきましょう。
// Insert the current entry followed by trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
let _ = $object.insert(($($key)+).into(), $value);
json_internal!(@object $object () ($($rest)*) ($($rest)*));
};
// Current entry followed by unexpected token.
(@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => {
json_unexpected!($unexpected);
};
// Insert the last entry without trailing comma.
(@object $object:ident [$($key:tt)+] ($value:expr)) => {
let _ = $object.insert(($($key)+).into(), $value);
};
値を読んだら、次はカンマか何もないか、どちらかになっているはずです。
それを確認したら、$object
にキーと値の組をinsertし、
続きがある場合は$object
の後のキーを()
に戻して再びjson_internal!
します。
そしてこれは恐らく、最終要素の末尾にカンマがついていた場合に通る処理です。
// Done.
(@object $object:ident () () ()) => {};
あと、以下がエラー処理用です。
// Missing value for last entry. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
// "unexpected end of macro invocation"
json_internal!();
};
// Missing colon and value for last entry. Trigger a reasonable error
// message.
(@object $object:ident ($($key:tt)+) () $copy:tt) => {
// "unexpected end of macro invocation"
json_internal!();
};
// Misplaced colon. Trigger a reasonable error message.
(@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `:`".
json_unexpected!($colon);
};
// Found a comma inside a key. Trigger a reasonable error message.
(@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
// Takes no arguments so "no rules expected the token `,`".
json_unexpected!($comma);
};
マクロの中でエラーが起こると分かりにくいんですが、マクロのマッチングミスで出るエラーメッセージを利用して、マシなエラーになるように心がけているっぽいです。
これでめでたく、json_internal!
を読むことができました。
まとめ
今回、serde-jsonのjson!
マクロが何をやっているのか読んでみました。
「うまいことやってるなぁ」という気持ちと、「黒魔術では?」という気持ちでいっぱいになりますが、json!
マクロ自体は非常に便利です。また、ここまでではなくても、何かマクロでDSLを書きたい場合に「JSONができたんだから、これも頑張ればできるはず」と思えるのは嬉しいことです。
みなさんも、Rustマクロ黒魔術にぜひとも挑戦してみてください。