use std::path::{Path, PathBuf};
const DEFAULT_VERSION: &str = "0.1.0";
pub use yog_ids::file::MANIFEST;
pub const BUILD_DIR: &str = yog_ids::project::BUILD;
pub const DEPS_SECTION: &str = "[dependencies]";
pub const EXPORT_DEPS_SECTION: &str = "[dependencies.exports]";
pub const DEFAULT_EDITION: &str = "2024";
pub const DEFAULT_LICENSE: &str = "MIT OR Apache-2.0";
const FALLBACK_API_VERSION: &str = "0.98";
pub fn project_root() -> Result<PathBuf, String> {
let root = std::env::current_dir().map_err(|e| e.to_string())?;
if root.join(MANIFEST).is_file() {
Ok(root)
} else {
Err(format!("no {MANIFEST} found in the current directory"))
}
}
#[derive(Debug)]
pub struct YogToml {
pub id: String,
pub name: String,
pub version: String,
pub description: String,
pub name_key: Option<String>,
pub description_key: Option<String>,
pub icon: Option<String>,
pub authors: Vec<String>,
pub license: String,
pub exports_license: Option<String>,
pub datapack: Option<String>,
pub edition: Option<String>,
pub yog_api_path: Option<String>,
pub yog_api_version: Option<String>,
pub dependencies: Vec<(String, String)>,
pub mod_dependencies: Vec<(String, String)>,
pub dev_dependencies: Vec<(String, String)>,
pub externals: Vec<External>,
}
#[derive(Debug, Default, Clone)]
pub struct External {
pub namespace: String,
pub ids: Vec<(RefKind, Vec<String>)>,
}
impl YogToml {
pub fn read(path: &Path) -> Result<Self, String> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("reading {}: {e}", path.display()))?;
parse_yog_toml(&text)
}
pub fn api_dep(&self) -> String {
if let Ok(p) = std::env::var(yog_ids::env::API_PATH) {
let abs = PathBuf::from(&p)
.canonicalize()
.unwrap_or_else(|_| PathBuf::from(&p));
return format!("yog-api = {{ path = {:?} }}", abs.to_string_lossy());
}
if let Some(p) = &self.yog_api_path {
return format!("yog-api = {{ path = {p:?} }}");
}
let version = {
let env_version = std::env::var(yog_ids::env::API_VERSION).ok();
self.yog_api_version
.as_deref()
.or(env_version.as_deref())
.map(|v| v.to_owned())
.unwrap_or_else(latest_yog_api_version)
};
format!("yog-api = {:?}", version)
}
}
use yog_data::{Text, Value};
use yog_formats::manifest::ManifestFile;
use yog_ids::rust_crate;
use crate::content::refs::RefKind;
fn toml_inline(v: &Value) -> String {
let quote = |s: &str| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
match v {
Value::Map(m) => {
let fields: Vec<String> = m
.iter()
.map(|(k, v)| {
let key = if k
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
k.to_string()
} else {
quote(k)
};
format!("{key} = {}", toml_inline(v))
})
.collect();
format!("{{ {} }}", fields.join(", "))
}
Value::List(items) => format!(
"[{}]",
items.iter().map(toml_inline).collect::<Vec<_>>().join(", ")
),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(f) => f.to_string(),
other => quote(other.as_str().unwrap_or_default()),
}
}
fn text_form(t: Text) -> Result<String, String> {
match t {
Text::Literal(s) => Ok(yog_data::typed::escape_at(&s)),
Text::Res(r) => Ok(r.to_string()),
Text::NameOf(_) | Text::Lang(_) => Err(
"a mod's name and description are text or a `.yres` text (`@Strings.mod_name`)".into(),
),
}
}
pub fn parse_yog_toml(text: &str) -> Result<YogToml, String> {
let f = yog_data::schema::from_toml::<ManifestFile>(text)
.map_err(|e| toml_problems(&e))?
.value;
let m = f.module;
let deps = dependencies(f.dependencies.0);
let name = m.name.map(text_form).transpose()?;
let description = m.description.map(text_form).transpose()?;
let icon = m.icon.map(|i| match i {
yog_data::Image::Source(s) => s,
yog_data::Image::Res(r) => r.to_string(),
});
Ok(YogToml {
name: name.unwrap_or_else(|| m.id.clone()),
version: m.version.unwrap_or_else(|| DEFAULT_VERSION.into()),
description: description.unwrap_or_default(),
name_key: None,
description_key: None,
icon,
authors: m.authors,
license: m.license.unwrap_or_else(|| DEFAULT_LICENSE.into()),
exports_license: m.exports_license,
datapack: m.datapack,
edition: m.edition,
yog_api_path: f.dev.and_then(|d| d.yog_api_path),
yog_api_version: deps.yog_api_version,
dependencies: deps.crates,
mod_dependencies: deps.mods,
dev_dependencies: f
.dev_dependencies
.0
.into_iter()
.map(|(k, v)| (k, toml_inline(&v)))
.collect(),
externals: f
.external
.0
.into_iter()
.map(|(namespace, e)| external(namespace, e))
.collect(),
id: m.id,
})
}
fn toml_problems(e: &yog_data::Errors) -> String {
e.0.iter()
.map(|l| l.clone().in_file(MANIFEST).to_string())
.collect::<Vec<_>>()
.join("\n")
}
struct Dependencies {
crates: Vec<(String, String)>,
mods: Vec<(String, String)>,
yog_api_version: Option<String>,
}
fn dependencies(section: Vec<(String, Value)>) -> Dependencies {
let mut deps = Dependencies {
crates: Vec::new(),
mods: Vec::new(),
yog_api_version: None,
};
for (name, spec) in section {
match (name.as_str(), &spec) {
("exports", Value::Map(exports)) => deps
.mods
.extend(exports.iter().map(|(k, v)| (k.to_string(), toml_inline(v)))),
(n, _)
if [
rust_crate::API,
rust_crate::API_RUST_NAME,
rust_crate::API_VERSION_KEY,
]
.contains(&n) =>
{
deps.yog_api_version = Some(
spec.as_str()
.map(str::to_string)
.unwrap_or_else(|| toml_inline(&spec)),
)
}
_ => deps.crates.push((name, toml_inline(&spec))),
}
}
deps
}
fn external(namespace: String, e: yog_formats::manifest::ExternalSection) -> External {
let ids = [
(RefKind::Item, e.items),
(RefKind::Block, e.blocks),
(RefKind::Entity, e.entities),
(RefKind::Recipe, e.recipes),
]
.into_iter()
.filter(|(_, v)| !v.is_empty())
.collect();
External { namespace, ids }
}
pub fn to_snake_name(id: &str) -> String {
let mut out = String::with_capacity(id.len());
for c in id.chars() {
if c == '-' || c == '_' {
if !out.ends_with('_') {
out.push('_');
}
} else if c.is_uppercase() {
if !out.is_empty() && !out.ends_with('_') {
out.push('_');
}
out.extend(c.to_lowercase());
} else {
out.push(c);
}
}
out
}
pub fn latest_yog_api_version() -> String {
match std::process::Command::new("cargo")
.args(["search", rust_crate::API, "--limit", "1"])
.output()
{
Ok(out) if out.status.success() => {
String::from_utf8_lossy(&out.stdout)
.lines()
.next()
.and_then(|l| {
let mut parts = l.split_whitespace();
parts.next(); if parts.next() == Some("=") {
parts.next().map(|v| v.trim_matches('"').to_owned())
} else {
None
}
})
.unwrap_or_else(|| FALLBACK_API_VERSION.into())
}
_ => FALLBACK_API_VERSION.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
const VLSI: &str = r#"
[mod]
id = "yog-vlsi"
name = "@Strings.mod_name"
version = "0.3.0"
description = "@Strings.mod_description"
authors = ["F000NK", "Yog Team"]
license = "AGPL-3.0-only"
edition = "2024"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
yog-api = "0.93"
yog-kit = "0.4"
[dependencies.exports]
yog-pipes = "0.5"
## a comment
[external.farmersdelight]
items = ["cutting_board", "tomato"]
blocks = ["stove"]
"#;
#[test]
fn reads_a_real_manifest() {
let t = parse_yog_toml(VLSI).unwrap();
assert_eq!(
(t.id.as_str(), t.name.as_str(), t.version.as_str()),
("yog-vlsi", "@Strings.mod_name", "0.3.0")
);
assert_eq!(t.authors, ["F000NK", "Yog Team"]);
assert_eq!(t.license, "AGPL-3.0-only");
assert_eq!(
t.yog_api_version.as_deref(),
Some("0.93"),
"yog-api is taken out of the dependencies"
);
let deps: Vec<&str> = t.dependencies.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(deps, ["serde", "serde_json", "yog-kit"]);
assert_eq!(
t.mod_dependencies,
[("yog-pipes".to_string(), "\"0.5\"".to_string())]
);
let ext = &t.externals[0];
assert_eq!(ext.namespace, "farmersdelight");
assert_eq!(
ext.ids[0],
(
RefKind::Item,
vec!["cutting_board".to_string(), "tomato".to_string()]
)
);
}
#[test]
fn problems_say_where() {
let e = parse_yog_toml(
"[mod]\nid = \"m\"\nautors = [\"me\"]\n\n[external.create]\nitem = [\"wrench\"]\n",
)
.unwrap_err();
assert_eq!(
e.lines().collect::<Vec<_>>(),
[
"yog.toml:3:1: mod.autors: unknown field `autors` — did you mean `authors`?",
"yog.toml:6:1: external.create.item: unknown field `item` — did you mean `items`?",
]
);
let t = parse_yog_toml("[mod]\nid = \"m\"\nname = \"@@me\"\n").unwrap();
assert_eq!(
t.name, "@@me",
"a literal @ stays escaped for resolve_metadata"
);
}
#[test]
fn needs_an_id() {
assert!(parse_yog_toml("[mod]\nname = \"x\"").is_err());
let t = parse_yog_toml("[mod]\nid = \"m\"").unwrap();
assert_eq!(
(t.name.as_str(), t.version.as_str()),
("m", "0.1.0"),
"defaults"
);
}
}