wapm_toml/
lib.rs

1//! The Manifest file is where the core metadata of a wapm package lives
2
3use semver::Version;
4use serde::{de::Error as _, Deserialize, Serialize};
5use std::collections::{hash_map::HashMap, BTreeSet};
6use std::fmt;
7use std::path::{Path, PathBuf};
8use thiserror::Error;
9
10pub mod rust;
11
12/// The ABI is a hint to WebAssembly runtimes about what additional imports to insert.
13/// It currently is only used for validation (in the validation subcommand).  The default value is `None`.
14#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
15pub enum Abi {
16    #[serde(rename = "emscripten")]
17    Emscripten,
18    #[serde(rename = "none")]
19    None,
20    #[serde(rename = "wasi")]
21    Wasi,
22    #[serde(rename = "wasm4")]
23    WASM4,
24}
25
26impl Abi {
27    pub fn to_str(&self) -> &str {
28        match self {
29            Abi::Emscripten => "emscripten",
30            Abi::Wasi => "wasi",
31            Abi::WASM4 => "wasm4",
32            Abi::None => "generic",
33        }
34    }
35    pub fn is_none(&self) -> bool {
36        self == &Abi::None
37    }
38    pub fn from_name(name: &str) -> Self {
39        match name.to_lowercase().as_ref() {
40            "emscripten" => Abi::Emscripten,
41            "wasi" => Abi::Wasi,
42            _ => Abi::None,
43        }
44    }
45}
46
47impl fmt::Display for Abi {
48    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
49        write!(f, "{}", self.to_str())
50    }
51}
52
53impl Default for Abi {
54    fn default() -> Self {
55        Abi::None
56    }
57}
58
59/// The name of the manifest file. This is hard-coded for now.
60pub static MANIFEST_FILE_NAME: &str = "wapm.toml";
61pub static PACKAGES_DIR_NAME: &str = "wapm_packages";
62
63pub static README_PATHS: &[&str; 5] = &[
64    "README",
65    "README.md",
66    "README.markdown",
67    "README.mdown",
68    "README.mkdn",
69];
70
71pub static LICENSE_PATHS: &[&str; 3] = &["LICENSE", "LICENSE.md", "COPYING"];
72
73/// Describes a command for a wapm module
74#[derive(Clone, Debug, Deserialize, Serialize)]
75pub struct Package {
76    pub name: String,
77    pub version: Version,
78    pub description: String,
79    pub license: Option<String>,
80    /// The location of the license file, useful for non-standard licenses
81    #[serde(rename = "license-file")]
82    pub license_file: Option<PathBuf>,
83    pub readme: Option<PathBuf>,
84    pub repository: Option<String>,
85    pub homepage: Option<String>,
86    #[serde(rename = "wasmer-extra-flags")]
87    pub wasmer_extra_flags: Option<String>,
88    #[serde(
89        rename = "disable-command-rename",
90        default,
91        skip_serializing_if = "std::ops::Not::not"
92    )]
93    pub disable_command_rename: bool,
94    /// Unlike, `disable-command-rename` which prevents `wapm run <Module name>`,
95    /// this flag enables the command rename of `wapm run <COMMAND_NAME>` into
96    /// just `<COMMAND_NAME>`. This is useful for programs that need to inspect
97    /// their `argv[0]` names and when the command name matches their executable
98    /// name.
99    #[serde(
100        rename = "rename-commands-to-raw-command-name",
101        default,
102        skip_serializing_if = "std::ops::Not::not"
103    )]
104    pub rename_commands_to_raw_command_name: bool,
105}
106
107#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
108#[serde(untagged)]
109pub enum Command {
110    V1(CommandV1),
111    V2(CommandV2),
112}
113
114impl Command {
115    pub fn get_name(&self) -> String {
116        match self {
117            Self::V1(c) => c.name.clone(),
118            Self::V2(c) => c.name.clone(),
119        }
120    }
121
122    pub fn get_module(&self) -> String {
123        match self {
124            Self::V1(c) => c.module.clone(),
125            // TODO(felix): how to migrate to the new API?
126            Self::V2(_) => String::new(),
127        }
128    }
129
130    pub fn get_package(&self) -> Option<String> {
131        match self {
132            Self::V1(c) => c.package.clone(),
133            // TODO(felix): how to migrate to the new version / "kind" API?
134            Self::V2(_) => None,
135        }
136    }
137
138    pub fn get_main_args(&self) -> Option<String> {
139        match self {
140            Self::V1(c) => c.main_args.clone(),
141            // TODO(felix): how to migrate to the new API?
142            // Self::V2(c) => serde_json::to_string(&c.annotations)
143            Self::V2(_) => None,
144        }
145    }
146}
147
148/// Describes a command for a wapm module
149#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
150#[serde(deny_unknown_fields)] // Note: needed to prevent accidentally parsing
151                              // a CommandV2 as a CommandV1
152pub struct CommandV1 {
153    pub name: String,
154    pub module: String,
155    pub main_args: Option<String>,
156    pub package: Option<String>,
157}
158
159#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
160pub struct CommandV2 {
161    pub name: String,
162    pub module: String,
163    pub runner: String,
164    pub annotations: Option<CommandAnnotations>,
165}
166
167impl CommandV2 {
168    pub fn get_annotations(&self, basepath: &Path) -> Result<Option<serde_cbor::Value>, String> {
169        match self.annotations.as_ref() {
170            Some(CommandAnnotations::Raw(v)) => Ok(Some(toml_to_cbor_value(v))),
171            Some(CommandAnnotations::File(FileCommandAnnotations { file, kind })) => {
172                let path = basepath.join(file.clone());
173                let file = std::fs::read_to_string(&path).map_err(|e| {
174                    format!(
175                        "Error reading {:?}.annotation ({:?}): {e}",
176                        self.name,
177                        path.display()
178                    )
179                })?;
180                match kind {
181                    FileKind::Json => {
182                        let value: serde_json::Value =
183                            serde_json::from_str(&file).map_err(|e| {
184                                format!(
185                                    "Error reading {:?}.annotation ({:?}): {e}",
186                                    self.name,
187                                    path.display()
188                                )
189                            })?;
190                        Ok(Some(json_to_cbor_value(&value)))
191                    }
192                    FileKind::Yaml => {
193                        let value: serde_yaml::Value =
194                            serde_yaml::from_str(&file).map_err(|e| {
195                                format!(
196                                    "Error reading {:?}.annotation ({:?}): {e}",
197                                    self.name,
198                                    path.display()
199                                )
200                            })?;
201                        Ok(Some(yaml_to_cbor_value(&value)))
202                    }
203                }
204            }
205            None => Ok(None),
206        }
207    }
208}
209
210pub fn toml_to_cbor_value(val: &toml::Value) -> serde_cbor::Value {
211    match val {
212        toml::Value::String(s) => serde_cbor::Value::Text(s.clone()),
213        toml::Value::Integer(i) => serde_cbor::Value::Integer(*i as i128),
214        toml::Value::Float(f) => serde_cbor::Value::Float(*f),
215        toml::Value::Boolean(b) => serde_cbor::Value::Bool(*b),
216        toml::Value::Datetime(d) => serde_cbor::Value::Text(format!("{}", d)),
217        toml::Value::Array(sq) => {
218            serde_cbor::Value::Array(sq.iter().map(toml_to_cbor_value).collect())
219        }
220        toml::Value::Table(m) => serde_cbor::Value::Map(
221            m.iter()
222                .map(|(k, v)| (serde_cbor::Value::Text(k.clone()), toml_to_cbor_value(v)))
223                .collect(),
224        ),
225    }
226}
227
228pub fn json_to_cbor_value(val: &serde_json::Value) -> serde_cbor::Value {
229    match val {
230        serde_json::Value::Null => serde_cbor::Value::Null,
231        serde_json::Value::Bool(b) => serde_cbor::Value::Bool(*b),
232        serde_json::Value::Number(n) => {
233            if let Some(i) = n.as_i64() {
234                serde_cbor::Value::Integer(i as i128)
235            } else if let Some(u) = n.as_u64() {
236                serde_cbor::Value::Integer(u as i128)
237            } else if let Some(f) = n.as_f64() {
238                serde_cbor::Value::Float(f)
239            } else {
240                serde_cbor::Value::Null
241            }
242        }
243        serde_json::Value::String(s) => serde_cbor::Value::Text(s.clone()),
244        serde_json::Value::Array(sq) => {
245            serde_cbor::Value::Array(sq.iter().map(json_to_cbor_value).collect())
246        }
247        serde_json::Value::Object(m) => serde_cbor::Value::Map(
248            m.iter()
249                .map(|(k, v)| (serde_cbor::Value::Text(k.clone()), json_to_cbor_value(v)))
250                .collect(),
251        ),
252    }
253}
254
255pub fn yaml_to_cbor_value(val: &serde_yaml::Value) -> serde_cbor::Value {
256    match val {
257        serde_yaml::Value::Null => serde_cbor::Value::Null,
258        serde_yaml::Value::Bool(b) => serde_cbor::Value::Bool(*b),
259        serde_yaml::Value::Number(n) => {
260            if let Some(i) = n.as_i64() {
261                serde_cbor::Value::Integer(i as i128)
262            } else if let Some(u) = n.as_u64() {
263                serde_cbor::Value::Integer(u as i128)
264            } else if let Some(f) = n.as_f64() {
265                serde_cbor::Value::Float(f)
266            } else {
267                serde_cbor::Value::Null
268            }
269        }
270        serde_yaml::Value::String(s) => serde_cbor::Value::Text(s.clone()),
271        serde_yaml::Value::Sequence(sq) => {
272            serde_cbor::Value::Array(sq.iter().map(yaml_to_cbor_value).collect())
273        }
274        serde_yaml::Value::Mapping(m) => serde_cbor::Value::Map(
275            m.iter()
276                .map(|(k, v)| (yaml_to_cbor_value(k), yaml_to_cbor_value(v)))
277                .collect(),
278        ),
279        serde_yaml::Value::Tagged(tag) => yaml_to_cbor_value(&tag.value),
280    }
281}
282
283#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
284#[serde(untagged)]
285#[repr(C)]
286pub enum CommandAnnotations {
287    File(FileCommandAnnotations),
288    Raw(toml::Value),
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
292pub struct FileCommandAnnotations {
293    pub file: PathBuf,
294    pub kind: FileKind,
295}
296
297#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)]
298pub enum FileKind {
299    #[serde(rename = "yaml")]
300    Yaml,
301    #[serde(rename = "json")]
302    Json,
303}
304
305#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
306pub struct Module {
307    pub name: String,
308    pub source: PathBuf,
309    #[serde(default = "Abi::default", skip_serializing_if = "Abi::is_none")]
310    pub abi: Abi,
311    #[serde(default)]
312    pub kind: Option<String>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub interfaces: Option<HashMap<String, String>>,
315    pub bindings: Option<Bindings>,
316}
317
318/// The interface exposed by a [`Module`].
319#[derive(Clone, Debug, PartialEq, Eq)]
320pub enum Bindings {
321    Wit(WitBindings),
322    Wai(WaiBindings),
323}
324
325impl Bindings {
326    /// Get all files that make up this interface.
327    ///
328    /// For all binding types except [`WitBindings`], this will recursively
329    /// look for any files that are imported.
330    ///
331    /// The caller can assume that any path that was referenced exists.
332    pub fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
333        match self {
334            Bindings::Wit(WitBindings { wit_exports, .. }) => {
335                // Note: we explicitly don't support imported files with WIT
336                // because wit-bindgen's wit-parser crate isn't on crates.io.
337
338                let path = base_directory.join(wit_exports);
339
340                if path.exists() {
341                    Ok(vec![path])
342                } else {
343                    Err(ImportsError::FileNotFound(path))
344                }
345            }
346            Bindings::Wai(wai) => wai.referenced_files(base_directory),
347        }
348    }
349}
350
351impl Serialize for Bindings {
352    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
353    where
354        S: serde::Serializer,
355    {
356        match self {
357            Bindings::Wit(w) => w.serialize(serializer),
358            Bindings::Wai(w) => w.serialize(serializer),
359        }
360    }
361}
362
363impl<'de> Deserialize<'de> for Bindings {
364    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
365    where
366        D: serde::Deserializer<'de>,
367    {
368        let value = toml::Value::deserialize(deserializer)?;
369
370        let keys = ["wit-bindgen", "wai-version"];
371        let [wit_bindgen, wai_version] = keys.map(|key| value.get(key).is_some());
372
373        match (wit_bindgen, wai_version) {
374            (true, false) => WitBindings::deserialize(value)
375                .map(Bindings::Wit)
376                .map_err(D::Error::custom),
377            (false, true) => WaiBindings::deserialize(value)
378                .map(Bindings::Wai)
379                .map_err(D::Error::custom),
380            (true, true) | (false, false) => {
381                let msg = format!(
382                    "expected one of \"{}\" to be provided, but not both",
383                    keys.join("\" or \""),
384                );
385                Err(D::Error::custom(msg))
386            }
387        }
388    }
389}
390
391#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
392#[serde(rename_all = "kebab-case")]
393pub struct WitBindings {
394    /// The version of the WIT format being used.
395    pub wit_bindgen: Version,
396    /// The `*.wit` file's location on disk.
397    pub wit_exports: PathBuf,
398}
399
400#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
401#[serde(rename_all = "kebab-case")]
402pub struct WaiBindings {
403    /// The version of the WAI format being used.
404    pub wai_version: Version,
405    /// The `*.wai` file defining the interface this package exposes.
406    pub exports: Option<PathBuf>,
407    /// The `*.wai` files for any functionality this package imports from the
408    /// host.
409    #[serde(default, skip_serializing_if = "Vec::is_empty")]
410    pub imports: Vec<PathBuf>,
411}
412
413impl WaiBindings {
414    fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
415        let WaiBindings {
416            exports, imports, ..
417        } = self;
418
419        // Note: WAI files may import other WAI files, so we start with all
420        // WAI files mentioned in the wapm.toml then recursively add their
421        // imports.
422
423        let initial_paths = exports
424            .iter()
425            .chain(imports)
426            .map(|relative_path| base_directory.join(relative_path));
427
428        let mut to_check: Vec<PathBuf> = Vec::new();
429
430        for path in initial_paths {
431            if !path.exists() {
432                return Err(ImportsError::FileNotFound(path));
433            }
434            to_check.push(path);
435        }
436
437        let mut files = BTreeSet::new();
438
439        while let Some(path) = to_check.pop() {
440            if files.contains(&path) {
441                continue;
442            }
443
444            to_check.extend(get_imported_wai_files(&path)?);
445            files.insert(path);
446        }
447
448        Ok(files.into_iter().collect())
449    }
450}
451
452/// Parse a `*.wai` file to find the absolute path for any other `*.wai` files
453/// it may import, relative to the original `*.wai` file.
454///
455/// This function makes sure any imported files exist.
456fn get_imported_wai_files(path: &Path) -> Result<Vec<PathBuf>, ImportsError> {
457    let _wai_src = std::fs::read_to_string(path).map_err(|error| ImportsError::Read {
458        path: path.to_path_buf(),
459        error,
460    })?;
461
462    let parent_dir = path.parent()
463            .expect("All paths should have a parent directory because we joined them relative to the base directory");
464
465    // TODO(Michael-F-Bryan): update the wai-parser crate to give you access to
466    // the imported interfaces. For now, we just pretend there are no import
467    // statements in the *.wai file.
468    let raw_imports: Vec<String> = Vec::new();
469
470    // Note: imported paths in a *.wai file are all relative, so we need to
471    // resolve their absolute path relative to the original *.wai file.
472    let mut resolved_paths = Vec::new();
473
474    for imported in raw_imports {
475        let absolute_path = parent_dir.join(imported);
476
477        if !absolute_path.exists() {
478            return Err(ImportsError::ImportedFileNotFound {
479                path: absolute_path,
480                referenced_by: path.to_path_buf(),
481            });
482        }
483
484        resolved_paths.push(absolute_path);
485    }
486
487    Ok(resolved_paths)
488}
489
490#[derive(Debug, thiserror::Error)]
491pub enum ImportsError {
492    #[error(
493        "The \"{}\" mentioned in the manifest doesn't exist",
494        _0.display(),
495    )]
496    FileNotFound(PathBuf),
497    #[error(
498        "The \"{}\" imported by \"{}\" doesn't exist",
499        path.display(),
500        referenced_by.display(),
501    )]
502    ImportedFileNotFound {
503        path: PathBuf,
504        referenced_by: PathBuf,
505    },
506    #[error("Unable to parse \"{}\" as a WAI file", path.display())]
507    WaiParse { path: PathBuf },
508    #[error("Unable to read \"{}\"", path.display())]
509    Read {
510        path: PathBuf,
511        #[source]
512        error: std::io::Error,
513    },
514}
515
516/// The manifest represents the file used to describe a Wasm package.
517///
518/// The `module` field represents the wasm file to be published.
519///
520/// The `source` is used to create bundles with the `fs` section.
521///
522/// The `fs` section represents fs assets that will be made available to the
523/// program relative to its starting current directory (there may be issues with WASI).
524/// These are pairs of paths.
525#[derive(Clone, Debug, Deserialize, Serialize)]
526pub struct Manifest {
527    pub package: Package,
528    pub dependencies: Option<HashMap<String, String>>,
529    pub module: Option<Vec<Module>>,
530    pub command: Option<Vec<Command>>,
531    /// Of the form Guest -> Host path
532    pub fs: Option<HashMap<String, PathBuf>>,
533    /// private data
534    /// store the directory path of the manifest file for use later accessing relative path fields
535    #[serde(skip)]
536    pub base_directory_path: PathBuf,
537}
538
539impl Manifest {
540    pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
541        toml::from_str(s)
542    }
543
544    fn locate_file(path: &Path, candidates: &[&str]) -> Option<PathBuf> {
545        for filename in candidates {
546            let path_buf = path.join(filename);
547            if path_buf.exists() {
548                return Some(filename.into());
549            }
550        }
551        None
552    }
553
554    /// Construct a manifest by searching in the specified directory for a manifest file
555    pub fn find_in_directory<T: AsRef<Path>>(path: T) -> Result<Self, ManifestError> {
556        if !path.as_ref().is_dir() {
557            return Err(ManifestError::MissingManifest(
558                path.as_ref().to_string_lossy().to_string(),
559            ));
560        }
561        let manifest_path_buf = path.as_ref().join(MANIFEST_FILE_NAME);
562        let contents = std::fs::read_to_string(&manifest_path_buf).map_err(|_e| {
563            ManifestError::MissingManifest(manifest_path_buf.to_string_lossy().to_string())
564        })?;
565        let mut manifest: Self = toml::from_str(contents.as_str())
566            .map_err(|e| ManifestError::TomlParseError(e.to_string()))?;
567        if manifest.package.readme.is_none() {
568            manifest.package.readme = Self::locate_file(path.as_ref(), &README_PATHS[..]);
569        }
570        if manifest.package.license_file.is_none() {
571            manifest.package.license_file = Self::locate_file(path.as_ref(), &LICENSE_PATHS[..]);
572        }
573        manifest.validate()?;
574        Ok(manifest)
575    }
576
577    pub fn validate(&self) -> Result<(), ManifestError> {
578        let module_map = self
579            .module
580            .as_ref()
581            .map(|modules| {
582                modules
583                    .iter()
584                    .map(|module| (module.name.clone(), module.clone()))
585                    .collect::<HashMap<String, Module>>()
586            })
587            .unwrap_or_default();
588
589        if let Some(ref commands) = self.command {
590            for command in commands {
591                if let Some(module) = module_map.get(&command.get_module()) {
592                    if module.abi == Abi::None && module.interfaces.is_none() {
593                        return Err(ManifestError::ValidationError(ValidationError::MissingABI(
594                            command.get_name(),
595                            module.name.clone(),
596                        )));
597                    }
598                } else {
599                    return Err(ManifestError::ValidationError(
600                        ValidationError::MissingModuleForCommand(
601                            command.get_name(),
602                            command.get_module(),
603                        ),
604                    ));
605                }
606            }
607        }
608        Ok(())
609    }
610
611    /// add a dependency
612    pub fn add_dependency(&mut self, dependency_name: String, dependency_version: String) {
613        let dependencies = self.dependencies.get_or_insert(Default::default());
614        dependencies.insert(dependency_name, dependency_version);
615    }
616
617    /// remove dependency by package name
618    pub fn remove_dependency(&mut self, dependency_name: &str) -> Option<String> {
619        let dependencies = self.dependencies.get_or_insert(Default::default());
620        dependencies.remove(dependency_name)
621    }
622
623    pub fn to_string(&self) -> anyhow::Result<String> {
624        Ok(toml::to_string(self)?)
625    }
626
627    pub fn manifest_path(&self) -> PathBuf {
628        self.base_directory_path.join(MANIFEST_FILE_NAME)
629    }
630
631    /// Write the manifest to permanent storage
632    pub fn save(&self) -> anyhow::Result<()> {
633        let manifest_string = self.to_string()?;
634        let manifest_path = self.manifest_path();
635        std::fs::write(manifest_path, manifest_string)
636            .map_err(|e| ManifestError::CannotSaveManifest(e.to_string()))?;
637        Ok(())
638    }
639}
640
641#[derive(Debug, Error)]
642pub enum ManifestError {
643    #[error("Manifest file not found at {0}")]
644    MissingManifest(String),
645    #[error("Could not save manifest file: {0}.")]
646    CannotSaveManifest(String),
647    #[error("Could not parse manifest because {0}.")]
648    TomlParseError(String),
649    #[error("Dependency version must be a string. Package name: {0}.")]
650    DependencyVersionMustBeString(String),
651    #[error("Package must have version that follows semantic versioning. {0}")]
652    SemVerError(String),
653    #[error("There was an error validating the manifest: {0}")]
654    ValidationError(ValidationError),
655}
656
657#[derive(Debug, Error)]
658pub enum ValidationError {
659    #[error(
660        "missing ABI field on module {0} used by command {1}; an ABI of `wasi` or `emscripten` is required",
661    )]
662    MissingABI(String, String),
663    #[error("missing module {0} in manifest used by command {1}")]
664    MissingModuleForCommand(String, String),
665}
666
667#[cfg(test)]
668mod serialization_tests {
669    use super::*;
670    use toml::toml;
671
672    #[test]
673    fn get_manifest() {
674        let wapm_toml = toml! {
675            [package]
676            name = "test"
677            version = "1.0.0"
678            repository = "test.git"
679            homepage = "test.com"
680            description = "The best package."
681        };
682        let manifest: Manifest = wapm_toml.try_into().unwrap();
683        assert!(!manifest.package.disable_command_rename);
684    }
685}
686
687#[cfg(test)]
688mod command_tests {
689    use super::*;
690    use toml::toml;
691
692    #[test]
693    fn get_commands() {
694        let wapm_toml = toml! {
695            [package]
696            name = "test"
697            version = "1.0.0"
698            repository = "test.git"
699            homepage = "test.com"
700            description = "The best package."
701            [[module]]
702            name = "test-pkg"
703            module = "target.wasm"
704            source = "source.wasm"
705            description = "description"
706            interfaces = {"wasi" = "0.0.0-unstable"}
707            [[command]]
708            name = "foo"
709            module = "test"
710            [[command]]
711            name = "baz"
712            module = "test"
713            main_args = "$@"
714        };
715        let manifest: Manifest = wapm_toml.try_into().unwrap();
716        let commands = &manifest.command.unwrap();
717        assert_eq!(2, commands.len());
718    }
719}
720
721#[cfg(test)]
722mod dependency_tests {
723    use super::*;
724    use std::{fs::File, io::Write};
725    use toml::toml;
726
727    #[test]
728    fn add_new_dependency() {
729        let tmp_dir = tempfile::tempdir().unwrap();
730        let tmp_dir_path: &std::path::Path = tmp_dir.as_ref();
731        let manifest_path = tmp_dir_path.join(MANIFEST_FILE_NAME);
732        let mut file = File::create(manifest_path).unwrap();
733        let wapm_toml = toml! {
734            [package]
735            name = "_/test"
736            version = "1.0.0"
737            description = "description"
738            [[module]]
739            name = "test"
740            source = "test.wasm"
741            interfaces = {}
742        };
743        let toml_string = toml::to_string(&wapm_toml).unwrap();
744        file.write_all(toml_string.as_bytes()).unwrap();
745        let mut manifest = Manifest::find_in_directory(tmp_dir).unwrap();
746
747        let dependency_name = "dep_pkg";
748        let dependency_version = semver::Version::new(0, 1, 0);
749
750        manifest.add_dependency(dependency_name.to_string(), dependency_version.to_string());
751        assert_eq!(1, manifest.dependencies.as_ref().unwrap().len());
752
753        // adding the same dependency twice changes nothing
754        manifest.add_dependency(dependency_name.to_string(), dependency_version.to_string());
755        assert_eq!(1, manifest.dependencies.as_ref().unwrap().len());
756
757        // adding a second different dependency will increase the count
758        let dependency_name_2 = "dep_pkg_2";
759        let dependency_version_2 = semver::Version::new(0, 2, 0);
760        manifest.add_dependency(
761            dependency_name_2.to_string(),
762            dependency_version_2.to_string(),
763        );
764        assert_eq!(2, manifest.dependencies.as_ref().unwrap().len());
765    }
766}
767
768#[cfg(test)]
769mod manifest_tests {
770    use std::fmt::Debug;
771
772    use serde::{de::DeserializeOwned, Deserialize};
773
774    use super::*;
775
776    #[test]
777    fn interface_test() {
778        let manifest_str = r#"
779[package]
780name = "test"
781version = "0.0.0"
782description = "This is a test package"
783license = "MIT"
784
785[[module]]
786name = "mod"
787source = "target/wasm32-wasi/release/mod.wasm"
788interfaces = {"wasi" = "0.0.0-unstable"}
789
790[[module]]
791name = "mod-with-exports"
792source = "target/wasm32-wasi/release/mod-with-exports.wasm"
793bindings = { wit-exports = "exports.wit", wit-bindgen = "0.0.0" }
794
795[[command]]
796name = "command"
797module = "mod"
798"#;
799        let manifest: Manifest = Manifest::parse(manifest_str).unwrap();
800        let modules = manifest.module.as_deref().unwrap();
801        assert_eq!(
802            modules[0].interfaces.as_ref().unwrap().get("wasi"),
803            Some(&"0.0.0-unstable".to_string())
804        );
805
806        assert_eq!(
807            modules[1],
808            Module {
809                name: "mod-with-exports".to_string(),
810                source: PathBuf::from("target/wasm32-wasi/release/mod-with-exports.wasm"),
811                abi: Abi::None,
812                kind: None,
813                interfaces: None,
814                bindings: Some(Bindings::Wit(WitBindings {
815                    wit_exports: PathBuf::from("exports.wit"),
816                    wit_bindgen: "0.0.0".parse().unwrap()
817                })),
818            },
819        );
820    }
821
822    #[test]
823    fn parse_wit_bindings() {
824        let table = toml::toml! {
825            name = "..."
826            source = "..."
827            bindings = { wit-bindgen = "0.1.0", wit-exports = "./file.wit" }
828        };
829
830        let module = Module::deserialize(table).unwrap();
831
832        assert_eq!(
833            module.bindings.as_ref().unwrap(),
834            &Bindings::Wit(WitBindings {
835                wit_bindgen: "0.1.0".parse().unwrap(),
836                wit_exports: PathBuf::from("./file.wit"),
837            }),
838        );
839        assert_round_trippable(&module);
840    }
841
842    #[test]
843    fn parse_wai_bindings() {
844        let table = toml::toml! {
845            name = "..."
846            source = "..."
847            bindings = { wai-version = "0.1.0", exports = "./file.wai", imports = ["a.wai", "../b.wai"] }
848        };
849
850        let module = Module::deserialize(table).unwrap();
851
852        assert_eq!(
853            module.bindings.as_ref().unwrap(),
854            &Bindings::Wai(WaiBindings {
855                wai_version: "0.1.0".parse().unwrap(),
856                exports: Some(PathBuf::from("./file.wai")),
857                imports: vec![PathBuf::from("a.wai"), PathBuf::from("../b.wai")],
858            }),
859        );
860        assert_round_trippable(&module);
861    }
862
863    #[track_caller]
864    fn assert_round_trippable<T>(value: &T)
865    where
866        T: Serialize + DeserializeOwned + PartialEq + Debug,
867    {
868        let repr = toml::to_string(value).unwrap();
869        let round_tripped: T = toml::from_str(&repr).unwrap();
870        assert_eq!(
871            round_tripped, *value,
872            "The value should convert to/from TOML losslessly"
873        );
874    }
875
876    #[test]
877    fn imports_and_exports_are_optional_with_wai() {
878        let table = toml::toml! {
879            name = "..."
880            source = "..."
881            bindings = { wai-version = "0.1.0" }
882        };
883
884        let module = Module::deserialize(table).unwrap();
885
886        assert_eq!(
887            module.bindings.as_ref().unwrap(),
888            &Bindings::Wai(WaiBindings {
889                wai_version: "0.1.0".parse().unwrap(),
890                exports: None,
891                imports: Vec::new(),
892            }),
893        );
894        assert_round_trippable(&module);
895    }
896
897    #[test]
898    fn ambiguous_bindings_table() {
899        let table = toml::toml! {
900            wai-version = "0.2.0"
901            wit-bindgen = "0.1.0"
902        };
903
904        let err = Bindings::deserialize(table).unwrap_err();
905
906        assert_eq!(
907            err.to_string(),
908            "expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both"
909        );
910    }
911
912    #[test]
913    fn bindings_table_that_is_neither_wit_nor_wai() {
914        let table = toml::toml! {
915            wai-bindgen = "lol, this should have been wai-version"
916            exports = "./file.wai"
917        };
918
919        let err = Bindings::deserialize(table).unwrap_err();
920
921        assert_eq!(
922            err.to_string(),
923            "expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both"
924        );
925    }
926
927    #[test]
928    fn command_v2_isnt_ambiguous_with_command_v1() {
929        let src = r#"
930[package]
931name = "hotg-ai/sine"
932version = "0.12.0"
933description = "sine"
934
935[dependencies]
936"hotg-ai/train_test_split" = "0.12.1"
937"hotg-ai/elastic_net" = "0.12.1"
938
939[[module]] # This is the same as atoms
940name = "sine"
941kind = "tensorflow-SavedModel" # It can also be "wasm" (default)
942source = "models/sine"
943
944[[command]]
945name = "run"
946runner = "rune"
947module = "sine"
948annotations = { file = "Runefile.yml", kind = "yaml" }
949"#;
950
951        let manifest: Manifest = toml::from_str(src).unwrap();
952
953        let commands = &manifest.command.as_deref().unwrap();
954        assert_eq!(commands.len(), 1);
955        assert_eq!(
956            commands[0],
957            Command::V2(CommandV2 {
958                name: "run".into(),
959                module: "sine".into(),
960                runner: "rune".into(),
961                annotations: Some(CommandAnnotations::File(FileCommandAnnotations {
962                    file: "Runefile.yml".into(),
963                    kind: FileKind::Yaml,
964                }))
965            })
966        );
967    }
968}