🎃

昔書いたJSON parserを読む

2021/12/16に公開約6,200字

本記事は「Wantedly 新卒 Advent Calendar 2021」の15日目の記事です。

遅れてなんか無いんだから!(嘘です。。。16日現在に公開しております)

いざ筆を執ったもののネタがなさすぎてプギャーーーというお気持ちでした。

ネタ探しに自分のGitHubリポジトリを漁っていたら見覚えのないjsonパーサーがありました。(なにそれ怖い)

ちょっとコード呼んでいくと面白そうな予感があるので、読みつつjsonパーサーのコードを理解したり、過去の自分のコードにツッコミを入れたりして遊んでいこうかなと思いました!

コードの説明はちゃんとするつもりですが雑談混じりの記事になります!

tumekiri

今回読んでいくコードの概要を紹介していきます。
リポジトリはこちらです。デデン!

https://github.com/k-nasa/tumekiri

json parserの名前はtumekiriというらしいですね。
たしかこの時期は最初に目についたものの名前をつけていた気がします。爪切りが近くにあったのでしょうねw

他にもmenbei(めんべいという明太子のおせんべい)というリポジトリがあったりします。
今はネーミングセンスを手に入れたのでこのようなことにはならない!はず!たぶん!

このJSONパーサー(以下パーサー)はRustで書かれています。
コードはテスト込みで300行とすごく短いですね。(cloneしてコンパイルしてみると普通に動いたのでちょっと嬉しい気持ちでした。)

~/lab/product/tumekiri / - master
:) % tokei
===============================================================================
 Language            Files        Lines         Code     Comments       Blanks
===============================================================================
 Markdown                1           16            0           11            5
 Rust                    2          414          376            3           35
 TOML                    1            9            6            1            2
===============================================================================
 Total                   4          439          382           15           42
===============================================================================

test codeを読む

READMEがゴミすぎて何の情報も得られなかったので、テストコードを見つつパーサーの仕様を見ていくことにします。(今でもREADMEやissue, prのdescriptionは情報が少ないことがあるのであんまり変わっていないみたい、、、)

テストケースを見てみると次のようにtypeごとにパースできるかをテストしていることが分かりますね。

#[test]
fn parse_string_test() {
...
}

#[test]
fn parse_number_test() {
...
}

#[test]
fn parse_array_test() {
...
}

#[test]
fn parse_object_test() {
...
}

#[test]
fn parse_bool_and_null_test() {
...
}

一番複雑そうなobjectのテストを見てみることにしましょう。

#[test]
fn parse_object_test() {
    let input = r#"
{
  "squadName": "Super hero squad",
  "homeTown": "Metro City",
  "formed": 2016,
  "secretBase": "Super tower"
  }
        "#;

    let parse_result = JsonParser::new(input.chars()).parse();

    assert!(parse_result.is_ok());

    let _value = parse_result.unwrap();

    // FIXME テスト書くのすごくめんどくさい。。。。プギャーーー
    // assert_eq!(value, JsonValue::Object());
}

なんてこった、、、このコードのコメントには脱帽ですね。。。
プギャーーーじゃねえよというお気持ちしか無いです、、、
せっかくなのでテストを代わりに書いてあげることにしました。

多分JsonObjectを組み立てるのが非常に嫌だったんでしょうねww
このテストは通ったのでまあいい感じに動いていそうなことが分かります。

#[test]
fn parse_object_test() {
    let input = r#"
{
  "squadName": "Super hero squad",
  "homeTown": "Metro City",
  "formed": 2016,
  "secretBase": "Super tower",
  }
        "#;

    let parse_result = JsonParser::new(input.chars()).parse();

    let value = parse_result.unwrap();

    let h = HashMap::from_iter(
        [
            (
                "squadName".to_owned(),
                JsonValue::String("Super hero squad".to_string()),
            ),
            (
                "homeTown".to_owned(),
                JsonValue::String("Metro City".to_string()),
            ),
            ("formed".to_owned(), JsonValue::Number(2016.0)),
            (
                "secretBase".to_owned(),
                JsonValue::String("Super tower".to_string()),
            ),
        ]
        .iter()
        .cloned(),
    );

    assert_eq!(value, JsonValue::Object(h));
}

code reading

では肝心のパーサーを読んでいきますか。
テストコードを見てみると次のようなコードのinput, outputをテストしていますね。

そのため今回JsonParserという構造体のparseメソッドがエントリーポイントとなっていそうです。

JsonParser::new(input.chars()).parse();

JsonParserの定義を見ていきましょうか

無駄にジェネリクスを使っていますね〜。Iteratorトレイトを実装しているやつなら何でもパースするみたいです。 受け取った文字列のイテレーターをPeekableというstructでラップして使っています。これは1つ先読み可能なイテレーターを返してくれる君です。イテレーターをすすめること無く次の値を先読み(覗き見)することが出来ます。

あとは行と列を持っていますね。(row, columnじゃないんだと思いました)

use std::iter::Peekable;

pub struct JsonParser<C: Iterator<Item = char>> {
    chars: Peekable<C>,
    col: usize,
    line: usize,
}


impl<C> JsonParser<C>
where
    C: Iterator<Item = char>,
{
    pub fn new(input: C) -> Self {
        JsonParser {
            chars: input.peekable(),
            col: 0,
            line: 0,
        }
    }
}

次のメインとなるparseメソッドを見ていきましょう
やっていることは次の2つに見えますね。

  1. row, colを元に現在のカーソルがある1文字読む
  2. どのjson value(type)に当てはまりそうか見て、各種類のparseメソッドを呼び出している
pub fn parse(&mut self) -> ParseResult {
    let first_char = match self.peek() {
        None => return self.error_result("Invalid input"),
        Some(c) => c,
    };

    match first_char {
        '"' => self.parse_string(),
        '0'..='9' | '-' => self.parse_number(),
        '{' => self.parse_object(),
        '[' => self.parse_array(),
        't' | 'f' | 'n' => self.parse_bool_and_null(),
        c => self.error_result(&format!("Unsupported charactor {}", c)),
    }
}

このときなぜself.nextではなくpeekなんだろうと思いましたが深く考えないことにしました
最初の一文字にカーソルがないとかそういった話なんでしょうかね?

peekメソッドを見ても理由はよくわかりませんでした。まあ良いか。
空白を読み飛ばしたり改行をいい感じに扱いつつinput charsを読んでいますね。

fn peek(&mut self) -> Option<char> {
    while let Some(&c) = self.chars.peek() {
        if c == '\n' {
            self.col = 0;
            self.line += 1;
        }

        self.col += 1;
        if !c.is_whitespace() {
            return Some(c);
        }
        self.chars.next();
    }

    None
}

疲れてしまったので最後にparse_stringでも読んで終わりにしようと思います!300行のコードを全部読むのは結構大変なので断念!

parse_stringが呼ばれるということはpeekしたときの文字が"であることが決まっているのでそれをチェックしていますね。

その後対応する閉じ"が来るまで読んで最終結果となるoutput変数にpushしているだけですね。

pub fn parse_string(&mut self) -> ParseResult {
    if self.chars.next() != Some('"') {
        return self.error_result("");
    }

    let mut output = String::new();

    loop {
        let c = match self.chars.next() {
            Some('"') => break,
            None => return self.error_result(""),

            Some(c) => c,
        };

        output.push(c)
    }

    Ok(JsonValue::String(output))
}

うーん。ラストにしてはかんたんなコードでしたが、まあ良いでしょう。

感想

思いつきでブログを書きながらコードを読んでみたのですが、僕は楽しめました。
機会があれば誰かと一緒にコードをワイワイ読んでみたいですね〜。

今回全部は読めなかったのですが、json parserを300行ほどで書いているというのは驚きでした。もっと大変なプログラムだと思っていたので。

これからもちょくちょくコードをどっかから拾ってきて読みつつまとめていくというのはやってみようと思います!シンプルにやっているだけで僕が楽しかったwww

ここまで読んでいるみなさんもちょっとは楽しんでいただけていたら嬉しいです!

おやすみなさい!

Discussion

ログインするとコメントできます