Skip to main content

libmaxminddb_rs/
metadata.rs

1//! MaxMind DB metadata model and builder.
2
3use std::collections::BTreeMap;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6#[cfg(feature = "writer")]
7use crate::Value;
8#[cfg(feature = "reader")]
9use crate::ValueRef;
10use crate::{Error, Result};
11
12/// Public MMDB metadata.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Metadata {
15    /// Number of nodes in the binary search tree.
16    pub node_count: u64,
17    /// Width, in bits, of each tree record (normally 24, 28, or 32).
18    pub record_size: u16,
19    /// Address-family version stored by the database: 4 or 6.
20    pub ip_version: u16,
21    /// Application-defined database type identifier.
22    pub database_type: String,
23    /// Languages advertised by the database.
24    pub languages: Vec<String>,
25    /// MMDB binary-format major version.
26    pub binary_format_major_version: u16,
27    /// MMDB binary-format minor version.
28    pub binary_format_minor_version: u16,
29    /// Database build timestamp as seconds since the Unix epoch.
30    pub build_epoch: u64,
31    /// Human-readable descriptions keyed by language code.
32    pub description: BTreeMap<String, String>,
33}
34
35impl Metadata {
36    #[cfg(feature = "reader")]
37    pub(crate) fn from_value(value: &ValueRef<'_>) -> Result<Self> {
38        let map = match value {
39            ValueRef::Map(v) => v,
40            _ => return Err(Error::InvalidMetadata("metadata root is not a map")),
41        };
42        let get = |key: &str| map.iter().find_map(|(k, v)| (*k == key).then_some(v));
43        Ok(Self {
44            node_count: get_u64(get("node_count"))
45                .ok_or(Error::InvalidMetadata("missing node_count"))?,
46            record_size: get_u16(get("record_size"))
47                .ok_or(Error::InvalidMetadata("missing record_size"))?,
48            ip_version: get_u16(get("ip_version"))
49                .ok_or(Error::InvalidMetadata("missing ip_version"))?,
50            database_type: get_string(get("database_type"))
51                .ok_or(Error::InvalidMetadata("missing database_type"))?,
52            languages: get_strings(get("languages")).unwrap_or_default(),
53            binary_format_major_version: get_u16(get("binary_format_major_version")).ok_or(
54                Error::InvalidMetadata("missing binary_format_major_version"),
55            )?,
56            binary_format_minor_version: get_u16(get("binary_format_minor_version")).ok_or(
57                Error::InvalidMetadata("missing binary_format_minor_version"),
58            )?,
59            build_epoch: get_u64(get("build_epoch"))
60                .ok_or(Error::InvalidMetadata("missing build_epoch"))?,
61            description: get_descriptions(get("description")).unwrap_or_default(),
62        })
63    }
64
65    #[cfg(feature = "writer")]
66    pub(crate) fn to_value(&self) -> Value {
67        let mut m = BTreeMap::new();
68        m.insert("node_count".into(), Value::Uint32(self.node_count as u32));
69        m.insert("record_size".into(), Value::Uint16(self.record_size));
70        m.insert("ip_version".into(), Value::Uint16(self.ip_version));
71        m.insert(
72            "database_type".into(),
73            Value::Utf8(self.database_type.clone()),
74        );
75        m.insert(
76            "languages".into(),
77            Value::Array(self.languages.iter().cloned().map(Value::Utf8).collect()),
78        );
79        m.insert(
80            "binary_format_major_version".into(),
81            Value::Uint16(self.binary_format_major_version),
82        );
83        m.insert(
84            "binary_format_minor_version".into(),
85            Value::Uint16(self.binary_format_minor_version),
86        );
87        m.insert("build_epoch".into(), Value::Uint64(self.build_epoch));
88        m.insert(
89            "description".into(),
90            Value::Map(
91                self.description
92                    .iter()
93                    .map(|(k, v)| (k.clone(), Value::Utf8(v.clone())))
94                    .collect(),
95            ),
96        );
97        Value::Map(m)
98    }
99}
100
101/// Builder for writer metadata.
102#[derive(Debug, Clone)]
103pub struct MetadataBuilder {
104    metadata: Metadata,
105}
106
107impl Default for MetadataBuilder {
108    fn default() -> Self {
109        let build_epoch = SystemTime::now()
110            .duration_since(UNIX_EPOCH)
111            .map_or(0, |d| d.as_secs());
112        Self {
113            metadata: Metadata {
114                node_count: 0,
115                record_size: 28,
116                ip_version: 6,
117                database_type: "libmaxminddb-rs".into(),
118                languages: vec!["en".into()],
119                binary_format_major_version: 2,
120                binary_format_minor_version: 0,
121                build_epoch,
122                description: BTreeMap::new(),
123            },
124        }
125    }
126}
127
128impl MetadataBuilder {
129    /// Creates a builder with MMDB v2 defaults.
130    ///
131    /// # Examples
132    ///
133    /// ```rust
134    /// use libmaxminddb_rs::MetadataBuilder;
135    /// let metadata = MetadataBuilder::new().build()?;
136    /// assert_eq!(metadata.ip_version, 6);
137    /// # Ok::<(), libmaxminddb_rs::Error>(())
138    /// ```
139    #[must_use]
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Sets the database type identifier.
145    ///
146    /// # Examples
147    ///
148    /// ```rust
149    /// use libmaxminddb_rs::MetadataBuilder;
150    /// let metadata = MetadataBuilder::new().database_type("Example-City").build()?;
151    /// assert_eq!(metadata.database_type, "Example-City");
152    /// # Ok::<(), libmaxminddb_rs::Error>(())
153    /// ```
154    #[must_use]
155    pub fn database_type(mut self, value: impl Into<String>) -> Self {
156        self.metadata.database_type = value.into();
157        self
158    }
159
160    /// Sets the database IP version (`4` or `6`).
161    /// [`Self::build`] rejects any other value.
162    ///
163    /// # Examples
164    ///
165    /// ```rust
166    /// use libmaxminddb_rs::MetadataBuilder;
167    /// let metadata = MetadataBuilder::new().ip_version(4).build()?;
168    /// assert_eq!(metadata.ip_version, 4);
169    /// # Ok::<(), libmaxminddb_rs::Error>(())
170    /// ```
171    #[must_use]
172    pub fn ip_version(mut self, value: u16) -> Self {
173        self.metadata.ip_version = value;
174        self
175    }
176
177    /// Adds one advertised language, deduplicating the list.
178    ///
179    /// # Examples
180    ///
181    /// ```rust
182    /// use libmaxminddb_rs::MetadataBuilder;
183    /// let metadata = MetadataBuilder::new().language("fr").build()?;
184    /// assert!(metadata.languages.contains(&"fr".to_string()));
185    /// # Ok::<(), libmaxminddb_rs::Error>(())
186    /// ```
187    #[must_use]
188    pub fn language(mut self, value: impl Into<String>) -> Self {
189        self.metadata.languages.push(value.into());
190        self.metadata.languages.sort();
191        self.metadata.languages.dedup();
192        self
193    }
194
195    /// Replaces the complete advertised-language list.
196    ///
197    /// # Examples
198    ///
199    /// ```rust
200    /// use libmaxminddb_rs::MetadataBuilder;
201    /// let metadata = MetadataBuilder::new().languages(["fr".to_string()]).build()?;
202    /// assert_eq!(metadata.languages, vec!["fr".to_string()]);
203    /// # Ok::<(), libmaxminddb_rs::Error>(())
204    /// ```
205    #[must_use]
206    pub fn languages(mut self, values: impl IntoIterator<Item = String>) -> Self {
207        self.metadata.languages = values.into_iter().collect();
208        self
209    }
210
211    /// Adds or replaces a localized database description.
212    ///
213    /// # Examples
214    ///
215    /// ```rust
216    /// use libmaxminddb_rs::MetadataBuilder;
217    /// let metadata = MetadataBuilder::new().description("en", "Example database").build()?;
218    /// assert_eq!(metadata.description["en"], "Example database");
219    /// # Ok::<(), libmaxminddb_rs::Error>(())
220    /// ```
221    #[must_use]
222    pub fn description(mut self, language: impl Into<String>, value: impl Into<String>) -> Self {
223        self.metadata
224            .description
225            .insert(language.into(), value.into());
226        self
227    }
228
229    /// Overrides the build epoch in seconds since Unix epoch.
230    ///
231    /// # Examples
232    ///
233    /// ```rust
234    /// use libmaxminddb_rs::MetadataBuilder;
235    /// let metadata = MetadataBuilder::new().build_epoch(1_700_000_000).build()?;
236    /// assert_eq!(metadata.build_epoch, 1_700_000_000);
237    /// # Ok::<(), libmaxminddb_rs::Error>(())
238    /// ```
239    #[must_use]
240    pub fn build_epoch(mut self, value: u64) -> Self {
241        self.metadata.build_epoch = value;
242        self
243    }
244
245    /// Validates the configured fields and produces metadata.
246    /// Returns [`Error::InvalidIpVersion`] for unsupported IP versions or
247    /// [`Error::InvalidMetadata`] for an empty database type.
248    ///
249    /// # Examples
250    ///
251    /// ```rust
252    /// use libmaxminddb_rs::MetadataBuilder;
253    /// let metadata = MetadataBuilder::new().ip_version(4).build()?;
254    /// assert_eq!(metadata.ip_version, 4);
255    /// # Ok::<(), libmaxminddb_rs::Error>(())
256    /// ```
257    pub fn build(self) -> Result<Metadata> {
258        if !matches!(self.metadata.ip_version, 4 | 6) {
259            return Err(Error::InvalidIpVersion(self.metadata.ip_version));
260        }
261        if self.metadata.database_type.is_empty() {
262            return Err(Error::InvalidMetadata("database_type must not be empty"));
263        }
264        Ok(self.metadata)
265    }
266}
267
268#[cfg(feature = "reader")]
269fn get_u16(value: Option<&ValueRef<'_>>) -> Option<u16> {
270    match value? {
271        ValueRef::Uint16(v) => Some(*v),
272        ValueRef::Uint32(v) => u16::try_from(*v).ok(),
273        ValueRef::Uint64(v) => u16::try_from(*v).ok(),
274        _ => None,
275    }
276}
277
278#[cfg(feature = "reader")]
279fn get_u64(value: Option<&ValueRef<'_>>) -> Option<u64> {
280    match value? {
281        ValueRef::Uint16(v) => Some(u64::from(*v)),
282        ValueRef::Uint32(v) => Some(u64::from(*v)),
283        ValueRef::Uint64(v) => Some(*v),
284        _ => None,
285    }
286}
287
288#[cfg(feature = "reader")]
289fn get_string(value: Option<&ValueRef<'_>>) -> Option<String> {
290    match value? {
291        ValueRef::Utf8(v) => Some((*v).to_owned()),
292        _ => None,
293    }
294}
295
296#[cfg(feature = "reader")]
297fn get_strings(value: Option<&ValueRef<'_>>) -> Option<Vec<String>> {
298    match value? {
299        ValueRef::Array(v) => v
300            .iter()
301            .map(|v| match v {
302                ValueRef::Utf8(s) => Some((*s).to_owned()),
303                _ => None,
304            })
305            .collect(),
306        _ => None,
307    }
308}
309
310#[cfg(feature = "reader")]
311fn get_descriptions(value: Option<&ValueRef<'_>>) -> Option<BTreeMap<String, String>> {
312    match value? {
313        ValueRef::Map(v) => v
314            .iter()
315            .map(|(k, v)| match v {
316                ValueRef::Utf8(s) => Some(((*k).to_owned(), (*s).to_owned())),
317                _ => None,
318            })
319            .collect(),
320        _ => None,
321    }
322}
323
324#[cfg(all(test, feature = "reader", feature = "writer"))]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn builder_deduplicates_languages_and_serializes_descriptions() {
330        let metadata = MetadataBuilder::new()
331            .database_type("Coverage-City")
332            .ip_version(4)
333            .languages(["fr".to_owned()])
334            .language("en")
335            .language("en")
336            .description("en", "first")
337            .description("en", "updated")
338            .build_epoch(1_700_000_000)
339            .build()
340            .unwrap();
341        assert_eq!(metadata.languages, ["en", "fr"]);
342        assert_eq!(metadata.description["en"], "updated");
343        assert_eq!(metadata.build_epoch, 1_700_000_000);
344        let Value::Map(encoded) = metadata.to_value() else {
345            panic!("metadata must encode as a map");
346        };
347        assert_eq!(
348            encoded["database_type"],
349            Value::Utf8("Coverage-City".into())
350        );
351        assert_eq!(
352            encoded["description"],
353            Value::Map(BTreeMap::from([(
354                "en".into(),
355                Value::Utf8("updated".into())
356            )]))
357        );
358        assert!(matches!(
359            MetadataBuilder::new().ip_version(5).build(),
360            Err(Error::InvalidIpVersion(5))
361        ));
362        assert!(matches!(
363            MetadataBuilder::new().database_type("").build(),
364            Err(Error::InvalidMetadata(_))
365        ));
366    }
367
368    #[test]
369    fn metadata_decoder_validates_required_and_optional_fields() {
370        let fields = vec![
371            ("node_count", ValueRef::Uint32(3)),
372            ("record_size", ValueRef::Uint16(28)),
373            ("ip_version", ValueRef::Uint64(6)),
374            ("database_type", ValueRef::Utf8("Coverage-City")),
375            ("languages", ValueRef::Array(vec![ValueRef::Utf8("en")])),
376            ("binary_format_major_version", ValueRef::Uint16(2)),
377            ("binary_format_minor_version", ValueRef::Uint32(0)),
378            ("build_epoch", ValueRef::Uint64(1)),
379            (
380                "description",
381                ValueRef::Map(vec![("en", ValueRef::Utf8("example"))]),
382            ),
383        ];
384        let decoded = Metadata::from_value(&ValueRef::Map(fields.clone())).unwrap();
385        assert_eq!(decoded.node_count, 3);
386        assert_eq!(decoded.description["en"], "example");
387        assert_eq!(decoded.languages, ["en"]);
388        assert!(Metadata::from_value(&ValueRef::Bool(true)).is_err());
389
390        for required in [
391            "node_count",
392            "record_size",
393            "ip_version",
394            "database_type",
395            "binary_format_major_version",
396            "binary_format_minor_version",
397            "build_epoch",
398        ] {
399            let without = fields
400                .iter()
401                .filter(|(key, _)| *key != required)
402                .cloned()
403                .collect();
404            assert!(
405                Metadata::from_value(&ValueRef::Map(without)).is_err(),
406                "{required}"
407            );
408        }
409
410        let optional_invalid = fields
411            .into_iter()
412            .map(|(key, value)| match key {
413                "languages" => (key, ValueRef::Array(vec![ValueRef::Bool(true)])),
414                "description" => (key, ValueRef::Map(vec![("en", ValueRef::Bool(true))])),
415                _ => (key, value),
416            })
417            .collect();
418        let decoded = Metadata::from_value(&ValueRef::Map(optional_invalid)).unwrap();
419        assert!(decoded.languages.is_empty());
420        assert!(decoded.description.is_empty());
421    }
422}