use std::collections::BTreeMap;
use serde::Deserialize;
use super::MANIFEST_FILE;
use super::files::PackError;
use super::pack::Pack;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum PackSide {
#[default]
Both,
Server,
Client,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct PackManifest {
pub id: String,
pub version: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub authors: Vec<String>,
#[serde(default)]
pub license: String,
#[serde(default)]
pub homepage: String,
#[serde(default)]
pub side: PackSide,
#[serde(default = "yes")]
pub join: bool,
#[serde(default)]
pub requires: BTreeMap<String, String>,
#[serde(default)]
pub setting: Vec<PackSetting>,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct PackSetting {
pub key: String,
#[cfg_attr(feature = "schema", schemars(with = "serde_json::Value"))]
pub default: toml::Value,
#[serde(default)]
pub doc: String,
#[serde(default)]
pub stage: crate::config::Stage,
#[serde(default)]
pub synced: bool,
pub min: Option<f64>,
pub max: Option<f64>,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "Vec<serde_json::Value>"))]
pub choices: Vec<toml::Value>,
#[serde(default)]
pub text: String,
}
pub(super) const fn yes() -> bool {
true
}
impl PackManifest {
pub fn parse(text: &str) -> Result<Self, PackError> {
let manifest: Self =
toml::from_str(text).map_err(|e| PackError::of(MANIFEST_FILE, text, &e))?;
if !crate::abi::is_valid_id(&manifest.id) {
return Err(PackError::at(
MANIFEST_FILE,
text,
find(text, "id"),
"id is <author>.<name>, each of [a-z0-9-]",
));
}
manifest.check_settings(text)?;
Ok(manifest)
}
#[must_use]
pub fn settings_schema(&self) -> Option<crate::config::Schema> {
if self.setting.is_empty() {
return None;
}
let fields = self.setting.iter().map(PackSetting::field).collect();
Some(crate::config::Schema::new(
&self.description,
"config",
fields,
))
}
fn check_settings(&self, text: &str) -> Result<(), PackError> {
let mut seen = std::collections::BTreeSet::new();
for setting in &self.setting {
let at = find(text, &format!("key = \"{}\"", setting.key));
let problem = |why: String| Err(PackError::at(MANIFEST_FILE, text, at, &why));
let key = &setting.key;
let bare = !key.is_empty()
&& key
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_');
if !bare {
return problem(format!("setting {key:?}: a key is of [a-z0-9_]"));
}
if !seen.insert(key.as_str()) {
return problem(format!("setting {key:?} is given twice"));
}
if setting.synced && setting.stage != crate::config::Stage::Startup {
return problem(format!(
"setting {key:?}: synced goes with stage = \"startup\"; a sync stage follows the server already"
));
}
if let Err(why) = setting.field().check(&setting.default) {
return problem(format!("setting {key:?}: the default is refused: {why}"));
}
}
Ok(())
}
}
impl PackSetting {
fn field(&self) -> crate::config::Field {
crate::config::Field::new(&self.key, self.stage, &self.default)
.with_range(self.min, self.max)
.with_choices(&self.choices)
.with_doc(&self.doc, &self.text)
.with_synced(self.synced)
}
}
impl PackManifest {
pub fn check(&self, text: &str, pack: &Pack) -> Result<(), PackError> {
if self.join || !pack.reaches_players() {
return Ok(());
}
Err(PackError::at(
MANIFEST_FILE,
text,
find(text, "join"),
"join = false, but the pack adds or changes what players' games read, so server and players must run it alike; remove the line",
))
}
}
pub(super) fn find(text: &str, key: &str) -> usize {
let mut at = 0;
for line in text.split_inclusive('\n') {
if line.trim_start().starts_with(key) {
return at + line.len() - line.trim_start().len();
}
at += line.len();
}
0
}