veloren_server/settings/
server_description.rs

1//! Versioned server description settings files.
2
3// NOTE: Needed to allow the second-to-last migration to call `try_into()`.
4
5use super::{MIGRATION_UPGRADE_GUARANTEE, SERVER_DESCRIPTION_FILENAME as FILENAME};
6use crate::settings::editable::{EditableSetting, Version};
7use core::convert::{Infallible, TryFrom, TryInto};
8use serde::{Deserialize, Serialize};
9
10/// NOTE: Always replace this with the latest server description version. Then
11/// update the `ServerDescriptionRaw`, the `TryFrom<ServerDescriptionRaw>` for
12/// `ServerDescription`, the previously most recent module, and add a new module
13/// for the latest version!  Please respect the migration upgrade guarantee
14/// found in the parent module with any upgrade.
15pub use self::v2::*;
16
17/// Versioned settings files, one per version (v0 is only here as an example; we
18/// do not expect to see any actual v0 settings files).
19#[derive(Deserialize, Serialize)]
20pub enum ServerDescriptionRaw {
21    V0(v0::ServerDescription),
22    V1(v1::ServerDescription),
23    V2(ServerDescriptions),
24}
25
26impl From<ServerDescriptions> for ServerDescriptionRaw {
27    fn from(value: ServerDescriptions) -> Self {
28        // Replace variant with that of current latest version.
29        Self::V2(value)
30    }
31}
32
33#[expect(clippy::infallible_try_from)] // TODO: evaluate
34impl TryFrom<ServerDescriptionRaw> for (Version, ServerDescriptions) {
35    type Error = <ServerDescriptions as EditableSetting>::Error;
36
37    fn try_from(
38        value: ServerDescriptionRaw,
39    ) -> Result<Self, <ServerDescriptions as EditableSetting>::Error> {
40        use ServerDescriptionRaw::*;
41        Ok(match value {
42            // Old versions
43            V0(value) => (Version::Old, value.try_into()?),
44            V1(value) => (Version::Old, value.try_into()?),
45            // Latest version (move to old section using the pattern of other old version when it
46            // is no longer latest).
47            V2(mut value) => (value.validate()?, value),
48        })
49    }
50}
51
52type Final = ServerDescriptions;
53
54impl EditableSetting for ServerDescriptions {
55    type Error = Infallible;
56    type Legacy = legacy::ServerDescription;
57    type Setting = ServerDescriptionRaw;
58
59    const FILENAME: &'static str = FILENAME;
60}
61
62mod legacy {
63    use super::{Final, MIGRATION_UPGRADE_GUARANTEE, v0 as next};
64    use core::convert::TryInto;
65    use serde::{Deserialize, Serialize};
66
67    #[derive(Deserialize, Serialize)]
68    #[serde(transparent)]
69    pub struct ServerDescription(pub(super) String);
70
71    impl From<ServerDescription> for Final {
72        /// Legacy migrations can be migrated to the latest version through the
73        /// process of "chaining" migrations, starting from
74        /// `next::ServerDescription`.
75        ///
76        /// Note that legacy files are always valid, which is why we implement
77        /// `From` rather than `TryFrom`.
78        fn from(value: ServerDescription) -> Self {
79            next::ServerDescription::migrate(value)
80                .try_into()
81                .expect(MIGRATION_UPGRADE_GUARANTEE)
82        }
83    }
84}
85
86/// This module represents a server description version that isn't actually
87/// used.  It is here and part of the migration process to provide an example
88/// for how to perform a migration for an old version; please use this as a
89/// reference when constructing new migrations.
90mod v0 {
91    use super::{Final, MIGRATION_UPGRADE_GUARANTEE, legacy as prev, v1 as next};
92    use crate::settings::editable::{EditableSetting, Version};
93    use core::convert::{TryFrom, TryInto};
94    use serde::{Deserialize, Serialize};
95
96    #[derive(Clone, Deserialize, Serialize)]
97    #[serde(transparent)]
98    pub struct ServerDescription(pub(super) String);
99
100    impl ServerDescription {
101        /// One-off migration from the previous version.  This must be
102        /// guaranteed to produce a valid settings file as long as it is
103        /// called with a valid settings file from the previous version.
104        pub(super) fn migrate(prev: prev::ServerDescription) -> Self { ServerDescription(prev.0) }
105
106        /// Perform any needed validation on this server description that can't
107        /// be done using parsing.
108        ///
109        /// The returned version being "Old" indicates the loaded setting has
110        /// been modified during validation (this is why validate takes
111        /// `&mut self`).
112        pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
113            Ok(Version::Latest)
114        }
115    }
116
117    /// Pretty much every `TryFrom` implementation except that of the very last
118    /// version should look exactly like this.
119    #[expect(clippy::infallible_try_from)] // TODO: evaluate
120    impl TryFrom<ServerDescription> for Final {
121        type Error = <Final as EditableSetting>::Error;
122
123        fn try_from(mut value: ServerDescription) -> Result<Final, Self::Error> {
124            value.validate()?;
125            Ok(next::ServerDescription::migrate(value)
126                .try_into()
127                .expect(MIGRATION_UPGRADE_GUARANTEE))
128        }
129    }
130}
131
132mod v1 {
133    use super::{Final, v0 as prev};
134    use crate::settings::editable::{EditableSetting, Version};
135    use core::ops::{Deref, DerefMut};
136    use serde::{Deserialize, Serialize};
137
138    #[derive(Clone, Deserialize, Serialize)]
139    #[serde(transparent)]
140    pub struct ServerDescription(pub(super) String);
141
142    impl Default for ServerDescription {
143        fn default() -> Self { Self("This is the best Veloren server".into()) }
144    }
145
146    impl Deref for ServerDescription {
147        type Target = String;
148
149        fn deref(&self) -> &Self::Target { &self.0 }
150    }
151
152    impl DerefMut for ServerDescription {
153        fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
154    }
155
156    impl ServerDescription {
157        /// One-off migration from the previous version.  This must be
158        /// guaranteed to produce a valid settings file as long as it is
159        /// called with a valid settings file from the previous version.
160        pub(super) fn migrate(prev: prev::ServerDescription) -> Self { ServerDescription(prev.0) }
161
162        /// Perform any needed validation on this server description that can't
163        /// be done using parsing.
164        ///
165        /// The returned version being "Old" indicates the loaded setting has
166        /// been modified during validation (this is why validate takes
167        /// `&mut self`).
168        pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
169            Ok(Version::Latest)
170        }
171    }
172
173    use super::v2 as next;
174    #[expect(clippy::infallible_try_from)] // TODO: evaluate
175    impl TryFrom<ServerDescription> for Final {
176        type Error = <Final as EditableSetting>::Error;
177
178        fn try_from(mut value: ServerDescription) -> Result<Final, Self::Error> {
179            value.validate()?;
180            Ok(next::ServerDescriptions::migrate(value))
181        }
182    }
183}
184
185mod v2 {
186    use std::collections::HashMap;
187
188    use super::{Final, v1 as prev};
189    use crate::settings::editable::{EditableSetting, Version};
190    use serde::{Deserialize, Serialize};
191
192    /// Map of all localized [`ServerDescription`]s
193    #[derive(Clone, Deserialize, Serialize)]
194    pub struct ServerDescriptions {
195        pub default_locale: String,
196        pub descriptions: HashMap<String, ServerDescription>,
197    }
198
199    #[derive(Clone, Deserialize, Serialize)]
200    pub struct ServerDescription {
201        pub motd: String,
202        pub rules: Option<String>,
203    }
204
205    impl Default for ServerDescriptions {
206        fn default() -> Self {
207            Self {
208                default_locale: "en".to_string(),
209                descriptions: HashMap::from([("en".to_string(), ServerDescription::default())]),
210            }
211        }
212    }
213
214    impl Default for ServerDescription {
215        fn default() -> Self {
216            Self {
217                motd: "This is the best Veloren server".into(),
218                rules: None,
219            }
220        }
221    }
222
223    impl ServerDescriptions {
224        fn unwrap_locale_or_default<'a, 'b: 'a>(&'b self, locale: Option<&'a str>) -> &'a str {
225            locale.map_or(&self.default_locale, |locale| {
226                if self.descriptions.contains_key(locale) {
227                    locale
228                } else {
229                    &self.default_locale
230                }
231            })
232        }
233
234        pub fn get(&self, locale: Option<&str>) -> Option<&ServerDescription> {
235            self.descriptions.get(self.unwrap_locale_or_default(locale))
236        }
237
238        /// Attempts to get the rules in the specified locale, falls back to
239        /// `default_locale` if no rules were specified in this locale
240        pub fn get_rules(&self, locale: Option<&str>) -> Option<&str> {
241            self.descriptions
242                .get(self.unwrap_locale_or_default(locale))
243                .and_then(|d| d.rules.as_deref())
244                .or_else(|| {
245                    self.descriptions
246                        .get(&self.default_locale)?
247                        .rules
248                        .as_deref()
249                })
250        }
251    }
252
253    impl ServerDescriptions {
254        /// One-off migration from the previous version.  This must be
255        /// guaranteed to produce a valid settings file as long as it is
256        /// called with a valid settings file from the previous version.
257        pub(super) fn migrate(prev: prev::ServerDescription) -> Self {
258            Self {
259                default_locale: "en".to_string(),
260                descriptions: HashMap::from([("en".to_string(), ServerDescription {
261                    motd: prev.0,
262                    rules: None,
263                })]),
264            }
265        }
266
267        /// Perform any needed validation on this server description that can't
268        /// be done using parsing.
269        ///
270        /// The returned version being "Old" indicates the loaded setting has
271        /// been modified during validation (this is why validate takes
272        /// `&mut self`).
273        pub(super) fn validate(&mut self) -> Result<Version, <Final as EditableSetting>::Error> {
274            if self.descriptions.is_empty() {
275                *self = Self::default();
276                Ok(Version::Old)
277            } else if !self.descriptions.contains_key(&self.default_locale) {
278                // default locale not present, select the a random one (as ordering in hashmaps
279                // isn't predictable)
280                self.default_locale = self
281                    .descriptions
282                    .keys()
283                    .next()
284                    .expect("We know descriptions isn't empty")
285                    .to_string();
286                Ok(Version::Old)
287            } else {
288                Ok(Version::Latest)
289            }
290        }
291    }
292
293    // NOTE: Whenever there is a version upgrade, copy this note as well as the
294    // commented-out code below to the next version, then uncomment the code
295    // for this version.
296    /*
297    use super::{v3 as next, MIGRATION_UPGRADE_GUARANTEE};
298    impl TryFrom<ServerDescription> for Final {
299        type Error = <Final as EditableSetting>::Error;
300
301        fn try_from(mut value: ServerDescription) -> Result<Final, Self::Error> {
302            value.validate()?;
303            Ok(next::ServerDescription::migrate(value).try_into().expect(MIGRATION_UPGRADE_GUARANTEE))
304        }
305    } */
306}