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) {
738 use sway_types::constants::STD;
739 if self.project.name == STD
744 || self.pkg_dep(STD).is_some()
745 || self.dep(STD).is_some()
746 || !self.project.implicit_std.unwrap_or(true)
747 {
748 return;
749 }
750 let deps = self.dependencies.get_or_insert_with(Default::default);
752 let std_dep = implicit_std_dep();
754 deps.insert(STD.to_string(), std_dep);
755 }
756
757 fn implicitly_include_default_build_profiles_if_missing(&mut self) {
761 let build_profiles = self.build_profile.get_or_insert_with(Default::default);
762
763 if build_profiles.get(BuildProfile::DEBUG).is_none() {
764 build_profiles.insert(BuildProfile::DEBUG.into(), BuildProfile::debug());
765 }
766 if build_profiles.get(BuildProfile::RELEASE).is_none() {
767 build_profiles.insert(BuildProfile::RELEASE.into(), BuildProfile::release());
768 }
769 }
770
771 pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
773 self.dependencies
774 .as_ref()
775 .and_then(|deps| deps.get(dep_name))
776 }
777
778 pub fn dep_detailed(&self, dep_name: &str) -> Option<&DependencyDetails> {
780 self.dep(dep_name).and_then(|dep| match dep {
781 Dependency::Simple(_) => None,
782 Dependency::Detailed(detailed) => Some(detailed),
783 })
784 }
785
786 pub fn contract_dep(&self, contract_dep_name: &str) -> Option<&ContractDependency> {
788 self.contract_dependencies
789 .as_ref()
790 .and_then(|contract_dependencies| contract_dependencies.get(contract_dep_name))
791 }
792
793 pub fn contract_dependency_detailed(
795 &self,
796 contract_dep_name: &str,
797 ) -> Option<&DependencyDetails> {
798 self.contract_dep(contract_dep_name)
799 .and_then(|contract_dep| match &contract_dep.dependency {
800 Dependency::Simple(_) => None,
801 Dependency::Detailed(detailed) => Some(detailed),
802 })
803 }
804
805 fn pkg_dep<'a>(&'a self, pkg_name: &str) -> Option<&'a str> {
810 for (dep_name, dep) in self.deps() {
811 if dep.package().unwrap_or(dep_name) == pkg_name {
812 return Some(dep_name);
813 }
814 }
815 None
816 }
817}
818
819impl std::ops::Deref for PackageManifestFile {
820 type Target = PackageManifest;
821 fn deref(&self) -> &Self::Target {
822 &self.manifest
823 }
824}
825
826fn implicit_std_dep() -> Dependency {
833 if let Ok(path) = std::env::var("FORC_IMPLICIT_STD_PATH") {
834 return Dependency::Detailed(DependencyDetails {
835 path: Some(path),
836 ..Default::default()
837 });
838 }
839
840 let tag = std::env::var("FORC_IMPLICIT_STD_GIT_TAG")
849 .ok()
850 .unwrap_or_else(|| format!("v{}", env!("CARGO_PKG_VERSION")));
851 const SWAY_GIT_REPO_URL: &str = "https://github.com/fuellabs/sway";
852
853 let branch = std::env::var("FORC_IMPLICIT_STD_GIT_BRANCH").ok();
855 let tag = branch.as_ref().map_or_else(|| Some(tag), |_| None);
856
857 let mut det = DependencyDetails {
858 git: std::env::var("FORC_IMPLICIT_STD_GIT")
859 .ok()
860 .or_else(|| Some(SWAY_GIT_REPO_URL.to_string())),
861 tag,
862 branch,
863 ..Default::default()
864 };
865
866 if let Some((_, build_metadata)) = det.tag.as_ref().and_then(|tag| tag.split_once('+')) {
867 let rev = build_metadata.split('.').last().map(|r| r.to_string());
869
870 det.tag = None;
873 det.rev = rev;
874 };
875
876 Dependency::Detailed(det)
877}
878
879fn default_entry() -> String {
880 PackageManifest::DEFAULT_ENTRY_FILE_NAME.to_string()
881}
882
883fn default_url() -> String {
884 constants::DEFAULT_NODE_URL.into()
885}
886
887#[derive(Clone, Debug)]
889pub struct WorkspaceManifestFile {
890 manifest: WorkspaceManifest,
892 path: PathBuf,
894}
895
896#[derive(Serialize, Deserialize, Clone, Debug)]
898#[serde(rename_all = "kebab-case")]
899pub struct WorkspaceManifest {
900 workspace: Workspace,
901 patch: Option<BTreeMap<String, PatchMap>>,
902}
903
904#[derive(Serialize, Deserialize, Clone, Debug)]
905#[serde(rename_all = "kebab-case")]
906pub struct Workspace {
907 pub members: Vec<PathBuf>,
908 pub metadata: Option<toml::Value>,
909}
910
911impl WorkspaceManifestFile {
912 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
914 self.patch
915 .as_ref()
916 .into_iter()
917 .flat_map(|patches| patches.iter())
918 }
919
920 pub fn members(&self) -> impl Iterator<Item = &PathBuf> + '_ {
922 self.workspace.members.iter()
923 }
924
925 pub fn member_paths(&self) -> Result<impl Iterator<Item = PathBuf> + '_> {
929 Ok(self
930 .workspace
931 .members
932 .iter()
933 .map(|member| self.dir().join(member)))
934 }
935
936 pub fn member_pkg_manifests(
938 &self,
939 ) -> Result<impl Iterator<Item = Result<PackageManifestFile>> + '_> {
940 let member_paths = self.member_paths()?;
941 let member_pkg_manifests = member_paths.map(PackageManifestFile::from_dir);
942 Ok(member_pkg_manifests)
943 }
944
945 pub fn is_member_path(&self, path: &Path) -> Result<bool> {
947 Ok(self.member_paths()?.any(|member_path| member_path == path))
948 }
949}
950
951impl GenericManifestFile for WorkspaceManifestFile {
952 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
957 let path = path.as_ref().canonicalize()?;
958 let parent = path
959 .parent()
960 .ok_or_else(|| anyhow!("Cannot get parent dir of {:?}", path))?;
961 let manifest = WorkspaceManifest::from_file(&path)?;
962 manifest.validate(parent)?;
963 Ok(Self { manifest, path })
964 }
965
966 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
972 let manifest_dir = manifest_dir.as_ref();
973 let dir = find_parent_manifest_dir_with_check(manifest_dir, |possible_manifest_dir| {
974 let possible_path = possible_manifest_dir.join(constants::MANIFEST_FILE_NAME);
977 Self::from_file(possible_path)
986 .err()
987 .map(|e| !e.to_string().contains("missing field `workspace`"))
988 .unwrap_or_else(|| true)
989 })
990 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
991 let path = dir.join(constants::MANIFEST_FILE_NAME);
992 Self::from_file(path)
993 }
994
995 fn path(&self) -> &Path {
996 &self.path
997 }
998
999 fn lock_path(&self) -> Result<PathBuf> {
1003 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
1004 }
1005
1006 fn member_manifests(&self) -> Result<MemberManifestFiles> {
1007 let mut member_manifest_files = BTreeMap::new();
1008 for member_manifest in self.member_pkg_manifests()? {
1009 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
1010 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
1011 }
1012
1013 Ok(member_manifest_files)
1014 }
1015}
1016
1017impl WorkspaceManifest {
1018 pub fn from_file(path: &Path) -> Result<Self> {
1020 let mut warnings = vec![];
1024 let manifest_str = std::fs::read_to_string(path)
1025 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
1026 let toml_de = toml::de::Deserializer::new(&manifest_str);
1027 let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
1028 let warning = format!("unused manifest key: {path}");
1029 warnings.push(warning);
1030 })
1031 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
1032 for warning in warnings {
1033 println_warning(&warning);
1034 }
1035 Ok(manifest)
1036 }
1037
1038 pub fn validate(&self, path: &Path) -> Result<()> {
1042 let mut pkg_name_to_paths: HashMap<String, Vec<PathBuf>> = HashMap::new();
1043 for member in &self.workspace.members {
1044 let member_path = path.join(member).join("Forc.toml");
1045 if !member_path.exists() {
1046 bail!(
1047 "{:?} is listed as a member of the workspace but {:?} does not exists",
1048 &member,
1049 member_path
1050 );
1051 }
1052 if Self::from_file(&member_path).is_ok() {
1053 bail!("Unexpected nested workspace '{}'. Workspaces are currently only allowed in the project root.", member.display());
1054 };
1055
1056 let member_manifest_file = PackageManifestFile::from_file(member_path.clone())?;
1057 let pkg_name = member_manifest_file.manifest.project.name;
1058 pkg_name_to_paths
1059 .entry(pkg_name)
1060 .or_default()
1061 .push(member_path);
1062 }
1063
1064 let duplicate_pkg_lines = pkg_name_to_paths
1066 .iter()
1067 .filter_map(|(pkg_name, paths)| {
1068 if paths.len() > 1 {
1069 let duplicate_paths = pkg_name_to_paths
1070 .get(pkg_name)
1071 .expect("missing duplicate paths");
1072 Some(format!("{pkg_name}: {duplicate_paths:#?}"))
1073 } else {
1074 None
1075 }
1076 })
1077 .collect::<Vec<_>>();
1078
1079 if !duplicate_pkg_lines.is_empty() {
1080 let error_message = duplicate_pkg_lines.join("\n");
1081 bail!(
1082 "Duplicate package names detected in the workspace:\n\n{}",
1083 error_message
1084 );
1085 }
1086 Ok(())
1087 }
1088}
1089
1090impl std::ops::Deref for WorkspaceManifestFile {
1091 type Target = WorkspaceManifest;
1092 fn deref(&self) -> &Self::Target {
1093 &self.manifest
1094 }
1095}
1096
1097pub fn find_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1101 use sway_types::constants::STD;
1102 const SWAY_STD_FOLDER: &str = "sway-lib-std";
1103 walkdir::WalkDir::new(dir)
1104 .into_iter()
1105 .filter_map(|entry| {
1106 entry
1107 .ok()
1108 .filter(|entry| entry.path().ends_with(constants::MANIFEST_FILE_NAME))
1109 })
1110 .find_map(|entry| {
1111 let path = entry.path();
1112 let manifest = PackageManifest::from_file(path).ok()?;
1113 if (manifest.project.name == pkg_name && pkg_name != STD)
1115 || (manifest.project.name == STD
1116 && path
1117 .components()
1118 .any(|comp| comp.as_os_str() == SWAY_STD_FOLDER))
1119 {
1120 Some(path.to_path_buf())
1121 } else {
1122 None
1123 }
1124 })
1125}
1126
1127pub fn find_dir_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1129 find_within(dir, pkg_name).and_then(|path| path.parent().map(Path::to_path_buf))
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use std::str::FromStr;
1135
1136 use super::*;
1137
1138 #[test]
1139 fn deserialize_contract_dependency() {
1140 let contract_dep_str = r#"{"path": "../", "salt": "0x1111111111111111111111111111111111111111111111111111111111111111" }"#;
1141
1142 let contract_dep_expected: ContractDependency =
1143 serde_json::from_str(contract_dep_str).unwrap();
1144
1145 let dependency_det = DependencyDetails {
1146 path: Some("../".to_owned()),
1147 ..Default::default()
1148 };
1149 let dependency = Dependency::Detailed(dependency_det);
1150 let contract_dep = ContractDependency {
1151 dependency,
1152 salt: HexSalt::from_str(
1153 "0x1111111111111111111111111111111111111111111111111111111111111111",
1154 )
1155 .unwrap(),
1156 };
1157 assert_eq!(contract_dep, contract_dep_expected)
1158 }
1159 #[test]
1160 fn test_invalid_dependency_details_mixed_together() {
1161 let dependency_details_path_branch = DependencyDetails {
1162 version: None,
1163 path: Some("example_path/".to_string()),
1164 git: None,
1165 branch: Some("test_branch".to_string()),
1166 tag: None,
1167 package: None,
1168 rev: None,
1169 ipfs: None,
1170 };
1171
1172 let dependency_details_branch = DependencyDetails {
1173 path: None,
1174 ..dependency_details_path_branch.clone()
1175 };
1176
1177 let dependency_details_ipfs_branch = DependencyDetails {
1178 path: None,
1179 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1180 ..dependency_details_path_branch.clone()
1181 };
1182
1183 let dependency_details_path_tag = DependencyDetails {
1184 version: None,
1185 path: Some("example_path/".to_string()),
1186 git: None,
1187 branch: None,
1188 tag: Some("v0.1.0".to_string()),
1189 package: None,
1190 rev: None,
1191 ipfs: None,
1192 };
1193
1194 let dependency_details_tag = DependencyDetails {
1195 path: None,
1196 ..dependency_details_path_tag.clone()
1197 };
1198
1199 let dependency_details_ipfs_tag = DependencyDetails {
1200 path: None,
1201 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1202 ..dependency_details_path_branch.clone()
1203 };
1204
1205 let dependency_details_path_rev = DependencyDetails {
1206 version: None,
1207 path: Some("example_path/".to_string()),
1208 git: None,
1209 branch: None,
1210 tag: None,
1211 package: None,
1212 ipfs: None,
1213 rev: Some("9f35b8e".to_string()),
1214 };
1215
1216 let dependency_details_rev = DependencyDetails {
1217 path: None,
1218 ..dependency_details_path_rev.clone()
1219 };
1220
1221 let dependency_details_ipfs_rev = DependencyDetails {
1222 path: None,
1223 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1224 ..dependency_details_path_branch.clone()
1225 };
1226
1227 let expected_mismatch_error = "Details reserved for git sources used without a git field";
1228 assert_eq!(
1229 dependency_details_path_branch
1230 .validate()
1231 .err()
1232 .map(|e| e.to_string()),
1233 Some(expected_mismatch_error.to_string())
1234 );
1235 assert_eq!(
1236 dependency_details_ipfs_branch
1237 .validate()
1238 .err()
1239 .map(|e| e.to_string()),
1240 Some(expected_mismatch_error.to_string())
1241 );
1242 assert_eq!(
1243 dependency_details_path_tag
1244 .validate()
1245 .err()
1246 .map(|e| e.to_string()),
1247 Some(expected_mismatch_error.to_string())
1248 );
1249 assert_eq!(
1250 dependency_details_ipfs_tag
1251 .validate()
1252 .err()
1253 .map(|e| e.to_string()),
1254 Some(expected_mismatch_error.to_string())
1255 );
1256 assert_eq!(
1257 dependency_details_path_rev
1258 .validate()
1259 .err()
1260 .map(|e| e.to_string()),
1261 Some(expected_mismatch_error.to_string())
1262 );
1263 assert_eq!(
1264 dependency_details_ipfs_rev
1265 .validate()
1266 .err()
1267 .map(|e| e.to_string()),
1268 Some(expected_mismatch_error.to_string())
1269 );
1270 assert_eq!(
1271 dependency_details_branch
1272 .validate()
1273 .err()
1274 .map(|e| e.to_string()),
1275 Some(expected_mismatch_error.to_string())
1276 );
1277 assert_eq!(
1278 dependency_details_tag
1279 .validate()
1280 .err()
1281 .map(|e| e.to_string()),
1282 Some(expected_mismatch_error.to_string())
1283 );
1284 assert_eq!(
1285 dependency_details_rev
1286 .validate()
1287 .err()
1288 .map(|e| e.to_string()),
1289 Some(expected_mismatch_error.to_string())
1290 );
1291 }
1292
1293 #[test]
1294 #[should_panic(expected = "duplicate key `foo` in table `dependencies`")]
1295 fn test_error_duplicate_deps_definition() {
1296 PackageManifest::from_dir("./tests/invalid/duplicate_keys").unwrap();
1297 }
1298
1299 #[test]
1300 fn test_error_duplicate_deps_definition_in_workspace() {
1301 let workspace =
1307 WorkspaceManifestFile::from_dir("./tests/invalid/patch_workspace_and_package").unwrap();
1308 let projects: Vec<_> = workspace
1309 .member_pkg_manifests()
1310 .unwrap()
1311 .collect::<Result<Vec<_>, _>>()
1312 .unwrap();
1313 assert_eq!(projects.len(), 1);
1314 let patches: Vec<_> = projects[0].resolve_patches().unwrap().collect();
1315 assert_eq!(patches.len(), 0);
1316
1317 let patches: Vec<_> = PackageManifestFile::from_dir("./tests/test_package")
1320 .unwrap()
1321 .resolve_patches()
1322 .unwrap()
1323 .collect();
1324 assert_eq!(patches.len(), 1);
1325 }
1326
1327 #[test]
1328 fn test_valid_dependency_details() {
1329 let dependency_details_path = DependencyDetails {
1330 version: None,
1331 path: Some("example_path/".to_string()),
1332 git: None,
1333 branch: None,
1334 tag: None,
1335 package: None,
1336 rev: None,
1337 ipfs: None,
1338 };
1339
1340 let git_source_string = "https://github.com/FuelLabs/sway".to_string();
1341 let dependency_details_git_tag = DependencyDetails {
1342 version: None,
1343 path: None,
1344 git: Some(git_source_string.clone()),
1345 branch: None,
1346 tag: Some("v0.1.0".to_string()),
1347 package: None,
1348 rev: None,
1349 ipfs: None,
1350 };
1351 let dependency_details_git_branch = DependencyDetails {
1352 version: None,
1353 path: None,
1354 git: Some(git_source_string.clone()),
1355 branch: Some("test_branch".to_string()),
1356 tag: None,
1357 package: None,
1358 rev: None,
1359 ipfs: None,
1360 };
1361 let dependency_details_git_rev = DependencyDetails {
1362 version: None,
1363 path: None,
1364 git: Some(git_source_string),
1365 branch: Some("test_branch".to_string()),
1366 tag: None,
1367 package: None,
1368 rev: Some("9f35b8e".to_string()),
1369 ipfs: None,
1370 };
1371
1372 let dependency_details_ipfs = DependencyDetails {
1373 version: None,
1374 path: None,
1375 git: None,
1376 branch: None,
1377 tag: None,
1378 package: None,
1379 rev: None,
1380 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1381 };
1382
1383 assert!(dependency_details_path.validate().is_ok());
1384 assert!(dependency_details_git_tag.validate().is_ok());
1385 assert!(dependency_details_git_branch.validate().is_ok());
1386 assert!(dependency_details_git_rev.validate().is_ok());
1387 assert!(dependency_details_ipfs.validate().is_ok());
1388 }
1389
1390 #[test]
1391 fn test_project_with_null_metadata() {
1392 let project = Project {
1393 authors: Some(vec!["Test Author".to_string()]),
1394 name: "test-project".to_string(),
1395 version: Some(Version::parse("0.1.0").unwrap()),
1396 description: Some("test description".to_string()),
1397 homepage: None,
1398 documentation: None,
1399 categories: None,
1400 keywords: None,
1401 repository: None,
1402 organization: None,
1403 license: "Apache-2.0".to_string(),
1404 entry: "main.sw".to_string(),
1405 implicit_std: None,
1406 forc_version: None,
1407 experimental: HashMap::new(),
1408 metadata: Some(toml::Value::from(toml::value::Table::new())),
1409 };
1410
1411 let serialized = toml::to_string(&project).unwrap();
1412 let deserialized: Project = toml::from_str(&serialized).unwrap();
1413
1414 assert_eq!(project.name, deserialized.name);
1415 assert_eq!(project.metadata, deserialized.metadata);
1416 }
1417
1418 #[test]
1419 fn test_project_without_metadata() {
1420 let project = Project {
1421 authors: Some(vec!["Test Author".to_string()]),
1422 name: "test-project".to_string(),
1423 version: Some(Version::parse("0.1.0").unwrap()),
1424 description: Some("test description".to_string()),
1425 homepage: Some(Url::parse("https://example.com").unwrap()),
1426 documentation: Some(Url::parse("https://docs.example.com").unwrap()),
1427 categories: Some(vec!["test-category".to_string()]),
1428 keywords: Some(vec!["test-keyword".to_string()]),
1429 repository: Some(Url::parse("https://example.com").unwrap()),
1430 organization: None,
1431 license: "Apache-2.0".to_string(),
1432 entry: "main.sw".to_string(),
1433 implicit_std: None,
1434 forc_version: None,
1435 experimental: HashMap::new(),
1436 metadata: None,
1437 };
1438
1439 let serialized = toml::to_string(&project).unwrap();
1440 let deserialized: Project = toml::from_str(&serialized).unwrap();
1441
1442 assert_eq!(project.name, deserialized.name);
1443 assert_eq!(project.version, deserialized.version);
1444 assert_eq!(project.description, deserialized.description);
1445 assert_eq!(project.homepage, deserialized.homepage);
1446 assert_eq!(project.documentation, deserialized.documentation);
1447 assert_eq!(project.repository, deserialized.repository);
1448 assert_eq!(project.metadata, deserialized.metadata);
1449 assert_eq!(project.metadata, None);
1450 assert_eq!(project.categories, deserialized.categories);
1451 assert_eq!(project.keywords, deserialized.keywords);
1452 }
1453
1454 #[test]
1455 fn test_project_metadata_from_toml() {
1456 let toml_str = r#"
1457 name = "test-project"
1458 license = "Apache-2.0"
1459 entry = "main.sw"
1460 authors = ["Test Author"]
1461 description = "A test project"
1462 version = "1.0.0"
1463 keywords = ["test", "project"]
1464 categories = ["test"]
1465
1466 [metadata]
1467 mykey = "https://example.com"
1468 "#;
1469
1470 let project: Project = toml::from_str(toml_str).unwrap();
1471 assert!(project.metadata.is_some());
1472
1473 let metadata = project.metadata.unwrap();
1474 let table = metadata.as_table().unwrap();
1475
1476 assert_eq!(
1477 table.get("mykey").unwrap().as_str().unwrap(),
1478 "https://example.com"
1479 );
1480 }
1481
1482 #[test]
1483 fn test_project_with_invalid_metadata() {
1484 let invalid_toml = r#"
1486 name = "test-project"
1487 license = "Apache-2.0"
1488 entry = "main.sw"
1489
1490 [metadata
1491 description = "Invalid TOML"
1492 "#;
1493
1494 let result: Result<Project, _> = toml::from_str(invalid_toml);
1495 assert!(result.is_err());
1496
1497 let invalid_toml = r#"
1499 name = "test-project"
1500 license = "Apache-2.0"
1501 entry = "main.sw"
1502
1503 [metadata]
1504 ] = "Invalid key"
1505 "#;
1506
1507 let result: Result<Project, _> = toml::from_str(invalid_toml);
1508 assert!(result.is_err());
1509
1510 let invalid_toml = r#"
1512 name = "test-project"
1513 license = "Apache-2.0"
1514 entry = "main.sw"
1515
1516 [metadata]
1517 nested = { key = "value1" }
1518
1519 [metadata.nested]
1520 key = "value2"
1521 "#;
1522
1523 let result: Result<Project, _> = toml::from_str(invalid_toml);
1524 assert!(result.is_err());
1525 assert!(result
1526 .err()
1527 .unwrap()
1528 .to_string()
1529 .contains("duplicate key `nested` in table `metadata`"));
1530 }
1531
1532 #[test]
1533 fn test_metadata_roundtrip() {
1534 let original_toml = r#"
1535 name = "test-project"
1536 license = "Apache-2.0"
1537 entry = "main.sw"
1538
1539 [metadata]
1540 boolean = true
1541 integer = 42
1542 float = 3.12
1543 string = "value"
1544 array = [1, 2, 3]
1545 mixed_array = [1, "two", true]
1546
1547 [metadata.nested]
1548 key = "value2"
1549 "#;
1550
1551 let project: Project = toml::from_str(original_toml).unwrap();
1552 let serialized = toml::to_string(&project).unwrap();
1553 let deserialized: Project = toml::from_str(&serialized).unwrap();
1554
1555 assert_eq!(project.metadata, deserialized.metadata);
1557
1558 let table_val = project.metadata.unwrap();
1560 let table = table_val.as_table().unwrap();
1561 assert!(table.get("boolean").unwrap().as_bool().unwrap());
1562 assert_eq!(table.get("integer").unwrap().as_integer().unwrap(), 42);
1563 assert_eq!(table.get("float").unwrap().as_float().unwrap(), 3.12);
1564 assert_eq!(table.get("string").unwrap().as_str().unwrap(), "value");
1565 assert_eq!(table.get("array").unwrap().as_array().unwrap().len(), 3);
1566 assert!(table.get("nested").unwrap().as_table().is_some());
1567 }
1568
1569 #[test]
1570 fn test_workspace_with_metadata() {
1571 let toml_str = r#"
1572 [workspace]
1573 members = ["package1", "package2"]
1574
1575 [workspace.metadata]
1576 description = "A test workspace"
1577 version = "1.0.0"
1578 authors = ["Test Author"]
1579 homepage = "https://example.com"
1580
1581 [workspace.metadata.ci]
1582 workflow = "main"
1583 timeout = 3600
1584 "#;
1585
1586 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1587 assert!(manifest.workspace.metadata.is_some());
1588
1589 let metadata = manifest.workspace.metadata.unwrap();
1590 let table = metadata.as_table().unwrap();
1591
1592 assert_eq!(
1593 table.get("description").unwrap().as_str().unwrap(),
1594 "A test workspace"
1595 );
1596 assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0");
1597
1598 let ci = table.get("ci").unwrap().as_table().unwrap();
1599 assert_eq!(ci.get("workflow").unwrap().as_str().unwrap(), "main");
1600 assert_eq!(ci.get("timeout").unwrap().as_integer().unwrap(), 3600);
1601 }
1602
1603 #[test]
1604 fn test_workspace_without_metadata() {
1605 let toml_str = r#"
1606 [workspace]
1607 members = ["package1", "package2"]
1608 "#;
1609
1610 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1611 assert!(manifest.workspace.metadata.is_none());
1612 }
1613
1614 #[test]
1615 fn test_workspace_empty_metadata() {
1616 let toml_str = r#"
1617 [workspace]
1618 members = ["package1", "package2"]
1619
1620 [workspace.metadata]
1621 "#;
1622
1623 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1624 assert!(manifest.workspace.metadata.is_some());
1625 let metadata = manifest.workspace.metadata.unwrap();
1626 assert!(metadata.as_table().unwrap().is_empty());
1627 }
1628
1629 #[test]
1630 fn test_workspace_complex_metadata() {
1631 let toml_str = r#"
1632 [workspace]
1633 members = ["package1", "package2"]
1634
1635 [workspace.metadata]
1636 numbers = [1, 2, 3]
1637 strings = ["a", "b", "c"]
1638 mixed = [1, "two", true]
1639
1640 [workspace.metadata.nested]
1641 key = "value"
1642
1643 [workspace.metadata.nested.deep]
1644 another = "value"
1645 "#;
1646
1647 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1648 let metadata = manifest.workspace.metadata.unwrap();
1649 let table = metadata.as_table().unwrap();
1650
1651 assert!(table.get("numbers").unwrap().as_array().is_some());
1652 assert!(table.get("strings").unwrap().as_array().is_some());
1653 assert!(table.get("mixed").unwrap().as_array().is_some());
1654
1655 let nested = table.get("nested").unwrap().as_table().unwrap();
1656 assert_eq!(nested.get("key").unwrap().as_str().unwrap(), "value");
1657
1658 let deep = nested.get("deep").unwrap().as_table().unwrap();
1659 assert_eq!(deep.get("another").unwrap().as_str().unwrap(), "value");
1660 }
1661
1662 #[test]
1663 fn test_workspace_metadata_roundtrip() {
1664 let original = WorkspaceManifest {
1665 workspace: Workspace {
1666 members: vec![PathBuf::from("package1"), PathBuf::from("package2")],
1667 metadata: Some(toml::Value::Table({
1668 let mut table = toml::value::Table::new();
1669 table.insert("key".to_string(), toml::Value::String("value".to_string()));
1670 table
1671 })),
1672 },
1673 patch: None,
1674 };
1675
1676 let serialized = toml::to_string(&original).unwrap();
1677 let deserialized: WorkspaceManifest = toml::from_str(&serialized).unwrap();
1678
1679 assert_eq!(original.workspace.members, deserialized.workspace.members);
1680 assert_eq!(original.workspace.metadata, deserialized.workspace.metadata);
1681 }
1682
1683 #[test]
1684 fn test_dependency_alias_project_name_collision() {
1685 let original_toml = r#"
1686 [project]
1687 authors = ["Fuel Labs <contact@fuel.sh>"]
1688 entry = "main.sw"
1689 license = "Apache-2.0"
1690 name = "lib_contract_abi"
1691
1692 [dependencies]
1693 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1694 "#;
1695
1696 let project = PackageManifest::from_string(original_toml.to_string());
1697 let err = project.unwrap_err();
1698 assert_eq!(err.to_string(), format!("Dependency \"lib_contract\" declares an alias (\"package\" field) that is the same as project name"))
1699 }
1700
1701 #[test]
1702 fn test_dependency_name_project_name_collision() {
1703 let original_toml = r#"
1704 [project]
1705 authors = ["Fuel Labs <contact@fuel.sh>"]
1706 entry = "main.sw"
1707 license = "Apache-2.0"
1708 name = "lib_contract"
1709
1710 [dependencies]
1711 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1712 "#;
1713
1714 let project = PackageManifest::from_string(original_toml.to_string());
1715 let err = project.unwrap_err();
1716 assert_eq!(
1717 err.to_string(),
1718 format!("Dependency \"lib_contract\" collides with project name.")
1719 )
1720 }
1721}