1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::material::{Material, Recipe};
6
7pub const FIXTURE: &str = include_str!("../fixtures/library.toml");
8
9#[derive(Clone, Debug, Default, PartialEq)]
10pub struct Library {
11 materials: BTreeMap<String, Material>,
12}
13
14#[derive(Default, Serialize, Deserialize)]
15#[serde(default, deny_unknown_fields)]
16struct File {
17 materials: BTreeMap<String, Recipe>,
18}
19
20impl Library {
21 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn fixture() -> Self {
26 Self::from_toml(FIXTURE).expect("the fixture library parses")
27 }
28
29 pub fn from_toml(text: &str) -> Result<Self, String> {
30 let file: File = toml::from_str(text).map_err(|err| err.to_string())?;
31 let mut materials = BTreeMap::new();
32 for (name, recipe) in file.materials {
33 if !valid_name(&name) {
34 return Err(format!(
35 "material name {name:?} needs printable characters without spaces at its ends"
36 ));
37 }
38 let material = recipe
39 .into_material()
40 .map_err(|err| format!("materials.{name}: {err}"))?;
41 materials.insert(name, material);
42 }
43 Ok(Self { materials })
44 }
45
46 pub fn to_toml(&self) -> Result<String, String> {
47 let file = File {
48 materials: self
49 .materials
50 .iter()
51 .map(|(name, material)| (name.clone(), Recipe::from(*material)))
52 .collect(),
53 };
54 toml::to_string(&file).map_err(|err| err.to_string())
55 }
56
57 pub fn get(&self, name: &str) -> Option<&Material> {
58 self.materials.get(name)
59 }
60
61 pub fn contains(&self, name: &str) -> bool {
62 self.materials.contains_key(name)
63 }
64
65 pub fn insert(&mut self, name: &str, material: Material) -> Result<Option<Material>, String> {
66 if !valid_name(name) {
67 return Err(format!(
68 "material name {name:?} needs printable characters without spaces at its ends"
69 ));
70 }
71 Ok(self.materials.insert(name.to_owned(), material))
72 }
73
74 pub fn names(&self) -> impl Iterator<Item = &str> {
75 self.materials.keys().map(String::as_str)
76 }
77
78 pub fn iter(&self) -> impl Iterator<Item = (&str, &Material)> {
79 self.materials
80 .iter()
81 .map(|(name, material)| (name.as_str(), material))
82 }
83
84 pub fn len(&self) -> usize {
85 self.materials.len()
86 }
87
88 pub fn is_empty(&self) -> bool {
89 self.materials.is_empty()
90 }
91}
92
93fn valid_name(name: &str) -> bool {
94 !name.is_empty() && name.trim() == name && !name.chars().any(char::is_control)
95}