1use std::path::{Path, PathBuf};
5
6pub use yog_ids::file::MANIFEST;
8pub const BUILD_DIR: &str = yog_ids::project::BUILD;
10pub const DEPS_SECTION: &str = "[dependencies]";
12pub const EXPORT_DEPS_SECTION: &str = "[dependencies.exports]";
14pub const DEFAULT_EDITION: &str = "2024";
16pub const DEFAULT_LICENSE: &str = "MIT OR Apache-2.0";
18const FALLBACK_API_VERSION: &str = "0.98";
20
21pub fn project_root() -> Result<PathBuf, String> {
23 let root = std::env::current_dir().map_err(|e| e.to_string())?;
24 if root.join(MANIFEST).is_file() {
25 Ok(root)
26 } else {
27 Err(format!("no {MANIFEST} found in the current directory"))
28 }
29}
30
31#[derive(Debug)]
33pub struct YogToml {
34 pub id: String,
35 pub name: String,
36 pub version: String,
37 pub description: String,
38 pub name_key: Option<String>,
41 pub description_key: Option<String>,
42 pub icon: Option<String>,
44 pub authors: Vec<String>,
45 pub license: String,
46 pub exports_license: Option<String>,
52 pub datapack: Option<String>,
54 pub edition: Option<String>,
55 pub yog_api_path: Option<String>,
58 pub yog_api_version: Option<String>,
60 pub dependencies: Vec<(String, String)>,
63 pub mod_dependencies: Vec<(String, String)>,
70 pub dev_dependencies: Vec<(String, String)>,
72 pub externals: Vec<External>,
76}
77
78#[derive(Debug, Default, Clone)]
80pub struct External {
81 pub namespace: String,
82 pub ids: Vec<(String, Vec<String>)>,
84}
85
86impl YogToml {
87 pub fn read(path: &Path) -> Result<Self, String> {
88 let text = std::fs::read_to_string(path)
89 .map_err(|e| format!("reading {}: {e}", path.display()))?;
90 parse_yog_toml(&text)
91 }
92
93 pub fn api_dep(&self) -> String {
100 if let Ok(p) = std::env::var("YOG_API_PATH") {
101 let abs = PathBuf::from(&p)
103 .canonicalize()
104 .unwrap_or_else(|_| PathBuf::from(&p));
105 return format!("yog-api = {{ path = {:?} }}", abs.to_string_lossy());
106 }
107 if let Some(p) = &self.yog_api_path {
108 return format!("yog-api = {{ path = {p:?} }}");
109 }
110 let version = {
111 let env_version = std::env::var("YOG_API_VERSION").ok();
112 self.yog_api_version
113 .as_deref()
114 .or(env_version.as_deref())
115 .map(|v| v.to_owned())
116 .unwrap_or_else(latest_yog_api_version)
117 };
118 format!("yog-api = {:?}", version)
119 }
120}
121
122use yog_data::{Text, Value};
125use yog_formats::manifest::ManifestFile;
126
127fn toml_inline(v: &Value) -> String {
129 let quote = |s: &str| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
130 match v {
131 Value::Map(m) => {
132 let fields: Vec<String> = m
133 .iter()
134 .map(|(k, v)| {
135 let key = if k.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { k.to_string() } else { quote(k) };
136 format!("{key} = {}", toml_inline(v))
137 })
138 .collect();
139 format!("{{ {} }}", fields.join(", "))
140 }
141 Value::List(items) => format!("[{}]", items.iter().map(toml_inline).collect::<Vec<_>>().join(", ")),
142 Value::Bool(b) => b.to_string(),
143 Value::Int(n) => n.to_string(),
144 Value::Float(f) => f.to_string(),
145 other => quote(other.as_str().unwrap_or_default()),
146 }
147}
148
149fn text_form(t: Text) -> Result<String, String> {
151 match t {
152 Text::Literal(s) => Ok(yog_data::typed::escape_at(&s)),
153 Text::Res(r) => Ok(format!("@{}.{}", r.set, r.key)),
154 Text::NameOf(_) | Text::Lang(_) => Err("a mod's name and description are text or a `.yres` text (`@Strings.mod_name`)".into()),
155 }
156}
157
158pub fn parse_yog_toml(text: &str) -> Result<YogToml, String> {
160 let read = yog_data::schema::from_toml::<ManifestFile>(text).map_err(|e| {
161 e.0.iter()
162 .map(|l| match l.at {
163 Some(at) => format!("yog.toml:{}: {}{}", at.line, if l.path.is_root() { String::new() } else { format!("{}: ", l.path) }, l.message),
164 None => format!("yog.toml: {}", l.message),
165 })
166 .collect::<Vec<_>>()
167 .join("\n")
168 })?;
169 let f = read.value;
170 let m = f.module;
171 let mut dependencies = Vec::new();
172 let mut mod_dependencies = Vec::new();
173 let mut yog_api_version = None;
174 for (name, spec) in f.dependencies.0 {
175 match (name.as_str(), &spec) {
176 ("exports", Value::Map(exports)) => mod_dependencies.extend(exports.iter().map(|(k, v)| (k.to_string(), toml_inline(v)))),
177 ("yog-api" | "yog_api" | "yog_api_version", _) => yog_api_version = Some(spec.as_str().map(str::to_string).unwrap_or_else(|| toml_inline(&spec))),
178 _ => dependencies.push((name, toml_inline(&spec))),
179 }
180 }
181 let externals = f
182 .external
183 .0
184 .into_iter()
185 .map(|(namespace, e)| External {
186 namespace,
187 ids: [("items", e.items), ("blocks", e.blocks), ("entities", e.entities), ("recipes", e.recipes)]
188 .into_iter()
189 .filter(|(_, v)| !v.is_empty())
190 .map(|(k, v)| (k.to_string(), v))
191 .collect(),
192 })
193 .collect();
194 let name = m.name.map(text_form).transpose()?;
195 let description = m.description.map(text_form).transpose()?;
196 let icon = m.icon.map(|i| match i {
197 yog_data::Image::Source(s) => s,
198 yog_data::Image::Res(r) => format!("@{}.{}", r.set, r.key),
199 });
200 Ok(YogToml {
201 name: name.unwrap_or_else(|| m.id.clone()),
202 version: m.version.unwrap_or_else(|| "0.1.0".into()),
203 description: description.unwrap_or_default(),
204 name_key: None,
205 description_key: None,
206 icon,
207 authors: m.authors,
208 license: m.license.unwrap_or_else(|| DEFAULT_LICENSE.into()),
209 exports_license: m.exports_license,
210 datapack: m.datapack,
211 edition: m.edition,
212 yog_api_path: f.dev.and_then(|d| d.yog_api_path),
213 yog_api_version,
214 dependencies,
215 mod_dependencies,
216 dev_dependencies: f.dev_dependencies.0.into_iter().map(|(k, v)| (k, toml_inline(&v))).collect(),
217 externals,
218 id: m.id,
219 })
220}
221
222pub fn to_snake_name(id: &str) -> String {
228 let mut out = String::with_capacity(id.len());
229 for c in id.chars() {
230 if c == '-' || c == '_' {
231 if !out.ends_with('_') {
232 out.push('_');
233 }
234 } else if c.is_uppercase() {
235 if !out.is_empty() && !out.ends_with('_') {
236 out.push('_');
237 }
238 out.extend(c.to_lowercase());
239 } else {
240 out.push(c);
241 }
242 }
243 out
244}
245
246pub fn workspace_rkyv_version() -> String {
248 env!("RKYV_VERSION").to_string()
249}
250
251pub fn latest_yog_api_version() -> String {
253 match std::process::Command::new("cargo")
254 .args(["search", "yog-api", "--limit", "1"])
255 .output()
256 {
257 Ok(out) if out.status.success() => {
258 String::from_utf8_lossy(&out.stdout)
259 .lines()
260 .next()
261 .and_then(|l| {
262 let mut parts = l.split_whitespace();
263 parts.next(); if parts.next() == Some("=") {
265 parts.next().map(|v| v.trim_matches('"').to_owned())
267 } else {
268 None
269 }
270 })
271 .unwrap_or_else(|| FALLBACK_API_VERSION.into())
272 }
273 _ => FALLBACK_API_VERSION.into(),
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 const VLSI: &str = r#"
284[mod]
285id = "yog-vlsi"
286name = "@Strings.mod_name"
287version = "0.3.0"
288description = "@Strings.mod_description"
289authors = ["F000NK", "Yog Team"]
290license = "AGPL-3.0-only"
291edition = "2024"
292
293[dependencies]
294serde = { version = "1", features = ["derive"] }
295serde_json = "1"
296yog-api = "0.93"
297yog-kit = "0.4"
298
299[dependencies.exports]
300yog-pipes = "0.5"
301
302## a comment
303[external.farmersdelight]
304items = ["cutting_board", "tomato"]
305blocks = ["stove"]
306"#;
307
308 #[test]
309 fn reads_a_real_manifest() {
310 let t = parse_yog_toml(VLSI).unwrap();
311 assert_eq!((t.id.as_str(), t.name.as_str(), t.version.as_str()), ("yog-vlsi", "@Strings.mod_name", "0.3.0"));
312 assert_eq!(t.authors, ["F000NK", "Yog Team"]);
313 assert_eq!(t.license, "AGPL-3.0-only");
314 assert_eq!(t.yog_api_version.as_deref(), Some("0.93"), "yog-api is taken out of the dependencies");
315 let deps: Vec<&str> = t.dependencies.iter().map(|(n, _)| n.as_str()).collect();
316 assert_eq!(deps, ["serde", "serde_json", "yog-kit"]);
317 assert_eq!(t.mod_dependencies, [("yog-pipes".to_string(), "\"0.5\"".to_string())]);
318 let ext = &t.externals[0];
319 assert_eq!(ext.namespace, "farmersdelight");
320 assert_eq!(ext.ids[0], ("items".to_string(), vec!["cutting_board".to_string(), "tomato".to_string()]));
321 }
322
323 #[test]
324 fn problems_say_where() {
325 let e = parse_yog_toml("[mod]\nid = \"m\"\nautors = [\"me\"]\n\n[external.create]\nitem = [\"wrench\"]\n").unwrap_err();
326 assert_eq!(e.lines().collect::<Vec<_>>(), [
327 "yog.toml:3: mod.autors: unknown field `autors` — did you mean `authors`?",
328 "yog.toml:6: external.create.item: unknown field `item` — did you mean `items`?",
329 ]);
330 let t = parse_yog_toml("[mod]\nid = \"m\"\nname = \"@@me\"\n").unwrap();
331 assert_eq!(t.name, "@@me", "a literal @ stays escaped for resolve_metadata");
332 }
333
334 #[test]
335 fn needs_an_id() {
336 assert!(parse_yog_toml("[mod]\nname = \"x\"").is_err());
337 let t = parse_yog_toml("[mod]\nid = \"m\"").unwrap();
338 assert_eq!((t.name.as_str(), t.version.as_str()), ("m", "0.1.0"), "defaults");
339 }
340}