1use std::collections::BTreeMap;
4use std::fmt::{self, Debug, Display, Formatter};
5use std::str::FromStr;
6
7use ecow::{eco_format, EcoString};
8use serde::de::IgnoredAny;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use unscanny::Scanner;
11
12use crate::is_ident;
13
14pub type UnknownFields = BTreeMap<EcoString, IgnoredAny>;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct PackageManifest {
23 pub package: PackageInfo,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub template: Option<TemplateInfo>,
28 #[serde(default)]
30 pub tool: ToolInfo,
31 #[serde(flatten, skip_serializing)]
33 pub unknown_fields: UnknownFields,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
70pub struct ToolInfo {
71 #[serde(flatten)]
73 pub sections: BTreeMap<EcoString, toml::Table>,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct TemplateInfo {
81 pub path: EcoString,
84 pub entrypoint: EcoString,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub thumbnail: Option<EcoString>,
91 #[serde(flatten, skip_serializing)]
93 pub unknown_fields: UnknownFields,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct PackageInfo {
101 pub name: EcoString,
103 pub version: PackageVersion,
105 pub entrypoint: EcoString,
107 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub authors: Vec<EcoString>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub license: Option<EcoString>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub description: Option<EcoString>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub homepage: Option<EcoString>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub repository: Option<EcoString>,
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
124 pub keywords: Vec<EcoString>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
128 pub categories: Vec<EcoString>,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
132 pub disciplines: Vec<EcoString>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub compiler: Option<VersionBound>,
136 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pub exclude: Vec<EcoString>,
140 #[serde(flatten, skip_serializing)]
142 pub unknown_fields: UnknownFields,
143}
144
145impl PackageManifest {
146 pub fn validate(&self, spec: &PackageSpec) -> Result<(), EcoString> {
148 if self.package.name != spec.name {
149 return Err(eco_format!(
150 "package manifest contains mismatched name `{}`",
151 self.package.name
152 ));
153 }
154
155 if self.package.version != spec.version {
156 return Err(eco_format!(
157 "package manifest contains mismatched version {}",
158 self.package.version
159 ));
160 }
161
162 if let Some(required) = self.package.compiler {
163 let current = PackageVersion::compiler();
164 if !current.matches_ge(&required) {
165 return Err(eco_format!(
166 "package requires typst {required} or newer \
167 (current version is {current})"
168 ));
169 }
170 }
171
172 Ok(())
173 }
174}
175
176#[derive(Clone, Eq, PartialEq, Hash)]
178pub struct PackageSpec {
179 pub namespace: EcoString,
181 pub name: EcoString,
183 pub version: PackageVersion,
185}
186
187impl PackageSpec {
188 pub fn versionless(&self) -> VersionlessPackageSpec {
189 VersionlessPackageSpec {
190 namespace: self.namespace.clone(),
191 name: self.name.clone(),
192 }
193 }
194}
195
196impl FromStr for PackageSpec {
197 type Err = EcoString;
198
199 fn from_str(s: &str) -> Result<Self, Self::Err> {
200 let mut s = unscanny::Scanner::new(s);
201 let namespace = parse_namespace(&mut s)?.into();
202 let name = parse_name(&mut s)?.into();
203 let version = parse_version(&mut s)?;
204 Ok(Self { namespace, name, version })
205 }
206}
207
208impl Debug for PackageSpec {
209 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
210 Display::fmt(self, f)
211 }
212}
213
214impl Display for PackageSpec {
215 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
216 write!(f, "@{}/{}:{}", self.namespace, self.name, self.version)
217 }
218}
219
220#[derive(Clone, Eq, PartialEq, Hash)]
222pub struct VersionlessPackageSpec {
223 pub namespace: EcoString,
225 pub name: EcoString,
227}
228
229impl VersionlessPackageSpec {
230 pub fn at(self, version: PackageVersion) -> PackageSpec {
232 PackageSpec {
233 namespace: self.namespace,
234 name: self.name,
235 version,
236 }
237 }
238}
239
240impl FromStr for VersionlessPackageSpec {
241 type Err = EcoString;
242
243 fn from_str(s: &str) -> Result<Self, Self::Err> {
244 let mut s = unscanny::Scanner::new(s);
245 let namespace = parse_namespace(&mut s)?.into();
246 let name = parse_name(&mut s)?.into();
247 if !s.done() {
248 Err("unexpected version in versionless package specification")?;
249 }
250 Ok(Self { namespace, name })
251 }
252}
253
254impl Debug for VersionlessPackageSpec {
255 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
256 Display::fmt(self, f)
257 }
258}
259
260impl Display for VersionlessPackageSpec {
261 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
262 write!(f, "@{}/{}", self.namespace, self.name)
263 }
264}
265
266fn parse_namespace<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
267 if !s.eat_if('@') {
268 Err("package specification must start with '@'")?;
269 }
270
271 let namespace = s.eat_until('/');
272 if namespace.is_empty() {
273 Err("package specification is missing namespace")?;
274 } else if !is_ident(namespace) {
275 Err(eco_format!("`{namespace}` is not a valid package namespace"))?;
276 }
277
278 Ok(namespace)
279}
280
281fn parse_name<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
282 s.eat_if('/');
283
284 let name = s.eat_until(':');
285 if name.is_empty() {
286 Err("package specification is missing name")?;
287 } else if !is_ident(name) {
288 Err(eco_format!("`{name}` is not a valid package name"))?;
289 }
290
291 Ok(name)
292}
293
294fn parse_version(s: &mut Scanner) -> Result<PackageVersion, EcoString> {
295 s.eat_if(':');
296
297 let version = s.after();
298 if version.is_empty() {
299 Err("package specification is missing version")?;
300 }
301
302 version.parse()
303}
304
305#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
307pub struct PackageVersion {
308 pub major: u32,
310 pub minor: u32,
312 pub patch: u32,
314}
315
316impl PackageVersion {
317 pub fn compiler() -> Self {
319 Self {
320 major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
321 minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
322 patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
323 }
324 }
325
326 pub fn matches_eq(&self, bound: &VersionBound) -> bool {
329 self.major == bound.major
330 && bound.minor.map_or(true, |minor| self.minor == minor)
331 && bound.patch.map_or(true, |patch| self.patch == patch)
332 }
333
334 pub fn matches_gt(&self, bound: &VersionBound) -> bool {
338 if self.major != bound.major {
339 return self.major > bound.major;
340 }
341 let Some(minor) = bound.minor else { return false };
342 if self.minor != minor {
343 return self.minor > minor;
344 }
345 let Some(patch) = bound.patch else { return false };
346 if self.patch != patch {
347 return self.patch > patch;
348 }
349 false
350 }
351
352 pub fn matches_lt(&self, bound: &VersionBound) -> bool {
356 if self.major != bound.major {
357 return self.major < bound.major;
358 }
359 let Some(minor) = bound.minor else { return false };
360 if self.minor != minor {
361 return self.minor < minor;
362 }
363 let Some(patch) = bound.patch else { return false };
364 if self.patch != patch {
365 return self.patch < patch;
366 }
367 false
368 }
369
370 pub fn matches_ge(&self, bound: &VersionBound) -> bool {
373 self.matches_eq(bound) || self.matches_gt(bound)
374 }
375
376 pub fn matches_le(&self, bound: &VersionBound) -> bool {
379 self.matches_eq(bound) || self.matches_lt(bound)
380 }
381}
382
383impl FromStr for PackageVersion {
384 type Err = EcoString;
385
386 fn from_str(s: &str) -> Result<Self, Self::Err> {
387 let mut parts = s.split('.');
388 let mut next = |kind| {
389 let part = parts
390 .next()
391 .filter(|s| !s.is_empty())
392 .ok_or_else(|| eco_format!("version number is missing {kind} version"))?;
393 part.parse::<u32>()
394 .map_err(|_| eco_format!("`{part}` is not a valid {kind} version"))
395 };
396
397 let major = next("major")?;
398 let minor = next("minor")?;
399 let patch = next("patch")?;
400 if let Some(rest) = parts.next() {
401 Err(eco_format!("version number has unexpected fourth component: `{rest}`"))?;
402 }
403
404 Ok(Self { major, minor, patch })
405 }
406}
407
408impl Debug for PackageVersion {
409 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
410 Display::fmt(self, f)
411 }
412}
413
414impl Display for PackageVersion {
415 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
416 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
417 }
418}
419
420impl Serialize for PackageVersion {
421 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
422 s.collect_str(self)
423 }
424}
425
426impl<'de> Deserialize<'de> for PackageVersion {
427 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
428 let string = EcoString::deserialize(d)?;
429 string.parse().map_err(serde::de::Error::custom)
430 }
431}
432
433#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
435pub struct VersionBound {
436 pub major: u32,
438 pub minor: Option<u32>,
440 pub patch: Option<u32>,
442}
443
444impl FromStr for VersionBound {
445 type Err = EcoString;
446
447 fn from_str(s: &str) -> Result<Self, Self::Err> {
448 let mut parts = s.split('.');
449 let mut next = |kind| {
450 if let Some(part) = parts.next() {
451 part.parse::<u32>().map(Some).map_err(|_| {
452 eco_format!("`{part}` is not a valid {kind} version bound")
453 })
454 } else {
455 Ok(None)
456 }
457 };
458
459 let major = next("major")?
460 .ok_or_else(|| eco_format!("version bound is missing major version"))?;
461 let minor = next("minor")?;
462 let patch = next("patch")?;
463 if let Some(rest) = parts.next() {
464 Err(eco_format!("version bound has unexpected fourth component: `{rest}`"))?;
465 }
466
467 Ok(Self { major, minor, patch })
468 }
469}
470
471impl Debug for VersionBound {
472 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
473 Display::fmt(self, f)
474 }
475}
476
477impl Display for VersionBound {
478 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
479 write!(f, "{}", self.major)?;
480 if let Some(minor) = self.minor {
481 write!(f, ".{minor}")?;
482 }
483 if let Some(patch) = self.patch {
484 write!(f, ".{patch}")?;
485 }
486 Ok(())
487 }
488}
489
490impl Serialize for VersionBound {
491 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
492 s.collect_str(self)
493 }
494}
495
496impl<'de> Deserialize<'de> for VersionBound {
497 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
498 let string = EcoString::deserialize(d)?;
499 string.parse().map_err(serde::de::Error::custom)
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use std::str::FromStr;
506
507 use super::*;
508
509 #[test]
510 fn version_version_match() {
511 let v1_1_1 = PackageVersion::from_str("1.1.1").unwrap();
512
513 assert!(v1_1_1.matches_eq(&VersionBound::from_str("1").unwrap()));
514 assert!(v1_1_1.matches_eq(&VersionBound::from_str("1.1").unwrap()));
515 assert!(!v1_1_1.matches_eq(&VersionBound::from_str("1.2").unwrap()));
516
517 assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1").unwrap()));
518 assert!(v1_1_1.matches_gt(&VersionBound::from_str("1.0").unwrap()));
519 assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1.1").unwrap()));
520
521 assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1").unwrap()));
522 assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap()));
523 assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap()));
524 }
525
526 #[test]
527 fn minimal_manifest() {
528 assert_eq!(
529 toml::from_str::<PackageManifest>(
530 r#"
531 [package]
532 name = "package"
533 version = "0.1.0"
534 entrypoint = "src/lib.typ"
535 "#
536 ),
537 Ok(PackageManifest {
538 package: PackageInfo {
539 name: "package".into(),
540 version: PackageVersion { major: 0, minor: 1, patch: 0 },
541 entrypoint: "src/lib.typ".into(),
542 authors: vec![],
543 license: None,
544 description: None,
545 homepage: None,
546 repository: None,
547 keywords: vec![],
548 categories: vec![],
549 disciplines: vec![],
550 compiler: None,
551 exclude: vec![],
552 unknown_fields: BTreeMap::new(),
553 },
554 template: None,
555 tool: ToolInfo { sections: BTreeMap::new() },
556 unknown_fields: BTreeMap::new(),
557 })
558 );
559 }
560
561 #[test]
562 fn tool_section() {
563 assert!(toml::from_str::<PackageManifest>(
566 r#"
567 [package]
568 name = "package"
569 version = "0.1.0"
570 entrypoint = "src/lib.typ"
571
572 [tool]
573 not-table = "str"
574 "#
575 )
576 .is_err());
577
578 #[derive(Debug, PartialEq, Serialize, Deserialize)]
579 struct MyTool {
580 key: EcoString,
581 }
582
583 let mut manifest: PackageManifest = toml::from_str(
584 r#"
585 [package]
586 name = "package"
587 version = "0.1.0"
588 entrypoint = "src/lib.typ"
589
590 [tool.my-tool]
591 key = "value"
592 "#,
593 )
594 .unwrap();
595
596 let my_tool = manifest.tool.sections.remove("my-tool").unwrap();
597 let my_tool = MyTool::deserialize(my_tool).unwrap();
598
599 assert_eq!(my_tool, MyTool { key: "value".into() });
600 }
601
602 #[test]
603 fn unknown_keys() {
604 let manifest: PackageManifest = toml::from_str(
605 r#"
606 [package]
607 name = "package"
608 version = "0.1.0"
609 entrypoint = "src/lib.typ"
610
611 [unknown]
612 "#,
613 )
614 .unwrap();
615
616 assert!(manifest.unknown_fields.contains_key("unknown"));
617 }
618}