Skip to content

Latest commit

 

History

History
164 lines (126 loc) · 4.47 KB

json.md

File metadata and controls

164 lines (126 loc) · 4.47 KB

Any 트레잇에 대한 글은 이곳을 참고해봅시다.

HashMap을 쓰던 Rusty한 하루였습니다.

HashMap::from([(1, 2), (3, 4)]);

from 함수를 사용해서(또는 .into()) HashMap을 생성할 수 있었습니다.

해시 맵이 아닌 json (JavaScript Object Notation) 값을 다루려면 serde-rs/json 등으로 json 값을 다룰 수 있습니다.

필자는 단순한 코드를 원하고, 크레이트를 사용하기 원하지 않았죠. 하지만 러스트에선 자바스크립트 계열 언어(타입스크립트 등)에 존재하는 json 기능이 없습니다. (자바스크립트도 원래는 JSON이 기본 기능이 아니긴 했습니다.)

그래서 만능 매크로를 선언 해보았습니다.

macro_rules! json {
    ($($key:expr => $value:expr),*) => {{
        use std::collections::*;

        let mut map: HashMap<&str, _> = HashMap::new();
        $(
            map.insert($key, $value);
        )*

        map
    }};
}

key: value 으로 작성했으면 좋겠지만, 파서의 한계로 => 를 사용하였습니다.

만약 serde-rs/json 같은 크레이트와 같은 매크로를 선언하고 싶다면, 절차적 매크로를 사용해봅시다.

이 매크로는 다음과 같이 사용할 수 있습니다:

json! {
    "a" => "foo",
    "b" => "bar",
    "c" => "baz"
};

사실 json이 아닌 HashMap이긴 합니다. 그런데 이것은 심각한 문제가 있습니다: 매크로에서 value 타입을 &str로 단정 짓는 바람에 다른 타입을 쓸 수 없었습니다:

json! {
    "a" => "foo",
    "b" => "bar",
    "c" => 3 // mismatched types
};

Any

이러면 의미가 없으니, 필자는 Any 트레잇을 사용해보았습니다. Any 트레잇은 모든 타입을 받을 수 있습니다. Any 트레잇은 TypeId와 같이 자주 사용되지만, 이 글에선 다루지 않습니다. 좀 복잡해질 수 도 있기 때문에 json 모듈을 따로 구현해두었습니다.

pub mod json {
    #[macro_export]
    macro_rules! json {
        ($($key:expr => $value:expr),*) => {{
            use std::{any::*, collections::*};

            let mut map: HashMap<&str, Box<dyn Any>> = HashMap::new();
            $(
                map.insert($key, Box::new($value));
            )*
            map
        }};
    }
}

let json = json! {
    "a" => 1,
    "b" => "qwerty",
    "c" => json! {
        "d" => [1, 2, 3]
    }
};

이제 드디어 모든 타입을 받을 수 있게 되었습니다.

Any 트레잇은 컴파일 시간에 크기를 알 수 없기 때문에, Box<T>를 사용하였습니다. 이제 downcast_ref 또는 downcast_mut으로 값에 접근할 수 있습니다:

if let Some(v) = json["b"].downcast_ref::<&str>() {
    assert_eq!(*v, "qwerty");
}

값 가져오기

그런데 누가 json 값을 가져오는데 downcast_...같은 복잡한 함수를 쓸까요? 그런건 아무도 안씁니다. 때문에 get 헬퍼 함수 및 가변 downcast 헬퍼 함수 get_mut을 구현해보았습니다.

pub mod json {
    use std::any::*;

    pub struct JsonValue<T: Any + ?Sized>(pub T);

    impl JsonValue<dyn Any> {
        pub fn get<T: Any>(&self) -> Option<&T> {
            self.0.downcast_ref::<T>()
        }

        pub fn get_mut<T: Any>(&mut self) -> Option<&mut T> {
            self.0.downcast_mut::<T>()
        }
    }

    #[macro_export]
    macro_rules! json {
        ($($key:expr => $value:expr),*) => {{
            use std::{any::*, collections::*};

            let mut map: HashMap<&str, Box<JsonValue<dyn Any>>> = HashMap::new();
            $(
                map.insert($key, Box::new(JsonValue($value)));
            )*
            map
        }};
    }
}

JsonValue 구조체를 정의해주었습니다. 제네릭 TAny를 바운드하였으며, 컴파일 타임에 알 수 없기 때문에 ?Sized를 붙여주었습니다.

코드는 복잡해 보이지만, 한층 더 편리한 러스트 프로그래밍을 할 수 있습니다.

use json::*;

let json = json! {
    "a" => 1,
    "b" => "qwerty",
    "c" => json! {
        "d" => [1, 2, 3]
    }
};

if let Some(v) = json["b"].get::<&str>() {
    assert_eq!(*v, "qwerty");
};

if let Some(v) = json.get_mut("b") {
    if let Some(v) = v.get_mut::<&str>() {
        *v = "foo";

        assert_eq!(*v, "foo");
    }
};