1use std::path::{Path, PathBuf};
5
6const DEFAULT_VERSION: &str = "0.1.0";
8
9pub use yog_ids::file::MANIFEST;
11pub const BUILD_DIR: &str = yog_ids::project::BUILD;
13pub const DEPS_SECTION: &str = "[dependencies]";
15pub const EXPORT_DEPS_SECTION: &str = "[dependencies.exports]";
17pub const DEFAULT_EDITION: &str = "2024";
19pub const DEFAULT_LICENSE: &str = "MIT OR Apache-2.0";
21const FALLBACK_API_VERSION: &str = "0.98";
23
24pub fn project_root() -> Result<PathBuf, String> {
26 let root = std::env::current_dir().map_err(|e| e.to_string())?;
27 if root.join(MANIFEST).is_file() {
28 Ok(root)
29 } else {
30 Err(format!("no {MANIFEST} found in the current directory"))
31 }
32}
33
34#[derive(Debug)]
36pub struct YogToml {
37 pub id: String,
38 pub name: String,
39 pub version: String,
40 pub description: String,
41 pub name_key: Option<String>,
44 pub description_key: Option<String>,
45 pub icon: Option<String>,
47 pub authors: Vec<String>,
48 pub license: String,
49 pub exports_license: Option<String>,
55 pub datapack: Option<String>,
57 pub edition: Option<String>,
58 pub yog_api_path: Option<String>,
61 pub yog_api_version: Option<String>,
63 pub dependencies: Vec<(String, String)>,
66 pub mod_dependencies: Vec<(String, String)>,
73 pub dev_dependencies: Vec<(String, String)>,
75 pub externals: Vec<External>,
79}
80
81#[derive(Debug, Default, Clone)]
83pub struct External {
84 pub namespace: String,
85 pub ids: Vec<(RefKind, Vec<String>)>,
87}
88
89impl YogToml {
90 pub fn read(path: &Path) -> Result<Self, String> {
91 let text = std::fs::read_to_string(path)
92 .map_err(|e| format!("reading {}: {e}", path.display()))?;
93 parse_yog_toml(&text)
94 }
95
96 pub fn api_dep(&self) -> String {
103 if let Ok(p) = std::env::var(yog_ids::env::API_PATH) {
104 let abs = PathBuf::from(&p)
106 .canonicalize()
107 .unwrap_or_else(|_| PathBuf::from(&p));
108 return format!("yog-api = {{ path = {:?} }}", abs.to_string_lossy());
109 }
110 if let Some(p) = &self.yog_api_path {
111 return format!("yog-api = {{ path = {p:?} }}");
112 }
113 let version = {
114 let env_version = std::env::var(yog_ids::env::API_VERSION).ok();
115 self.yog_api_version
116 .as_deref()
117 .or(env_version.as_deref())
118 .map(|v| v.to_owned())
119 .unwrap_or_else(latest_yog_api_version)
120 };
121 format!("yog-api = {:?}", version)
122 }
123}
124
125use yog_data::{Text, Value};
128use yog_formats::manifest::ManifestFile;
129use yog_ids::rust_crate;
130
131use crate::content::refs::RefKind;
132
133fn toml_inline(v: &Value) -> String {
135 let quote = |s: &str| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
136 match v {
137 Value::Map(m) => {
138 let fields: Vec<String> = m
139 .iter()
140 .map(|(k, v)| {
141 let key = if k
142 .chars()
143 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
144 {
145 k.to_string()
146 } else {
147 quote(k)
148 };
149 format!("{key} = {}", toml_inline(v))
150 })
151 .collect();
152 format!("{{ {} }}", fields.join(", "))
153 }
154 Value::List(items) => format!(
155 "[{}]",
156 items.iter().map(toml_inline).collect::<Vec<_>>().join(", ")
157 ),
158 Value::Bool(b) => b.to_string(),
159 Value::Int(n) => n.to_string(),
160 Value::Float(f) => f.to_string(),
161 other => quote(other.as_str().unwrap_or_default()),
162 }
163}
164
165fn text_form(t: Text) -> Result<String, String> {
167 match t {
168 Text::Literal(s) => Ok(yog_data::typed::escape_at(&s)),
169 Text::Res(r) => Ok(r.to_string()),
170 Text::NameOf(_) | Text::Lang(_) => Err(
171 "a mod's name and description are text or a `.yres` text (`@Strings.mod_name`)".into(),
172 ),
173 }
174}
175
176pub fn parse_yog_toml(text: &str) -> Result<YogToml, String> {
178 let f = yog_data::schema::from_toml::<ManifestFile>(text)
179 .map_err(|e| toml_problems(&e))?
180 .value;
181 let m = f.module;
182 let deps = dependencies(f.dependencies.0);
183 let name = m.name.map(text_form).transpose()?;
184 let description = m.description.map(text_form).transpose()?;
185 let icon = m.icon.map(|i| match i {
186 yog_data::Image::Source(s) => s,
187 yog_data::Image::Res(r) => r.to_string(),
188 });
189 Ok(YogToml {
190 name: name.unwrap_or_else(|| m.id.clone()),
191 version: m.version.unwrap_or_else(|| DEFAULT_VERSION.into()),
192 description: description.unwrap_or_default(),
193 name_key: None,
194 description_key: None,
195 icon,
196 authors: m.authors,
197 license: m.license.unwrap_or_else(|| DEFAULT_LICENSE.into()),
198 exports_license: m.exports_license,
199 datapack: m.datapack,
200 edition: m.edition,
201 yog_api_path: f.dev.and_then(|d| d.yog_api_path),
202 yog_api_version: deps.yog_api_version,
203 dependencies: deps.crates,
204 mod_dependencies: deps.mods,
205 dev_dependencies: f
206 .dev_dependencies
207 .0
208 .into_iter()
209 .map(|(k, v)| (k, toml_inline(&v)))
210 .collect(),
211 externals: f
212 .external
213 .0
214 .into_iter()
215 .map(|(namespace, e)| external(namespace, e))
216 .collect(),
217 id: m.id,
218 })
219}
220
221fn toml_problems(e: &yog_data::Errors) -> String {
223 e.0.iter()
224 .map(|l| match l.at {
225 Some(at) => {
226 let path = if l.path.is_root() {
227 String::new()
228 } else {
229 format!("{}: ", l.path)
230 };
231 format!("yog.toml:{}: {path}{}", at.line, l.message)
232 }
233 None => format!("yog.toml: {}", l.message),
234 })
235 .collect::<Vec<_>>()
236 .join("\n")
237}
238
239struct Dependencies {
242 crates: Vec<(String, String)>,
243 mods: Vec<(String, String)>,
244 yog_api_version: Option<String>,
245}
246
247fn dependencies(section: Vec<(String, Value)>) -> Dependencies {
248 let mut deps = Dependencies {
249 crates: Vec::new(),
250 mods: Vec::new(),
251 yog_api_version: None,
252 };
253 for (name, spec) in section {
254 match (name.as_str(), &spec) {
255 ("exports", Value::Map(exports)) => deps
256 .mods
257 .extend(exports.iter().map(|(k, v)| (k.to_string(), toml_inline(v)))),
258 (n, _)
259 if [
260 rust_crate::API,
261 rust_crate::API_RUST_NAME,
262 rust_crate::API_VERSION_KEY,
263 ]
264 .contains(&n) =>
265 {
266 deps.yog_api_version = Some(
267 spec.as_str()
268 .map(str::to_string)
269 .unwrap_or_else(|| toml_inline(&spec)),
270 )
271 }
272 _ => deps.crates.push((name, toml_inline(&spec))),
273 }
274 }
275 deps
276}
277
278fn external(namespace: String, e: yog_formats::manifest::ExternalSection) -> External {
280 let ids = [
281 (RefKind::Item, e.items),
282 (RefKind::Block, e.blocks),
283 (RefKind::Entity, e.entities),
284 (RefKind::Recipe, e.recipes),
285 ]
286 .into_iter()
287 .filter(|(_, v)| !v.is_empty())
288 .collect();
289 External { namespace, ids }
290}
291
292pub fn to_snake_name(id: &str) -> String {
298 let mut out = String::with_capacity(id.len());
299 for c in id.chars() {
300 if c == '-' || c == '_' {
301 if !out.ends_with('_') {
302 out.push('_');
303 }
304 } else if c.is_uppercase() {
305 if !out.is_empty() && !out.ends_with('_') {
306 out.push('_');
307 }
308 out.extend(c.to_lowercase());
309 } else {
310 out.push(c);
311 }
312 }
313 out
314}
315
316pub fn latest_yog_api_version() -> String {
318 match std::process::Command::new("cargo")
319 .args(["search", rust_crate::API, "--limit", "1"])
320 .output()
321 {
322 Ok(out) if out.status.success() => {
323 String::from_utf8_lossy(&out.stdout)
324 .lines()
325 .next()
326 .and_then(|l| {
327 let mut parts = l.split_whitespace();
328 parts.next(); if parts.next() == Some("=") {
330 parts.next().map(|v| v.trim_matches('"').to_owned())
332 } else {
333 None
334 }
335 })
336 .unwrap_or_else(|| FALLBACK_API_VERSION.into())
337 }
338 _ => FALLBACK_API_VERSION.into(),
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 const VLSI: &str = r#"
349[mod]
350id = "yog-vlsi"
351name = "@Strings.mod_name"
352version = "0.3.0"
353description = "@Strings.mod_description"
354authors = ["F000NK", "Yog Team"]
355license = "AGPL-3.0-only"
356edition = "2024"
357
358[dependencies]
359serde = { version = "1", features = ["derive"] }
360serde_json = "1"
361yog-api = "0.93"
362yog-kit = "0.4"
363
364[dependencies.exports]
365yog-pipes = "0.5"
366
367## a comment
368[external.farmersdelight]
369items = ["cutting_board", "tomato"]
370blocks = ["stove"]
371"#;
372
373 #[test]
374 fn reads_a_real_manifest() {
375 let t = parse_yog_toml(VLSI).unwrap();
376 assert_eq!(
377 (t.id.as_str(), t.name.as_str(), t.version.as_str()),
378 ("yog-vlsi", "@Strings.mod_name", "0.3.0")
379 );
380 assert_eq!(t.authors, ["F000NK", "Yog Team"]);
381 assert_eq!(t.license, "AGPL-3.0-only");
382 assert_eq!(
383 t.yog_api_version.as_deref(),
384 Some("0.93"),
385 "yog-api is taken out of the dependencies"
386 );
387 let deps: Vec<&str> = t.dependencies.iter().map(|(n, _)| n.as_str()).collect();
388 assert_eq!(deps, ["serde", "serde_json", "yog-kit"]);
389 assert_eq!(
390 t.mod_dependencies,
391 [("yog-pipes".to_string(), "\"0.5\"".to_string())]
392 );
393 let ext = &t.externals[0];
394 assert_eq!(ext.namespace, "farmersdelight");
395 assert_eq!(
396 ext.ids[0],
397 (
398 RefKind::Item,
399 vec!["cutting_board".to_string(), "tomato".to_string()]
400 )
401 );
402 }
403
404 #[test]
405 fn problems_say_where() {
406 let e = parse_yog_toml(
407 "[mod]\nid = \"m\"\nautors = [\"me\"]\n\n[external.create]\nitem = [\"wrench\"]\n",
408 )
409 .unwrap_err();
410 assert_eq!(
411 e.lines().collect::<Vec<_>>(),
412 [
413 "yog.toml:3: mod.autors: unknown field `autors` — did you mean `authors`?",
414 "yog.toml:6: external.create.item: unknown field `item` — did you mean `items`?",
415 ]
416 );
417 let t = parse_yog_toml("[mod]\nid = \"m\"\nname = \"@@me\"\n").unwrap();
418 assert_eq!(
419 t.name, "@@me",
420 "a literal @ stays escaped for resolve_metadata"
421 );
422 }
423
424 #[test]
425 fn needs_an_id() {
426 assert!(parse_yog_toml("[mod]\nname = \"x\"").is_err());
427 let t = parse_yog_toml("[mod]\nid = \"m\"").unwrap();
428 assert_eq!(
429 (t.name.as_str(), t.version.as_str()),
430 ("m", "0.1.0"),
431 "defaults"
432 );
433 }
434}