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};
16use sway_core::{fuel_prelude::fuel_tx, language::parsed::TreeType, parse_tree_type, BuildTarget};
17use sway_error::handler::Handler;
18use sway_types::span::Source;
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 pub force_dbg_in_release: Option<bool>,
217}
218
219fn validate_package_name<'de, D>(deserializer: D) -> Result<String, D::Error>
221where
222 D: de::Deserializer<'de>,
223{
224 let name: String = Deserialize::deserialize(deserializer)?;
225 match validate_project_name(&name) {
226 Ok(_) => Ok(name),
227 Err(e) => Err(de::Error::custom(e.to_string())),
228 }
229}
230
231#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
232#[serde(rename_all = "kebab-case")]
233pub struct Network {
234 #[serde(default = "default_url")]
235 pub url: String,
236}
237
238#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
239pub struct HexSalt(pub fuel_tx::Salt);
240
241impl FromStr for HexSalt {
242 type Err = anyhow::Error;
243
244 fn from_str(s: &str) -> Result<Self, Self::Err> {
245 let normalized = s
247 .strip_prefix("0x")
248 .ok_or_else(|| anyhow::anyhow!("hex salt declaration needs to start with 0x"))?;
249 let salt: fuel_tx::Salt =
250 fuel_tx::Salt::from_str(normalized).map_err(|e| anyhow::anyhow!("{e}"))?;
251 let hex_salt = Self(salt);
252 Ok(hex_salt)
253 }
254}
255
256impl Display for HexSalt {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 let salt = self.0;
259 write!(f, "{}", salt)
260 }
261}
262
263fn default_hex_salt() -> HexSalt {
264 HexSalt(fuel_tx::Salt::default())
265}
266
267#[serde_as]
268#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
269#[serde(rename_all = "kebab-case")]
270pub struct ContractDependency {
271 #[serde(flatten)]
272 pub dependency: Dependency,
273 #[serde_as(as = "DisplayFromStr")]
274 #[serde(default = "default_hex_salt")]
275 pub salt: HexSalt,
276}
277
278#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
279#[serde(untagged)]
280pub enum Dependency {
281 Simple(String),
284 Detailed(DependencyDetails),
288}
289
290#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
291#[serde(rename_all = "kebab-case")]
292pub struct DependencyDetails {
293 pub(crate) version: Option<String>,
294 pub(crate) namespace: Option<String>,
295 pub path: Option<String>,
296 pub(crate) git: Option<String>,
297 pub(crate) branch: Option<String>,
298 pub(crate) tag: Option<String>,
299 pub(crate) package: Option<String>,
300 pub(crate) rev: Option<String>,
301 pub(crate) ipfs: Option<String>,
302}
303
304#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
306#[serde(rename_all = "kebab-case")]
307pub struct Proxy {
308 pub enabled: bool,
309 pub address: Option<String>,
313}
314
315impl DependencyDetails {
316 pub fn validate(&self) -> anyhow::Result<()> {
322 let DependencyDetails {
323 git,
324 branch,
325 tag,
326 rev,
327 version,
328 ipfs,
329 namespace,
330 ..
331 } = self;
332
333 if git.is_none() && (branch.is_some() || tag.is_some() || rev.is_some()) {
334 bail!("Details reserved for git sources used without a git field");
335 }
336
337 if version.is_some() && git.is_some() {
338 bail!("Both version and git details provided for same dependency");
339 }
340
341 if version.is_some() && ipfs.is_some() {
342 bail!("Both version and ipfs details provided for same dependency");
343 }
344
345 if version.is_none() && namespace.is_some() {
346 bail!("Namespace can only be specified for sources with version");
347 }
348 Ok(())
349 }
350}
351
352impl Dependency {
353 pub fn package(&self) -> Option<&str> {
355 match *self {
356 Self::Simple(_) => None,
357 Self::Detailed(ref det) => det.package.as_deref(),
358 }
359 }
360
361 pub fn version(&self) -> Option<&str> {
363 match *self {
364 Self::Simple(ref version) => Some(version),
365 Self::Detailed(ref det) => det.version.as_deref(),
366 }
367 }
368}
369
370impl PackageManifestFile {
371 fn resolve_patches(&self) -> Result<impl Iterator<Item = (String, PatchMap)>> {
378 if let Some(workspace) = self.workspace().ok().flatten() {
379 if self.patch.is_some() {
381 println_warning("Patch for the non root package will be ignored.");
382 println_warning(&format!(
383 "Specify patch at the workspace root: {}",
384 workspace.path().to_str().unwrap_or_default()
385 ));
386 }
387 Ok(workspace
388 .patch
389 .as_ref()
390 .cloned()
391 .unwrap_or_default()
392 .into_iter())
393 } else {
394 Ok(self.patch.as_ref().cloned().unwrap_or_default().into_iter())
395 }
396 }
397
398 pub fn resolve_patch(&self, patch_name: &str) -> Result<Option<PatchMap>> {
404 Ok(self
405 .resolve_patches()?
406 .find(|(p_name, _)| patch_name == p_name.as_str())
407 .map(|(_, patch)| patch))
408 }
409
410 pub fn entry_path(&self) -> PathBuf {
415 self.dir()
416 .join(constants::SRC_DIR)
417 .join(&self.project.entry)
418 }
419
420 pub fn entry_string(&self) -> Result<Source> {
422 let entry_path = self.entry_path();
423 let entry_string = std::fs::read_to_string(entry_path)?;
424 Ok(entry_string.as_str().into())
425 }
426
427 pub fn program_type(&self) -> Result<TreeType> {
429 let entry_string = self.entry_string()?;
430 let handler = Handler::default();
431 let parse_res = parse_tree_type(&handler, entry_string);
432
433 parse_res.map_err(|_| {
434 let (errors, _warnings) = handler.consume();
435 parsing_failed(&self.project.name, &errors)
436 })
437 }
438
439 pub fn check_program_type(&self, expected_types: &[TreeType]) -> Result<()> {
442 let parsed_type = self.program_type()?;
443 if !expected_types.contains(&parsed_type) {
444 bail!(wrong_program_type(
445 &self.project.name,
446 expected_types,
447 parsed_type
448 ));
449 } else {
450 Ok(())
451 }
452 }
453
454 pub fn build_profile(&self, profile_name: &str) -> Option<&BuildProfile> {
456 self.build_profile
457 .as_ref()
458 .and_then(|profiles| profiles.get(profile_name))
459 }
460
461 pub fn dep_path(&self, dep_name: &str) -> Option<PathBuf> {
463 let dir = self.dir();
464 let details = self.dep_detailed(dep_name)?;
465 details.path.as_ref().and_then(|path_str| {
466 let path = Path::new(path_str);
467 match path.is_absolute() {
468 true => Some(path.to_owned()),
469 false => dir.join(path).canonicalize().ok(),
470 }
471 })
472 }
473
474 pub fn workspace(&self) -> Result<Option<WorkspaceManifestFile>> {
476 let parent_dir = match self.dir().parent() {
477 None => return Ok(None),
478 Some(dir) => dir,
479 };
480 let ws_manifest = match WorkspaceManifestFile::from_dir(parent_dir) {
481 Ok(manifest) => manifest,
482 Err(e) => {
483 if e.to_string().contains("could not find") {
487 return Ok(None);
488 } else {
489 return Err(e);
490 }
491 }
492 };
493 if ws_manifest.is_member_path(self.dir())? {
494 Ok(Some(ws_manifest))
495 } else {
496 Ok(None)
497 }
498 }
499
500 pub fn project_name(&self) -> &str {
502 &self.project.name
503 }
504
505 pub fn validate(&self) -> Result<()> {
511 self.manifest.validate()?;
512 let mut entry_path = self.path.clone();
513 entry_path.pop();
514 let entry_path = entry_path
515 .join(constants::SRC_DIR)
516 .join(&self.project.entry);
517 if !entry_path.exists() {
518 bail!(
519 "failed to validate path from entry field {:?} in Forc manifest file.",
520 self.project.entry
521 )
522 }
523
524 let mut pkg_dir = self.path.to_path_buf();
530 pkg_dir.pop();
531 if let Some(nested_package) = find_nested_manifest_dir(&pkg_dir) {
532 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())
534 }
535 Ok(())
536 }
537}
538
539impl GenericManifestFile for PackageManifestFile {
540 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
549 let path = path.as_ref().canonicalize()?;
550 let manifest = PackageManifest::from_file(&path)?;
551 let manifest_file = Self { manifest, path };
552 manifest_file.validate()?;
553 Ok(manifest_file)
554 }
555
556 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
562 let manifest_dir = manifest_dir.as_ref();
563 let dir = find_parent_manifest_dir(manifest_dir)
564 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
565 let path = dir.join(constants::MANIFEST_FILE_NAME);
566 Self::from_file(path)
567 }
568
569 fn path(&self) -> &Path {
570 &self.path
571 }
572
573 fn lock_path(&self) -> Result<PathBuf> {
579 let workspace_manifest = self.workspace()?;
581 if let Some(workspace_manifest) = workspace_manifest {
582 workspace_manifest.lock_path()
583 } else {
584 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
585 }
586 }
587
588 fn member_manifests(&self) -> Result<MemberManifestFiles> {
589 let mut member_manifest_files = BTreeMap::new();
590 if let Some(workspace_manifest_file) = self.workspace()? {
592 for member_manifest in workspace_manifest_file.member_pkg_manifests()? {
593 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
594 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
595 }
596 } else {
597 let member_name = &self.project.name;
598 member_manifest_files.insert(member_name.clone(), self.clone());
599 }
600
601 Ok(member_manifest_files)
602 }
603}
604
605impl PackageManifest {
606 pub const DEFAULT_ENTRY_FILE_NAME: &'static str = "main.sw";
607
608 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
617 let path = path.as_ref();
621 let contents = std::fs::read_to_string(path)
622 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
623 Self::from_string(contents)
624 }
625
626 pub fn from_string(contents: String) -> Result<Self> {
635 let mut warnings = vec![];
639 let toml_de = toml::de::Deserializer::new(&contents);
640 let mut manifest: Self = serde_ignored::deserialize(toml_de, |path| {
641 let warning = format!("unused manifest key: {path}");
642 warnings.push(warning);
643 })
644 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
645 for warning in warnings {
646 println_warning(&warning);
647 }
648 manifest.implicitly_include_std_if_missing();
649 manifest.implicitly_include_default_build_profiles_if_missing();
650 manifest.validate()?;
651 Ok(manifest)
652 }
653
654 pub fn validate(&self) -> Result<()> {
662 validate_project_name(&self.project.name)?;
663 if let Some(ref org) = self.project.organization {
664 validate_name(org, "organization name")?;
665 }
666 for (dep_name, dependency_details) in self.deps_detailed() {
667 dependency_details.validate()?;
668 if dependency_details
669 .package
670 .as_ref()
671 .is_some_and(|package_alias| package_alias == &self.project.name)
672 {
673 bail!(format!("Dependency \"{dep_name}\" declares an alias (\"package\" field) that is the same as project name"))
674 }
675 if dep_name == &self.project.name {
676 bail!(format!(
677 "Dependency \"{dep_name}\" collides with project name."
678 ))
679 }
680 }
681 Ok(())
682 }
683
684 pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
689 let dir = dir.as_ref();
690 let manifest_dir =
691 find_parent_manifest_dir(dir).ok_or_else(|| manifest_file_missing(dir))?;
692 let file_path = manifest_dir.join(constants::MANIFEST_FILE_NAME);
693 Self::from_file(file_path)
694 }
695
696 pub fn deps(&self) -> impl Iterator<Item = (&String, &Dependency)> {
698 self.dependencies
699 .as_ref()
700 .into_iter()
701 .flat_map(|deps| deps.iter())
702 }
703
704 pub fn build_profiles(&self) -> impl Iterator<Item = (&String, &BuildProfile)> {
706 self.build_profile
707 .as_ref()
708 .into_iter()
709 .flat_map(|deps| deps.iter())
710 }
711
712 pub fn contract_deps(&self) -> impl Iterator<Item = (&String, &ContractDependency)> {
714 self.contract_dependencies
715 .as_ref()
716 .into_iter()
717 .flat_map(|deps| deps.iter())
718 }
719
720 pub fn deps_detailed(&self) -> impl Iterator<Item = (&String, &DependencyDetails)> {
722 self.deps().filter_map(|(name, dep)| match dep {
723 Dependency::Detailed(ref det) => Some((name, det)),
724 Dependency::Simple(_) => None,
725 })
726 }
727
728 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
730 self.patch
731 .as_ref()
732 .into_iter()
733 .flat_map(|patches| patches.iter())
734 }
735
736 pub fn patch(&self, patch_name: &str) -> Option<&PatchMap> {
738 self.patch
739 .as_ref()
740 .and_then(|patches| patches.get(patch_name))
741 }
742
743 pub fn proxy(&self) -> Option<&Proxy> {
745 self.proxy.as_ref()
746 }
747
748 fn implicitly_include_std_if_missing(&mut self) {
754 use sway_types::constants::STD;
755 if self.project.name == STD
760 || self.pkg_dep(STD).is_some()
761 || self.dep(STD).is_some()
762 || !self.project.implicit_std.unwrap_or(true)
763 {
764 return;
765 }
766 let deps = self.dependencies.get_or_insert_with(Default::default);
768 let std_dep = implicit_std_dep();
770 deps.insert(STD.to_string(), std_dep);
771 }
772
773 fn implicitly_include_default_build_profiles_if_missing(&mut self) {
777 let build_profiles = self.build_profile.get_or_insert_with(Default::default);
778
779 if build_profiles.get(BuildProfile::DEBUG).is_none() {
780 build_profiles.insert(BuildProfile::DEBUG.into(), BuildProfile::debug());
781 }
782 if build_profiles.get(BuildProfile::RELEASE).is_none() {
783 build_profiles.insert(BuildProfile::RELEASE.into(), BuildProfile::release());
784 }
785 }
786
787 pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
789 self.dependencies
790 .as_ref()
791 .and_then(|deps| deps.get(dep_name))
792 }
793
794 pub fn dep_detailed(&self, dep_name: &str) -> Option<&DependencyDetails> {
796 self.dep(dep_name).and_then(|dep| match dep {
797 Dependency::Simple(_) => None,
798 Dependency::Detailed(detailed) => Some(detailed),
799 })
800 }
801
802 pub fn contract_dep(&self, contract_dep_name: &str) -> Option<&ContractDependency> {
804 self.contract_dependencies
805 .as_ref()
806 .and_then(|contract_dependencies| contract_dependencies.get(contract_dep_name))
807 }
808
809 pub fn contract_dependency_detailed(
811 &self,
812 contract_dep_name: &str,
813 ) -> Option<&DependencyDetails> {
814 self.contract_dep(contract_dep_name)
815 .and_then(|contract_dep| match &contract_dep.dependency {
816 Dependency::Simple(_) => None,
817 Dependency::Detailed(detailed) => Some(detailed),
818 })
819 }
820
821 fn pkg_dep<'a>(&'a self, pkg_name: &str) -> Option<&'a str> {
826 for (dep_name, dep) in self.deps() {
827 if dep.package().unwrap_or(dep_name) == pkg_name {
828 return Some(dep_name);
829 }
830 }
831 None
832 }
833}
834
835impl std::ops::Deref for PackageManifestFile {
836 type Target = PackageManifest;
837 fn deref(&self) -> &Self::Target {
838 &self.manifest
839 }
840}
841
842fn implicit_std_dep() -> Dependency {
849 if let Ok(path) = std::env::var("FORC_IMPLICIT_STD_PATH") {
850 return Dependency::Detailed(DependencyDetails {
851 path: Some(path),
852 ..Default::default()
853 });
854 }
855
856 let tag = std::env::var("FORC_IMPLICIT_STD_GIT_TAG")
865 .ok()
866 .unwrap_or_else(|| format!("v{}", env!("CARGO_PKG_VERSION")));
867 const SWAY_GIT_REPO_URL: &str = "https://github.com/fuellabs/sway";
868
869 let branch = std::env::var("FORC_IMPLICIT_STD_GIT_BRANCH").ok();
871 let tag = branch.as_ref().map_or_else(|| Some(tag), |_| None);
872
873 let mut det = DependencyDetails {
874 git: std::env::var("FORC_IMPLICIT_STD_GIT")
875 .ok()
876 .or_else(|| Some(SWAY_GIT_REPO_URL.to_string())),
877 tag,
878 branch,
879 ..Default::default()
880 };
881
882 if let Some((_, build_metadata)) = det.tag.as_ref().and_then(|tag| tag.split_once('+')) {
883 let rev = build_metadata.split('.').last().map(|r| r.to_string());
885
886 det.tag = None;
889 det.rev = rev;
890 };
891
892 Dependency::Detailed(det)
893}
894
895fn default_entry() -> String {
896 PackageManifest::DEFAULT_ENTRY_FILE_NAME.to_string()
897}
898
899fn default_url() -> String {
900 constants::DEFAULT_NODE_URL.into()
901}
902
903#[derive(Clone, Debug)]
905pub struct WorkspaceManifestFile {
906 manifest: WorkspaceManifest,
908 path: PathBuf,
910}
911
912#[derive(Serialize, Deserialize, Clone, Debug)]
914#[serde(rename_all = "kebab-case")]
915pub struct WorkspaceManifest {
916 workspace: Workspace,
917 patch: Option<BTreeMap<String, PatchMap>>,
918}
919
920#[derive(Serialize, Deserialize, Clone, Debug)]
921#[serde(rename_all = "kebab-case")]
922pub struct Workspace {
923 pub members: Vec<PathBuf>,
924 pub metadata: Option<toml::Value>,
925}
926
927impl WorkspaceManifestFile {
928 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
930 self.patch
931 .as_ref()
932 .into_iter()
933 .flat_map(|patches| patches.iter())
934 }
935
936 pub fn members(&self) -> impl Iterator<Item = &PathBuf> + '_ {
938 self.workspace.members.iter()
939 }
940
941 pub fn member_paths(&self) -> Result<impl Iterator<Item = PathBuf> + '_> {
945 Ok(self
946 .workspace
947 .members
948 .iter()
949 .map(|member| self.dir().join(member)))
950 }
951
952 pub fn member_pkg_manifests(
954 &self,
955 ) -> Result<impl Iterator<Item = Result<PackageManifestFile>> + '_> {
956 let member_paths = self.member_paths()?;
957 let member_pkg_manifests = member_paths.map(PackageManifestFile::from_dir);
958 Ok(member_pkg_manifests)
959 }
960
961 pub fn is_member_path(&self, path: &Path) -> Result<bool> {
963 Ok(self.member_paths()?.any(|member_path| member_path == path))
964 }
965}
966
967impl GenericManifestFile for WorkspaceManifestFile {
968 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
973 let path = path.as_ref().canonicalize()?;
974 let parent = path
975 .parent()
976 .ok_or_else(|| anyhow!("Cannot get parent dir of {:?}", path))?;
977 let manifest = WorkspaceManifest::from_file(&path)?;
978 manifest.validate(parent)?;
979 Ok(Self { manifest, path })
980 }
981
982 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
988 let manifest_dir = manifest_dir.as_ref();
989 let dir = find_parent_manifest_dir_with_check(manifest_dir, |possible_manifest_dir| {
990 let possible_path = possible_manifest_dir.join(constants::MANIFEST_FILE_NAME);
993 Self::from_file(possible_path)
1002 .err()
1003 .map(|e| !e.to_string().contains("missing field `workspace`"))
1004 .unwrap_or_else(|| true)
1005 })
1006 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
1007 let path = dir.join(constants::MANIFEST_FILE_NAME);
1008 Self::from_file(path)
1009 }
1010
1011 fn path(&self) -> &Path {
1012 &self.path
1013 }
1014
1015 fn lock_path(&self) -> Result<PathBuf> {
1019 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
1020 }
1021
1022 fn member_manifests(&self) -> Result<MemberManifestFiles> {
1023 let mut member_manifest_files = BTreeMap::new();
1024 for member_manifest in self.member_pkg_manifests()? {
1025 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
1026 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
1027 }
1028
1029 Ok(member_manifest_files)
1030 }
1031}
1032
1033impl WorkspaceManifest {
1034 pub fn from_file(path: &Path) -> Result<Self> {
1036 let mut warnings = vec![];
1040 let manifest_str = std::fs::read_to_string(path)
1041 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
1042 let toml_de = toml::de::Deserializer::new(&manifest_str);
1043 let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
1044 let warning = format!("unused manifest key: {path}");
1045 warnings.push(warning);
1046 })
1047 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
1048 for warning in warnings {
1049 println_warning(&warning);
1050 }
1051 Ok(manifest)
1052 }
1053
1054 pub fn validate(&self, path: &Path) -> Result<()> {
1058 let mut pkg_name_to_paths: HashMap<String, Vec<PathBuf>> = HashMap::new();
1059 for member in &self.workspace.members {
1060 let member_path = path.join(member).join("Forc.toml");
1061 if !member_path.exists() {
1062 bail!(
1063 "{:?} is listed as a member of the workspace but {:?} does not exists",
1064 &member,
1065 member_path
1066 );
1067 }
1068 if Self::from_file(&member_path).is_ok() {
1069 bail!("Unexpected nested workspace '{}'. Workspaces are currently only allowed in the project root.", member.display());
1070 };
1071
1072 let member_manifest_file = PackageManifestFile::from_file(member_path.clone())?;
1073 let pkg_name = member_manifest_file.manifest.project.name;
1074 pkg_name_to_paths
1075 .entry(pkg_name)
1076 .or_default()
1077 .push(member_path);
1078 }
1079
1080 let duplicate_pkg_lines = pkg_name_to_paths
1082 .iter()
1083 .filter_map(|(pkg_name, paths)| {
1084 if paths.len() > 1 {
1085 let duplicate_paths = pkg_name_to_paths
1086 .get(pkg_name)
1087 .expect("missing duplicate paths");
1088 Some(format!("{pkg_name}: {duplicate_paths:#?}"))
1089 } else {
1090 None
1091 }
1092 })
1093 .collect::<Vec<_>>();
1094
1095 if !duplicate_pkg_lines.is_empty() {
1096 let error_message = duplicate_pkg_lines.join("\n");
1097 bail!(
1098 "Duplicate package names detected in the workspace:\n\n{}",
1099 error_message
1100 );
1101 }
1102 Ok(())
1103 }
1104}
1105
1106impl std::ops::Deref for WorkspaceManifestFile {
1107 type Target = WorkspaceManifest;
1108 fn deref(&self) -> &Self::Target {
1109 &self.manifest
1110 }
1111}
1112
1113pub fn find_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1117 use crate::source::reg::REG_DIR_NAME;
1118 use sway_types::constants::STD;
1119 const SWAY_STD_FOLDER: &str = "sway-lib-std";
1120 walkdir::WalkDir::new(dir)
1121 .into_iter()
1122 .filter_map(|entry| {
1123 entry
1124 .ok()
1125 .filter(|entry| entry.path().ends_with(constants::MANIFEST_FILE_NAME))
1126 })
1127 .find_map(|entry| {
1128 let path = entry.path();
1129 let manifest = PackageManifest::from_file(path).ok()?;
1130 if (manifest.project.name == pkg_name && pkg_name != STD)
1134 || (manifest.project.name == STD
1135 && path.components().any(|comp| {
1136 comp.as_os_str() == SWAY_STD_FOLDER || comp.as_os_str() == REG_DIR_NAME
1137 }))
1138 {
1139 Some(path.to_path_buf())
1140 } else {
1141 None
1142 }
1143 })
1144}
1145
1146pub fn find_dir_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1148 find_within(dir, pkg_name).and_then(|path| path.parent().map(Path::to_path_buf))
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use std::str::FromStr;
1154
1155 use super::*;
1156
1157 #[test]
1158 fn deserialize_contract_dependency() {
1159 let contract_dep_str = r#"{"path": "../", "salt": "0x1111111111111111111111111111111111111111111111111111111111111111" }"#;
1160
1161 let contract_dep_expected: ContractDependency =
1162 serde_json::from_str(contract_dep_str).unwrap();
1163
1164 let dependency_det = DependencyDetails {
1165 path: Some("../".to_owned()),
1166 ..Default::default()
1167 };
1168 let dependency = Dependency::Detailed(dependency_det);
1169 let contract_dep = ContractDependency {
1170 dependency,
1171 salt: HexSalt::from_str(
1172 "0x1111111111111111111111111111111111111111111111111111111111111111",
1173 )
1174 .unwrap(),
1175 };
1176 assert_eq!(contract_dep, contract_dep_expected)
1177 }
1178 #[test]
1179 fn test_invalid_dependency_details_mixed_together() {
1180 let dependency_details_path_branch = DependencyDetails {
1181 version: None,
1182 path: Some("example_path/".to_string()),
1183 git: None,
1184 branch: Some("test_branch".to_string()),
1185 tag: None,
1186 package: None,
1187 rev: None,
1188 ipfs: None,
1189 namespace: None,
1190 };
1191
1192 let dependency_details_branch = DependencyDetails {
1193 path: None,
1194 ..dependency_details_path_branch.clone()
1195 };
1196
1197 let dependency_details_ipfs_branch = DependencyDetails {
1198 path: None,
1199 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1200 ..dependency_details_path_branch.clone()
1201 };
1202
1203 let dependency_details_path_tag = DependencyDetails {
1204 version: None,
1205 path: Some("example_path/".to_string()),
1206 git: None,
1207 branch: None,
1208 tag: Some("v0.1.0".to_string()),
1209 package: None,
1210 rev: None,
1211 ipfs: None,
1212 namespace: None,
1213 };
1214
1215 let dependency_details_tag = DependencyDetails {
1216 path: None,
1217 ..dependency_details_path_tag.clone()
1218 };
1219
1220 let dependency_details_ipfs_tag = DependencyDetails {
1221 path: None,
1222 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1223 ..dependency_details_path_branch.clone()
1224 };
1225
1226 let dependency_details_path_rev = DependencyDetails {
1227 version: None,
1228 path: Some("example_path/".to_string()),
1229 git: None,
1230 branch: None,
1231 tag: None,
1232 package: None,
1233 ipfs: None,
1234 rev: Some("9f35b8e".to_string()),
1235 namespace: None,
1236 };
1237
1238 let dependency_details_rev = DependencyDetails {
1239 path: None,
1240 ..dependency_details_path_rev.clone()
1241 };
1242
1243 let dependency_details_ipfs_rev = DependencyDetails {
1244 path: None,
1245 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1246 ..dependency_details_path_branch.clone()
1247 };
1248
1249 let expected_mismatch_error = "Details reserved for git sources used without a git field";
1250 assert_eq!(
1251 dependency_details_path_branch
1252 .validate()
1253 .err()
1254 .map(|e| e.to_string()),
1255 Some(expected_mismatch_error.to_string())
1256 );
1257 assert_eq!(
1258 dependency_details_ipfs_branch
1259 .validate()
1260 .err()
1261 .map(|e| e.to_string()),
1262 Some(expected_mismatch_error.to_string())
1263 );
1264 assert_eq!(
1265 dependency_details_path_tag
1266 .validate()
1267 .err()
1268 .map(|e| e.to_string()),
1269 Some(expected_mismatch_error.to_string())
1270 );
1271 assert_eq!(
1272 dependency_details_ipfs_tag
1273 .validate()
1274 .err()
1275 .map(|e| e.to_string()),
1276 Some(expected_mismatch_error.to_string())
1277 );
1278 assert_eq!(
1279 dependency_details_path_rev
1280 .validate()
1281 .err()
1282 .map(|e| e.to_string()),
1283 Some(expected_mismatch_error.to_string())
1284 );
1285 assert_eq!(
1286 dependency_details_ipfs_rev
1287 .validate()
1288 .err()
1289 .map(|e| e.to_string()),
1290 Some(expected_mismatch_error.to_string())
1291 );
1292 assert_eq!(
1293 dependency_details_branch
1294 .validate()
1295 .err()
1296 .map(|e| e.to_string()),
1297 Some(expected_mismatch_error.to_string())
1298 );
1299 assert_eq!(
1300 dependency_details_tag
1301 .validate()
1302 .err()
1303 .map(|e| e.to_string()),
1304 Some(expected_mismatch_error.to_string())
1305 );
1306 assert_eq!(
1307 dependency_details_rev
1308 .validate()
1309 .err()
1310 .map(|e| e.to_string()),
1311 Some(expected_mismatch_error.to_string())
1312 );
1313 }
1314 #[test]
1315 #[should_panic(expected = "Namespace can only be specified for sources with version")]
1316 fn test_error_namespace_without_version() {
1317 PackageManifest::from_dir("./tests/invalid/namespace_without_version").unwrap();
1318 }
1319
1320 #[test]
1321 #[should_panic(expected = "Both version and git details provided for same dependency")]
1322 fn test_error_version_with_git_for_same_dep() {
1323 PackageManifest::from_dir("./tests/invalid/version_and_git_same_dep").unwrap();
1324 }
1325
1326 #[test]
1327 #[should_panic(expected = "Both version and ipfs details provided for same dependency")]
1328 fn test_error_version_with_ipfs_for_same_dep() {
1329 PackageManifest::from_dir("./tests/invalid/version_and_ipfs_same_dep").unwrap();
1330 }
1331
1332 #[test]
1333 #[should_panic(expected = "duplicate key `foo` in table `dependencies`")]
1334 fn test_error_duplicate_deps_definition() {
1335 PackageManifest::from_dir("./tests/invalid/duplicate_keys").unwrap();
1336 }
1337
1338 #[test]
1339 fn test_error_duplicate_deps_definition_in_workspace() {
1340 let workspace =
1346 WorkspaceManifestFile::from_dir("./tests/invalid/patch_workspace_and_package").unwrap();
1347 let projects: Vec<_> = workspace
1348 .member_pkg_manifests()
1349 .unwrap()
1350 .collect::<Result<Vec<_>, _>>()
1351 .unwrap();
1352 assert_eq!(projects.len(), 1);
1353 let patches: Vec<_> = projects[0].resolve_patches().unwrap().collect();
1354 assert_eq!(patches.len(), 0);
1355
1356 let patches: Vec<_> = PackageManifestFile::from_dir("./tests/test_package")
1359 .unwrap()
1360 .resolve_patches()
1361 .unwrap()
1362 .collect();
1363 assert_eq!(patches.len(), 1);
1364 }
1365
1366 #[test]
1367 fn test_valid_dependency_details() {
1368 let dependency_details_path = DependencyDetails {
1369 version: None,
1370 path: Some("example_path/".to_string()),
1371 git: None,
1372 branch: None,
1373 tag: None,
1374 package: None,
1375 rev: None,
1376 ipfs: None,
1377 namespace: None,
1378 };
1379
1380 let git_source_string = "https://github.com/FuelLabs/sway".to_string();
1381 let dependency_details_git_tag = DependencyDetails {
1382 version: None,
1383 path: None,
1384 git: Some(git_source_string.clone()),
1385 branch: None,
1386 tag: Some("v0.1.0".to_string()),
1387 package: None,
1388 rev: None,
1389 ipfs: None,
1390 namespace: None,
1391 };
1392 let dependency_details_git_branch = DependencyDetails {
1393 version: None,
1394 path: None,
1395 git: Some(git_source_string.clone()),
1396 branch: Some("test_branch".to_string()),
1397 tag: None,
1398 package: None,
1399 rev: None,
1400 ipfs: None,
1401 namespace: None,
1402 };
1403 let dependency_details_git_rev = DependencyDetails {
1404 version: None,
1405 path: None,
1406 git: Some(git_source_string),
1407 branch: Some("test_branch".to_string()),
1408 tag: None,
1409 package: None,
1410 rev: Some("9f35b8e".to_string()),
1411 ipfs: None,
1412 namespace: None,
1413 };
1414
1415 let dependency_details_ipfs = DependencyDetails {
1416 version: None,
1417 path: None,
1418 git: None,
1419 branch: None,
1420 tag: None,
1421 package: None,
1422 rev: None,
1423 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1424 namespace: None,
1425 };
1426
1427 assert!(dependency_details_path.validate().is_ok());
1428 assert!(dependency_details_git_tag.validate().is_ok());
1429 assert!(dependency_details_git_branch.validate().is_ok());
1430 assert!(dependency_details_git_rev.validate().is_ok());
1431 assert!(dependency_details_ipfs.validate().is_ok());
1432 }
1433
1434 #[test]
1435 fn test_project_with_null_metadata() {
1436 let project = Project {
1437 authors: Some(vec!["Test Author".to_string()]),
1438 name: "test-project".to_string(),
1439 version: Some(Version::parse("0.1.0").unwrap()),
1440 description: Some("test description".to_string()),
1441 homepage: None,
1442 documentation: None,
1443 categories: None,
1444 keywords: None,
1445 repository: None,
1446 organization: None,
1447 license: "Apache-2.0".to_string(),
1448 entry: "main.sw".to_string(),
1449 implicit_std: None,
1450 forc_version: None,
1451 experimental: HashMap::new(),
1452 metadata: Some(toml::Value::from(toml::value::Table::new())),
1453 force_dbg_in_release: None,
1454 };
1455
1456 let serialized = toml::to_string(&project).unwrap();
1457 let deserialized: Project = toml::from_str(&serialized).unwrap();
1458
1459 assert_eq!(project.name, deserialized.name);
1460 assert_eq!(project.metadata, deserialized.metadata);
1461 }
1462
1463 #[test]
1464 fn test_project_without_metadata() {
1465 let project = Project {
1466 authors: Some(vec!["Test Author".to_string()]),
1467 name: "test-project".to_string(),
1468 version: Some(Version::parse("0.1.0").unwrap()),
1469 description: Some("test description".to_string()),
1470 homepage: Some(Url::parse("https://example.com").unwrap()),
1471 documentation: Some(Url::parse("https://docs.example.com").unwrap()),
1472 categories: Some(vec!["test-category".to_string()]),
1473 keywords: Some(vec!["test-keyword".to_string()]),
1474 repository: Some(Url::parse("https://example.com").unwrap()),
1475 organization: None,
1476 license: "Apache-2.0".to_string(),
1477 entry: "main.sw".to_string(),
1478 implicit_std: None,
1479 forc_version: None,
1480 experimental: HashMap::new(),
1481 metadata: None,
1482 force_dbg_in_release: None,
1483 };
1484
1485 let serialized = toml::to_string(&project).unwrap();
1486 let deserialized: Project = toml::from_str(&serialized).unwrap();
1487
1488 assert_eq!(project.name, deserialized.name);
1489 assert_eq!(project.version, deserialized.version);
1490 assert_eq!(project.description, deserialized.description);
1491 assert_eq!(project.homepage, deserialized.homepage);
1492 assert_eq!(project.documentation, deserialized.documentation);
1493 assert_eq!(project.repository, deserialized.repository);
1494 assert_eq!(project.metadata, deserialized.metadata);
1495 assert_eq!(project.metadata, None);
1496 assert_eq!(project.categories, deserialized.categories);
1497 assert_eq!(project.keywords, deserialized.keywords);
1498 }
1499
1500 #[test]
1501 fn test_project_metadata_from_toml() {
1502 let toml_str = r#"
1503 name = "test-project"
1504 license = "Apache-2.0"
1505 entry = "main.sw"
1506 authors = ["Test Author"]
1507 description = "A test project"
1508 version = "1.0.0"
1509 keywords = ["test", "project"]
1510 categories = ["test"]
1511
1512 [metadata]
1513 mykey = "https://example.com"
1514 "#;
1515
1516 let project: Project = toml::from_str(toml_str).unwrap();
1517 assert!(project.metadata.is_some());
1518
1519 let metadata = project.metadata.unwrap();
1520 let table = metadata.as_table().unwrap();
1521
1522 assert_eq!(
1523 table.get("mykey").unwrap().as_str().unwrap(),
1524 "https://example.com"
1525 );
1526 }
1527
1528 #[test]
1529 fn test_project_with_invalid_metadata() {
1530 let invalid_toml = r#"
1532 name = "test-project"
1533 license = "Apache-2.0"
1534 entry = "main.sw"
1535
1536 [metadata
1537 description = "Invalid TOML"
1538 "#;
1539
1540 let result: Result<Project, _> = toml::from_str(invalid_toml);
1541 assert!(result.is_err());
1542
1543 let invalid_toml = r#"
1545 name = "test-project"
1546 license = "Apache-2.0"
1547 entry = "main.sw"
1548
1549 [metadata]
1550 ] = "Invalid key"
1551 "#;
1552
1553 let result: Result<Project, _> = toml::from_str(invalid_toml);
1554 assert!(result.is_err());
1555
1556 let invalid_toml = r#"
1558 name = "test-project"
1559 license = "Apache-2.0"
1560 entry = "main.sw"
1561
1562 [metadata]
1563 nested = { key = "value1" }
1564
1565 [metadata.nested]
1566 key = "value2"
1567 "#;
1568
1569 let result: Result<Project, _> = toml::from_str(invalid_toml);
1570 assert!(result.is_err());
1571 assert!(result
1572 .err()
1573 .unwrap()
1574 .to_string()
1575 .contains("duplicate key `nested` in table `metadata`"));
1576 }
1577
1578 #[test]
1579 fn test_metadata_roundtrip() {
1580 let original_toml = r#"
1581 name = "test-project"
1582 license = "Apache-2.0"
1583 entry = "main.sw"
1584
1585 [metadata]
1586 boolean = true
1587 integer = 42
1588 float = 3.12
1589 string = "value"
1590 array = [1, 2, 3]
1591 mixed_array = [1, "two", true]
1592
1593 [metadata.nested]
1594 key = "value2"
1595 "#;
1596
1597 let project: Project = toml::from_str(original_toml).unwrap();
1598 let serialized = toml::to_string(&project).unwrap();
1599 let deserialized: Project = toml::from_str(&serialized).unwrap();
1600
1601 assert_eq!(project.metadata, deserialized.metadata);
1603
1604 let table_val = project.metadata.unwrap();
1606 let table = table_val.as_table().unwrap();
1607 assert!(table.get("boolean").unwrap().as_bool().unwrap());
1608 assert_eq!(table.get("integer").unwrap().as_integer().unwrap(), 42);
1609 assert_eq!(table.get("float").unwrap().as_float().unwrap(), 3.12);
1610 assert_eq!(table.get("string").unwrap().as_str().unwrap(), "value");
1611 assert_eq!(table.get("array").unwrap().as_array().unwrap().len(), 3);
1612 assert!(table.get("nested").unwrap().as_table().is_some());
1613 }
1614
1615 #[test]
1616 fn test_workspace_with_metadata() {
1617 let toml_str = r#"
1618 [workspace]
1619 members = ["package1", "package2"]
1620
1621 [workspace.metadata]
1622 description = "A test workspace"
1623 version = "1.0.0"
1624 authors = ["Test Author"]
1625 homepage = "https://example.com"
1626
1627 [workspace.metadata.ci]
1628 workflow = "main"
1629 timeout = 3600
1630 "#;
1631
1632 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1633 assert!(manifest.workspace.metadata.is_some());
1634
1635 let metadata = manifest.workspace.metadata.unwrap();
1636 let table = metadata.as_table().unwrap();
1637
1638 assert_eq!(
1639 table.get("description").unwrap().as_str().unwrap(),
1640 "A test workspace"
1641 );
1642 assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0");
1643
1644 let ci = table.get("ci").unwrap().as_table().unwrap();
1645 assert_eq!(ci.get("workflow").unwrap().as_str().unwrap(), "main");
1646 assert_eq!(ci.get("timeout").unwrap().as_integer().unwrap(), 3600);
1647 }
1648
1649 #[test]
1650 fn test_workspace_without_metadata() {
1651 let toml_str = r#"
1652 [workspace]
1653 members = ["package1", "package2"]
1654 "#;
1655
1656 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1657 assert!(manifest.workspace.metadata.is_none());
1658 }
1659
1660 #[test]
1661 fn test_workspace_empty_metadata() {
1662 let toml_str = r#"
1663 [workspace]
1664 members = ["package1", "package2"]
1665
1666 [workspace.metadata]
1667 "#;
1668
1669 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1670 assert!(manifest.workspace.metadata.is_some());
1671 let metadata = manifest.workspace.metadata.unwrap();
1672 assert!(metadata.as_table().unwrap().is_empty());
1673 }
1674
1675 #[test]
1676 fn test_workspace_complex_metadata() {
1677 let toml_str = r#"
1678 [workspace]
1679 members = ["package1", "package2"]
1680
1681 [workspace.metadata]
1682 numbers = [1, 2, 3]
1683 strings = ["a", "b", "c"]
1684 mixed = [1, "two", true]
1685
1686 [workspace.metadata.nested]
1687 key = "value"
1688
1689 [workspace.metadata.nested.deep]
1690 another = "value"
1691 "#;
1692
1693 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1694 let metadata = manifest.workspace.metadata.unwrap();
1695 let table = metadata.as_table().unwrap();
1696
1697 assert!(table.get("numbers").unwrap().as_array().is_some());
1698 assert!(table.get("strings").unwrap().as_array().is_some());
1699 assert!(table.get("mixed").unwrap().as_array().is_some());
1700
1701 let nested = table.get("nested").unwrap().as_table().unwrap();
1702 assert_eq!(nested.get("key").unwrap().as_str().unwrap(), "value");
1703
1704 let deep = nested.get("deep").unwrap().as_table().unwrap();
1705 assert_eq!(deep.get("another").unwrap().as_str().unwrap(), "value");
1706 }
1707
1708 #[test]
1709 fn test_workspace_metadata_roundtrip() {
1710 let original = WorkspaceManifest {
1711 workspace: Workspace {
1712 members: vec![PathBuf::from("package1"), PathBuf::from("package2")],
1713 metadata: Some(toml::Value::Table({
1714 let mut table = toml::value::Table::new();
1715 table.insert("key".to_string(), toml::Value::String("value".to_string()));
1716 table
1717 })),
1718 },
1719 patch: None,
1720 };
1721
1722 let serialized = toml::to_string(&original).unwrap();
1723 let deserialized: WorkspaceManifest = toml::from_str(&serialized).unwrap();
1724
1725 assert_eq!(original.workspace.members, deserialized.workspace.members);
1726 assert_eq!(original.workspace.metadata, deserialized.workspace.metadata);
1727 }
1728
1729 #[test]
1730 fn test_dependency_alias_project_name_collision() {
1731 let original_toml = r#"
1732 [project]
1733 authors = ["Fuel Labs <contact@fuel.sh>"]
1734 entry = "main.sw"
1735 license = "Apache-2.0"
1736 name = "lib_contract_abi"
1737
1738 [dependencies]
1739 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1740 "#;
1741
1742 let project = PackageManifest::from_string(original_toml.to_string());
1743 let err = project.unwrap_err();
1744 assert_eq!(err.to_string(), format!("Dependency \"lib_contract\" declares an alias (\"package\" field) that is the same as project name"))
1745 }
1746
1747 #[test]
1748 fn test_dependency_name_project_name_collision() {
1749 let original_toml = r#"
1750 [project]
1751 authors = ["Fuel Labs <contact@fuel.sh>"]
1752 entry = "main.sw"
1753 license = "Apache-2.0"
1754 name = "lib_contract"
1755
1756 [dependencies]
1757 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1758 "#;
1759
1760 let project = PackageManifest::from_string(original_toml.to_string());
1761 let err = project.unwrap_err();
1762 assert_eq!(
1763 err.to_string(),
1764 format!("Dependency \"lib_contract\" collides with project name.")
1765 )
1766 }
1767}