1pub mod build_profile;
2
3use crate::pkg::{manifest_file_missing, parsing_failed, wrong_program_type};
4use anyhow::{anyhow, bail, Context, Result};
5use forc_tracing::println_warning;
6use forc_util::{validate_name, validate_project_name};
7use semver::Version;
8use serde::{de, Deserialize, Serialize};
9use serde_with::{serde_as, DisplayFromStr};
10use std::{
11 collections::{BTreeMap, HashMap},
12 fmt::Display,
13 path::{Path, PathBuf},
14 str::FromStr,
15 sync::Arc,
16};
17use sway_core::{fuel_prelude::fuel_tx, language::parsed::TreeType, parse_tree_type, BuildTarget};
18use sway_error::handler::Handler;
19use sway_utils::{
20 constants, find_nested_manifest_dir, find_parent_manifest_dir,
21 find_parent_manifest_dir_with_check,
22};
23use url::Url;
24
25use self::build_profile::BuildProfile;
26
27pub type MemberName = String;
29pub type MemberManifestFiles = BTreeMap<MemberName, PackageManifestFile>;
31
32pub trait GenericManifestFile {
33 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self>
34 where
35 Self: Sized;
36 fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self>
37 where
38 Self: Sized;
39
40 fn path(&self) -> &Path;
44
45 fn dir(&self) -> &Path {
49 self.path()
50 .parent()
51 .expect("failed to retrieve manifest directory")
52 }
53
54 fn lock_path(&self) -> Result<PathBuf>;
56
57 fn member_manifests(&self) -> Result<MemberManifestFiles>;
59}
60
61#[derive(Clone, Debug)]
62pub enum ManifestFile {
63 Package(Box<PackageManifestFile>),
64 Workspace(WorkspaceManifestFile),
65}
66
67impl GenericManifestFile for ManifestFile {
68 fn from_dir<P: AsRef<Path>>(path: P) -> Result<Self> {
71 let maybe_pkg_manifest = PackageManifestFile::from_dir(path.as_ref());
72 let manifest_file = if let Err(e) = maybe_pkg_manifest {
73 if e.to_string().contains("missing field `project`") {
74 let workspace_manifest_file = WorkspaceManifestFile::from_dir(path.as_ref())?;
76 ManifestFile::Workspace(workspace_manifest_file)
77 } else {
78 bail!("{}", e)
79 }
80 } else if let Ok(pkg_manifest) = maybe_pkg_manifest {
81 ManifestFile::Package(Box::new(pkg_manifest))
82 } else {
83 bail!(
84 "Cannot find a valid `Forc.toml` at {}",
85 path.as_ref().to_string_lossy()
86 )
87 };
88 Ok(manifest_file)
89 }
90
91 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
94 let maybe_pkg_manifest = PackageManifestFile::from_file(path.as_ref());
95 let manifest_file = if let Err(e) = maybe_pkg_manifest {
96 if e.to_string().contains("missing field `project`") {
97 let workspace_manifest_file = WorkspaceManifestFile::from_file(path.as_ref())?;
99 ManifestFile::Workspace(workspace_manifest_file)
100 } else {
101 bail!("{}", e)
102 }
103 } else if let Ok(pkg_manifest) = maybe_pkg_manifest {
104 ManifestFile::Package(Box::new(pkg_manifest))
105 } else {
106 bail!(
107 "Cannot find a valid `Forc.toml` at {}",
108 path.as_ref().to_string_lossy()
109 )
110 };
111 Ok(manifest_file)
112 }
113
114 fn path(&self) -> &Path {
118 match self {
119 ManifestFile::Package(pkg_manifest_file) => pkg_manifest_file.path(),
120 ManifestFile::Workspace(workspace_manifest_file) => workspace_manifest_file.path(),
121 }
122 }
123
124 fn member_manifests(&self) -> Result<MemberManifestFiles> {
125 match self {
126 ManifestFile::Package(pkg_manifest_file) => pkg_manifest_file.member_manifests(),
127 ManifestFile::Workspace(workspace_manifest_file) => {
128 workspace_manifest_file.member_manifests()
129 }
130 }
131 }
132
133 fn lock_path(&self) -> Result<PathBuf> {
135 match self {
136 ManifestFile::Package(pkg_manifest) => pkg_manifest.lock_path(),
137 ManifestFile::Workspace(workspace_manifest) => workspace_manifest.lock_path(),
138 }
139 }
140}
141
142impl TryInto<PackageManifestFile> for ManifestFile {
143 type Error = anyhow::Error;
144
145 fn try_into(self) -> Result<PackageManifestFile> {
146 match self {
147 ManifestFile::Package(pkg_manifest_file) => Ok(*pkg_manifest_file),
148 ManifestFile::Workspace(_) => {
149 bail!("Cannot convert workspace manifest to package manifest")
150 }
151 }
152 }
153}
154
155impl TryInto<WorkspaceManifestFile> for ManifestFile {
156 type Error = anyhow::Error;
157
158 fn try_into(self) -> Result<WorkspaceManifestFile> {
159 match self {
160 ManifestFile::Package(_) => {
161 bail!("Cannot convert package manifest to workspace manifest")
162 }
163 ManifestFile::Workspace(workspace_manifest_file) => Ok(workspace_manifest_file),
164 }
165 }
166}
167
168type PatchMap = BTreeMap<String, Dependency>;
169
170#[derive(Clone, Debug, PartialEq)]
172pub struct PackageManifestFile {
173 manifest: PackageManifest,
175 path: PathBuf,
177}
178
179#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
181#[serde(rename_all = "kebab-case")]
182pub struct PackageManifest {
183 pub project: Project,
184 pub network: Option<Network>,
185 pub dependencies: Option<BTreeMap<String, Dependency>>,
186 pub patch: Option<BTreeMap<String, PatchMap>>,
187 pub build_target: Option<BTreeMap<String, BuildTarget>>,
189 build_profile: Option<BTreeMap<String, BuildProfile>>,
190 pub contract_dependencies: Option<BTreeMap<String, ContractDependency>>,
191 pub proxy: Option<Proxy>,
192}
193
194#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
195#[serde(rename_all = "kebab-case")]
196pub struct Project {
197 pub authors: Option<Vec<String>>,
198 #[serde(deserialize_with = "validate_package_name")]
199 pub name: String,
200 pub version: Option<Version>,
201 pub description: Option<String>,
202 pub organization: Option<String>,
203 pub license: String,
204 pub homepage: Option<Url>,
205 pub repository: Option<Url>,
206 pub documentation: Option<Url>,
207 pub categories: Option<Vec<String>>,
208 pub keywords: Option<Vec<String>>,
209 #[serde(default = "default_entry")]
210 pub entry: String,
211 pub implicit_std: Option<bool>,
212 pub forc_version: Option<semver::Version>,
213 #[serde(default)]
214 pub experimental: HashMap<String, bool>,
215 pub metadata: Option<toml::Value>,
216}
217
218fn validate_package_name<'de, D>(deserializer: D) -> Result<String, D::Error>
220where
221 D: de::Deserializer<'de>,
222{
223 let name: String = Deserialize::deserialize(deserializer)?;
224 match validate_project_name(&name) {
225 Ok(_) => Ok(name),
226 Err(e) => Err(de::Error::custom(e.to_string())),
227 }
228}
229
230#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
231#[serde(rename_all = "kebab-case")]
232pub struct Network {
233 #[serde(default = "default_url")]
234 pub url: String,
235}
236
237#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
238pub struct HexSalt(pub fuel_tx::Salt);
239
240impl FromStr for HexSalt {
241 type Err = anyhow::Error;
242
243 fn from_str(s: &str) -> Result<Self, Self::Err> {
244 let normalized = s
246 .strip_prefix("0x")
247 .ok_or_else(|| anyhow::anyhow!("hex salt declaration needs to start with 0x"))?;
248 let salt: fuel_tx::Salt =
249 fuel_tx::Salt::from_str(normalized).map_err(|e| anyhow::anyhow!("{e}"))?;
250 let hex_salt = Self(salt);
251 Ok(hex_salt)
252 }
253}
254
255impl Display for HexSalt {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 let salt = self.0;
258 write!(f, "{}", salt)
259 }
260}
261
262fn default_hex_salt() -> HexSalt {
263 HexSalt(fuel_tx::Salt::default())
264}
265
266#[serde_as]
267#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
268#[serde(rename_all = "kebab-case")]
269pub struct ContractDependency {
270 #[serde(flatten)]
271 pub dependency: Dependency,
272 #[serde_as(as = "DisplayFromStr")]
273 #[serde(default = "default_hex_salt")]
274 pub salt: HexSalt,
275}
276
277#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
278#[serde(untagged)]
279pub enum Dependency {
280 Simple(String),
283 Detailed(DependencyDetails),
287}
288
289#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
290#[serde(rename_all = "kebab-case")]
291pub struct DependencyDetails {
292 pub(crate) version: Option<String>,
293 pub path: Option<String>,
294 pub(crate) git: Option<String>,
295 pub(crate) branch: Option<String>,
296 pub(crate) tag: Option<String>,
297 pub(crate) package: Option<String>,
298 pub(crate) rev: Option<String>,
299 pub(crate) ipfs: Option<String>,
300}
301
302#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
304#[serde(rename_all = "kebab-case")]
305pub struct Proxy {
306 pub enabled: bool,
307 pub address: Option<String>,
311}
312
313impl DependencyDetails {
314 pub fn validate(&self) -> anyhow::Result<()> {
320 let DependencyDetails {
321 git,
322 branch,
323 tag,
324 rev,
325 ..
326 } = self;
327
328 if git.is_none() && (branch.is_some() || tag.is_some() || rev.is_some()) {
329 bail!("Details reserved for git sources used without a git field");
330 }
331
332 Ok(())
333 }
334}
335
336impl Dependency {
337 pub fn package(&self) -> Option<&str> {
339 match *self {
340 Self::Simple(_) => None,
341 Self::Detailed(ref det) => det.package.as_deref(),
342 }
343 }
344
345 pub fn version(&self) -> Option<&str> {
347 match *self {
348 Self::Simple(ref version) => Some(version),
349 Self::Detailed(ref det) => det.version.as_deref(),
350 }
351 }
352}
353
354impl PackageManifestFile {
355 fn resolve_patches(&self) -> Result<impl Iterator<Item = (String, PatchMap)>> {
362 if let Some(workspace) = self.workspace().ok().flatten() {
363 if self.patch.is_some() {
365 println_warning("Patch for the non root package will be ignored.");
366 println_warning(&format!(
367 "Specify patch at the workspace root: {}",
368 workspace.path().to_str().unwrap_or_default()
369 ));
370 }
371 Ok(workspace
372 .patch
373 .as_ref()
374 .cloned()
375 .unwrap_or_default()
376 .into_iter())
377 } else {
378 Ok(self.patch.as_ref().cloned().unwrap_or_default().into_iter())
379 }
380 }
381
382 pub fn resolve_patch(&self, patch_name: &str) -> Result<Option<PatchMap>> {
388 Ok(self
389 .resolve_patches()?
390 .find(|(p_name, _)| patch_name == p_name.as_str())
391 .map(|(_, patch)| patch))
392 }
393
394 pub fn entry_path(&self) -> PathBuf {
399 self.dir()
400 .join(constants::SRC_DIR)
401 .join(&self.project.entry)
402 }
403
404 pub fn entry_string(&self) -> Result<Arc<str>> {
406 let entry_path = self.entry_path();
407 let entry_string = std::fs::read_to_string(entry_path)?;
408 Ok(Arc::from(entry_string))
409 }
410
411 pub fn program_type(&self) -> Result<TreeType> {
413 let entry_string = self.entry_string()?;
414 let handler = Handler::default();
415 let parse_res = parse_tree_type(&handler, entry_string);
416
417 parse_res.map_err(|_| {
418 let (errors, _warnings) = handler.consume();
419 parsing_failed(&self.project.name, &errors)
420 })
421 }
422
423 pub fn check_program_type(&self, expected_types: &[TreeType]) -> Result<()> {
426 let parsed_type = self.program_type()?;
427 if !expected_types.contains(&parsed_type) {
428 bail!(wrong_program_type(
429 &self.project.name,
430 expected_types,
431 parsed_type
432 ));
433 } else {
434 Ok(())
435 }
436 }
437
438 pub fn build_profile(&self, profile_name: &str) -> Option<&BuildProfile> {
440 self.build_profile
441 .as_ref()
442 .and_then(|profiles| profiles.get(profile_name))
443 }
444
445 pub fn dep_path(&self, dep_name: &str) -> Option<PathBuf> {
447 let dir = self.dir();
448 let details = self.dep_detailed(dep_name)?;
449 details.path.as_ref().and_then(|path_str| {
450 let path = Path::new(path_str);
451 match path.is_absolute() {
452 true => Some(path.to_owned()),
453 false => dir.join(path).canonicalize().ok(),
454 }
455 })
456 }
457
458 pub fn workspace(&self) -> Result<Option<WorkspaceManifestFile>> {
460 let parent_dir = match self.dir().parent() {
461 None => return Ok(None),
462 Some(dir) => dir,
463 };
464 let ws_manifest = match WorkspaceManifestFile::from_dir(parent_dir) {
465 Ok(manifest) => manifest,
466 Err(e) => {
467 if e.to_string().contains("could not find") {
471 return Ok(None);
472 } else {
473 return Err(e);
474 }
475 }
476 };
477 if ws_manifest.is_member_path(self.dir())? {
478 Ok(Some(ws_manifest))
479 } else {
480 Ok(None)
481 }
482 }
483
484 pub fn project_name(&self) -> &str {
486 &self.project.name
487 }
488
489 pub fn validate(&self) -> Result<()> {
495 self.manifest.validate()?;
496 let mut entry_path = self.path.clone();
497 entry_path.pop();
498 let entry_path = entry_path
499 .join(constants::SRC_DIR)
500 .join(&self.project.entry);
501 if !entry_path.exists() {
502 bail!(
503 "failed to validate path from entry field {:?} in Forc manifest file.",
504 self.project.entry
505 )
506 }
507
508 let mut pkg_dir = self.path.to_path_buf();
514 pkg_dir.pop();
515 if let Some(nested_package) = find_nested_manifest_dir(&pkg_dir) {
516 bail!("Nested packages are not supported, please consider separating the nested package at {} from the package at {}, or if it makes sense consider creating a workspace.", nested_package.display(), pkg_dir.display())
518 }
519 Ok(())
520 }
521}
522
523impl GenericManifestFile for PackageManifestFile {
524 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
533 let path = path.as_ref().canonicalize()?;
534 let manifest = PackageManifest::from_file(&path)?;
535 let manifest_file = Self { manifest, path };
536 manifest_file.validate()?;
537 Ok(manifest_file)
538 }
539
540 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
546 let manifest_dir = manifest_dir.as_ref();
547 let dir = find_parent_manifest_dir(manifest_dir)
548 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
549 let path = dir.join(constants::MANIFEST_FILE_NAME);
550 Self::from_file(path)
551 }
552
553 fn path(&self) -> &Path {
554 &self.path
555 }
556
557 fn lock_path(&self) -> Result<PathBuf> {
563 let workspace_manifest = self.workspace()?;
565 if let Some(workspace_manifest) = workspace_manifest {
566 workspace_manifest.lock_path()
567 } else {
568 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
569 }
570 }
571
572 fn member_manifests(&self) -> Result<MemberManifestFiles> {
573 let mut member_manifest_files = BTreeMap::new();
574 if let Some(workspace_manifest_file) = self.workspace()? {
576 for member_manifest in workspace_manifest_file.member_pkg_manifests()? {
577 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
578 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
579 }
580 } else {
581 let member_name = &self.project.name;
582 member_manifest_files.insert(member_name.clone(), self.clone());
583 }
584
585 Ok(member_manifest_files)
586 }
587}
588
589impl PackageManifest {
590 pub const DEFAULT_ENTRY_FILE_NAME: &'static str = "main.sw";
591
592 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
601 let path = path.as_ref();
605 let contents = std::fs::read_to_string(path)
606 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
607 Self::from_string(contents)
608 }
609
610 pub fn from_string(contents: String) -> Result<Self> {
619 let mut warnings = vec![];
623 let toml_de = toml::de::Deserializer::new(&contents);
624 let mut manifest: Self = serde_ignored::deserialize(toml_de, |path| {
625 let warning = format!("unused manifest key: {path}");
626 warnings.push(warning);
627 })
628 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
629 for warning in warnings {
630 println_warning(&warning);
631 }
632 manifest.implicitly_include_std_if_missing();
633 manifest.implicitly_include_default_build_profiles_if_missing();
634 manifest.validate()?;
635 Ok(manifest)
636 }
637
638 pub fn validate(&self) -> Result<()> {
646 validate_project_name(&self.project.name)?;
647 if let Some(ref org) = self.project.organization {
648 validate_name(org, "organization name")?;
649 }
650 for (dep_name, dependency_details) in self.deps_detailed() {
651 dependency_details.validate()?;
652 if dependency_details
653 .package
654 .as_ref()
655 .is_some_and(|package_alias| package_alias == &self.project.name)
656 {
657 bail!(format!("Dependency \"{dep_name}\" declares an alias (\"package\" field) that is the same as project name"))
658 }
659 if dep_name == &self.project.name {
660 bail!(format!(
661 "Dependency \"{dep_name}\" collides with project name."
662 ))
663 }
664 }
665 Ok(())
666 }
667
668 pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
673 let dir = dir.as_ref();
674 let manifest_dir =
675 find_parent_manifest_dir(dir).ok_or_else(|| manifest_file_missing(dir))?;
676 let file_path = manifest_dir.join(constants::MANIFEST_FILE_NAME);
677 Self::from_file(file_path)
678 }
679
680 pub fn deps(&self) -> impl Iterator<Item = (&String, &Dependency)> {
682 self.dependencies
683 .as_ref()
684 .into_iter()
685 .flat_map(|deps| deps.iter())
686 }
687
688 pub fn build_profiles(&self) -> impl Iterator<Item = (&String, &BuildProfile)> {
690 self.build_profile
691 .as_ref()
692 .into_iter()
693 .flat_map(|deps| deps.iter())
694 }
695
696 pub fn contract_deps(&self) -> impl Iterator<Item = (&String, &ContractDependency)> {
698 self.contract_dependencies
699 .as_ref()
700 .into_iter()
701 .flat_map(|deps| deps.iter())
702 }
703
704 pub fn deps_detailed(&self) -> impl Iterator<Item = (&String, &DependencyDetails)> {
706 self.deps().filter_map(|(name, dep)| match dep {
707 Dependency::Detailed(ref det) => Some((name, det)),
708 Dependency::Simple(_) => None,
709 })
710 }
711
712 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
714 self.patch
715 .as_ref()
716 .into_iter()
717 .flat_map(|patches| patches.iter())
718 }
719
720 pub fn patch(&self, patch_name: &str) -> Option<&PatchMap> {
722 self.patch
723 .as_ref()
724 .and_then(|patches| patches.get(patch_name))
725 }
726
727 pub fn proxy(&self) -> Option<&Proxy> {
729 self.proxy.as_ref()
730 }
731
732 fn implicitly_include_std_if_missing(&mut self) {
741 use sway_types::constants::{CORE, STD};
742 if self.project.name == CORE
747 || self.project.name == STD
748 || self.pkg_dep(CORE).is_some()
749 || self.pkg_dep(STD).is_some()
750 || self.dep(STD).is_some()
751 || !self.project.implicit_std.unwrap_or(true)
752 {
753 return;
754 }
755 let deps = self.dependencies.get_or_insert_with(Default::default);
757 let std_dep = implicit_std_dep();
759 deps.insert(STD.to_string(), std_dep);
760 }
761
762 fn implicitly_include_default_build_profiles_if_missing(&mut self) {
766 let build_profiles = self.build_profile.get_or_insert_with(Default::default);
767
768 if build_profiles.get(BuildProfile::DEBUG).is_none() {
769 build_profiles.insert(BuildProfile::DEBUG.into(), BuildProfile::debug());
770 }
771 if build_profiles.get(BuildProfile::RELEASE).is_none() {
772 build_profiles.insert(BuildProfile::RELEASE.into(), BuildProfile::release());
773 }
774 }
775
776 pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
778 self.dependencies
779 .as_ref()
780 .and_then(|deps| deps.get(dep_name))
781 }
782
783 pub fn dep_detailed(&self, dep_name: &str) -> Option<&DependencyDetails> {
785 self.dep(dep_name).and_then(|dep| match dep {
786 Dependency::Simple(_) => None,
787 Dependency::Detailed(detailed) => Some(detailed),
788 })
789 }
790
791 pub fn contract_dep(&self, contract_dep_name: &str) -> Option<&ContractDependency> {
793 self.contract_dependencies
794 .as_ref()
795 .and_then(|contract_dependencies| contract_dependencies.get(contract_dep_name))
796 }
797
798 pub fn contract_dependency_detailed(
800 &self,
801 contract_dep_name: &str,
802 ) -> Option<&DependencyDetails> {
803 self.contract_dep(contract_dep_name)
804 .and_then(|contract_dep| match &contract_dep.dependency {
805 Dependency::Simple(_) => None,
806 Dependency::Detailed(detailed) => Some(detailed),
807 })
808 }
809
810 fn pkg_dep<'a>(&'a self, pkg_name: &str) -> Option<&'a str> {
815 for (dep_name, dep) in self.deps() {
816 if dep.package().unwrap_or(dep_name) == pkg_name {
817 return Some(dep_name);
818 }
819 }
820 None
821 }
822}
823
824impl std::ops::Deref for PackageManifestFile {
825 type Target = PackageManifest;
826 fn deref(&self) -> &Self::Target {
827 &self.manifest
828 }
829}
830
831fn implicit_std_dep() -> Dependency {
838 if let Ok(path) = std::env::var("FORC_IMPLICIT_STD_PATH") {
839 return Dependency::Detailed(DependencyDetails {
840 path: Some(path),
841 ..Default::default()
842 });
843 }
844
845 let tag = std::env::var("FORC_IMPLICIT_STD_GIT_TAG")
854 .ok()
855 .unwrap_or_else(|| format!("v{}", env!("CARGO_PKG_VERSION")));
856 const SWAY_GIT_REPO_URL: &str = "https://github.com/fuellabs/sway";
857
858 let branch = std::env::var("FORC_IMPLICIT_STD_GIT_BRANCH").ok();
860 let tag = branch.as_ref().map_or_else(|| Some(tag), |_| None);
861
862 let mut det = DependencyDetails {
863 git: std::env::var("FORC_IMPLICIT_STD_GIT")
864 .ok()
865 .or_else(|| Some(SWAY_GIT_REPO_URL.to_string())),
866 tag,
867 branch,
868 ..Default::default()
869 };
870
871 if let Some((_, build_metadata)) = det.tag.as_ref().and_then(|tag| tag.split_once('+')) {
872 let rev = build_metadata.split('.').last().map(|r| r.to_string());
874
875 det.tag = None;
878 det.rev = rev;
879 };
880
881 Dependency::Detailed(det)
882}
883
884fn default_entry() -> String {
885 PackageManifest::DEFAULT_ENTRY_FILE_NAME.to_string()
886}
887
888fn default_url() -> String {
889 constants::DEFAULT_NODE_URL.into()
890}
891
892#[derive(Clone, Debug)]
894pub struct WorkspaceManifestFile {
895 manifest: WorkspaceManifest,
897 path: PathBuf,
899}
900
901#[derive(Serialize, Deserialize, Clone, Debug)]
903#[serde(rename_all = "kebab-case")]
904pub struct WorkspaceManifest {
905 workspace: Workspace,
906 patch: Option<BTreeMap<String, PatchMap>>,
907}
908
909#[derive(Serialize, Deserialize, Clone, Debug)]
910#[serde(rename_all = "kebab-case")]
911pub struct Workspace {
912 pub members: Vec<PathBuf>,
913 pub metadata: Option<toml::Value>,
914}
915
916impl WorkspaceManifestFile {
917 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
919 self.patch
920 .as_ref()
921 .into_iter()
922 .flat_map(|patches| patches.iter())
923 }
924
925 pub fn members(&self) -> impl Iterator<Item = &PathBuf> + '_ {
927 self.workspace.members.iter()
928 }
929
930 pub fn member_paths(&self) -> Result<impl Iterator<Item = PathBuf> + '_> {
934 Ok(self
935 .workspace
936 .members
937 .iter()
938 .map(|member| self.dir().join(member)))
939 }
940
941 pub fn member_pkg_manifests(
943 &self,
944 ) -> Result<impl Iterator<Item = Result<PackageManifestFile>> + '_> {
945 let member_paths = self.member_paths()?;
946 let member_pkg_manifests = member_paths.map(PackageManifestFile::from_dir);
947 Ok(member_pkg_manifests)
948 }
949
950 pub fn is_member_path(&self, path: &Path) -> Result<bool> {
952 Ok(self.member_paths()?.any(|member_path| member_path == path))
953 }
954}
955
956impl GenericManifestFile for WorkspaceManifestFile {
957 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
962 let path = path.as_ref().canonicalize()?;
963 let parent = path
964 .parent()
965 .ok_or_else(|| anyhow!("Cannot get parent dir of {:?}", path))?;
966 let manifest = WorkspaceManifest::from_file(&path)?;
967 manifest.validate(parent)?;
968 Ok(Self { manifest, path })
969 }
970
971 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
977 let manifest_dir = manifest_dir.as_ref();
978 let dir = find_parent_manifest_dir_with_check(manifest_dir, |possible_manifest_dir| {
979 let possible_path = possible_manifest_dir.join(constants::MANIFEST_FILE_NAME);
982 Self::from_file(possible_path)
991 .err()
992 .map(|e| !e.to_string().contains("missing field `workspace`"))
993 .unwrap_or_else(|| true)
994 })
995 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
996 let path = dir.join(constants::MANIFEST_FILE_NAME);
997 Self::from_file(path)
998 }
999
1000 fn path(&self) -> &Path {
1001 &self.path
1002 }
1003
1004 fn lock_path(&self) -> Result<PathBuf> {
1008 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
1009 }
1010
1011 fn member_manifests(&self) -> Result<MemberManifestFiles> {
1012 let mut member_manifest_files = BTreeMap::new();
1013 for member_manifest in self.member_pkg_manifests()? {
1014 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
1015 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
1016 }
1017
1018 Ok(member_manifest_files)
1019 }
1020}
1021
1022impl WorkspaceManifest {
1023 pub fn from_file(path: &Path) -> Result<Self> {
1025 let mut warnings = vec![];
1029 let manifest_str = std::fs::read_to_string(path)
1030 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
1031 let toml_de = toml::de::Deserializer::new(&manifest_str);
1032 let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
1033 let warning = format!("unused manifest key: {path}");
1034 warnings.push(warning);
1035 })
1036 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
1037 for warning in warnings {
1038 println_warning(&warning);
1039 }
1040 Ok(manifest)
1041 }
1042
1043 pub fn validate(&self, path: &Path) -> Result<()> {
1047 let mut pkg_name_to_paths: HashMap<String, Vec<PathBuf>> = HashMap::new();
1048 for member in &self.workspace.members {
1049 let member_path = path.join(member).join("Forc.toml");
1050 if !member_path.exists() {
1051 bail!(
1052 "{:?} is listed as a member of the workspace but {:?} does not exists",
1053 &member,
1054 member_path
1055 );
1056 }
1057 if Self::from_file(&member_path).is_ok() {
1058 bail!("Unexpected nested workspace '{}'. Workspaces are currently only allowed in the project root.", member.display());
1059 };
1060
1061 let member_manifest_file = PackageManifestFile::from_file(member_path.clone())?;
1062 let pkg_name = member_manifest_file.manifest.project.name;
1063 pkg_name_to_paths
1064 .entry(pkg_name)
1065 .or_default()
1066 .push(member_path);
1067 }
1068
1069 let duplicate_pkg_lines = pkg_name_to_paths
1071 .iter()
1072 .filter_map(|(pkg_name, paths)| {
1073 if paths.len() > 1 {
1074 let duplicate_paths = pkg_name_to_paths
1075 .get(pkg_name)
1076 .expect("missing duplicate paths");
1077 Some(format!("{pkg_name}: {duplicate_paths:#?}"))
1078 } else {
1079 None
1080 }
1081 })
1082 .collect::<Vec<_>>();
1083
1084 if !duplicate_pkg_lines.is_empty() {
1085 let error_message = duplicate_pkg_lines.join("\n");
1086 bail!(
1087 "Duplicate package names detected in the workspace:\n\n{}",
1088 error_message
1089 );
1090 }
1091 Ok(())
1092 }
1093}
1094
1095impl std::ops::Deref for WorkspaceManifestFile {
1096 type Target = WorkspaceManifest;
1097 fn deref(&self) -> &Self::Target {
1098 &self.manifest
1099 }
1100}
1101
1102pub fn find_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1106 use sway_types::constants::STD;
1107 const SWAY_STD_FOLDER: &str = "sway-lib-std";
1108 walkdir::WalkDir::new(dir)
1109 .into_iter()
1110 .filter_map(|entry| {
1111 entry
1112 .ok()
1113 .filter(|entry| entry.path().ends_with(constants::MANIFEST_FILE_NAME))
1114 })
1115 .find_map(|entry| {
1116 let path = entry.path();
1117 let manifest = PackageManifest::from_file(path).ok()?;
1118 if (manifest.project.name == pkg_name && pkg_name != STD)
1120 || (manifest.project.name == STD
1121 && path
1122 .components()
1123 .any(|comp| comp.as_os_str() == SWAY_STD_FOLDER))
1124 {
1125 Some(path.to_path_buf())
1126 } else {
1127 None
1128 }
1129 })
1130}
1131
1132pub fn find_dir_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1134 find_within(dir, pkg_name).and_then(|path| path.parent().map(Path::to_path_buf))
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use std::str::FromStr;
1140
1141 use super::*;
1142
1143 #[test]
1144 fn deserialize_contract_dependency() {
1145 let contract_dep_str = r#"{"path": "../", "salt": "0x1111111111111111111111111111111111111111111111111111111111111111" }"#;
1146
1147 let contract_dep_expected: ContractDependency =
1148 serde_json::from_str(contract_dep_str).unwrap();
1149
1150 let dependency_det = DependencyDetails {
1151 path: Some("../".to_owned()),
1152 ..Default::default()
1153 };
1154 let dependency = Dependency::Detailed(dependency_det);
1155 let contract_dep = ContractDependency {
1156 dependency,
1157 salt: HexSalt::from_str(
1158 "0x1111111111111111111111111111111111111111111111111111111111111111",
1159 )
1160 .unwrap(),
1161 };
1162 assert_eq!(contract_dep, contract_dep_expected)
1163 }
1164 #[test]
1165 fn test_invalid_dependency_details_mixed_together() {
1166 let dependency_details_path_branch = DependencyDetails {
1167 version: None,
1168 path: Some("example_path/".to_string()),
1169 git: None,
1170 branch: Some("test_branch".to_string()),
1171 tag: None,
1172 package: None,
1173 rev: None,
1174 ipfs: None,
1175 };
1176
1177 let dependency_details_branch = DependencyDetails {
1178 path: None,
1179 ..dependency_details_path_branch.clone()
1180 };
1181
1182 let dependency_details_ipfs_branch = DependencyDetails {
1183 path: None,
1184 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1185 ..dependency_details_path_branch.clone()
1186 };
1187
1188 let dependency_details_path_tag = DependencyDetails {
1189 version: None,
1190 path: Some("example_path/".to_string()),
1191 git: None,
1192 branch: None,
1193 tag: Some("v0.1.0".to_string()),
1194 package: None,
1195 rev: None,
1196 ipfs: None,
1197 };
1198
1199 let dependency_details_tag = DependencyDetails {
1200 path: None,
1201 ..dependency_details_path_tag.clone()
1202 };
1203
1204 let dependency_details_ipfs_tag = DependencyDetails {
1205 path: None,
1206 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1207 ..dependency_details_path_branch.clone()
1208 };
1209
1210 let dependency_details_path_rev = DependencyDetails {
1211 version: None,
1212 path: Some("example_path/".to_string()),
1213 git: None,
1214 branch: None,
1215 tag: None,
1216 package: None,
1217 ipfs: None,
1218 rev: Some("9f35b8e".to_string()),
1219 };
1220
1221 let dependency_details_rev = DependencyDetails {
1222 path: None,
1223 ..dependency_details_path_rev.clone()
1224 };
1225
1226 let dependency_details_ipfs_rev = DependencyDetails {
1227 path: None,
1228 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1229 ..dependency_details_path_branch.clone()
1230 };
1231
1232 let expected_mismatch_error = "Details reserved for git sources used without a git field";
1233 assert_eq!(
1234 dependency_details_path_branch
1235 .validate()
1236 .err()
1237 .map(|e| e.to_string()),
1238 Some(expected_mismatch_error.to_string())
1239 );
1240 assert_eq!(
1241 dependency_details_ipfs_branch
1242 .validate()
1243 .err()
1244 .map(|e| e.to_string()),
1245 Some(expected_mismatch_error.to_string())
1246 );
1247 assert_eq!(
1248 dependency_details_path_tag
1249 .validate()
1250 .err()
1251 .map(|e| e.to_string()),
1252 Some(expected_mismatch_error.to_string())
1253 );
1254 assert_eq!(
1255 dependency_details_ipfs_tag
1256 .validate()
1257 .err()
1258 .map(|e| e.to_string()),
1259 Some(expected_mismatch_error.to_string())
1260 );
1261 assert_eq!(
1262 dependency_details_path_rev
1263 .validate()
1264 .err()
1265 .map(|e| e.to_string()),
1266 Some(expected_mismatch_error.to_string())
1267 );
1268 assert_eq!(
1269 dependency_details_ipfs_rev
1270 .validate()
1271 .err()
1272 .map(|e| e.to_string()),
1273 Some(expected_mismatch_error.to_string())
1274 );
1275 assert_eq!(
1276 dependency_details_branch
1277 .validate()
1278 .err()
1279 .map(|e| e.to_string()),
1280 Some(expected_mismatch_error.to_string())
1281 );
1282 assert_eq!(
1283 dependency_details_tag
1284 .validate()
1285 .err()
1286 .map(|e| e.to_string()),
1287 Some(expected_mismatch_error.to_string())
1288 );
1289 assert_eq!(
1290 dependency_details_rev
1291 .validate()
1292 .err()
1293 .map(|e| e.to_string()),
1294 Some(expected_mismatch_error.to_string())
1295 );
1296 }
1297
1298 #[test]
1299 #[should_panic(expected = "duplicate key `foo` in table `dependencies`")]
1300 fn test_error_duplicate_deps_definition() {
1301 PackageManifest::from_dir("./tests/invalid/duplicate_keys").unwrap();
1302 }
1303
1304 #[test]
1305 fn test_error_duplicate_deps_definition_in_workspace() {
1306 let workspace =
1312 WorkspaceManifestFile::from_dir("./tests/invalid/patch_workspace_and_package").unwrap();
1313 let projects: Vec<_> = workspace
1314 .member_pkg_manifests()
1315 .unwrap()
1316 .collect::<Result<Vec<_>, _>>()
1317 .unwrap();
1318 assert_eq!(projects.len(), 1);
1319 let patches: Vec<_> = projects[0].resolve_patches().unwrap().collect();
1320 assert_eq!(patches.len(), 0);
1321
1322 let patches: Vec<_> = PackageManifestFile::from_dir("./tests/test_package")
1325 .unwrap()
1326 .resolve_patches()
1327 .unwrap()
1328 .collect();
1329 assert_eq!(patches.len(), 1);
1330 }
1331
1332 #[test]
1333 fn test_valid_dependency_details() {
1334 let dependency_details_path = DependencyDetails {
1335 version: None,
1336 path: Some("example_path/".to_string()),
1337 git: None,
1338 branch: None,
1339 tag: None,
1340 package: None,
1341 rev: None,
1342 ipfs: None,
1343 };
1344
1345 let git_source_string = "https://github.com/FuelLabs/sway".to_string();
1346 let dependency_details_git_tag = DependencyDetails {
1347 version: None,
1348 path: None,
1349 git: Some(git_source_string.clone()),
1350 branch: None,
1351 tag: Some("v0.1.0".to_string()),
1352 package: None,
1353 rev: None,
1354 ipfs: None,
1355 };
1356 let dependency_details_git_branch = DependencyDetails {
1357 version: None,
1358 path: None,
1359 git: Some(git_source_string.clone()),
1360 branch: Some("test_branch".to_string()),
1361 tag: None,
1362 package: None,
1363 rev: None,
1364 ipfs: None,
1365 };
1366 let dependency_details_git_rev = DependencyDetails {
1367 version: None,
1368 path: None,
1369 git: Some(git_source_string),
1370 branch: Some("test_branch".to_string()),
1371 tag: None,
1372 package: None,
1373 rev: Some("9f35b8e".to_string()),
1374 ipfs: None,
1375 };
1376
1377 let dependency_details_ipfs = DependencyDetails {
1378 version: None,
1379 path: None,
1380 git: None,
1381 branch: None,
1382 tag: None,
1383 package: None,
1384 rev: None,
1385 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1386 };
1387
1388 assert!(dependency_details_path.validate().is_ok());
1389 assert!(dependency_details_git_tag.validate().is_ok());
1390 assert!(dependency_details_git_branch.validate().is_ok());
1391 assert!(dependency_details_git_rev.validate().is_ok());
1392 assert!(dependency_details_ipfs.validate().is_ok());
1393 }
1394
1395 #[test]
1396 fn test_project_with_null_metadata() {
1397 let project = Project {
1398 authors: Some(vec!["Test Author".to_string()]),
1399 name: "test-project".to_string(),
1400 version: Some(Version::parse("0.1.0").unwrap()),
1401 description: Some("test description".to_string()),
1402 homepage: None,
1403 documentation: None,
1404 categories: None,
1405 keywords: None,
1406 repository: None,
1407 organization: None,
1408 license: "Apache-2.0".to_string(),
1409 entry: "main.sw".to_string(),
1410 implicit_std: None,
1411 forc_version: None,
1412 experimental: HashMap::new(),
1413 metadata: Some(toml::Value::from(toml::value::Table::new())),
1414 };
1415
1416 let serialized = toml::to_string(&project).unwrap();
1417 let deserialized: Project = toml::from_str(&serialized).unwrap();
1418
1419 assert_eq!(project.name, deserialized.name);
1420 assert_eq!(project.metadata, deserialized.metadata);
1421 }
1422
1423 #[test]
1424 fn test_project_without_metadata() {
1425 let project = Project {
1426 authors: Some(vec!["Test Author".to_string()]),
1427 name: "test-project".to_string(),
1428 version: Some(Version::parse("0.1.0").unwrap()),
1429 description: Some("test description".to_string()),
1430 homepage: Some(Url::parse("https://example.com").unwrap()),
1431 documentation: Some(Url::parse("https://docs.example.com").unwrap()),
1432 categories: Some(vec!["test-category".to_string()]),
1433 keywords: Some(vec!["test-keyword".to_string()]),
1434 repository: Some(Url::parse("https://example.com").unwrap()),
1435 organization: None,
1436 license: "Apache-2.0".to_string(),
1437 entry: "main.sw".to_string(),
1438 implicit_std: None,
1439 forc_version: None,
1440 experimental: HashMap::new(),
1441 metadata: None,
1442 };
1443
1444 let serialized = toml::to_string(&project).unwrap();
1445 let deserialized: Project = toml::from_str(&serialized).unwrap();
1446
1447 assert_eq!(project.name, deserialized.name);
1448 assert_eq!(project.version, deserialized.version);
1449 assert_eq!(project.description, deserialized.description);
1450 assert_eq!(project.homepage, deserialized.homepage);
1451 assert_eq!(project.documentation, deserialized.documentation);
1452 assert_eq!(project.repository, deserialized.repository);
1453 assert_eq!(project.metadata, deserialized.metadata);
1454 assert_eq!(project.metadata, None);
1455 assert_eq!(project.categories, deserialized.categories);
1456 assert_eq!(project.keywords, deserialized.keywords);
1457 }
1458
1459 #[test]
1460 fn test_project_metadata_from_toml() {
1461 let toml_str = r#"
1462 name = "test-project"
1463 license = "Apache-2.0"
1464 entry = "main.sw"
1465 authors = ["Test Author"]
1466 description = "A test project"
1467 version = "1.0.0"
1468 keywords = ["test", "project"]
1469 categories = ["test"]
1470
1471 [metadata]
1472 mykey = "https://example.com"
1473 "#;
1474
1475 let project: Project = toml::from_str(toml_str).unwrap();
1476 assert!(project.metadata.is_some());
1477
1478 let metadata = project.metadata.unwrap();
1479 let table = metadata.as_table().unwrap();
1480
1481 assert_eq!(
1482 table.get("mykey").unwrap().as_str().unwrap(),
1483 "https://example.com"
1484 );
1485 }
1486
1487 #[test]
1488 fn test_project_with_invalid_metadata() {
1489 let invalid_toml = r#"
1491 name = "test-project"
1492 license = "Apache-2.0"
1493 entry = "main.sw"
1494
1495 [metadata
1496 description = "Invalid TOML"
1497 "#;
1498
1499 let result: Result<Project, _> = toml::from_str(invalid_toml);
1500 assert!(result.is_err());
1501
1502 let invalid_toml = r#"
1504 name = "test-project"
1505 license = "Apache-2.0"
1506 entry = "main.sw"
1507
1508 [metadata]
1509 ] = "Invalid key"
1510 "#;
1511
1512 let result: Result<Project, _> = toml::from_str(invalid_toml);
1513 assert!(result.is_err());
1514
1515 let invalid_toml = r#"
1517 name = "test-project"
1518 license = "Apache-2.0"
1519 entry = "main.sw"
1520
1521 [metadata]
1522 nested = { key = "value1" }
1523
1524 [metadata.nested]
1525 key = "value2"
1526 "#;
1527
1528 let result: Result<Project, _> = toml::from_str(invalid_toml);
1529 assert!(result.is_err());
1530 assert!(result
1531 .err()
1532 .unwrap()
1533 .to_string()
1534 .contains("duplicate key `nested` in table `metadata`"));
1535 }
1536
1537 #[test]
1538 fn test_metadata_roundtrip() {
1539 let original_toml = r#"
1540 name = "test-project"
1541 license = "Apache-2.0"
1542 entry = "main.sw"
1543
1544 [metadata]
1545 boolean = true
1546 integer = 42
1547 float = 3.12
1548 string = "value"
1549 array = [1, 2, 3]
1550 mixed_array = [1, "two", true]
1551
1552 [metadata.nested]
1553 key = "value2"
1554 "#;
1555
1556 let project: Project = toml::from_str(original_toml).unwrap();
1557 let serialized = toml::to_string(&project).unwrap();
1558 let deserialized: Project = toml::from_str(&serialized).unwrap();
1559
1560 assert_eq!(project.metadata, deserialized.metadata);
1562
1563 let table_val = project.metadata.unwrap();
1565 let table = table_val.as_table().unwrap();
1566 assert!(table.get("boolean").unwrap().as_bool().unwrap());
1567 assert_eq!(table.get("integer").unwrap().as_integer().unwrap(), 42);
1568 assert_eq!(table.get("float").unwrap().as_float().unwrap(), 3.12);
1569 assert_eq!(table.get("string").unwrap().as_str().unwrap(), "value");
1570 assert_eq!(table.get("array").unwrap().as_array().unwrap().len(), 3);
1571 assert!(table.get("nested").unwrap().as_table().is_some());
1572 }
1573
1574 #[test]
1575 fn test_workspace_with_metadata() {
1576 let toml_str = r#"
1577 [workspace]
1578 members = ["package1", "package2"]
1579
1580 [workspace.metadata]
1581 description = "A test workspace"
1582 version = "1.0.0"
1583 authors = ["Test Author"]
1584 homepage = "https://example.com"
1585
1586 [workspace.metadata.ci]
1587 workflow = "main"
1588 timeout = 3600
1589 "#;
1590
1591 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1592 assert!(manifest.workspace.metadata.is_some());
1593
1594 let metadata = manifest.workspace.metadata.unwrap();
1595 let table = metadata.as_table().unwrap();
1596
1597 assert_eq!(
1598 table.get("description").unwrap().as_str().unwrap(),
1599 "A test workspace"
1600 );
1601 assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0");
1602
1603 let ci = table.get("ci").unwrap().as_table().unwrap();
1604 assert_eq!(ci.get("workflow").unwrap().as_str().unwrap(), "main");
1605 assert_eq!(ci.get("timeout").unwrap().as_integer().unwrap(), 3600);
1606 }
1607
1608 #[test]
1609 fn test_workspace_without_metadata() {
1610 let toml_str = r#"
1611 [workspace]
1612 members = ["package1", "package2"]
1613 "#;
1614
1615 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1616 assert!(manifest.workspace.metadata.is_none());
1617 }
1618
1619 #[test]
1620 fn test_workspace_empty_metadata() {
1621 let toml_str = r#"
1622 [workspace]
1623 members = ["package1", "package2"]
1624
1625 [workspace.metadata]
1626 "#;
1627
1628 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1629 assert!(manifest.workspace.metadata.is_some());
1630 let metadata = manifest.workspace.metadata.unwrap();
1631 assert!(metadata.as_table().unwrap().is_empty());
1632 }
1633
1634 #[test]
1635 fn test_workspace_complex_metadata() {
1636 let toml_str = r#"
1637 [workspace]
1638 members = ["package1", "package2"]
1639
1640 [workspace.metadata]
1641 numbers = [1, 2, 3]
1642 strings = ["a", "b", "c"]
1643 mixed = [1, "two", true]
1644
1645 [workspace.metadata.nested]
1646 key = "value"
1647
1648 [workspace.metadata.nested.deep]
1649 another = "value"
1650 "#;
1651
1652 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1653 let metadata = manifest.workspace.metadata.unwrap();
1654 let table = metadata.as_table().unwrap();
1655
1656 assert!(table.get("numbers").unwrap().as_array().is_some());
1657 assert!(table.get("strings").unwrap().as_array().is_some());
1658 assert!(table.get("mixed").unwrap().as_array().is_some());
1659
1660 let nested = table.get("nested").unwrap().as_table().unwrap();
1661 assert_eq!(nested.get("key").unwrap().as_str().unwrap(), "value");
1662
1663 let deep = nested.get("deep").unwrap().as_table().unwrap();
1664 assert_eq!(deep.get("another").unwrap().as_str().unwrap(), "value");
1665 }
1666
1667 #[test]
1668 fn test_workspace_metadata_roundtrip() {
1669 let original = WorkspaceManifest {
1670 workspace: Workspace {
1671 members: vec![PathBuf::from("package1"), PathBuf::from("package2")],
1672 metadata: Some(toml::Value::Table({
1673 let mut table = toml::value::Table::new();
1674 table.insert("key".to_string(), toml::Value::String("value".to_string()));
1675 table
1676 })),
1677 },
1678 patch: None,
1679 };
1680
1681 let serialized = toml::to_string(&original).unwrap();
1682 let deserialized: WorkspaceManifest = toml::from_str(&serialized).unwrap();
1683
1684 assert_eq!(original.workspace.members, deserialized.workspace.members);
1685 assert_eq!(original.workspace.metadata, deserialized.workspace.metadata);
1686 }
1687
1688 #[test]
1689 fn test_dependency_alias_project_name_collision() {
1690 let original_toml = r#"
1691 [project]
1692 authors = ["Fuel Labs <contact@fuel.sh>"]
1693 entry = "main.sw"
1694 license = "Apache-2.0"
1695 name = "lib_contract_abi"
1696
1697 [dependencies]
1698 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1699 "#;
1700
1701 let project = PackageManifest::from_string(original_toml.to_string());
1702 let err = project.unwrap_err();
1703 assert_eq!(err.to_string(), format!("Dependency \"lib_contract\" declares an alias (\"package\" field) that is the same as project name"))
1704 }
1705
1706 #[test]
1707 fn test_dependency_name_project_name_collision() {
1708 let original_toml = r#"
1709 [project]
1710 authors = ["Fuel Labs <contact@fuel.sh>"]
1711 entry = "main.sw"
1712 license = "Apache-2.0"
1713 name = "lib_contract"
1714
1715 [dependencies]
1716 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1717 "#;
1718
1719 let project = PackageManifest::from_string(original_toml.to_string());
1720 let err = project.unwrap_err();
1721 assert_eq!(
1722 err.to_string(),
1723 format!("Dependency \"lib_contract\" collides with project name.")
1724 )
1725 }
1726}