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| l.clone().in_file(MANIFEST).to_string())
225 .collect::<Vec<_>>()
226 .join("\n")
227}
228
229struct Dependencies {
232 crates: Vec<(String, String)>,
233 mods: Vec<(String, String)>,
234 yog_api_version: Option<String>,
235}
236
237fn dependencies(section: Vec<(String, Value)>) -> Dependencies {
238 let mut deps = Dependencies {
239 crates: Vec::new(),
240 mods: Vec::new(),
241 yog_api_version: None,
242 };
243 for (name, spec) in section {
244 match (name.as_str(), &spec) {
245 ("exports", Value::Map(exports)) => deps
246 .mods
247 .extend(exports.iter().map(|(k, v)| (k.to_string(), toml_inline(v)))),
248 (n, _)
249 if [
250 rust_crate::API,
251 rust_crate::API_RUST_NAME,
252 rust_crate::API_VERSION_KEY,
253 ]
254 .contains(&n) =>
255 {
256 deps.yog_api_version = Some(
257 spec.as_str()
258 .map(str::to_string)
259 .unwrap_or_else(|| toml_inline(&spec)),
260 )
261 }
262 _ => deps.crates.push((name, toml_inline(&spec))),
263 }
264 }
265 deps
266}
267
268fn external(namespace: String, e: yog_formats::manifest::ExternalSection) -> External {
270 let ids = [
271 (RefKind::Item, e.items),
272 (RefKind::Block, e.blocks),
273 (RefKind::Entity, e.entities),
274 (RefKind::Recipe, e.recipes),
275 ]
276 .into_iter()
277 .filter(|(_, v)| !v.is_empty())
278 .collect();
279 External { namespace, ids }
280}
281
282pub fn to_snake_name(id: &str) -> String {
288 let mut out = String::with_capacity(id.len());
289 for c in id.chars() {
290 if c == '-' || c == '_' {
291 if !out.ends_with('_') {
292 out.push('_');
293 }
294 } else if c.is_uppercase() {
295 if !out.is_empty() && !out.ends_with('_') {
296 out.push('_');
297 }
298 out.extend(c.to_lowercase());
299 } else {
300 out.push(c);
301 }
302 }
303 out
304}
305
306pub fn latest_yog_api_version() -> String {
308 match std::process::Command::new("cargo")
309 .args(["search", rust_crate::API, "--limit", "1"])
310 .output()
311 {
312 Ok(out) if out.status.success() => {
313 String::from_utf8_lossy(&out.stdout)
314 .lines()
315 .next()
316 .and_then(|l| {
317 let mut parts = l.split_whitespace();
318 parts.next(); if parts.next() == Some("=") {
320 parts.next().map(|v| v.trim_matches('"').to_owned())
322 } else {
323 None
324 }
325 })
326 .unwrap_or_else(|| FALLBACK_API_VERSION.into())
327 }
328 _ => FALLBACK_API_VERSION.into(),
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 const VLSI: &str = r#"
339[mod]
340id = "yog-vlsi"
341name = "@Strings.mod_name"
342version = "0.3.0"
343description = "@Strings.mod_description"
344authors = ["F000NK", "Yog Team"]
345license = "AGPL-3.0-only"
346edition = "2024"
347
348[dependencies]
349serde = { version = "1", features = ["derive"] }
350serde_json = "1"
351yog-api = "0.93"
352yog-kit = "0.4"
353
354[dependencies.exports]
355yog-pipes = "0.5"
356
357## a comment
358[external.farmersdelight]
359items = ["cutting_board", "tomato"]
360blocks = ["stove"]
361"#;
362
363 #[test]
364 fn reads_a_real_manifest() {
365 let t = parse_yog_toml(VLSI).unwrap();
366 assert_eq!(
367 (t.id.as_str(), t.name.as_str(), t.version.as_str()),
368 ("yog-vlsi", "@Strings.mod_name", "0.3.0")
369 );
370 assert_eq!(t.authors, ["F000NK", "Yog Team"]);
371 assert_eq!(t.license, "AGPL-3.0-only");
372 assert_eq!(
373 t.yog_api_version.as_deref(),
374 Some("0.93"),
375 "yog-api is taken out of the dependencies"
376 );
377 let deps: Vec<&str> = t.dependencies.iter().map(|(n, _)| n.as_str()).collect();
378 assert_eq!(deps, ["serde", "serde_json", "yog-kit"]);
379 assert_eq!(
380 t.mod_dependencies,
381 [("yog-pipes".to_string(), "\"0.5\"".to_string())]
382 );
383 let ext = &t.externals[0];
384 assert_eq!(ext.namespace, "farmersdelight");
385 assert_eq!(
386 ext.ids[0],
387 (
388 RefKind::Item,
389 vec!["cutting_board".to_string(), "tomato".to_string()]
390 )
391 );
392 }
393
394 #[test]
395 fn problems_say_where() {
396 let e = parse_yog_toml(
397 "[mod]\nid = \"m\"\nautors = [\"me\"]\n\n[external.create]\nitem = [\"wrench\"]\n",
398 )
399 .unwrap_err();
400 assert_eq!(
401 e.lines().collect::<Vec<_>>(),
402 [
403 "yog.toml:3:1: mod.autors: unknown field `autors` — did you mean `authors`?",
404 "yog.toml:6:1: external.create.item: unknown field `item` — did you mean `items`?",
405 ]
406 );
407 let t = parse_yog_toml("[mod]\nid = \"m\"\nname = \"@@me\"\n").unwrap();
408 assert_eq!(
409 t.name, "@@me",
410 "a literal @ stays escaped for resolve_metadata"
411 );
412 }
413
414 #[test]
415 fn needs_an_id() {
416 assert!(parse_yog_toml("[mod]\nname = \"x\"").is_err());
417 let t = parse_yog_toml("[mod]\nid = \"m\"").unwrap();
418 assert_eq!(
419 (t.name.as_str(), t.version.as_str()),
420 ("m", "0.1.0"),
421 "defaults"
422 );
423 }
424}