1use std::io::{BufRead, Write};
15use std::path::{Path, PathBuf};
16
17use base64::Engine as _;
18use serde_json::{Map, Value, json};
19use yog_formats::yres::{Entry, Kind};
20
21pub fn run() -> Result<(), String> {
23 let stdin = std::io::stdin();
24 let mut out = std::io::stdout().lock();
25 for line in stdin.lock().lines() {
26 let line = line.map_err(|e| e.to_string())?;
27 if line.trim().is_empty() {
28 continue;
29 }
30 let reply = match serde_json::from_str::<Value>(&line) {
31 Ok(req) => {
32 let id = req.get("id").cloned().unwrap_or(Value::Null);
33 let method = req.get("method").and_then(Value::as_str).unwrap_or_default();
34 let params = req.get("params").cloned().unwrap_or(Value::Null);
35 match dispatch(method, ¶ms) {
36 Ok(result) => json!({ "id": id, "result": result }),
37 Err(error) => json!({ "id": id, "error": error }),
38 }
39 }
40 Err(e) => json!({ "id": null, "error": format!("not a request: {e}") }),
41 };
42 writeln!(out, "{reply}").map_err(|e| e.to_string())?;
43 out.flush().map_err(|e| e.to_string())?;
44 }
45 Ok(())
46}
47
48fn str_param<'a>(params: &'a Value, name: &str) -> Result<&'a str, Value> {
50 params.get(name).and_then(Value::as_str).ok_or_else(|| json!(format!("missing string `{name}`")))
51}
52
53fn path_param(params: &Value, name: &str) -> Result<PathBuf, Value> {
55 str_param(params, name).map(PathBuf::from)
56}
57
58fn data_param(params: &Value, name: &str) -> Result<yog_data::Value, Value> {
60 yog_data::text::parse_value(str_param(params, name)?).map_err(|e| json!(format!("`{name}`: {e}")))
61}
62
63fn base64(bytes: &[u8]) -> String {
65 base64::engine::general_purpose::STANDARD.encode(bytes)
66}
67
68pub fn dispatch(method: &str, params: &Value) -> Result<Value, Value> {
71 match method {
72 "media.import" => {
74 let file = path_param(params, "file")?;
75 let bytes = std::fs::read(&file).map_err(|e| json!(format!("{}: {e}", file.display())))?;
76 let kind = yog_formats::yres::kind_of(&yog_formats::yres::Value::Bytes(bytes.clone()));
77 Ok(json!({ "value": yog_data::typed::typed_string(&yog_data::Value::Bytes(bytes)), "type": kind.name() }))
78 }
79 "version" => Ok(json!(env!("CARGO_PKG_VERSION"))),
81 "yres.parse" => yres_parse(str_param(params, "text")?, params.get("set").and_then(Value::as_str).zip(params.get("mod").and_then(Value::as_str))),
84 "yres.write" => yres_write(params.get("entries").ok_or_else(|| json!("missing `entries`"))?),
86 "data.parse" => {
88 let value = yog_data::text::parse_value(str_param(params, "text")?)
89 .map_err(|e| json!({ "message": e.to_string(), "line": e.at.line, "col": e.at.col }))?;
90 Ok(json!(yog_data::patch::to_json(&value)))
91 }
92 "data.patch" => Ok(json!(yog_data::patch::patch(str_param(params, "text")?, &data_param(params, "next")?))),
94 "data.format" => {
96 let level = params.get("level").and_then(Value::as_u64).unwrap_or(0) as usize;
97 Ok(json!(yog_data::patch::format(&data_param(params, "value")?, level)))
98 }
99 "typed.classify" => {
102 let values = params.get("values").and_then(Value::as_array).ok_or_else(|| json!("missing list `values`"))?;
103 Ok(Value::Array(values.iter().map(|v| typed_json(v.as_str().unwrap_or_default())).collect()))
104 }
105 "format.shape" => {
107 let name = str_param(params, "format")?;
108 let shape = yog_formats::shape_of(name).ok_or_else(|| json!(format!("no format `{name}`")))?;
109 Ok(yog_data::json::to_json(&shape.describe()))
110 }
111 "mod.id" => Ok(json!(crate::media::mod_id(&mod_root(&path_param(params, "path")?)?))),
113 "media.built_asset" => {
115 let root = mod_root(&path_param(params, "path")?)?;
116 Ok(json!(crate::media::built_asset(&root, str_param(params, "ns")?, str_param(params, "asset")?).map(|b| base64(&b))))
117 }
118 "media.textures" => {
120 let root = mod_root(&path_param(params, "path")?)?;
121 let list: Vec<Value> = crate::media::textures(&root)
122 .into_iter()
123 .map(|t| json!({ "id": t.id, "file": t.file, "image": format!("{}{}.{}", yog_ids::sep::TYPED, t.texture.image.set, t.texture.image.key), "atlas": t.texture.atlas }))
124 .collect();
125 Ok(Value::Array(list))
126 }
127 "media.is_own_texture" => {
129 let root = mod_root(&path_param(params, "path")?)?;
130 Ok(json!(crate::media::is_own_texture(&root, str_param(params, "id")?)))
131 }
132 "media.pictures" => {
134 let root = mod_root(&path_param(params, "path")?)?;
135 let list: Vec<Value> = crate::media::pictures(&root)
136 .into_iter()
137 .map(|p| json!({ "set": p.set, "name": p.name, "path": p.path(), "file": p.file }))
138 .collect();
139 Ok(Value::Array(list))
140 }
141 "media.save_texture_picture" => {
143 let root = mod_root(&path_param(params, "path")?)?;
144 let png = base64::engine::general_purpose::STANDARD.decode(str_param(params, "png")?).map_err(|e| json!(e.to_string()))?;
145 crate::media::save_texture_picture(&root, str_param(params, "id")?, png).map(|f| json!(f)).map_err(Value::from)
146 }
147 "media.save_texture_animation" => {
149 let root = mod_root(&path_param(params, "path")?)?;
150 let animation = params.get("mcmeta").filter(|m| !m.is_null()).and_then(yog_formats::texture::Animation::from_mcmeta);
151 crate::media::save_texture_animation(&root, str_param(params, "id")?, animation).map(|f| json!(f)).map_err(Value::from)
152 }
153 other => crate::design::service::dispatch(other, params).unwrap_or_else(|| Err(json!(format!("no method `{other}`")))),
155 }
156}
157
158fn typed_json(s: &str) -> Value {
160 match yog_data::typed::classify(s) {
161 Ok(yog_data::Value::Ref(r)) => json!({ "t": "ref", "kind": r.kind, "id": r.id }),
162 Ok(yog_data::Value::Tag(r)) => json!({ "t": "tag", "kind": r.kind, "id": r.id }),
163 Ok(yog_data::Value::Res(r)) => json!({ "t": "res", "set": r.set, "key": r.key }),
164 Ok(yog_data::Value::Image(source)) => json!({ "t": "image", "source": source }),
165 Ok(yog_data::Value::Lang(key)) => json!({ "t": "lang", "key": key }),
166 Ok(yog_data::Value::Bytes(_)) => json!({ "t": "bytes" }),
167 Ok(yog_data::Value::Str(v)) => json!({ "t": "str", "v": v }),
168 Ok(other) => json!({ "t": "str", "v": yog_data::typed::typed_string(&other).unwrap_or_default() }),
169 Err(error) => json!({ "error": error }),
170 }
171}
172
173fn mod_root(file: &Path) -> Result<PathBuf, Value> {
175 crate::design::project::mod_root(file).ok_or_else(|| json!(format!("{} isn't in a mod", file.display())))
176}
177
178fn yres_parse(text: &str, set_and_mod: Option<(&str, &str)>) -> Result<Value, Value> {
179 let entries = yog_formats::yres::parse(text).map_err(|e| {
180 let line = e.split(':').next().and_then(|l| l.trim().parse::<u64>().ok());
182 json!({ "message": e, "line": line })
183 })?;
184 Ok(json!({ "entries": entries.iter().map(|e| entry_json(e, set_and_mod)).collect::<Vec<_>>() }))
185}
186
187fn entry_json(e: &Entry, set_and_mod: Option<(&str, &str)>) -> Value {
190 let mut m = Map::new();
191 m.insert("name".into(), json!(e.name));
192 m.insert("type".into(), json!(e.kind.name()));
193 m.insert("value".into(), match &e.value {
194 yog_formats::yres::Value::Str(s) => json!(s),
195 yog_formats::yres::Value::Num(n) => serde_json::from_str(n).unwrap_or(Value::Null),
196 yog_formats::yres::Value::Bool(b) => json!(b),
197 yog_formats::yres::Value::Bytes(b) => json!(yog_data::typed::typed_string(&yog_data::Value::Bytes(b.clone()))),
198 });
199 m.insert("line".into(), json!(e.line));
200 m.insert("accessor".into(), json!(crate::yres::accessor_name(&e.name)));
201 if let Some(problem) = crate::yres::name_problem(&e.name) {
202 m.insert("nameProblem".into(), json!(problem));
203 }
204 if let Some(problem) = crate::yres::value_problem(e) {
205 m.insert("valueProblem".into(), json!(problem));
206 }
207 if let Some(argb) = crate::yres::color_of(e) {
208 m.insert("argb".into(), json!(argb));
209 }
210 if let (Some((set, mod_id)), Kind::String) = (set_and_mod, e.kind) {
211 let key = e.key.clone().unwrap_or_else(|| crate::yres::default_key(set, mod_id, &e.name));
212 m.insert("translationKey".into(), json!(key));
213 }
214 for (k, v) in [("comment", &e.comment), ("key", &e.key), ("event", &e.event), ("subtitle", &e.subtitle)] {
215 if let Some(v) = v {
216 m.insert(k.into(), json!(v));
217 }
218 }
219 Value::Object(m)
220}
221
222fn yres_write(entries: &Value) -> Result<Value, Value> {
223 let list = entries.as_array().ok_or_else(|| json!("`entries` is a list"))?;
224 let mut out = Vec::with_capacity(list.len());
225 for e in list {
226 let name = e.get("name").and_then(Value::as_str).ok_or_else(|| json!("an entry without `name`"))?;
227 let kind = e.get("type").and_then(Value::as_str).unwrap_or(Kind::String.name());
228 let kind = KINDS.iter().copied().find(|k| k.name() == kind).ok_or_else(|| json!(format!("{name}: no type `{kind}`")))?;
229 let value = match e.get("value") {
230 Some(Value::String(s)) if s.starts_with(yog_ids::sep::TYPED) => match yog_data::typed::classify(s) {
231 Ok(yog_data::Value::Bytes(b)) => yog_formats::yres::Value::Bytes(b),
232 _ => yog_formats::yres::Value::Str(s.clone()),
233 },
234 Some(Value::String(s)) => yog_formats::yres::Value::Str(s.clone()),
235 Some(Value::Bool(b)) => yog_formats::yres::Value::Bool(*b),
236 Some(Value::Number(n)) => yog_formats::yres::Value::Num(n.to_string()),
237 _ => return Err(json!(format!("{name}: no value"))),
238 };
239 let field = |k: &str| e.get(k).and_then(Value::as_str).map(str::to_string);
240 out.push(Entry { name: name.to_string(), kind, value, comment: field("comment"), key: field("key"), event: field("event"), subtitle: field("subtitle"), line: 0 });
241 }
242 Ok(json!(yog_formats::yres::write(&out)))
243}
244
245const KINDS: [Kind; 7] = [Kind::String, Kind::Image, Kind::File, Kind::Sound, Kind::Color, Kind::Number, Kind::Bool];
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn yres_round_trips_through_the_service() {
254 let text = r##"{ "title": "Hi", "n": 3, "logo": "@bytes:iVBORw0KGgo=", "c": { "type": "color", "value": "#FF0000", "comment": "red" } }"##;
255 let parsed = dispatch("yres.parse", &json!({ "text": text })).unwrap();
256 let entries = &parsed["entries"];
257 assert_eq!(entries[2]["type"], "image");
258 assert_eq!(entries[2]["value"], "@bytes:iVBORw0KGgo=");
259 let named = dispatch("yres.parse", &json!({ "text": "{ \"cad.0_x\": \"a\", \"1a\": \"b\" }", "set": "Items", "mod": "m" })).unwrap();
260 assert_eq!(named["entries"][0]["accessor"], "cad_0_x");
261 assert!(named["entries"][0].get("nameProblem").is_none());
262 assert!(named["entries"][1]["nameProblem"].is_string());
263 assert_eq!(named["entries"][0]["translationKey"], "item.m.cad.0_x");
264 let values = dispatch("yres.parse", &json!({ "text": r##"{ "c": { "type": "color", "value": "#F00" }, "d": { "type": "color", "value": "#80FF0000" } }"## })).unwrap();
265 assert!(values["entries"][0]["valueProblem"].is_string());
266 assert_eq!(values["entries"][1]["argb"], 0x80FF_0000_u32);
267 let written = dispatch("yres.write", &json!({ "entries": entries })).unwrap();
268 let again = dispatch("yres.parse", &json!({ "text": written.as_str().unwrap() })).unwrap();
269 assert_eq!(again["entries"][3]["comment"], "red");
270 assert_eq!(dispatch("yres.parse", &json!({ "text": "{ \"a\": [] }" })).unwrap_err()["line"], 1);
271 assert!(dispatch("nope", &Value::Null).is_err());
272 }
273
274 #[test]
275 fn typed_values_are_classified_by_the_service() {
276 let r = dispatch("typed.classify", &json!({ "values": ["@item:stick", "@tag:item:c:dusts", "@Strings.a.b", "@@x", "plain", "@lang:gui.done", "@image:sprite:x", "@Item:x"] })).unwrap();
277 assert_eq!(r[0], json!({ "t": "ref", "kind": "item", "id": "minecraft:stick" }));
278 assert_eq!(r[1], json!({ "t": "tag", "kind": "item", "id": "c:dusts" }));
279 assert_eq!(r[2], json!({ "t": "res", "set": "Strings", "key": "a.b" }));
280 assert_eq!(r[3], json!({ "t": "str", "v": "@x" }));
281 assert_eq!(r[4], json!({ "t": "str", "v": "plain" }));
282 assert_eq!(r[5]["t"], "lang");
283 assert_eq!(r[6], json!({ "t": "image", "source": "sprite:x" }));
284 assert!(r[7]["error"].is_string());
285 }
286
287 #[test]
288 fn data_is_read_and_patched_through_the_service() {
289 let text = "{\n // c\n a: 1,\n b: \"@item:x\"\n}";
290 assert_eq!(dispatch("data.parse", &json!({ "text": text })).unwrap(), json!(r#"{"a":1,"b":"@item:x"}"#));
291 let patched = dispatch("data.patch", &json!({ "text": text, "next": r#"{"a":2,"b":"@item:x"}"# })).unwrap();
292 assert_eq!(patched, json!(text.replace("a: 1", "a: 2")));
293 assert_eq!(dispatch("data.parse", &json!({ "text": "{ a: }" })).unwrap_err()["line"], 1);
294 assert_eq!(dispatch("data.format", &json!({ "value": "[1, 2]" })).unwrap(), json!("[1, 2]"));
295 }
296}