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 let typst_version = typst_utils::version();
368 Self {
369 major: typst_version.major(),
370 minor: typst_version.minor(),
371 patch: typst_version.patch(),
372 }
373 }
374
375 pub fn matches_eq(&self, bound: &VersionBound) -> bool {
378 self.major == bound.major
379 && bound.minor.is_none_or(|minor| self.minor == minor)
380 && bound.patch.is_none_or(|patch| self.patch == patch)
381 }
382
383 pub fn matches_gt(&self, bound: &VersionBound) -> bool {
387 if self.major != bound.major {
388 return self.major > bound.major;
389 }
390 let Some(minor) = bound.minor else { return false };
391 if self.minor != minor {
392 return self.minor > minor;
393 }
394 let Some(patch) = bound.patch else { return false };
395 if self.patch != patch {
396 return self.patch > patch;
397 }
398 false
399 }
400
401 pub fn matches_lt(&self, bound: &VersionBound) -> bool {
405 if self.major != bound.major {
406 return self.major < bound.major;
407 }
408 let Some(minor) = bound.minor else { return false };
409 if self.minor != minor {
410 return self.minor < minor;
411 }
412 let Some(patch) = bound.patch else { return false };
413 if self.patch != patch {
414 return self.patch < patch;
415 }
416 false
417 }
418
419 pub fn matches_ge(&self, bound: &VersionBound) -> bool {
422 self.matches_eq(bound) || self.matches_gt(bound)
423 }
424
425 pub fn matches_le(&self, bound: &VersionBound) -> bool {
428 self.matches_eq(bound) || self.matches_lt(bound)
429 }
430}
431
432impl FromStr for PackageVersion {
433 type Err = EcoString;
434
435 fn from_str(s: &str) -> Result<Self, Self::Err> {
436 let mut parts = s.split('.');
437 let mut next = |kind| {
438 let part = parts
439 .next()
440 .filter(|s| !s.is_empty())
441 .ok_or_else(|| eco_format!("version number is missing {kind} version"))?;
442 part.parse::<u32>()
443 .map_err(|_| eco_format!("`{part}` is not a valid {kind} version"))
444 };
445
446 let major = next("major")?;
447 let minor = next("minor")?;
448 let patch = next("patch")?;
449 if let Some(rest) = parts.next() {
450 Err(eco_format!("version number has unexpected fourth component: `{rest}`"))?;
451 }
452
453 Ok(Self { major, minor, patch })
454 }
455}
456
457impl Debug for PackageVersion {
458 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
459 Display::fmt(self, f)
460 }
461}
462
463impl Display for PackageVersion {
464 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
465 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
466 }
467}
468
469impl Serialize for PackageVersion {
470 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
471 s.collect_str(self)
472 }
473}
474
475impl<'de> Deserialize<'de> for PackageVersion {
476 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
477 let string = EcoString::deserialize(d)?;
478 string.parse().map_err(serde::de::Error::custom)
479 }
480}
481
482#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
484pub struct VersionBound {
485 pub major: u32,
487 pub minor: Option<u32>,
489 pub patch: Option<u32>,
491}
492
493impl FromStr for VersionBound {
494 type Err = EcoString;
495
496 fn from_str(s: &str) -> Result<Self, Self::Err> {
497 let mut parts = s.split('.');
498 let mut next = |kind| {
499 if let Some(part) = parts.next() {
500 part.parse::<u32>().map(Some).map_err(|_| {
501 eco_format!("`{part}` is not a valid {kind} version bound")
502 })
503 } else {
504 Ok(None)
505 }
506 };
507
508 let major = next("major")?
509 .ok_or_else(|| eco_format!("version bound is missing major version"))?;
510 let minor = next("minor")?;
511 let patch = next("patch")?;
512 if let Some(rest) = parts.next() {
513 Err(eco_format!("version bound has unexpected fourth component: `{rest}`"))?;
514 }
515
516 Ok(Self { major, minor, patch })
517 }
518}
519
520impl Debug for VersionBound {
521 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
522 Display::fmt(self, f)
523 }
524}
525
526impl Display for VersionBound {
527 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
528 write!(f, "{}", self.major)?;
529 if let Some(minor) = self.minor {
530 write!(f, ".{minor}")?;
531 }
532 if let Some(patch) = self.patch {
533 write!(f, ".{patch}")?;
534 }
535 Ok(())
536 }
537}
538
539impl Serialize for VersionBound {
540 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
541 s.collect_str(self)
542 }
543}
544
545impl<'de> Deserialize<'de> for VersionBound {
546 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
547 let string = EcoString::deserialize(d)?;
548 string.parse().map_err(serde::de::Error::custom)
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use std::str::FromStr;
555
556 use super::*;
557
558 #[test]
559 fn version_version_match() {
560 let v1_1_1 = PackageVersion::from_str("1.1.1").unwrap();
561
562 assert!(v1_1_1.matches_eq(&VersionBound::from_str("1").unwrap()));
563 assert!(v1_1_1.matches_eq(&VersionBound::from_str("1.1").unwrap()));
564 assert!(!v1_1_1.matches_eq(&VersionBound::from_str("1.2").unwrap()));
565
566 assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1").unwrap()));
567 assert!(v1_1_1.matches_gt(&VersionBound::from_str("1.0").unwrap()));
568 assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1.1").unwrap()));
569
570 assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1").unwrap()));
571 assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap()));
572 assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap()));
573 }
574
575 #[test]
576 fn minimal_manifest() {
577 assert_eq!(
578 toml::from_str::<PackageManifest>(
579 r#"
580 [package]
581 name = "package"
582 version = "0.1.0"
583 entrypoint = "src/lib.typ"
584 "#
585 ),
586 Ok(PackageManifest {
587 package: PackageInfo::new(
588 "package",
589 PackageVersion { major: 0, minor: 1, patch: 0 },
590 "src/lib.typ"
591 ),
592 template: None,
593 tool: ToolInfo { sections: BTreeMap::new() },
594 unknown_fields: BTreeMap::new(),
595 })
596 );
597 }
598
599 #[test]
600 fn tool_section() {
601 assert!(
604 toml::from_str::<PackageManifest>(
605 r#"
606 [package]
607 name = "package"
608 version = "0.1.0"
609 entrypoint = "src/lib.typ"
610
611 [tool]
612 not-table = "str"
613 "#
614 )
615 .is_err()
616 );
617
618 #[derive(Debug, PartialEq, Serialize, Deserialize)]
619 struct MyTool {
620 key: EcoString,
621 }
622
623 let mut manifest: PackageManifest = toml::from_str(
624 r#"
625 [package]
626 name = "package"
627 version = "0.1.0"
628 entrypoint = "src/lib.typ"
629
630 [tool.my-tool]
631 key = "value"
632 "#,
633 )
634 .unwrap();
635
636 let my_tool = manifest.tool.sections.remove("my-tool").unwrap();
637 let my_tool = MyTool::deserialize(my_tool).unwrap();
638
639 assert_eq!(my_tool, MyTool { key: "value".into() });
640 }
641
642 #[test]
643 fn unknown_keys() {
644 let manifest: PackageManifest = toml::from_str(
645 r#"
646 [package]
647 name = "package"
648 version = "0.1.0"
649 entrypoint = "src/lib.typ"
650
651 [unknown]
652 "#,
653 )
654 .unwrap();
655
656 assert!(manifest.unknown_fields.contains_key("unknown"));
657 }
658}