earthmc 2.0.6

Async client for interacting with the EarthMC API
Documentation
use std::collections::BTreeMap;

use serde::{Deserialize, Serialize, de::Error as _};

use crate::{
    endpoints::location::position_xyz_object,
    model::{location::PositionXYZ, query::UuidQuery},
};

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Shops {
    shops: Vec<Shop>,
}

impl Shops {
    pub(crate) fn into_vec(self) -> Vec<Shop> {
        self.shops
    }
}

impl<'de> Deserialize<'de> for Shops {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let shops_by_id = BTreeMap::<String, Shop>::deserialize(deserializer)?;
        let mut shops_by_id = shops_by_id
            .into_iter()
            .map(|(id, shop)| {
                Ok((id.parse::<u32>().map_err(D::Error::custom)?, shop))
            })
            .collect::<Result<Vec<_>, D::Error>>()?;

        shops_by_id.sort_by_key(|(id, _)| *id);

        Ok(Self {
            shops: shops_by_id.into_iter().map(|(_, shop)| shop).collect(),
        })
    }
}

#[derive(Clone, Serialize)]
pub(crate) struct ShopQuery {
    query: UuidQuery,
    key: String,
}

impl ShopQuery {
    pub(crate) fn new(query: UuidQuery, key: String) -> Self {
        Self { query, key }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct Shop {
    pub item: String,
    pub price: f64,
    pub amount: u32,
    #[serde(rename = "type")]
    pub kind: ShopKind,
    pub stock: u32,
    #[serde(with = "position_xyz_object")]
    pub location: PositionXYZ,
}

#[derive(
    Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord,
)]
#[serde(rename_all = "lowercase")]
pub enum ShopKind {
    Buying,
    Selling,
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use serde_json::json;
    use uuid::Uuid;

    use crate::{
        endpoints::shop::{Shop, ShopKind, ShopQuery, Shops},
        model::{location::PositionXYZ, query::UuidQuery},
    };

    #[test]
    fn shop_query_serializes_key_in_body() {
        let uuid =
            Uuid::from_str("5b8274bf-b162-4336-85a0-48f9d5380a78").unwrap();
        let query = ShopQuery::new(UuidQuery::from(uuid), "API_KEY".into());

        assert_eq!(
            serde_json::to_value(query).unwrap(),
            json!({
                "query": ["5b8274bf-b162-4336-85a0-48f9d5380a78"],
                "key": "API_KEY"
            })
        );
    }

    #[test]
    fn deserializes_shop_response() {
        let response: Vec<Shops> = serde_json::from_value(json!([
            {
                "0": {
                    "item": "TOTEM_OF_UNDYING",
                    "price": 150.0,
                    "amount": 1,
                    "type": "selling",
                    "stock": 3,
                    "location": {
                        "x": 0,
                        "y": 1,
                        "z": 2
                    }
                },
                "1": {
                    "item": "DIAMOND_CHESTPLATE",
                    "price": 2.0,
                    "amount": 1,
                    "type": "selling",
                    "stock": 15,
                    "location": {
                        "x": 3,
                        "y": 4,
                        "z": 5
                    }
                }
            }
        ]))
        .unwrap();

        assert_eq!(
            response[0].shops.as_slice(),
            [
                Shop {
                    item: "TOTEM_OF_UNDYING".into(),
                    price: 150.0,
                    amount: 1,
                    kind: ShopKind::Selling,
                    stock: 3,
                    location: PositionXYZ { x: 0, y: 1, z: 2 },
                },
                Shop {
                    item: "DIAMOND_CHESTPLATE".into(),
                    price: 2.0,
                    amount: 1,
                    kind: ShopKind::Selling,
                    stock: 15,
                    location: PositionXYZ { x: 3, y: 4, z: 5 },
                },
            ]
        );
    }

    #[test]
    fn deserializes_numeric_shop_keys_in_numeric_order() {
        let shops: Shops = serde_json::from_value(json!({
            "10": {
                "item": "DIAMOND",
                "price": 1.0,
                "amount": 1,
                "type": "buying",
                "stock": 10,
                "location": { "x": 0, "y": 0, "z": 0 }
            },
            "2": {
                "item": "EMERALD",
                "price": 2.0,
                "amount": 1,
                "type": "selling",
                "stock": 20,
                "location": { "x": 0, "y": 0, "z": 0 }
            }
        }))
        .unwrap();

        assert_eq!(shops.shops[0].item, "EMERALD");
        assert_eq!(shops.shops[1].item, "DIAMOND");
    }
}