use crate::{
engine::{runtime::RuntimeKind, EngineKind},
manifest::{
overrides::{OverrideKey, OverrideSpecifier},
target::Target,
},
names::PackageName,
ser_display_deser_fromstr,
source::specifiers::DependencySpecifiers,
};
use relative_path::RelativePathBuf;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashMap},
fmt::Display,
hash::Hash,
str::FromStr,
};
use tracing::instrument;
pub mod overrides;
pub mod target;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
pub struct Manifest {
pub name: PackageName,
pub version: Version,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<url::Url>,
pub target: Target,
#[serde(default)]
pub private: bool,
#[serde(default, skip_serializing)]
pub scripts: BTreeMap<String, Script>,
#[serde(
default,
skip_serializing,
deserialize_with = "crate::util::deserialize_gix_url_map"
)]
#[cfg_attr(test, schemars(with = "BTreeMap<String, url::Url>"))]
pub indices: BTreeMap<String, gix::Url>,
#[cfg(feature = "wally-compat")]
#[serde(
default,
skip_serializing,
deserialize_with = "crate::util::deserialize_gix_url_map"
)]
#[cfg_attr(test, schemars(with = "BTreeMap<String, url::Url>"))]
pub wally_indices: BTreeMap<String, gix::Url>,
#[serde(default, skip_serializing)]
pub overrides: BTreeMap<OverrideKey, OverrideSpecifier>,
#[serde(default)]
pub includes: Vec<String>,
#[cfg(feature = "patches")]
#[serde(default, skip_serializing)]
#[cfg_attr(
test,
schemars(
with = "BTreeMap<crate::names::PackageNames, BTreeMap<crate::source::ids::VersionId, std::path::PathBuf>>"
)
)]
pub patches: BTreeMap<
crate::names::PackageNames,
BTreeMap<crate::source::ids::VersionId, RelativePathBuf>,
>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_members: Vec<String>,
#[serde(default, skip_serializing)]
pub place: BTreeMap<target::RobloxPlaceKind, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[cfg_attr(test, schemars(with = "BTreeMap<EngineKind, String>"))]
pub engines: BTreeMap<EngineKind, VersionReq>,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "crate::util::deserialize_no_dup_keys"
)]
pub dependencies: BTreeMap<Alias, DependencySpecifiers>,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "crate::util::deserialize_no_dup_keys"
)]
pub peer_dependencies: BTreeMap<Alias, DependencySpecifiers>,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
deserialize_with = "crate::util::deserialize_no_dup_keys"
)]
pub dev_dependencies: BTreeMap<Alias, DependencySpecifiers>,
#[cfg_attr(test, schemars(skip))]
#[serde(flatten)]
pub user_defined_fields: HashMap<String, toml::Value>,
}
#[derive(Debug, Clone)]
pub struct Alias(String);
ser_display_deser_fromstr!(Alias);
impl Ord for Alias {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.to_lowercase().cmp(&other.0.to_lowercase())
}
}
impl PartialOrd for Alias {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Alias {
fn eq(&self, other: &Self) -> bool {
self.0.to_lowercase() == other.0.to_lowercase()
}
}
impl Eq for Alias {}
impl Hash for Alias {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_lowercase().hash(state);
}
}
impl Display for Alias {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(&self.0)
}
}
impl FromStr for Alias {
type Err = errors::AliasFromStr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(errors::AliasFromStr::Empty);
}
if s.len() > 48 {
return Err(errors::AliasFromStr::TooLong(s.to_string()));
}
if matches!(
s.to_ascii_lowercase().as_str(),
"con" | "prn" | "aux" | "nul" | "com1" | "com2" | "com3" | "com4" | "com5" | "com6" | "com7"
| "com8" | "com9" | "com¹" | "com²" | "com³" | "lpt1" | "lpt2" | "lpt3" | "lpt4" | "lpt5" | "lpt6"
| "lpt7" | "lpt8" | "lpt9" | "lpt¹" | "lpt²" | "lpt³"
| "self"
| "cart"
) {
return Err(errors::AliasFromStr::Reserved(s.to_string()));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err(errors::AliasFromStr::InvalidCharacters(s.to_string()));
}
if EngineKind::from_str(s).is_ok() {
return Err(errors::AliasFromStr::EngineName(s.to_string()));
}
Ok(Self(s.to_string()))
}
}
#[cfg(test)]
impl schemars::JsonSchema for Alias {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Alias".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": r"^[a-zA-Z0-9_-]+$",
})
}
}
impl Alias {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
pub enum Script {
#[cfg_attr(test, schemars(with = "std::path::PathBuf"))]
Path(RelativePathBuf),
RuntimePath {
runtime: RuntimeKind,
#[cfg_attr(test, schemars(with = "std::path::PathBuf"))]
path: RelativePathBuf,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum DependencyType {
Standard,
Peer,
Dev,
}
impl DependencyType {
pub const VARIANTS: &'static [DependencyType] = &[
DependencyType::Standard,
DependencyType::Peer,
DependencyType::Dev,
];
}
impl Manifest {
#[instrument(skip(self), ret(level = "trace"), level = "debug")]
pub fn all_dependencies(
&self,
) -> Result<BTreeMap<Alias, (DependencySpecifiers, DependencyType)>, errors::AllDependenciesError>
{
let mut all_deps = BTreeMap::new();
for (deps, ty) in [
(&self.dependencies, DependencyType::Standard),
(&self.peer_dependencies, DependencyType::Peer),
(&self.dev_dependencies, DependencyType::Dev),
] {
for (alias, spec) in deps {
if all_deps.insert(alias.clone(), (spec.clone(), ty)).is_some() {
return Err(errors::AllDependenciesError::AliasConflict(alias.clone()));
}
}
}
Ok(all_deps)
}
}
pub mod errors {
use crate::manifest::Alias;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AliasFromStr {
#[error("the alias is empty")]
Empty,
#[error("alias `{0}` has more than 48 characters")]
TooLong(String),
#[error("alias `{0}` is reserved")]
Reserved(String),
#[error("alias `{0}` contains characters outside a-z, A-Z, 0-9, -, and _")]
InvalidCharacters(String),
#[error("alias `{0}` is an engine name")]
EngineName(String),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AllDependenciesError {
#[error("another specifier is already using the alias {0}")]
AliasConflict(Alias),
}
}
#[cfg(test)]
mod tests {
#[test]
pub fn generate_schema() {
let schema = schemars::schema_for!(super::Manifest);
let schema = serde_json::to_string_pretty(&schema).unwrap();
std::fs::write("manifest.schema.json", schema).unwrap();
}
}