use std::collections::BTreeMap;
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
use ecow::{eco_format, EcoString};
use serde::de::IgnoredAny;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use unscanny::Scanner;
use crate::is_ident;
pub type UnknownFields = BTreeMap<EcoString, IgnoredAny>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PackageManifest {
pub package: PackageInfo,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template: Option<TemplateInfo>,
#[serde(default)]
pub tool: ToolInfo,
#[serde(flatten, skip_serializing)]
pub unknown_fields: UnknownFields,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ToolInfo {
#[serde(flatten)]
pub sections: BTreeMap<EcoString, toml::Table>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemplateInfo {
pub path: EcoString,
pub entrypoint: EcoString,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thumbnail: Option<EcoString>,
#[serde(flatten, skip_serializing)]
pub unknown_fields: UnknownFields,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PackageInfo {
pub name: EcoString,
pub version: PackageVersion,
pub entrypoint: EcoString,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<EcoString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<EcoString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<EcoString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub homepage: Option<EcoString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<EcoString>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<EcoString>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub categories: Vec<EcoString>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub disciplines: Vec<EcoString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<VersionBound>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude: Vec<EcoString>,
#[serde(flatten, skip_serializing)]
pub unknown_fields: UnknownFields,
}
impl PackageManifest {
pub fn validate(&self, spec: &PackageSpec) -> Result<(), EcoString> {
if self.package.name != spec.name {
return Err(eco_format!(
"package manifest contains mismatched name `{}`",
self.package.name
));
}
if self.package.version != spec.version {
return Err(eco_format!(
"package manifest contains mismatched version {}",
self.package.version
));
}
if let Some(required) = self.package.compiler {
let current = PackageVersion::compiler();
if !current.matches_ge(&required) {
return Err(eco_format!(
"package requires typst {required} or newer \
(current version is {current})"
));
}
}
Ok(())
}
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct PackageSpec {
pub namespace: EcoString,
pub name: EcoString,
pub version: PackageVersion,
}
impl PackageSpec {
pub fn versionless(&self) -> VersionlessPackageSpec {
VersionlessPackageSpec {
namespace: self.namespace.clone(),
name: self.name.clone(),
}
}
}
impl FromStr for PackageSpec {
type Err = EcoString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s = unscanny::Scanner::new(s);
let namespace = parse_namespace(&mut s)?.into();
let name = parse_name(&mut s)?.into();
let version = parse_version(&mut s)?;
Ok(Self { namespace, name, version })
}
}
impl Debug for PackageSpec {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for PackageSpec {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "@{}/{}:{}", self.namespace, self.name, self.version)
}
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct VersionlessPackageSpec {
pub namespace: EcoString,
pub name: EcoString,
}
impl VersionlessPackageSpec {
pub fn at(self, version: PackageVersion) -> PackageSpec {
PackageSpec {
namespace: self.namespace,
name: self.name,
version,
}
}
}
impl FromStr for VersionlessPackageSpec {
type Err = EcoString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s = unscanny::Scanner::new(s);
let namespace = parse_namespace(&mut s)?.into();
let name = parse_name(&mut s)?.into();
if !s.done() {
Err("unexpected version in versionless package specification")?;
}
Ok(Self { namespace, name })
}
}
impl Debug for VersionlessPackageSpec {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for VersionlessPackageSpec {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "@{}/{}", self.namespace, self.name)
}
}
fn parse_namespace<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
if !s.eat_if('@') {
Err("package specification must start with '@'")?;
}
let namespace = s.eat_until('/');
if namespace.is_empty() {
Err("package specification is missing namespace")?;
} else if !is_ident(namespace) {
Err(eco_format!("`{namespace}` is not a valid package namespace"))?;
}
Ok(namespace)
}
fn parse_name<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
s.eat_if('/');
let name = s.eat_until(':');
if name.is_empty() {
Err("package specification is missing name")?;
} else if !is_ident(name) {
Err(eco_format!("`{name}` is not a valid package name"))?;
}
Ok(name)
}
fn parse_version(s: &mut Scanner) -> Result<PackageVersion, EcoString> {
s.eat_if(':');
let version = s.after();
if version.is_empty() {
Err("package specification is missing version")?;
}
version.parse()
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct PackageVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl PackageVersion {
pub fn compiler() -> Self {
Self {
major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
}
}
pub fn matches_eq(&self, bound: &VersionBound) -> bool {
self.major == bound.major
&& bound.minor.map_or(true, |minor| self.minor == minor)
&& bound.patch.map_or(true, |patch| self.patch == patch)
}
pub fn matches_gt(&self, bound: &VersionBound) -> bool {
if self.major != bound.major {
return self.major > bound.major;
}
let Some(minor) = bound.minor else { return false };
if self.minor != minor {
return self.minor > minor;
}
let Some(patch) = bound.patch else { return false };
if self.patch != patch {
return self.patch > patch;
}
false
}
pub fn matches_lt(&self, bound: &VersionBound) -> bool {
if self.major != bound.major {
return self.major < bound.major;
}
let Some(minor) = bound.minor else { return false };
if self.minor != minor {
return self.minor < minor;
}
let Some(patch) = bound.patch else { return false };
if self.patch != patch {
return self.patch < patch;
}
false
}
pub fn matches_ge(&self, bound: &VersionBound) -> bool {
self.matches_eq(bound) || self.matches_gt(bound)
}
pub fn matches_le(&self, bound: &VersionBound) -> bool {
self.matches_eq(bound) || self.matches_lt(bound)
}
}
impl FromStr for PackageVersion {
type Err = EcoString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('.');
let mut next = |kind| {
let part = parts
.next()
.filter(|s| !s.is_empty())
.ok_or_else(|| eco_format!("version number is missing {kind} version"))?;
part.parse::<u32>()
.map_err(|_| eco_format!("`{part}` is not a valid {kind} version"))
};
let major = next("major")?;
let minor = next("minor")?;
let patch = next("patch")?;
if let Some(rest) = parts.next() {
Err(eco_format!("version number has unexpected fourth component: `{rest}`"))?;
}
Ok(Self { major, minor, patch })
}
}
impl Debug for PackageVersion {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for PackageVersion {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl Serialize for PackageVersion {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for PackageVersion {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let string = EcoString::deserialize(d)?;
string.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct VersionBound {
pub major: u32,
pub minor: Option<u32>,
pub patch: Option<u32>,
}
impl FromStr for VersionBound {
type Err = EcoString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('.');
let mut next = |kind| {
if let Some(part) = parts.next() {
part.parse::<u32>().map(Some).map_err(|_| {
eco_format!("`{part}` is not a valid {kind} version bound")
})
} else {
Ok(None)
}
};
let major = next("major")?
.ok_or_else(|| eco_format!("version bound is missing major version"))?;
let minor = next("minor")?;
let patch = next("patch")?;
if let Some(rest) = parts.next() {
Err(eco_format!("version bound has unexpected fourth component: `{rest}`"))?;
}
Ok(Self { major, minor, patch })
}
}
impl Debug for VersionBound {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for VersionBound {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.major)?;
if let Some(minor) = self.minor {
write!(f, ".{minor}")?;
}
if let Some(patch) = self.patch {
write!(f, ".{patch}")?;
}
Ok(())
}
}
impl Serialize for VersionBound {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for VersionBound {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let string = EcoString::deserialize(d)?;
string.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn version_version_match() {
let v1_1_1 = PackageVersion::from_str("1.1.1").unwrap();
assert!(v1_1_1.matches_eq(&VersionBound::from_str("1").unwrap()));
assert!(v1_1_1.matches_eq(&VersionBound::from_str("1.1").unwrap()));
assert!(!v1_1_1.matches_eq(&VersionBound::from_str("1.2").unwrap()));
assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1").unwrap()));
assert!(v1_1_1.matches_gt(&VersionBound::from_str("1.0").unwrap()));
assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1.1").unwrap()));
assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1").unwrap()));
assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap()));
assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap()));
}
#[test]
fn minimal_manifest() {
assert_eq!(
toml::from_str::<PackageManifest>(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
"#
),
Ok(PackageManifest {
package: PackageInfo {
name: "package".into(),
version: PackageVersion { major: 0, minor: 1, patch: 0 },
entrypoint: "src/lib.typ".into(),
authors: vec![],
license: None,
description: None,
homepage: None,
repository: None,
keywords: vec![],
categories: vec![],
disciplines: vec![],
compiler: None,
exclude: vec![],
unknown_fields: BTreeMap::new(),
},
template: None,
tool: ToolInfo { sections: BTreeMap::new() },
unknown_fields: BTreeMap::new(),
})
);
}
#[test]
fn tool_section() {
assert!(toml::from_str::<PackageManifest>(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
[tool]
not-table = "str"
"#
)
.is_err());
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct MyTool {
key: EcoString,
}
let mut manifest: PackageManifest = toml::from_str(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
[tool.my-tool]
key = "value"
"#,
)
.unwrap();
let my_tool = manifest.tool.sections.remove("my-tool").unwrap();
let my_tool = MyTool::deserialize(my_tool).unwrap();
assert_eq!(my_tool, MyTool { key: "value".into() });
}
#[test]
fn unknown_keys() {
let manifest: PackageManifest = toml::from_str(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
[unknown]
"#,
)
.unwrap();
assert!(manifest.unknown_fields.contains_key("unknown"));
}
}