1use 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#[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
59pub 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#[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 #[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 #[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 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 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 Self::V2(_) => None,
144 }
145 }
146}
147
148#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
150#[serde(deny_unknown_fields)] pub 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#[derive(Clone, Debug, PartialEq, Eq)]
320pub enum Bindings {
321 Wit(WitBindings),
322 Wai(WaiBindings),
323}
324
325impl Bindings {
326 pub fn referenced_files(&self, base_directory: &Path) -> Result<Vec<PathBuf>, ImportsError> {
333 match self {
334 Bindings::Wit(WitBindings { wit_exports, .. }) => {
335 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 pub wit_bindgen: Version,
396 pub wit_exports: PathBuf,
398}
399
400#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
401#[serde(rename_all = "kebab-case")]
402pub struct WaiBindings {
403 pub wai_version: Version,
405 pub exports: Option<PathBuf>,
407 #[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 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
452fn 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 let raw_imports: Vec<String> = Vec::new();
469
470 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#[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 pub fs: Option<HashMap<String, PathBuf>>,
533 #[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 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 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 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 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 manifest.add_dependency(dependency_name.to_string(), dependency_version.to_string());
755 assert_eq!(1, manifest.dependencies.as_ref().unwrap().len());
756
757 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}