rush_ecs_parser/ports/
toml.rs

1//! Parser Port for TOML File Format
2
3use crate::{adapter::Parser, error::utils::ensure_syntax};
4use anyhow::Result;
5use rush_ecs_core::blueprint::{
6    Blueprint, BlueprintString, Component, ComponentType, ComponentValue, Entity,
7};
8use std::collections::BTreeMap;
9use toml::{Table, Value};
10
11/// TOML Blueprint Parser
12///
13/// This [`Parser`] expects a properly formatted
14/// Blueprint [`String`] that follows:
15///
16/// Rush TOML DSL Specification
17///
18/// Example
19///
20/// ```toml
21/// [world]
22/// name = "Sonic's World"
23/// description = "This is Sonic's world"
24/// regions = ["farm", "house"]
25///
26/// [entity]
27/// player = { name = "String", x = "f64", y = "f64", w = "f64", h = "f64", speed = "f64" }
28/// apple = { x = "f64", y = "f64"}
29///
30/// [farm]
31/// player = [
32///    { name = "npc", x = 0.0, y = 0.0, w = 0.0, h = 0.0, speed = 0.0 }
33/// ]
34/// apple = [
35///     { x = 0, y = 0}
36/// ]
37///
38/// [house]
39/// player = [
40///     { name = "npc", x = 0.0, y = 0.0, w = 0.0, h = 0.0, speed = 0.0 }
41/// ]
42/// ```
43///
44#[derive(Clone, Debug, Default)]
45pub struct TomlParser {}
46
47impl Parser for TomlParser {
48    fn parse_string(&self, blueprint_string: BlueprintString) -> Result<Blueprint> {
49        // expecting a valid TOML
50        let table: Table = blueprint_string.parse::<Table>().expect("invalid TOML");
51
52        // ensure syntax for top-level Tables and Properties are met
53
54        // WORLD
55
56        ensure_syntax(
57            "World table must exist".to_string(),
58            table.contains_key("world"),
59        );
60        ensure_syntax(
61            "World table must be a table".to_string(),
62            table["world"].is_table(),
63        );
64
65        let world_table = table["world"].as_table().unwrap();
66
67        ensure_syntax(
68            "World must have a name".to_string(),
69            world_table.contains_key("name"),
70        );
71
72        ensure_syntax(
73            "World name must be a string".to_string(),
74            world_table["name"].is_str(),
75        );
76
77        ensure_syntax(
78            "World must have a description".to_string(),
79            world_table.contains_key("description"),
80        );
81
82        ensure_syntax(
83            "World description must be a string".to_string(),
84            world_table["description"].is_str(),
85        );
86
87        ensure_syntax(
88            "World must have a regions property".to_string(),
89            world_table.contains_key("regions"),
90        );
91        ensure_syntax(
92            "World regions property must be an array".to_string(),
93            world_table["regions"].is_array(),
94        );
95        ensure_syntax(
96            "World must have at least 1 region".to_string(),
97            !world_table["regions"].as_array().unwrap().is_empty(),
98        );
99        ensure_syntax(
100            "World regions property must be an array of strings".to_string(),
101            world_table["regions"].as_array().unwrap()[0].is_str(),
102        );
103
104        // get regions into Vec<String>
105        let regions = world_table["regions"]
106            .as_array()
107            .unwrap()
108            .iter()
109            .map(|r| r.as_str().unwrap().to_string()) // unwrap ok
110            .collect::<Vec<_>>();
111
112        // REGIONS
113
114        // every region stated in the world table must have
115        // a table of instances in the blueprint
116        for region in regions.iter() {
117            ensure_syntax(
118                format!("Region {region} table must exist"),
119                // certain region exists
120                table.contains_key(region),
121            );
122        }
123
124        // ENTITY
125
126        ensure_syntax(
127            "Enttiy table must exist".to_string(),
128            table.contains_key("entity"),
129        );
130        ensure_syntax(
131            "Entity table must be a table".to_string(),
132            table["entity"].is_table(),
133        );
134
135        let entity_table = table["entity"].as_table().unwrap();
136        let entities = entity_table.keys().cloned().collect::<Vec<_>>();
137
138        ensure_syntax(
139            "Entity table must have at least 1 entity properties".to_string(),
140            // not empty
141            !entities.is_empty() &&
142            // must be a table of properties e.g. { x = 0, y = 0 }
143            entity_table[&entities[0]].is_table(),
144        );
145
146        // parse World's name
147        let world_name = world_table["name"].as_str().unwrap().to_string();
148        let world_description = world_table["description"].as_str().unwrap().to_string();
149
150        // create Blueprint
151        let mut blueprint = Blueprint::new(world_name, world_description);
152
153        // TODO: Move this closer to Load Instances
154        // preload Instance Keys
155        blueprint.preload(regions.clone(), entities.clone());
156
157        // load Regions into World
158        for region_name in regions.iter() {
159            // load into World tree
160            if let Some(region_table) = table[region_name].as_table() {
161                // get entities from keys in the table
162                let entities = region_table.keys().cloned().collect::<Vec<Entity>>();
163                blueprint.add_region(region_name.clone(), entities);
164            }
165        }
166
167        // load Entities into World
168        for entity_name in entities.into_iter() {
169            if let Some(component_table) = entity_table[&entity_name].as_table() {
170                // load Entities
171                let mut component_type_tree: BTreeMap<Component, ComponentType> = BTreeMap::new();
172                for component_name in component_table.keys() {
173                    // unwrap ok, a value is expected
174                    let value = component_table
175                        .get(component_name)
176                        .unwrap()
177                        .as_str()
178                        .unwrap()
179                        .to_string();
180                    component_type_tree.insert(component_name.to_string(), value);
181                }
182                blueprint.add_entity(entity_name, component_type_tree);
183            }
184        }
185
186        // TODO: Refactor nested For loops for readability
187        //
188        // load Instances
189        //
190        // In blueprint.rs:
191        //  pub instances: BTreeMap<Region, BTreeMap<Entity, Vec<ComponentTree>>>,
192
193        let blueprint_regions = blueprint.regions.clone();
194
195        for region_name in regions.into_iter() {
196            // if there are entities in region
197            if let Some(entities_in_region) = blueprint_regions.get(&region_name) {
198                // get each entity in region
199                for entity_name in entities_in_region.iter() {
200                    if let Some(instances) = table[&region_name][entity_name].as_array() {
201                        for instance in instances.iter() {
202                            // build each entity's component tree
203                            if let Some(entity_components) = instance.as_table() {
204                                let mut component_tree: BTreeMap<Component, ComponentValue> =
205                                    BTreeMap::new();
206
207                                // get (component, value) pairs
208                                for (toml_component, toml_value) in entity_components.into_iter() {
209                                    let component = toml_component.to_string();
210                                    let value = match toml_value {
211                                        Value::String(v) => ComponentValue::String(v.to_string()),
212                                        Value::Float(v) => ComponentValue::Float(*v),
213                                        Value::Integer(v) => ComponentValue::Integer(*v),
214                                        Value::Boolean(v) => ComponentValue::Boolean(*v),
215                                        _ => panic!("Unsupported data type"),
216                                    };
217
218                                    component_tree.insert(component, value);
219                                }
220
221                                blueprint.add_instance(
222                                    region_name.clone(),
223                                    entity_name.to_string(),
224                                    component_tree,
225                                )?;
226                            }
227                        }
228                    }
229                }
230            }
231        }
232
233        Ok(blueprint)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::utils::file_to_string;
241    use std::path::Path;
242
243    #[test]
244    fn test_toml_parser_file() {
245        let path = Path::new("mock/fixtures/ports/blueprint.toml");
246        let blueprint_string = file_to_string(path);
247
248        let toml_parser = TomlParser::default();
249        let blueprint = toml_parser.parse_string(blueprint_string).unwrap();
250        println!("{:?}", blueprint);
251        // TODO: Assert value
252        assert!(true)
253    }
254}