pint_manifest/
lib.rs

1//! The manifest type and its implementations.
2
3use serde::{Deserialize, Serialize};
4use std::{
5    collections::{BTreeMap, HashSet},
6    fmt, fs, io, ops,
7    path::{Path, PathBuf},
8    str,
9};
10use thiserror::Error;
11
12/// A manifest loaded from a file.
13#[derive(Clone, Debug, Eq, PartialEq, Hash)]
14pub struct ManifestFile {
15    /// The deserialized manifest.
16    manifest: Manifest,
17    /// The canonical path to the manifest file.
18    path: PathBuf,
19}
20
21/// A package manifest.
22///
23/// This is the Rust representation of the `pint.toml` manifest file.
24#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
25pub struct Manifest {
26    /// High-level information about the package like name, license.
27    #[serde(rename = "package")]
28    pub pkg: Package,
29    /// All library dependencies declared by this package.
30    #[serde(default, rename = "dependencies", with = "serde_opt")]
31    pub deps: Dependencies,
32    /// All contract dependencies declared by this package.
33    #[serde(default, rename = "contract-dependencies", with = "serde_opt")]
34    pub contract_deps: ContractDependencies,
35}
36
37/// High-level information about the package.
38#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
39pub struct Package {
40    /// The name of the package.
41    pub name: String,
42    /// The license for the package.
43    pub license: Option<String>,
44    /// Whether the package is a contract or library.
45    #[serde(default)]
46    pub kind: PackageKind,
47    /// The location of the file within `src` that is the entry-point to
48    /// compilation.
49    ///
50    /// If unspecified, the default entry point will be derived from the package
51    /// kind:
52    ///
53    /// - `Contract`: "contract.pnt"
54    /// - `Library`: "lib.pnt"
55    #[serde(rename = "entry-point")]
56    pub entry_point: Option<String>,
57}
58
59/// Whether the package is to be compiled as a contract or library.
60#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
61pub enum PackageKind {
62    /// The package is to be compiled as a contract.
63    #[default]
64    #[serde(rename = "contract")]
65    Contract,
66    /// The package is a library of items (types, macros, consts, modules).
67    #[serde(rename = "library")]
68    Library,
69}
70
71/// The table of library dependencies.
72pub type Dependencies = BTreeMap<String, Dependency>;
73/// The table of contract dependencies.
74pub type ContractDependencies = BTreeMap<String, Dependency>;
75
76/// Represents a dependency on another pint package.
77#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
78pub struct Dependency {
79    /// The source of the dependency.
80    #[serde(flatten)]
81    pub source: dependency::Source,
82    /// Optionally specify the expected package name in the case that it differs
83    /// to the name given to the dependency.
84    pub package: Option<String>,
85}
86
87/// The manifest specifies an invalid package name.
88#[derive(Debug, Error)]
89pub enum InvalidName {
90    /// Must only contain ASCII non-uppercase alphanumeric chars, dashes or underscores.
91    #[error("must only contain ASCII non-uppercase alphanumeric chars, dashes or underscores")]
92    InvalidChar,
93    /// Must begin with an alphabetic character.
94    #[error("must begin with an alphabetic character")]
95    NonAlphabeticStart,
96    /// Must end with an alphanumeric character.
97    #[error("must end with an alphanumeric character")]
98    NonAlphanumericEnd,
99    /// Must not be a pint language keyword.
100    #[error("must not be a pint language keyword")]
101    PintKeyword,
102    /// The given name is a word reserved by pint.
103    #[error("the given name is a word reserved by pint")]
104    Reserved,
105}
106
107/// The parsed package kind was invalid.
108#[derive(Debug, Error)]
109#[error(r#"failed to parse package kind, expected "contract" or "library""#)]
110pub struct InvalidPkgKind;
111
112/// The manifest failed its validation check.
113#[derive(Debug, Error)]
114pub enum InvalidManifest {
115    /// Manifest specifies an invalid package name.
116    #[error("manifest specifies an invalid package name {0:?}: {1}")]
117    PkgName(String, InvalidName),
118    /// Manifest specifies an invalid dependency name.
119    #[error("manifest specifies an invalid dependency name {0:?}: {1}")]
120    DepName(String, InvalidName),
121    /// Dependency name appears more than once.
122    #[error("dependency name {0:?} appears more than once")]
123    DupDepName(String),
124}
125
126/// Failure to parse and construct a manifest from a string.
127#[derive(Debug, Error)]
128pub enum ManifestError {
129    /// Failed to deserialize the manifest from toml.
130    #[error("failed to deserialize manifest from toml: {0}")]
131    Toml(#[from] toml::de::Error),
132    /// Manifest failed the validation check.
133    #[error("invalid manifest: {0}")]
134    Invalid(#[from] InvalidManifest),
135}
136
137/// Failed to load a manifest from file.
138#[derive(Debug, Error)]
139pub enum ManifestFileError {
140    #[error("an IO error occurred while constructing the `ManifestFile`: {0}")]
141    Io(#[from] io::Error),
142    /// Failed to construct the manifest.
143    #[error("{0}")]
144    Manifest(#[from] ManifestError),
145}
146
147impl Manifest {
148    /// The default contract compilation entry point within the src dir.
149    pub const DEFAULT_CONTRACT_ENTRY_POINT: &'static str = "contract.pnt";
150    /// The default library compilation entry point within the src dir.
151    pub const DEFAULT_LIBRARY_ENTRY_POINT: &'static str = "lib.pnt";
152
153    /// The specified entry point, or the default entry point based on the package kind.
154    pub fn entry_point_str(&self) -> &str {
155        self.pkg
156            .entry_point
157            .as_deref()
158            .unwrap_or(match self.pkg.kind {
159                PackageKind::Contract => Self::DEFAULT_CONTRACT_ENTRY_POINT,
160                PackageKind::Library => Self::DEFAULT_LIBRARY_ENTRY_POINT,
161            })
162    }
163}
164
165impl ManifestFile {
166    /// The expected file name of the pint manifest file.
167    pub const FILE_NAME: &'static str = "pint.toml";
168
169    /// Load a manifest directly from its file path.
170    ///
171    /// The given path will be [`canonicalize`][std::path::Path::canonicalize]d.
172    pub fn from_path(path: &Path) -> Result<Self, ManifestFileError> {
173        let path = path.canonicalize()?;
174        let string = fs::read_to_string(&path)?;
175        let manifest: Manifest = string.parse()?;
176        let manifest_file = Self { manifest, path };
177        Ok(manifest_file)
178    }
179
180    /// The canonical path to the manifest.
181    pub fn path(&self) -> &Path {
182        &self.path
183    }
184
185    /// The parent directory of the manifest file.
186    pub fn dir(&self) -> &Path {
187        self.path
188            .parent()
189            .expect("manifest file has no parent directory")
190    }
191
192    /// The full path to the package's `src` directory.
193    pub fn src_dir(&self) -> PathBuf {
194        self.dir().join("src")
195    }
196
197    /// The full path to the package's `out` directory for build artifacts.
198    pub fn out_dir(&self) -> PathBuf {
199        self.dir().join("out")
200    }
201
202    /// The path to the compilation entry point src file.
203    pub fn entry_point(&self) -> PathBuf {
204        self.src_dir().join(self.entry_point_str())
205    }
206
207    /// The dependency or contract dependency with the given dependency name.
208    pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
209        self.deps
210            .get(dep_name)
211            .or_else(|| self.contract_deps.get(dep_name))
212    }
213
214    /// Given the name of a `path` dependency, returns the full canonical `Path` to the dependency.
215    pub fn dep_path(&self, dep_name: &str) -> Option<PathBuf> {
216        let dir = self.dir();
217        let dep = self.dep(dep_name)?;
218        match &dep.source {
219            dependency::Source::Path(dep) => match dep.path.is_absolute() {
220                true => Some(dep.path.to_owned()),
221                false => dir.join(&dep.path).canonicalize().ok(),
222            },
223        }
224    }
225}
226
227impl fmt::Display for PackageKind {
228    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229        let s = match self {
230            Self::Contract => "contract",
231            Self::Library => "library",
232        };
233        write!(f, "{}", s)
234    }
235}
236
237impl str::FromStr for PackageKind {
238    type Err = InvalidPkgKind;
239    fn from_str(s: &str) -> Result<Self, Self::Err> {
240        let kind = match s {
241            "contract" => Self::Contract,
242            "library" => Self::Library,
243            _ => return Err(InvalidPkgKind),
244        };
245        Ok(kind)
246    }
247}
248
249impl str::FromStr for Manifest {
250    type Err = ManifestError;
251    fn from_str(s: &str) -> Result<Self, Self::Err> {
252        let toml_de = toml::de::Deserializer::new(s);
253        let mut ignored_paths = vec![];
254        let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
255            // TODO: Trace these ignored TOML paths as warnings?
256            ignored_paths.push(format!("{path}"));
257        })?;
258        check(&manifest)?;
259        Ok(manifest)
260    }
261}
262
263impl ops::Deref for ManifestFile {
264    type Target = Manifest;
265    fn deref(&self) -> &Self::Target {
266        &self.manifest
267    }
268}
269
270/// Validate the given manifest.
271pub fn check(manifest: &Manifest) -> Result<(), InvalidManifest> {
272    // Check the package name.
273    check_name(&manifest.pkg.name)
274        .map_err(|e| InvalidManifest::PkgName(manifest.pkg.name.to_string(), e))?;
275
276    // Check the dependency names.
277    let mut names = HashSet::new();
278    for name in manifest.deps.keys().chain(manifest.contract_deps.keys()) {
279        // Check name validity.
280        check_name(name).map_err(|e| InvalidManifest::DepName(manifest.pkg.name.to_string(), e))?;
281
282        // Check for duplicates.
283        if !names.insert(name) {
284            return Err(InvalidManifest::DupDepName(name.to_string()));
285        }
286    }
287
288    Ok(())
289}
290
291/// Package names must only contain ASCII non-uppercase alphanumeric chars, dashes or underscores.
292pub fn check_name_char(ch: char) -> bool {
293    (ch.is_ascii_alphanumeric() && !ch.is_uppercase()) || ch == '-' || ch == '_'
294}
295
296/// Check the validity of the given package name.
297pub fn check_name(name: &str) -> Result<(), InvalidName> {
298    if !name.chars().all(check_name_char) {
299        return Err(InvalidName::InvalidChar);
300    }
301
302    if matches!(name.chars().next(), Some(ch) if !ch.is_ascii_alphabetic()) {
303        return Err(InvalidName::NonAlphabeticStart);
304    }
305
306    if matches!(name.chars().last(), Some(ch) if !ch.is_ascii_alphanumeric()) {
307        return Err(InvalidName::NonAlphanumericEnd);
308    }
309
310    if PINT_KEYWORDS.contains(&name) {
311        return Err(InvalidName::PintKeyword);
312    }
313
314    if RESERVED.contains(&name) {
315        return Err(InvalidName::Reserved);
316    }
317
318    Ok(())
319}
320
321const PINT_KEYWORDS: &[&str] = &[
322    "as",
323    "bool",
324    "b256",
325    "cond",
326    "constraint",
327    "else",
328    "exists",
329    "forall",
330    "if",
331    "in",
332    "int",
333    "interface",
334    "macro",
335    "match",
336    "nil",
337    "predicate",
338    "mut",
339    "real",
340    "self",
341    "let",
342    "storage",
343    "string",
344    "type",
345    "union",
346    "use",
347    "where",
348];
349
350const RESERVED: &[&str] = &[
351    "contract",
352    "dep",
353    "dependency",
354    "lib",
355    "library",
356    "mod",
357    "module",
358    "root",
359];
360
361/// Different dependency types supported by the manifest.
362pub mod dependency {
363    use serde::{Deserialize, Serialize};
364
365    /// The source from which the dependency may be retrieved.
366    #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
367    #[serde(untagged)]
368    pub enum Source {
369        /// Depends on another package directly via a path to its root directory.
370        Path(Path),
371    }
372
373    /// A path dependency.
374    #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
375    pub struct Path {
376        /// The path to the dependency's root directory.
377        pub path: std::path::PathBuf,
378    }
379}
380
381/// Serialize and Deserialize implementations that serialize via `Option`.
382/// Designed for use with `#[serde(with = "serde_opt")]`.
383mod serde_opt {
384    use serde::{Deserialize, Deserializer, Serialize, Serializer};
385
386    // If `t` is equal to the default value, serializes as `None`, otherwise as `Some(t)`.
387    pub(crate) fn serialize<S, T>(t: &T, s: S) -> Result<S::Ok, S::Error>
388    where
389        S: Serializer,
390        T: Default + PartialEq + Serialize,
391    {
392        let opt = (t != &T::default()).then_some(t);
393        opt.serialize(s)
394    }
395
396    // Deserializes into `Option<T>`, then calls `unwrap_or_default`.
397    pub(crate) fn deserialize<'de, D, T>(d: D) -> Result<T, D::Error>
398    where
399        D: Deserializer<'de>,
400        T: Default + Deserialize<'de>,
401    {
402        let opt: Option<T> = <_>::deserialize(d)?;
403        Ok(opt.unwrap_or_default())
404    }
405}