ABOUT ME

-

Total
-
  • Rust: serde json Enum 공부
    컴퓨터/Rust 2021. 7. 28. 19:20
    728x90
    반응형

    serde

     

    Overview · Serde

    Serde Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.

    serde.rs

     

    할 것

    지난 글에서 trait를 이용해서 Serialize를 하였는데 (dyn erased_serde::Serialize)

    여기서 Deserialize까지 하고 싶었더니 복잡하게 되었다.

    polymorphism을 파이썬처럼 생각하고 짰더니 어려워진 것 같다.

    Rust에서는 enum이 하스켈처럼 다양하게 사용될 수 있는 것 같다. 자바에서 enum과 정말 다르다.

    간단 예제

    // Create an `enum` to classify a web event. Note how both
    // names and type information together specify the variant:
    // `PageLoad != PageUnload` and `KeyPress(char) != Paste(String)`.
    // Each is different and independent.
    enum WebEvent {
        // An `enum` may either be `unit-like`,
        PageLoad,
        PageUnload,
        // like tuple structs,
        KeyPress(char),
        Paste(String),
        // or c-like structures.
        Click { x: i64, y: i64 },
    }
    
    // A function which takes a `WebEvent` enum as an argument and
    // returns nothing.
    fn inspect(event: WebEvent) {
        match event {
            WebEvent::PageLoad => println!("page loaded"),
            WebEvent::PageUnload => println!("page unloaded"),
            // Destructure `c` from inside the `enum`.
            WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
            WebEvent::Paste(s) => println!("pasted \"{}\".", s),
            // Destructure `Click` into `x` and `y`.
            WebEvent::Click { x, y } => {
                println!("clicked at x={}, y={}.", x, y);
            },
        }
    }
    
    fn main() {
        let pressed = WebEvent::KeyPress('x');
        // `to_owned()` creates an owned `String` from a string slice.
        let pasted  = WebEvent::Paste("my text".to_owned());
        let click   = WebEvent::Click { x: 20, y: 80 };
        let load    = WebEvent::PageLoad;
        let unload  = WebEvent::PageUnload;
    
        inspect(pressed);
        inspect(pasted);
        inspect(click);
        inspect(load);
        inspect(unload);
    }

     

    Enum polymorphism

    trait는 외부 코드로 타입을 더 추가할 수 있지만 enum은 정해진 타입만 사용 가능하다.

    trait는 컴파일 타임에 Size를 알 수 없어서 Box<dyn trait> 형식으로 trait object로 쓰여야 한다.

    사용은 때에 맞게 써야 할 것 같다.

    읽기 좋은 글: Polymorphism in Rust: Enum vs Trait

    enum Shape {
        Rectangle { width: f64, height: f64 },
        Circle { radius: f64 },
        RightAngleTriangle { base: f64, height: f64 },
    }
    
    impl Shape {
        fn area(&self) -> f64 {
            match self {
                Shape::Rectangle { width, height } => 
                    width * height,
                Shape::Circle { radius } => 
                    std::f64::consts::PI * radius.powi(2),
                Shape::RightAngleTriangle { base, height } => 
                    0.5 * base * height,
            }
        }
    }

     

    Serde Enum variants

    serde에서 어떻게 사용할까?

    JSON 데이터가 아래와 같이 되어있을 때 Vec<Object> 하나로 표현하고 싶을 수 있다.

    [{"label":"CALL LABEL","action":"phone","phoneNumber":"0","messageText":"MESSAGE"},
    {"label":"SHARE LABEL","action":"share"},{"label":"MSG LABEL","action":"message"},
    {"label":"LABEL","action":"webLink","webLinkUrl":"https://"}]

     

    Internally Tagged 방식을 이용해서

    action에 따라 알아서 알맞은 타입으로 매칭 되게 할 수도 있다.

    아래 방식으로 하면 다른 field 들은 other에 넣던가 flatten 할 수 있지만 (Playground 링크)

    #[derive(Serialize, Deserialize, Debug)]
    #[serde(tag = "action")]
    pub enum Button {
        #[serde(rename = "phone")]
        Call(CallButton),
        #[serde(rename = "link")]
        Link(LinkButton),
        #[serde(rename = "share")]
        Share(ShareButton),
        #[serde(rename = "message")]
        Msg(MsgButton),
    }

    똑같이 label, action 필드 두 개를 갖지만 다른 버튼일 수도 있다.

    {"label":"SHARE LABEL","action":"share"},{"label":"MSG LABEL","action":"message"}

    ShareButton과 MsgButton으로 구분하고 싶어서

    직접 Deserialize를 구현해보았다. Rust를 많이 사용하지 않아서 난잡해 보인다.

    Button을 다 합쳐서 그냥 skip_if attribute를 사용해서 통합해도 될 것 같다.

     

    untagged는 { "some" : { ... } } 처럼 외부에 이름이 있는 게 아니라 이름 없이

    알아서 구분하라고 지정하고 싶을 때 쓸 수 있다.

    코드

    use serde::{de::Error, Deserialize, Deserializer, Serialize};
    use serde_json::{Map, Value};
    use std::collections::HashMap;
    
    #[derive(Serialize, Debug)]
    #[serde(untagged)]
    pub enum Button {
        Call(CallButton),
        Link(LinkButton),
        Share(ShareButton),
        Msg(MsgButton),
    }
    
    impl<'de> Deserialize<'de> for Button {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            let text: Map<String, Value> = Map::deserialize(deserializer)?;
            let mut keys = HashMap::new();
            for (key, value) in &text {
                let _value = value.as_str().unwrap();
                keys.insert(key.to_owned(), _value.to_string());
            }
    
            let mut button: Button = match text.get("action").unwrap().as_str() {
                Some("webLink") => Self::Link(LinkButton::new("label".to_string())),
                Some("share") => Self::Share(ShareButton::new("label".to_string())),
                Some("message") => Self::Msg(MsgButton::new("label".to_string())),
                Some("phone") => Self::Call(CallButton::new("label".to_string())),
                _ => Self::Msg(MsgButton::new("label".to_string())),
            };
    
            for (key, value) in &text {
                let _value = value.as_str().unwrap();
                match &mut button {
                    Self::Link(link) => match link {
                        LinkButton {
                            ref mut label,
                            ref action,
                            ref mut web_link_url,
                            ref mut message_text,
                        } => {
                            if let Some(l) = keys.get("label") {
                                *label = l.to_string();
                            }
                            if let Some(l) = keys.get("webLinkUrl") {
                                *web_link_url = l.to_string();
                            }
                            if let Some(l) = keys.get("messageText") {
                                *message_text = Some(l.to_string());
                            }
                        }
                    },
                    Self::Share(share) => match share {
                        ShareButton {
                            ref mut label,
                            ref action,
                            ref mut message_text,
                        } => {
                            if let Some(l) = keys.get("label") {
                                *label = l.to_string();
                            }
                            if let Some(l) = keys.get("messageText") {
                                *message_text = Some(l.to_string());
                            }
                        }
                    },
                    Self::Msg(msg) => match msg {
                        MsgButton {
                            ref mut label,
                            ref action,
                            ref mut message_text,
                        } => {
                            if let Some(l) = keys.get("label") {
                                *label = l.to_string();
                            }
                            if let Some(l) = keys.get("messageText") {
                                *message_text = Some(l.to_string());
                            }
                        }
                    },
                    Self::Call(call) => match call {
                        CallButton {
                            ref mut label,
                            ref action,
                            ref mut phone_number,
                            ref mut message_text,
                        } => {
                            if let Some(l) = keys.get("label") {
                                *label = l.to_string();
                            }
                            if let Some(l) = keys.get("phoneNumber") {
                                *phone_number = l.to_string();
                            }
                            if let Some(l) = keys.get("messageText") {
                                *message_text = Some(l.to_string());
                            }
                        }
                    },
                }
            }
    
            Ok(button)
        }
    }
    
    
    // 버튼은 아래처럼 생긴 것들이다
    #[derive(Serialize, Deserialize, Debug)]
    #[serde(rename_all = "camelCase")]
    #[serde(deny_unknown_fields)]
    pub struct CallButton {
        label: String,
        action: String,
        phone_number: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        message_text: Option<String>,
    }
    
    impl CallButton {
        pub fn set_number(mut self, number: String) -> Self {
            self.phone_number = number;
            self
        }
    
        pub fn new(label: String) -> Self {
            CallButton {
                label: label,
                action: "phone".to_string(),
                phone_number: "0".to_string(),
                message_text: None,
            }
        }
    
        pub fn set_label(mut self, label: String) -> Self {
            self.label = label;
            self
        }
    
        pub fn set_msg(mut self, msg: String) -> Self {
            self.message_text = Some(msg);
            self
        }
    }
    
    // ........................................
    
    #[cfg(test)]
    mod test {
        use super::*;
    
        #[test]
        fn button_enum_test() {
            let data = r#"[{"label":"CALL LABEL","action":"phone","phoneNumber":"0","messageText":"MESSAGE"},{"label":"SHARE LABEL","action":"share"},{"label":"MSG LABEL","action":"message"},{"label":"LABEL","action":"webLink","webLinkUrl":"https://"}]"#;
            let buttons: Vec<Button> = serde_json::from_str(data).unwrap();
    
            println!("{:?}", buttons);
            println!("{}", serde_json::to_string_pretty(&buttons).expect("Woah"));
            // println!("{}", serde_json::to_string(&buttons).expect("Woah"));
        }
    }

     

    결과

    [Call(CallButton { label: "CALL LABEL", action: "phone", phone_number: "0", message_text: Some("MESSAGE") }), Share(ShareButton { label: "SHARE LABEL", action: "share", message_text: None }), Msg(MsgButton { label: "MSG LABEL", action: "message", message_text: None }), Link(LinkButton { label: "LABEL", action: "webLink", web_link_url: "https://", message_text: None })]
    [
      {
        "label": "CALL LABEL",
        "action": "phone",
        "phoneNumber": "0",
        "messageText": "MESSAGE"
      },
      {
        "label": "SHARE LABEL",
        "action": "share"
      },
      {
        "label": "MSG LABEL",
        "action": "message"
      },
      {
        "label": "LABEL",
        "action": "webLink",
        "webLinkUrl": "https://"
      }
    ]

     

    match Enum variants

    아래처럼 SimpleText struct에 구현해 둔 메소드들도 사용할 수 있다.

    #[derive(Serialize, Deserialize, Debug)]
    #[serde(untagged)]
    pub enum Types {
        List(ListCard),
        Basic(BasicCard),
        Simple(SimpleText),
        Carousel(Carousel),
    }
    
    match output {
        Types::Simple(s) => {
            map.insert("simpleText".to_string(), s.html());
        }
        Types::Carousel(_) => {
            map.insert("carousel".to_string(), "carousel".to_string());
        }
        Types::Basic(_) => {
            map.insert("basicCard".to_string(), "carousel".to_string());
        }
        Types::List(_) => {
            map.insert("listCard".to_string(), "carousel".to_string());
        }
    }

     

    참고

    현재 공부 정리 프로젝트

     

    GitHub - Alfex4936/Rust-Server: Rust 언어를 이용한 백엔드 + 프론트엔드 + 카카오톡 챗봇 프로젝트

    Rust 언어를 이용한 백엔드 + 프론트엔드 + 카카오톡 챗봇 프로젝트

    github.com

     

    728x90

    댓글