Skip to main content

concinnity_world/world/
io.rs

1// world.jsonl I/O: parsing, serialization, and file-patch utilities. This is
2// authoring input handling -- the compile pipeline and the editor read and
3// rewrite world.jsonl through here. The shipped runtime plays compiled blobs
4// and never touches world.jsonl, so this lives in the build crate, not core.
5
6/// An asset entry after $include resolution and type parsing.
7#[derive(Clone)]
8pub struct WorldJsonlAsset {
9    /// The asset's declared name.
10    pub name: String,
11    /// The asset's registry type name.
12    pub asset_type: String,
13    /// The asset's authored args.
14    pub args: serde_json::Value,
15}
16
17impl WorldJsonlAsset {
18    /// Build a typed entry from a raw JSON asset object. `name` and `type` are
19    /// expected to be present; a missing field degrades to an empty string
20    /// rather than failing.
21    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
41/// Parse a world.jsonl string into a flat list of raw asset objects.
42///
43/// Each non-blank, non-comment line must be a valid JSON object. The order
44/// of entries is preserved. Returns an error on the first malformed line.
45pub 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
58/// Serialize a list of asset objects back to world.jsonl format.
59///
60/// Each entry is written as a compact single-line JSON object followed by a
61/// newline. The result is a valid world.jsonl file.
62pub 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
71/// Read src_path, apply a fallible mutation to the asset list, and write
72/// the result to dst_path. src and dst may be the same path or different.
73pub 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
95/// Read world.jsonl at json_path, mutate the asset list in-place, write back.
96pub 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
106/// Read asset names from world.jsonl without a full parse, for error messages.
107pub 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}