veloren_serverbrowser_api/
lib.rs

1pub use v1::{Field, FieldContent, GameServer, GameServerList};
2
3pub mod v1 {
4    use country_parser::Country;
5    use serde::{
6        de::{Deserializer, Error, Unexpected},
7        ser::Serializer,
8        Deserialize, Serialize,
9    };
10    use std::collections::HashMap;
11
12    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
13    pub struct GameServerList {
14        /// List of all servers registered to this serverbrowser
15        pub servers: Vec<GameServer>,
16    }
17
18    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
19    pub struct GameServer {
20        /// The name of the server.
21        pub name: String,
22        /// The address through which the server might be accessed on the open
23        /// internet. This field may be an IPv4 address, IPv6 address,
24        /// URL, must not contain a port
25        pub address: String,
26        /// Port of the gameserver address (usually `14004`)
27        pub port: u16,
28        /// The server description.
29        pub description: String,
30        /// The ISO 3166‑1 Alpha-2 code that the server is physically based in
31        /// (note: this field is intended as an indication of factors
32        /// like ping, not the language of the server). (e.g. "US")
33        #[serde(deserialize_with = "deserialize_country")]
34        #[serde(serialize_with = "serialize_country")]
35        #[serde(default)]
36        pub location: Option<Country>,
37        /// The auth server that must be used to connect to this server.
38        /// If you want to use the official auth server use `Some("https://auth.veloren.net")`
39        pub auth_server: String,
40        /// Port used for server information queries (default: 14006), the
41        /// address will be the same as the gameserver address.
42        /// `None` indicates that the server does not accept information
43        /// queries.
44        pub query_port: Option<u16>,
45        /// The version channel used by the server. `None` means not running a
46        /// channel distributed by Airshipper. If in doubt, `"weekly"`
47        /// is probably correct.
48        pub channel: Option<String>,
49        /// Whether the server is officially affiliated with the Veloren
50        /// project.
51        pub official: bool,
52        /// Any extra attributes provided by the server.
53        ///
54        /// The key is a machine-readable ID. Frontends may choose to display
55        /// these fields in a different way (for example, adding an
56        /// icon) based on this machine-readable ID, if they recognise it. There
57        /// is no specific list of valid IDs and recognition is based on
58        /// convention. Some examples of IDs include:
59        ///
60        /// - `website`
61        /// - `email`
62        /// - `discord`
63        /// - `mastodon`
64        /// - `reddit`
65        /// - `youtube`
66        #[serde(
67            default,
68            skip_serializing_if = "HashMap::is_empty",
69            serialize_with = "ordered_map"
70        )]
71        pub extra: HashMap<String, Field>,
72    }
73
74    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
75    pub struct Field {
76        /// A human-readable suggested name. Frontends are permitted to override
77        /// this name for purposes such as localisation or aesthetic
78        /// purposes.
79        pub name: String,
80        /// The content of the field.
81        pub content: FieldContent,
82    }
83
84    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
85    #[non_exhaustive]
86    #[serde(rename_all = "lowercase")]
87    pub enum FieldContent {
88        /// This field's content should be interpreted as human-readable
89        /// plaintext.
90        Text(String),
91        /// This field's content should be interpreted as a URL.
92        Url(String),
93        /// This field's content was of an unknown format. This cannot be
94        /// serialized but only exists to guarantee forward compatibility
95        #[serde(other)]
96        #[serde(skip_serializing)]
97        Unknown,
98    }
99
100    fn deserialize_country<'de, D: Deserializer<'de>>(de: D) -> Result<Option<Country>, D::Error> {
101        country_parser::parse(String::deserialize(de)?)
102            .map(Some)
103            .ok_or_else(|| {
104                D::Error::invalid_value(
105                    Unexpected::Other("invalid country"),
106                    &"valid ISO-3166 country",
107                )
108            })
109    }
110
111    fn serialize_country<S: Serializer>(
112        country: &Option<Country>,
113        ser: S,
114    ) -> Result<S::Ok, S::Error> {
115        match country {
116            Some(country) => ser.serialize_str(&country.iso2),
117            None => ser.serialize_none(),
118        }
119    }
120
121    impl GameServer {
122        pub fn new(
123            name: &str,
124            address: &str,
125            port: u16,
126            query_port: Option<u16>,
127            desc: &str,
128            location: Option<Country>,
129            auth: &str,
130            channel: Option<&str>,
131            official: bool,
132            extra: HashMap<String, Field>,
133        ) -> Self {
134            Self {
135                name: name.to_string(),
136                address: address.to_string(),
137                port,
138                query_port,
139                description: desc.to_string(),
140                location,
141                auth_server: auth.to_string(),
142                channel: channel.map(|c| c.to_string()),
143                official,
144                extra,
145            }
146        }
147    }
148
149    fn ordered_map<S>(value: &HashMap<String, Field>, serializer: S) -> Result<S::Ok, S::Error>
150    where
151        S: Serializer,
152    {
153        let ordered: std::collections::BTreeMap<_, _> = value.iter().collect();
154        ordered.serialize(serializer)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn check_server_list_ron_deserialize() {
164        ron::de::from_reader::<_, GameServerList>(
165            &include_bytes!("../examples/v1/example_server_list.ron")[..],
166        )
167        .unwrap();
168    }
169
170    #[test]
171    fn check_server_list_json_deserialize() {
172        serde_json::de::from_reader::<_, GameServerList>(
173            &include_bytes!("../examples/v1/example_server_list.json")[..],
174        )
175        .unwrap();
176    }
177
178    #[test]
179    fn check_server_list_json_roundtrip() {
180        let data = serde_json::de::from_reader::<_, GameServerList>(
181            &include_bytes!("../examples/v1/example_server_list.json")[..],
182        )
183        .unwrap();
184        serde_json::to_string_pretty(&data).unwrap();
185    }
186
187    #[test]
188    fn serialize_unknown_is_not_possible() {
189        let field = Field {
190            name: "never_serialze".to_string(),
191            content: FieldContent::Unknown,
192        };
193        let result = serde_json::to_string(&field);
194        assert!(result.is_err());
195        assert!(result.unwrap_err().is_data());
196    }
197
198    #[test]
199    fn check_json_schema() {
200        use jsonschema::Validator;
201        use serde_json::Value;
202        let schema = serde_json::de::from_reader::<_, Value>(
203            &include_bytes!("../examples/v1/schema.json")[..],
204        )
205        .unwrap();
206        Validator::new(&schema).expect("A valid schema");
207    }
208
209    #[test]
210    fn validate_json_schema() {
211        use jsonschema::Validator;
212        use serde_json::Value;
213        let schema = serde_json::de::from_reader::<_, Value>(
214            &include_bytes!("../examples/v1/schema.json")[..],
215        )
216        .unwrap();
217        let json = serde_json::de::from_reader::<_, Value>(
218            &include_bytes!("../examples/v1/example_server_list.json")[..],
219        )
220        .unwrap();
221        let compiled = Validator::new(&schema).expect("A valid schema");
222        let result = compiled.validate(&json);
223        if let Err(errors) = result {
224            for error in errors {
225                println!("Validation error: {}", error);
226                println!("Instance path: {}", error.instance_path);
227            }
228            panic!("json schema isn't valid");
229        }
230    }
231}