1use 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#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Metadata {
15 pub node_count: u64,
17 pub record_size: u16,
19 pub ip_version: u16,
21 pub database_type: String,
23 pub languages: Vec<String>,
25 pub binary_format_major_version: u16,
27 pub binary_format_minor_version: u16,
29 pub build_epoch: u64,
31 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#[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 #[must_use]
140 pub fn new() -> Self {
141 Self::default()
142 }
143
144 #[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 #[must_use]
172 pub fn ip_version(mut self, value: u16) -> Self {
173 self.metadata.ip_version = value;
174 self
175 }
176
177 #[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 #[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 #[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 #[must_use]
240 pub fn build_epoch(mut self, value: u64) -> Self {
241 self.metadata.build_epoch = value;
242 self
243 }
244
245 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}