1use std::collections::BTreeMap;
4use std::fmt::{self, Debug, Display, Formatter};
5use std::str::FromStr;
6
7use ecow::{EcoString, eco_format};
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, Default, Clone, 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 new(package: PackageInfo) -> Self {
148 PackageManifest {
149 package,
150 template: None,
151 tool: ToolInfo::default(),
152 unknown_fields: UnknownFields::new(),
153 }
154 }
155
156 pub fn validate(&self, spec: &PackageSpec) -> Result<(), EcoString> {
158 if self.package.name != spec.name {
159 return Err(eco_format!(
160 "package manifest contains mismatched name `{}`",
161 self.package.name
162 ));
163 }
164
165 if self.package.version != spec.version {
166 return Err(eco_format!(
167 "package manifest contains mismatched version {}",
168 self.package.version
169 ));
170 }
171
172 if let Some(required) = self.package.compiler {
173 let current = PackageVersion::compiler();
174 if !current.matches_ge(&required) {
175 return Err(eco_format!(
176 "package requires Typst {required} or newer \
177 (current version is {current})"
178 ));
179 }
180 }
181
182 Ok(())
183 }
184}
185
186impl TemplateInfo {
187 pub fn new(path: impl Into<EcoString>, entrypoint: impl Into<EcoString>) -> Self {
189 TemplateInfo {
190 path: path.into(),
191 entrypoint: entrypoint.into(),
192 thumbnail: None,
193 unknown_fields: UnknownFields::new(),
194 }
195 }
196}
197
198impl PackageInfo {
199 pub fn new(
201 name: impl Into<EcoString>,
202 version: PackageVersion,
203 entrypoint: impl Into<EcoString>,
204 ) -> Self {
205 PackageInfo {
206 name: name.into(),
207 version,
208 entrypoint: entrypoint.into(),
209 authors: vec![],
210 categories: vec![],
211 compiler: None,
212 description: None,
213 disciplines: vec![],
214 exclude: vec![],
215 homepage: None,
216 keywords: vec![],
217 license: None,
218 repository: None,
219 unknown_fields: BTreeMap::new(),
220 }
221 }
222}
223
224#[derive(Clone, Eq, PartialEq, Hash)]
226pub struct PackageSpec {
227 pub namespace: EcoString,
229 pub name: EcoString,
231 pub version: PackageVersion,
233}
234
235impl PackageSpec {
236 pub fn versionless(&self) -> VersionlessPackageSpec {
237 VersionlessPackageSpec {
238 namespace: self.namespace.clone(),
239 name: self.name.clone(),
240 }
241 }
242}
243
244impl FromStr for PackageSpec {
245 type Err = EcoString;
246
247 fn from_str(s: &str) -> Result<Self, Self::Err> {
248 let mut s = unscanny::Scanner::new(s);
249 let namespace = parse_namespace(&mut s)?.into();
250 let name = parse_name(&mut s)?.into();
251 let version = parse_version(&mut s)?;
252 Ok(Self { namespace, name, version })
253 }
254}
255
256impl Debug for PackageSpec {
257 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
258 Display::fmt(self, f)
259 }
260}
261
262impl Display for PackageSpec {
263 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
264 write!(f, "@{}/{}:{}", self.namespace, self.name, self.version)
265 }
266}
267
268#[derive(Clone, Eq, PartialEq, Hash)]
270pub struct VersionlessPackageSpec {
271 pub namespace: EcoString,
273 pub name: EcoString,
275}
276
277impl VersionlessPackageSpec {
278 pub fn at(self, version: PackageVersion) -> PackageSpec {
280 PackageSpec {
281 namespace: self.namespace,
282 name: self.name,
283 version,
284 }
285 }
286}
287
288impl FromStr for VersionlessPackageSpec {
289 type Err = EcoString;
290
291 fn from_str(s: &str) -> Result<Self, Self::Err> {
292 let mut s = unscanny::Scanner::new(s);
293 let namespace = parse_namespace(&mut s)?.into();
294 let name = parse_name(&mut s)?.into();
295 if !s.done() {
296 Err("unexpected version in versionless package specification")?;
297 }
298 Ok(Self { namespace, name })
299 }
300}
301
302impl Debug for VersionlessPackageSpec {
303 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
304 Display::fmt(self, f)
305 }
306}
307
308impl Display for VersionlessPackageSpec {
309 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
310 write!(f, "@{}/{}", self.namespace, self.name)
311 }
312}
313
314fn parse_namespace<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
315 if !s.eat_if('@') {
316 Err("package specification must start with '@'")?;
317 }
318
319 let namespace = s.eat_until('/');
320 if namespace.is_empty() {
321 Err("package specification is missing namespace")?;
322 } else if !is_ident(namespace) {
323 Err(eco_format!("`{namespace}` is not a valid package namespace"))?;
324 }
325
326 Ok(namespace)
327}
328
329fn parse_name<'s>(s: &mut Scanner<'s>) -> Result<&'s str, EcoString> {
330 s.eat_if('/');
331
332 let name = s.eat_until(':');
333 if name.is_empty() {
334 Err("package specification is missing name")?;
335 } else if !is_ident(name) {
336 Err(eco_format!("`{name}` is not a valid package name"))?;
337 }
338
339 Ok(name)
340}
341
342fn parse_version(s: &mut Scanner) -> Result<PackageVersion, EcoString> {
343 s.eat_if(':');
344
345 let version = s.after();
346 if version.is_empty() {
347 Err("package specification is missing version")?;
348 }
349
350 version.parse()
351}
352
353#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
355pub struct PackageVersion {
356 pub major: u32,
358 pub minor: u32,
360 pub patch: u32,
362}
363
364impl PackageVersion {
365 pub fn compiler() -> Self {
367 Self {
368 major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
369 minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
370 patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
371 }
372 }
373
374 pub fn matches_eq(&self, bound: &VersionBound) -> bool {
377 self.major == bound.major
378 && bound.minor.is_none_or(|minor| self.minor == minor)
379 && bound.patch.is_none_or(|patch| self.patch == patch)
380 }
381
382 pub fn matches_gt(&self, bound: &VersionBound) -> bool {
386 if self.major != bound.major {
387 return self.major > bound.major;
388 }
389 let Some(minor) = bound.minor else { return false };
390 if self.minor != minor {
391 return self.minor > minor;
392 }
393 let Some(patch) = bound.patch else { return false };
394 if self.patch != patch {
395 return self.patch > patch;
396 }
397 false
398 }
399
400 pub fn matches_lt(&self, bound: &VersionBound) -> bool {
404 if self.major != bound.major {
405 return self.major < bound.major;
406 }
407 let Some(minor) = bound.minor else { return false };
408 if self.minor != minor {
409 return self.minor < minor;
410 }
411 let Some(patch) = bound.patch else { return false };
412 if self.patch != patch {
413 return self.patch < patch;
414 }
415 false
416 }
417
418 pub fn matches_ge(&self, bound: &VersionBound) -> bool {
421 self.matches_eq(bound) || self.matches_gt(bound)
422 }
423
424 pub fn matches_le(&self, bound: &VersionBound) -> bool {
427 self.matches_eq(bound) || self.matches_lt(bound)
428 }
429}
430
431impl FromStr for PackageVersion {
432 type Err = EcoString;
433
434 fn from_str(s: &str) -> Result<Self, Self::Err> {
435 let mut parts = s.split('.');
436 let mut next = |kind| {
437 let part = parts
438 .next()
439 .filter(|s| !s.is_empty())
440 .ok_or_else(|| eco_format!("version number is missing {kind} version"))?;
441 part.parse::<u32>()
442 .map_err(|_| eco_format!("`{part}` is not a valid {kind} version"))
443 };
444
445 let major = next("major")?;
446 let minor = next("minor")?;
447 let patch = next("patch")?;
448 if let Some(rest) = parts.next() {
449 Err(eco_format!("version number has unexpected fourth component: `{rest}`"))?;
450 }
451
452 Ok(Self { major, minor, patch })
453 }
454}
455
456impl Debug for PackageVersion {
457 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
458 Display::fmt(self, f)
459 }
460}
461
462impl Display for PackageVersion {
463 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
464 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
465 }
466}
467
468impl Serialize for PackageVersion {
469 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
470 s.collect_str(self)
471 }
472}
473
474impl<'de> Deserialize<'de> for PackageVersion {
475 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
476 let string = EcoString::deserialize(d)?;
477 string.parse().map_err(serde::de::Error::custom)
478 }
479}
480
481#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
483pub struct VersionBound {
484 pub major: u32,
486 pub minor: Option<u32>,
488 pub patch: Option<u32>,
490}
491
492impl FromStr for VersionBound {
493 type Err = EcoString;
494
495 fn from_str(s: &str) -> Result<Self, Self::Err> {
496 let mut parts = s.split('.');
497 let mut next = |kind| {
498 if let Some(part) = parts.next() {
499 part.parse::<u32>().map(Some).map_err(|_| {
500 eco_format!("`{part}` is not a valid {kind} version bound")
501 })
502 } else {
503 Ok(None)
504 }
505 };
506
507 let major = next("major")?
508 .ok_or_else(|| eco_format!("version bound is missing major version"))?;
509 let minor = next("minor")?;
510 let patch = next("patch")?;
511 if let Some(rest) = parts.next() {
512 Err(eco_format!("version bound has unexpected fourth component: `{rest}`"))?;
513 }
514
515 Ok(Self { major, minor, patch })
516 }
517}
518
519impl Debug for VersionBound {
520 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
521 Display::fmt(self, f)
522 }
523}
524
525impl Display for VersionBound {
526 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
527 write!(f, "{}", self.major)?;
528 if let Some(minor) = self.minor {
529 write!(f, ".{minor}")?;
530 }
531 if let Some(patch) = self.patch {
532 write!(f, ".{patch}")?;
533 }
534 Ok(())
535 }
536}
537
538impl Serialize for VersionBound {
539 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
540 s.collect_str(self)
541 }
542}
543
544impl<'de> Deserialize<'de> for VersionBound {
545 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
546 let string = EcoString::deserialize(d)?;
547 string.parse().map_err(serde::de::Error::custom)
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use std::str::FromStr;
554
555 use super::*;
556
557 #[test]
558 fn version_version_match() {
559 let v1_1_1 = PackageVersion::from_str("1.1.1").unwrap();
560
561 assert!(v1_1_1.matches_eq(&VersionBound::from_str("1").unwrap()));
562 assert!(v1_1_1.matches_eq(&VersionBound::from_str("1.1").unwrap()));
563 assert!(!v1_1_1.matches_eq(&VersionBound::from_str("1.2").unwrap()));
564
565 assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1").unwrap()));
566 assert!(v1_1_1.matches_gt(&VersionBound::from_str("1.0").unwrap()));
567 assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1.1").unwrap()));
568
569 assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1").unwrap()));
570 assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap()));
571 assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap()));
572 }
573
574 #[test]
575 fn minimal_manifest() {
576 assert_eq!(
577 toml::from_str::<PackageManifest>(
578 r#"
579 [package]
580 name = "package"
581 version = "0.1.0"
582 entrypoint = "src/lib.typ"
583 "#
584 ),
585 Ok(PackageManifest {
586 package: PackageInfo::new(
587 "package",
588 PackageVersion { major: 0, minor: 1, patch: 0 },
589 "src/lib.typ"
590 ),
591 template: None,
592 tool: ToolInfo { sections: BTreeMap::new() },
593 unknown_fields: BTreeMap::new(),
594 })
595 );
596 }
597
598 #[test]
599 fn tool_section() {
600 assert!(
603 toml::from_str::<PackageManifest>(
604 r#"
605 [package]
606 name = "package"
607 version = "0.1.0"
608 entrypoint = "src/lib.typ"
609
610 [tool]
611 not-table = "str"
612 "#
613 )
614 .is_err()
615 );
616
617 #[derive(Debug, PartialEq, Serialize, Deserialize)]
618 struct MyTool {
619 key: EcoString,
620 }
621
622 let mut manifest: PackageManifest = toml::from_str(
623 r#"
624 [package]
625 name = "package"
626 version = "0.1.0"
627 entrypoint = "src/lib.typ"
628
629 [tool.my-tool]
630 key = "value"
631 "#,
632 )
633 .unwrap();
634
635 let my_tool = manifest.tool.sections.remove("my-tool").unwrap();
636 let my_tool = MyTool::deserialize(my_tool).unwrap();
637
638 assert_eq!(my_tool, MyTool { key: "value".into() });
639 }
640
641 #[test]
642 fn unknown_keys() {
643 let manifest: PackageManifest = toml::from_str(
644 r#"
645 [package]
646 name = "package"
647 version = "0.1.0"
648 entrypoint = "src/lib.typ"
649
650 [unknown]
651 "#,
652 )
653 .unwrap();
654
655 assert!(manifest.unknown_fields.contains_key("unknown"));
656 }
657}