concinnity_world/world/
io.rs1#[derive(Clone)]
8pub struct WorldJsonlAsset {
9 pub name: String,
11 pub asset_type: String,
13 pub args: serde_json::Value,
15}
16
17impl WorldJsonlAsset {
18 pub fn from_value(v: &serde_json::Value) -> Self {
22 WorldJsonlAsset {
23 name: v
24 .get("name")
25 .and_then(|n| n.as_str())
26 .unwrap_or("")
27 .to_string(),
28 asset_type: v
29 .get("type")
30 .and_then(|t| t.as_str())
31 .unwrap_or("")
32 .to_string(),
33 args: v
34 .get("args")
35 .cloned()
36 .unwrap_or_else(|| serde_json::Value::Object(Default::default())),
37 }
38 }
39}
40
41pub fn parse_world_jsonl(content: &str) -> Result<Vec<serde_json::Value>, serde_json::Error> {
46 let mut assets = Vec::new();
47 for line in content.lines() {
48 let trimmed = line.trim();
49 if trimmed.is_empty() || trimmed.starts_with("//") {
50 continue;
51 }
52 let value: serde_json::Value = serde_json::from_str(trimmed)?;
53 assets.push(value);
54 }
55 Ok(assets)
56}
57
58pub fn write_world_jsonl(assets: &[serde_json::Value]) -> serde_json::Result<String> {
63 let mut out = String::new();
64 for asset in assets {
65 out.push_str(&serde_json::to_string(asset)?);
66 out.push('\n');
67 }
68 Ok(out)
69}
70
71pub fn patch_world_jsonl_to<F>(src_path: &str, dst_path: &str, f: F) -> std::io::Result<()>
74where
75 F: FnOnce(&mut Vec<serde_json::Value>) -> std::io::Result<()>,
76{
77 let content = std::fs::read_to_string(src_path).map_err(|e| {
78 tracing::error!("Could not read {}: {}", src_path, e);
79 e
80 })?;
81
82 let mut assets = parse_world_jsonl(&content).map_err(|e| {
83 std::io::Error::new(
84 std::io::ErrorKind::InvalidData,
85 format!("Failed to parse {}: {}", src_path, e),
86 )
87 })?;
88
89 f(&mut assets)?;
90
91 let out = write_world_jsonl(&assets).map_err(|e| std::io::Error::other(e.to_string()))?;
92 std::fs::write(dst_path, out)
93}
94
95pub fn patch_world_jsonl<F>(json_path: &str, f: F) -> std::io::Result<()>
97where
98 F: FnOnce(&mut Vec<serde_json::Value>),
99{
100 patch_world_jsonl_to(json_path, json_path, |assets| {
101 f(assets);
102 Ok(())
103 })
104}
105
106pub fn known_names(json_path: &str) -> std::io::Result<Vec<String>> {
108 let content = std::fs::read_to_string(json_path)?;
109 let assets = parse_world_jsonl(&content)
110 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
111 Ok(assets
112 .iter()
113 .filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string))
114 .collect())
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn parse_world_jsonl_empty_string_returns_empty() {
123 let assets = parse_world_jsonl("").unwrap();
124 assert!(assets.is_empty());
125 }
126
127 #[test]
128 fn parse_world_jsonl_skips_blank_and_comment_lines() {
129 let content = "\n \n// this is a comment\n";
130 let assets = parse_world_jsonl(content).unwrap();
131 assert!(assets.is_empty());
132 }
133
134 #[test]
135 fn parse_world_jsonl_returns_entries_in_order() {
136 let content = r#"{"name":"a","type":"Logger"}
137{"name":"b","type":"Window"}
138"#;
139 let assets = parse_world_jsonl(content).unwrap();
140 assert_eq!(assets.len(), 2);
141 assert_eq!(assets[0]["name"], "a");
142 assert_eq!(assets[1]["name"], "b");
143 }
144
145 #[test]
146 fn parse_world_jsonl_errors_on_invalid_json() {
147 let result = parse_world_jsonl("{not valid json}");
148 assert!(result.is_err());
149 }
150
151 #[test]
152 fn write_world_jsonl_one_line_per_entry() {
153 let assets = vec![
154 serde_json::json!({"name": "a", "type": "Logger"}),
155 serde_json::json!({"name": "b", "type": "Window"}),
156 ];
157 let out = write_world_jsonl(&assets).unwrap();
158 let lines: Vec<&str> = out.lines().collect();
159 assert_eq!(lines.len(), 2);
160 assert!(lines[0].contains("\"a\""));
161 assert!(lines[1].contains("\"b\""));
162 }
163
164 #[test]
165 fn write_world_jsonl_round_trips_through_parse() {
166 let assets = vec![serde_json::json!({"name": "x", "type": "Logger", "args": {}})];
167 let out = write_world_jsonl(&assets).unwrap();
168 let parsed = parse_world_jsonl(&out).unwrap();
169 assert_eq!(parsed.len(), 1);
170 assert_eq!(parsed[0]["name"], "x");
171 }
172
173 #[test]
174 fn patch_world_jsonl_to_applies_mutation() {
175 let dir = tempfile::tempdir().unwrap();
176 let src = dir.path().join("world.jsonl");
177 let dst = dir.path().join("out.jsonl");
178 std::fs::write(&src, "{\"name\":\"a\",\"type\":\"Logger\"}\n").unwrap();
179
180 patch_world_jsonl_to(src.to_str().unwrap(), dst.to_str().unwrap(), |assets| {
181 assets.push(serde_json::json!({"name":"b","type":"Window"}));
182 Ok(())
183 })
184 .unwrap();
185
186 let content = std::fs::read_to_string(&dst).unwrap();
187 let parsed = parse_world_jsonl(&content).unwrap();
188 assert_eq!(parsed.len(), 2);
189 assert_eq!(parsed[1]["name"], "b");
190 }
191
192 #[test]
193 fn patch_world_jsonl_to_propagates_mutation_error() {
194 let dir = tempfile::tempdir().unwrap();
195 let src = dir.path().join("world.jsonl");
196 std::fs::write(&src, "{\"name\":\"a\",\"type\":\"Logger\"}\n").unwrap();
197
198 let result =
199 patch_world_jsonl_to(src.to_str().unwrap(), src.to_str().unwrap(), |_assets| {
200 Err(std::io::Error::other("boom"))
201 });
202 assert!(result.is_err());
203 }
204
205 #[test]
206 fn known_names_extracts_names() {
207 let dir = tempfile::tempdir().unwrap();
208 let path = dir.path().join("world.jsonl");
209 std::fs::write(
210 &path,
211 "{\"name\":\"a\",\"type\":\"Logger\"}\n{\"name\":\"b\",\"type\":\"Window\"}\n",
212 )
213 .unwrap();
214 let names = known_names(path.to_str().unwrap()).unwrap();
215 assert_eq!(names, vec!["a", "b"]);
216 }
217
218 #[test]
219 fn known_names_empty_file_returns_empty() {
220 let dir = tempfile::tempdir().unwrap();
221 let path = dir.path().join("world.jsonl");
222 std::fs::write(&path, "").unwrap();
223 let names = known_names(path.to_str().unwrap()).unwrap();
224 assert!(names.is_empty());
225 }
226}