Any 트레잇을 사용해서 JSON 비스무리한 매크로 만들기
포스트
취소

Any 트레잇을 사용해서 JSON 비스무리한 매크로 만들기

개요

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

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

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

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

필자는 단순한 코드를 원하고, 크레이트를 사용하기 원하지 않았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
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 같은 크레이트와 같은 매크로를 선언하고 싶다면, 절차적 매크로를 사용해봅시다.

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

1
2
3
4
5
json! {
    "a" => "foo",
    "b" => "bar",
    "c" => "baz"
};

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

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

Any

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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으로 값에 접근할 수 있습니다:

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

값 가져오기

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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를 붙여주었습니다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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");
    }
};