1pub mod build_profile;
2pub mod dep_modifier;
3
4use crate::pkg::{manifest_file_missing, parsing_failed, wrong_program_type};
5use anyhow::{anyhow, bail, Context, Result};
6use forc_tracing::println_warning;
7use forc_util::{validate_name, validate_project_name};
8use semver::Version;
9use serde::{de, Deserialize, Serialize};
10use serde_with::{serde_as, DisplayFromStr};
11use std::{
12 collections::{BTreeMap, HashMap},
13 fmt::Display,
14 path::{Path, PathBuf},
15 str::FromStr,
16};
17use sway_core::{fuel_prelude::fuel_tx, language::parsed::TreeType, parse_tree_type, BuildTarget};
18use sway_error::handler::Handler;
19use sway_types::span::Source;
20use sway_utils::{
21 constants, find_nested_manifest_dir, find_parent_manifest_dir,
22 find_parent_manifest_dir_with_check,
23};
24use url::Url;
25
26use self::build_profile::BuildProfile;
27
28pub type MemberName = String;
30pub type MemberManifestFiles = BTreeMap<MemberName, PackageManifestFile>;
32
33pub trait GenericManifestFile {
34 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self>
35 where
36 Self: Sized;
37 fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self>
38 where
39 Self: Sized;
40
41 fn path(&self) -> &Path;
45
46 fn dir(&self) -> &Path {
50 self.path()
51 .parent()
52 .expect("failed to retrieve manifest directory")
53 }
54
55 fn lock_path(&self) -> Result<PathBuf>;
57
58 fn member_manifests(&self) -> Result<MemberManifestFiles>;
60}
61
62#[derive(Clone, Debug)]
63pub enum ManifestFile {
64 Package(Box<PackageManifestFile>),
65 Workspace(WorkspaceManifestFile),
66}
67
68impl ManifestFile {
69 pub fn is_workspace(&self) -> bool {
70 matches!(self, ManifestFile::Workspace(_))
71 }
72
73 pub fn root_dir(&self) -> PathBuf {
74 match self {
75 ManifestFile::Package(pkg_manifest_file) => pkg_manifest_file
76 .workspace()
77 .ok()
78 .flatten()
79 .map(|ws| ws.dir().to_path_buf())
80 .unwrap_or_else(|| pkg_manifest_file.dir().to_path_buf()),
81 ManifestFile::Workspace(workspace_manifest_file) => {
82 workspace_manifest_file.dir().to_path_buf()
83 }
84 }
85 }
86}
87
88impl GenericManifestFile for ManifestFile {
89 fn from_dir<P: AsRef<Path>>(path: P) -> Result<Self> {
92 let maybe_pkg_manifest = PackageManifestFile::from_dir(path.as_ref());
93 let manifest_file = if let Err(e) = maybe_pkg_manifest {
94 if e.to_string().contains("missing field `project`") {
95 let workspace_manifest_file = WorkspaceManifestFile::from_dir(path.as_ref())?;
97 ManifestFile::Workspace(workspace_manifest_file)
98 } else {
99 bail!("{}", e)
100 }
101 } else if let Ok(pkg_manifest) = maybe_pkg_manifest {
102 ManifestFile::Package(Box::new(pkg_manifest))
103 } else {
104 bail!(
105 "Cannot find a valid `Forc.toml` at {}",
106 path.as_ref().to_string_lossy()
107 )
108 };
109 Ok(manifest_file)
110 }
111
112 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
115 let maybe_pkg_manifest = PackageManifestFile::from_file(path.as_ref());
116 let manifest_file = if let Err(e) = maybe_pkg_manifest {
117 if e.to_string().contains("missing field `project`") {
118 let workspace_manifest_file = WorkspaceManifestFile::from_file(path.as_ref())?;
120 ManifestFile::Workspace(workspace_manifest_file)
121 } else {
122 bail!("{}", e)
123 }
124 } else if let Ok(pkg_manifest) = maybe_pkg_manifest {
125 ManifestFile::Package(Box::new(pkg_manifest))
126 } else {
127 bail!(
128 "Cannot find a valid `Forc.toml` at {}",
129 path.as_ref().to_string_lossy()
130 )
131 };
132 Ok(manifest_file)
133 }
134
135 fn path(&self) -> &Path {
139 match self {
140 ManifestFile::Package(pkg_manifest_file) => pkg_manifest_file.path(),
141 ManifestFile::Workspace(workspace_manifest_file) => workspace_manifest_file.path(),
142 }
143 }
144
145 fn member_manifests(&self) -> Result<MemberManifestFiles> {
146 match self {
147 ManifestFile::Package(pkg_manifest_file) => pkg_manifest_file.member_manifests(),
148 ManifestFile::Workspace(workspace_manifest_file) => {
149 workspace_manifest_file.member_manifests()
150 }
151 }
152 }
153
154 fn lock_path(&self) -> Result<PathBuf> {
156 match self {
157 ManifestFile::Package(pkg_manifest) => pkg_manifest.lock_path(),
158 ManifestFile::Workspace(workspace_manifest) => workspace_manifest.lock_path(),
159 }
160 }
161}
162
163impl TryInto<PackageManifestFile> for ManifestFile {
164 type Error = anyhow::Error;
165
166 fn try_into(self) -> Result<PackageManifestFile> {
167 match self {
168 ManifestFile::Package(pkg_manifest_file) => Ok(*pkg_manifest_file),
169 ManifestFile::Workspace(_) => {
170 bail!("Cannot convert workspace manifest to package manifest")
171 }
172 }
173 }
174}
175
176impl TryInto<WorkspaceManifestFile> for ManifestFile {
177 type Error = anyhow::Error;
178
179 fn try_into(self) -> Result<WorkspaceManifestFile> {
180 match self {
181 ManifestFile::Package(_) => {
182 bail!("Cannot convert package manifest to workspace manifest")
183 }
184 ManifestFile::Workspace(workspace_manifest_file) => Ok(workspace_manifest_file),
185 }
186 }
187}
188
189type PatchMap = BTreeMap<String, Dependency>;
190
191#[derive(Clone, Debug, PartialEq)]
193pub struct PackageManifestFile {
194 manifest: PackageManifest,
196 path: PathBuf,
198}
199
200#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
202#[serde(rename_all = "kebab-case")]
203pub struct PackageManifest {
204 pub project: Project,
205 pub network: Option<Network>,
206 pub dependencies: Option<BTreeMap<String, Dependency>>,
207 pub patch: Option<BTreeMap<String, PatchMap>>,
208 pub build_target: Option<BTreeMap<String, BuildTarget>>,
210 build_profile: Option<BTreeMap<String, BuildProfile>>,
211 pub contract_dependencies: Option<BTreeMap<String, ContractDependency>>,
212 pub proxy: Option<Proxy>,
213}
214
215#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
216#[serde(rename_all = "kebab-case")]
217pub struct Project {
218 pub authors: Option<Vec<String>>,
219 #[serde(deserialize_with = "validate_package_name")]
220 pub name: String,
221 pub version: Option<Version>,
222 pub description: Option<String>,
223 pub organization: Option<String>,
224 pub license: String,
225 pub homepage: Option<Url>,
226 pub repository: Option<Url>,
227 pub documentation: Option<Url>,
228 pub categories: Option<Vec<String>>,
229 pub keywords: Option<Vec<String>>,
230 #[serde(default = "default_entry")]
231 pub entry: String,
232 pub implicit_std: Option<bool>,
233 pub forc_version: Option<semver::Version>,
234 #[serde(default)]
235 pub experimental: HashMap<String, bool>,
236 pub metadata: Option<toml::Value>,
237 pub force_dbg_in_release: Option<bool>,
238}
239
240fn validate_package_name<'de, D>(deserializer: D) -> Result<String, D::Error>
242where
243 D: de::Deserializer<'de>,
244{
245 let name: String = Deserialize::deserialize(deserializer)?;
246 match validate_project_name(&name) {
247 Ok(_) => Ok(name),
248 Err(e) => Err(de::Error::custom(e.to_string())),
249 }
250}
251
252#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
253#[serde(rename_all = "kebab-case")]
254pub struct Network {
255 #[serde(default = "default_url")]
256 pub url: String,
257}
258
259#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
260pub struct HexSalt(pub fuel_tx::Salt);
261
262impl FromStr for HexSalt {
263 type Err = anyhow::Error;
264
265 fn from_str(s: &str) -> Result<Self, Self::Err> {
266 let normalized = s
268 .strip_prefix("0x")
269 .ok_or_else(|| anyhow::anyhow!("hex salt declaration needs to start with 0x"))?;
270 let salt: fuel_tx::Salt =
271 fuel_tx::Salt::from_str(normalized).map_err(|e| anyhow::anyhow!("{e}"))?;
272 let hex_salt = Self(salt);
273 Ok(hex_salt)
274 }
275}
276
277impl Display for HexSalt {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 let salt = self.0;
280 write!(f, "{}", salt)
281 }
282}
283
284fn default_hex_salt() -> HexSalt {
285 HexSalt(fuel_tx::Salt::default())
286}
287
288#[serde_as]
289#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
290#[serde(rename_all = "kebab-case")]
291pub struct ContractDependency {
292 #[serde(flatten)]
293 pub dependency: Dependency,
294 #[serde_as(as = "DisplayFromStr")]
295 #[serde(default = "default_hex_salt")]
296 pub salt: HexSalt,
297}
298
299#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
300#[serde(untagged)]
301pub enum Dependency {
302 Simple(String),
305 Detailed(DependencyDetails),
309}
310
311#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
312#[serde(rename_all = "kebab-case")]
313pub struct DependencyDetails {
314 pub(crate) version: Option<String>,
315 pub(crate) namespace: Option<String>,
316 pub path: Option<String>,
317 pub(crate) git: Option<String>,
318 pub(crate) branch: Option<String>,
319 pub(crate) tag: Option<String>,
320 pub(crate) package: Option<String>,
321 pub(crate) rev: Option<String>,
322 pub(crate) ipfs: Option<String>,
323}
324
325#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
327#[serde(rename_all = "kebab-case")]
328pub struct Proxy {
329 pub enabled: bool,
330 pub address: Option<String>,
334}
335
336impl DependencyDetails {
337 pub fn validate(&self) -> anyhow::Result<()> {
343 let DependencyDetails {
344 git,
345 branch,
346 tag,
347 rev,
348 version,
349 ipfs,
350 namespace,
351 path,
352 ..
353 } = self;
354
355 if git.is_none() && (branch.is_some() || tag.is_some() || rev.is_some()) {
356 bail!("Details reserved for git sources used without a git field");
357 }
358
359 if git.is_some() && branch.is_some() && tag.is_some() && rev.is_some() {
360 bail!("Cannot specify `branch`, `tag`, and `rev` together for dependency with a Git source");
361 }
362
363 if git.is_some() && branch.is_some() && tag.is_some() {
364 bail!("Cannot specify both `branch` and `tag` for dependency with a Git source");
365 }
366
367 if git.is_some() && rev.is_some() && tag.is_some() {
368 bail!("Cannot specify both `rev` and `tag` for dependency with a Git source");
369 }
370
371 if git.is_some() && branch.is_some() && rev.is_some() {
372 bail!("Cannot specify both `branch` and `rev` for dependency with a Git source");
373 }
374
375 if version.is_some() && git.is_some() {
376 bail!("Both version and git details provided for same dependency");
377 }
378
379 if version.is_some() && ipfs.is_some() {
380 bail!("Both version and ipfs details provided for same dependency");
381 }
382
383 if version.is_none() && namespace.is_some() {
384 bail!("Namespace can only be specified for sources with version");
385 }
386
387 if version.is_some() && path.is_some() {
388 bail!("Both version and path details provided for same dependency");
389 }
390
391 Ok(())
392 }
393
394 pub fn is_source_empty(&self) -> bool {
395 self.git.is_none() && self.path.is_none() && self.ipfs.is_none()
396 }
397}
398
399impl Dependency {
400 pub fn package(&self) -> Option<&str> {
402 match *self {
403 Self::Simple(_) => None,
404 Self::Detailed(ref det) => det.package.as_deref(),
405 }
406 }
407
408 pub fn version(&self) -> Option<&str> {
410 match *self {
411 Self::Simple(ref version) => Some(version),
412 Self::Detailed(ref det) => det.version.as_deref(),
413 }
414 }
415}
416
417impl PackageManifestFile {
418 fn resolve_patches(&self) -> Result<impl Iterator<Item = (String, PatchMap)>> {
425 if let Some(workspace) = self.workspace().ok().flatten() {
426 if self.patch.is_some() {
428 println_warning("Patch for the non root package will be ignored.");
429 println_warning(&format!(
430 "Specify patch at the workspace root: {}",
431 workspace.path().to_str().unwrap_or_default()
432 ));
433 }
434 Ok(workspace
435 .patch
436 .as_ref()
437 .cloned()
438 .unwrap_or_default()
439 .into_iter())
440 } else {
441 Ok(self.patch.as_ref().cloned().unwrap_or_default().into_iter())
442 }
443 }
444
445 pub fn resolve_patch(&self, patch_name: &str) -> Result<Option<PatchMap>> {
451 Ok(self
452 .resolve_patches()?
453 .find(|(p_name, _)| patch_name == p_name.as_str())
454 .map(|(_, patch)| patch))
455 }
456
457 pub fn entry_path(&self) -> PathBuf {
462 self.dir()
463 .join(constants::SRC_DIR)
464 .join(&self.project.entry)
465 }
466
467 pub fn entry_string(&self) -> Result<Source> {
469 let entry_path = self.entry_path();
470 let entry_string = std::fs::read_to_string(entry_path)?;
471 Ok(entry_string.as_str().into())
472 }
473
474 pub fn program_type(&self) -> Result<TreeType> {
476 let entry_string = self.entry_string()?;
477 let handler = Handler::default();
478 let parse_res = parse_tree_type(&handler, entry_string);
479
480 parse_res.map_err(|_| {
481 let (errors, _warnings) = handler.consume();
482 parsing_failed(&self.project.name, &errors)
483 })
484 }
485
486 pub fn check_program_type(&self, expected_types: &[TreeType]) -> Result<()> {
489 let parsed_type = self.program_type()?;
490 if !expected_types.contains(&parsed_type) {
491 bail!(wrong_program_type(
492 &self.project.name,
493 expected_types,
494 parsed_type
495 ));
496 } else {
497 Ok(())
498 }
499 }
500
501 pub fn build_profile(&self, profile_name: &str) -> Option<&BuildProfile> {
503 self.build_profile
504 .as_ref()
505 .and_then(|profiles| profiles.get(profile_name))
506 }
507
508 pub fn dep_path(&self, dep_name: &str) -> Option<PathBuf> {
510 let dir = self.dir();
511 let details = self.dep_detailed(dep_name)?;
512 details.path.as_ref().and_then(|path_str| {
513 let path = Path::new(path_str);
514 match path.is_absolute() {
515 true => Some(path.to_owned()),
516 false => dir.join(path).canonicalize().ok(),
517 }
518 })
519 }
520
521 pub fn workspace(&self) -> Result<Option<WorkspaceManifestFile>> {
523 let parent_dir = match self.dir().parent() {
524 None => return Ok(None),
525 Some(dir) => dir,
526 };
527 let ws_manifest = match WorkspaceManifestFile::from_dir(parent_dir) {
528 Ok(manifest) => manifest,
529 Err(e) => {
530 if e.to_string().contains("could not find") {
534 return Ok(None);
535 } else {
536 return Err(e);
537 }
538 }
539 };
540 if ws_manifest.is_member_path(self.dir())? {
541 Ok(Some(ws_manifest))
542 } else {
543 Ok(None)
544 }
545 }
546
547 pub fn project_name(&self) -> &str {
549 &self.project.name
550 }
551
552 pub fn validate(&self) -> Result<()> {
558 self.manifest.validate()?;
559 let mut entry_path = self.path.clone();
560 entry_path.pop();
561 let entry_path = entry_path
562 .join(constants::SRC_DIR)
563 .join(&self.project.entry);
564 if !entry_path.exists() {
565 bail!(
566 "failed to validate path from entry field {:?} in Forc manifest file.",
567 self.project.entry
568 )
569 }
570
571 let mut pkg_dir = self.path.to_path_buf();
577 pkg_dir.pop();
578 if let Some(nested_package) = find_nested_manifest_dir(&pkg_dir) {
579 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())
581 }
582 Ok(())
583 }
584}
585
586impl GenericManifestFile for PackageManifestFile {
587 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
596 let path = path.as_ref().canonicalize()?;
597 let manifest = PackageManifest::from_file(&path)?;
598 let manifest_file = Self { manifest, path };
599 manifest_file.validate()?;
600 Ok(manifest_file)
601 }
602
603 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
609 let manifest_dir = manifest_dir.as_ref();
610 let dir = find_parent_manifest_dir(manifest_dir)
611 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
612 let path = dir.join(constants::MANIFEST_FILE_NAME);
613 Self::from_file(path)
614 }
615
616 fn path(&self) -> &Path {
617 &self.path
618 }
619
620 fn lock_path(&self) -> Result<PathBuf> {
626 let workspace_manifest = self.workspace()?;
628 if let Some(workspace_manifest) = workspace_manifest {
629 workspace_manifest.lock_path()
630 } else {
631 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
632 }
633 }
634
635 fn member_manifests(&self) -> Result<MemberManifestFiles> {
636 let mut member_manifest_files = BTreeMap::new();
637 if let Some(workspace_manifest_file) = self.workspace()? {
639 for member_manifest in workspace_manifest_file.member_pkg_manifests()? {
640 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
641 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
642 }
643 } else {
644 let member_name = &self.project.name;
645 member_manifest_files.insert(member_name.clone(), self.clone());
646 }
647
648 Ok(member_manifest_files)
649 }
650}
651
652impl PackageManifest {
653 pub const DEFAULT_ENTRY_FILE_NAME: &'static str = "main.sw";
654
655 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
664 let path = path.as_ref();
668 let contents = std::fs::read_to_string(path)
669 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
670 Self::from_string(contents)
671 }
672
673 pub fn from_string(contents: String) -> Result<Self> {
682 let mut warnings = vec![];
686 let toml_de = toml::de::Deserializer::new(&contents);
687 let mut manifest: Self = serde_ignored::deserialize(toml_de, |path| {
688 let warning = format!("unused manifest key: {path}");
689 warnings.push(warning);
690 })
691 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
692 for warning in warnings {
693 println_warning(&warning);
694 }
695 manifest.implicitly_include_std_if_missing();
696 manifest.implicitly_include_default_build_profiles_if_missing();
697 manifest.validate()?;
698 Ok(manifest)
699 }
700
701 pub fn validate(&self) -> Result<()> {
709 validate_project_name(&self.project.name)?;
710 if let Some(ref org) = self.project.organization {
711 validate_name(org, "organization name")?;
712 }
713 for (dep_name, dependency_details) in self.deps_detailed() {
714 dependency_details.validate()?;
715 if dependency_details
716 .package
717 .as_ref()
718 .is_some_and(|package_alias| package_alias == &self.project.name)
719 {
720 bail!(format!("Dependency \"{dep_name}\" declares an alias (\"package\" field) that is the same as project name"))
721 }
722 if dep_name == &self.project.name {
723 bail!(format!(
724 "Dependency \"{dep_name}\" collides with project name."
725 ))
726 }
727 }
728 Ok(())
729 }
730
731 pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
736 let dir = dir.as_ref();
737 let manifest_dir =
738 find_parent_manifest_dir(dir).ok_or_else(|| manifest_file_missing(dir))?;
739 let file_path = manifest_dir.join(constants::MANIFEST_FILE_NAME);
740 Self::from_file(file_path)
741 }
742
743 pub fn deps(&self) -> impl Iterator<Item = (&String, &Dependency)> {
745 self.dependencies
746 .as_ref()
747 .into_iter()
748 .flat_map(|deps| deps.iter())
749 }
750
751 pub fn build_profiles(&self) -> impl Iterator<Item = (&String, &BuildProfile)> {
753 self.build_profile
754 .as_ref()
755 .into_iter()
756 .flat_map(|deps| deps.iter())
757 }
758
759 pub fn contract_deps(&self) -> impl Iterator<Item = (&String, &ContractDependency)> {
761 self.contract_dependencies
762 .as_ref()
763 .into_iter()
764 .flat_map(|deps| deps.iter())
765 }
766
767 pub fn deps_detailed(&self) -> impl Iterator<Item = (&String, &DependencyDetails)> {
769 self.deps().filter_map(|(name, dep)| match dep {
770 Dependency::Detailed(ref det) => Some((name, det)),
771 Dependency::Simple(_) => None,
772 })
773 }
774
775 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
777 self.patch
778 .as_ref()
779 .into_iter()
780 .flat_map(|patches| patches.iter())
781 }
782
783 pub fn patch(&self, patch_name: &str) -> Option<&PatchMap> {
785 self.patch
786 .as_ref()
787 .and_then(|patches| patches.get(patch_name))
788 }
789
790 pub fn proxy(&self) -> Option<&Proxy> {
792 self.proxy.as_ref()
793 }
794
795 fn implicitly_include_std_if_missing(&mut self) {
801 use sway_types::constants::STD;
802 if self.project.name == STD
807 || self.pkg_dep(STD).is_some()
808 || self.dep(STD).is_some()
809 || !self.project.implicit_std.unwrap_or(true)
810 {
811 return;
812 }
813 let deps = self.dependencies.get_or_insert_with(Default::default);
815 let std_dep = implicit_std_dep();
817 deps.insert(STD.to_string(), std_dep);
818 }
819
820 fn implicitly_include_default_build_profiles_if_missing(&mut self) {
824 let build_profiles = self.build_profile.get_or_insert_with(Default::default);
825
826 if build_profiles.get(BuildProfile::DEBUG).is_none() {
827 build_profiles.insert(BuildProfile::DEBUG.into(), BuildProfile::debug());
828 }
829 if build_profiles.get(BuildProfile::RELEASE).is_none() {
830 build_profiles.insert(BuildProfile::RELEASE.into(), BuildProfile::release());
831 }
832 }
833
834 pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
836 self.dependencies
837 .as_ref()
838 .and_then(|deps| deps.get(dep_name))
839 }
840
841 pub fn dep_detailed(&self, dep_name: &str) -> Option<&DependencyDetails> {
843 self.dep(dep_name).and_then(|dep| match dep {
844 Dependency::Simple(_) => None,
845 Dependency::Detailed(detailed) => Some(detailed),
846 })
847 }
848
849 pub fn contract_dep(&self, contract_dep_name: &str) -> Option<&ContractDependency> {
851 self.contract_dependencies
852 .as_ref()
853 .and_then(|contract_dependencies| contract_dependencies.get(contract_dep_name))
854 }
855
856 pub fn contract_dependency_detailed(
858 &self,
859 contract_dep_name: &str,
860 ) -> Option<&DependencyDetails> {
861 self.contract_dep(contract_dep_name)
862 .and_then(|contract_dep| match &contract_dep.dependency {
863 Dependency::Simple(_) => None,
864 Dependency::Detailed(detailed) => Some(detailed),
865 })
866 }
867
868 fn pkg_dep<'a>(&'a self, pkg_name: &str) -> Option<&'a str> {
873 for (dep_name, dep) in self.deps() {
874 if dep.package().unwrap_or(dep_name) == pkg_name {
875 return Some(dep_name);
876 }
877 }
878 None
879 }
880}
881
882impl std::ops::Deref for PackageManifestFile {
883 type Target = PackageManifest;
884 fn deref(&self) -> &Self::Target {
885 &self.manifest
886 }
887}
888
889fn implicit_std_dep() -> Dependency {
896 if let Ok(path) = std::env::var("FORC_IMPLICIT_STD_PATH") {
897 return Dependency::Detailed(DependencyDetails {
898 path: Some(path),
899 ..Default::default()
900 });
901 }
902
903 let tag = std::env::var("FORC_IMPLICIT_STD_GIT_TAG").ok();
904 let branch = std::env::var("FORC_IMPLICIT_STD_GIT_BRANCH").ok();
905 let git_target = std::env::var("FORC_IMPLICIT_STD_GIT").ok();
906
907 let det = if tag.is_some() || branch.is_some() || git_target.is_some() {
910 const SWAY_GIT_REPO_URL: &str = "https://github.com/fuellabs/sway";
911 let tag = tag.unwrap_or_else(|| format!("v{}", env!("CARGO_PKG_VERSION")));
920
921 let tag = branch.as_ref().map_or_else(|| Some(tag), |_| None);
923 let mut det = DependencyDetails {
924 git: git_target.or_else(|| Some(SWAY_GIT_REPO_URL.to_string())),
925 tag,
926 branch,
927 ..Default::default()
928 };
929
930 if let Some((_, build_metadata)) = det.tag.as_ref().and_then(|tag| tag.split_once('+')) {
931 let rev = build_metadata.split('.').next_back().map(|r| r.to_string());
933
934 det.tag = None;
937 det.rev = rev;
938 };
939 det
940 } else {
941 let current_version = env!("CARGO_PKG_VERSION").to_string();
942 DependencyDetails {
943 version: Some(current_version),
944 ..Default::default()
945 }
946 };
947
948 Dependency::Detailed(det)
949}
950
951fn default_entry() -> String {
952 PackageManifest::DEFAULT_ENTRY_FILE_NAME.to_string()
953}
954
955fn default_url() -> String {
956 constants::DEFAULT_NODE_URL.into()
957}
958
959#[derive(Clone, Debug)]
961pub struct WorkspaceManifestFile {
962 manifest: WorkspaceManifest,
964 path: PathBuf,
966}
967
968#[derive(Serialize, Deserialize, Clone, Debug)]
970#[serde(rename_all = "kebab-case")]
971pub struct WorkspaceManifest {
972 workspace: Workspace,
973 patch: Option<BTreeMap<String, PatchMap>>,
974}
975
976#[derive(Serialize, Deserialize, Clone, Debug)]
977#[serde(rename_all = "kebab-case")]
978pub struct Workspace {
979 pub members: Vec<PathBuf>,
980 pub metadata: Option<toml::Value>,
981}
982
983impl WorkspaceManifestFile {
984 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
986 self.patch
987 .as_ref()
988 .into_iter()
989 .flat_map(|patches| patches.iter())
990 }
991
992 pub fn members(&self) -> impl Iterator<Item = &PathBuf> + '_ {
994 self.workspace.members.iter()
995 }
996
997 pub fn member_paths(&self) -> Result<impl Iterator<Item = PathBuf> + '_> {
1001 Ok(self
1002 .workspace
1003 .members
1004 .iter()
1005 .map(|member| self.dir().join(member)))
1006 }
1007
1008 pub fn member_pkg_manifests(
1010 &self,
1011 ) -> Result<impl Iterator<Item = Result<PackageManifestFile>> + '_> {
1012 let member_paths = self.member_paths()?;
1013 let member_pkg_manifests = member_paths.map(PackageManifestFile::from_dir);
1014 Ok(member_pkg_manifests)
1015 }
1016
1017 pub fn is_member_path(&self, path: &Path) -> Result<bool> {
1019 Ok(self.member_paths()?.any(|member_path| member_path == path))
1020 }
1021}
1022
1023impl GenericManifestFile for WorkspaceManifestFile {
1024 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
1029 let path = path.as_ref().canonicalize()?;
1030 let parent = path
1031 .parent()
1032 .ok_or_else(|| anyhow!("Cannot get parent dir of {:?}", path))?;
1033 let manifest = WorkspaceManifest::from_file(&path)?;
1034 manifest.validate(parent)?;
1035 Ok(Self { manifest, path })
1036 }
1037
1038 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
1044 let manifest_dir = manifest_dir.as_ref();
1045 let dir = find_parent_manifest_dir_with_check(manifest_dir, |possible_manifest_dir| {
1046 let possible_path = possible_manifest_dir.join(constants::MANIFEST_FILE_NAME);
1049 Self::from_file(possible_path)
1058 .err()
1059 .map(|e| !e.to_string().contains("missing field `workspace`"))
1060 .unwrap_or_else(|| true)
1061 })
1062 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
1063 let path = dir.join(constants::MANIFEST_FILE_NAME);
1064 Self::from_file(path)
1065 }
1066
1067 fn path(&self) -> &Path {
1068 &self.path
1069 }
1070
1071 fn lock_path(&self) -> Result<PathBuf> {
1075 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
1076 }
1077
1078 fn member_manifests(&self) -> Result<MemberManifestFiles> {
1079 let mut member_manifest_files = BTreeMap::new();
1080 for member_manifest in self.member_pkg_manifests()? {
1081 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
1082 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
1083 }
1084
1085 Ok(member_manifest_files)
1086 }
1087}
1088
1089impl WorkspaceManifest {
1090 pub fn from_file(path: &Path) -> Result<Self> {
1092 let mut warnings = vec![];
1096 let manifest_str = std::fs::read_to_string(path)
1097 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
1098 let toml_de = toml::de::Deserializer::new(&manifest_str);
1099 let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
1100 let warning = format!("unused manifest key: {path}");
1101 warnings.push(warning);
1102 })
1103 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
1104 for warning in warnings {
1105 println_warning(&warning);
1106 }
1107 Ok(manifest)
1108 }
1109
1110 pub fn validate(&self, path: &Path) -> Result<()> {
1114 let mut pkg_name_to_paths: HashMap<String, Vec<PathBuf>> = HashMap::new();
1115 for member in &self.workspace.members {
1116 let member_path = path.join(member).join("Forc.toml");
1117 if !member_path.exists() {
1118 bail!(
1119 "{:?} is listed as a member of the workspace but {:?} does not exists",
1120 &member,
1121 member_path
1122 );
1123 }
1124 if Self::from_file(&member_path).is_ok() {
1125 bail!("Unexpected nested workspace '{}'. Workspaces are currently only allowed in the project root.", member.display());
1126 };
1127
1128 let member_manifest_file = PackageManifestFile::from_file(member_path.clone())?;
1129 let pkg_name = member_manifest_file.manifest.project.name;
1130 pkg_name_to_paths
1131 .entry(pkg_name)
1132 .or_default()
1133 .push(member_path);
1134 }
1135
1136 let duplicate_pkg_lines = pkg_name_to_paths
1138 .iter()
1139 .filter_map(|(pkg_name, paths)| {
1140 if paths.len() > 1 {
1141 let duplicate_paths = pkg_name_to_paths
1142 .get(pkg_name)
1143 .expect("missing duplicate paths");
1144 Some(format!("{pkg_name}: {duplicate_paths:#?}"))
1145 } else {
1146 None
1147 }
1148 })
1149 .collect::<Vec<_>>();
1150
1151 if !duplicate_pkg_lines.is_empty() {
1152 let error_message = duplicate_pkg_lines.join("\n");
1153 bail!(
1154 "Duplicate package names detected in the workspace:\n\n{}",
1155 error_message
1156 );
1157 }
1158 Ok(())
1159 }
1160}
1161
1162impl std::ops::Deref for WorkspaceManifestFile {
1163 type Target = WorkspaceManifest;
1164 fn deref(&self) -> &Self::Target {
1165 &self.manifest
1166 }
1167}
1168
1169pub fn find_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1173 use crate::source::reg::REG_DIR_NAME;
1174 use sway_types::constants::STD;
1175 const SWAY_STD_FOLDER: &str = "sway-lib-std";
1176 walkdir::WalkDir::new(dir)
1177 .into_iter()
1178 .filter_map(|entry| {
1179 entry
1180 .ok()
1181 .filter(|entry| entry.path().ends_with(constants::MANIFEST_FILE_NAME))
1182 })
1183 .find_map(|entry| {
1184 let path = entry.path();
1185 let manifest = PackageManifest::from_file(path).ok()?;
1186 if (manifest.project.name == pkg_name && pkg_name != STD)
1190 || (manifest.project.name == STD
1191 && path.components().any(|comp| {
1192 comp.as_os_str() == SWAY_STD_FOLDER || comp.as_os_str() == REG_DIR_NAME
1193 }))
1194 {
1195 Some(path.to_path_buf())
1196 } else {
1197 None
1198 }
1199 })
1200}
1201
1202pub fn find_dir_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1204 find_within(dir, pkg_name).and_then(|path| path.parent().map(Path::to_path_buf))
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use std::str::FromStr;
1210
1211 use super::*;
1212
1213 #[test]
1214 fn deserialize_contract_dependency() {
1215 let contract_dep_str = r#"{"path": "../", "salt": "0x1111111111111111111111111111111111111111111111111111111111111111" }"#;
1216
1217 let contract_dep_expected: ContractDependency =
1218 serde_json::from_str(contract_dep_str).unwrap();
1219
1220 let dependency_det = DependencyDetails {
1221 path: Some("../".to_owned()),
1222 ..Default::default()
1223 };
1224 let dependency = Dependency::Detailed(dependency_det);
1225 let contract_dep = ContractDependency {
1226 dependency,
1227 salt: HexSalt::from_str(
1228 "0x1111111111111111111111111111111111111111111111111111111111111111",
1229 )
1230 .unwrap(),
1231 };
1232 assert_eq!(contract_dep, contract_dep_expected)
1233 }
1234 #[test]
1235 fn test_invalid_dependency_details_mixed_together() {
1236 let dependency_details_path_branch = DependencyDetails {
1237 version: None,
1238 path: Some("example_path/".to_string()),
1239 git: None,
1240 branch: Some("test_branch".to_string()),
1241 tag: None,
1242 package: None,
1243 rev: None,
1244 ipfs: None,
1245 namespace: None,
1246 };
1247
1248 let dependency_details_branch = DependencyDetails {
1249 path: None,
1250 ..dependency_details_path_branch.clone()
1251 };
1252
1253 let dependency_details_ipfs_branch = DependencyDetails {
1254 path: None,
1255 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1256 ..dependency_details_path_branch.clone()
1257 };
1258
1259 let dependency_details_path_tag = DependencyDetails {
1260 version: None,
1261 path: Some("example_path/".to_string()),
1262 git: None,
1263 branch: None,
1264 tag: Some("v0.1.0".to_string()),
1265 package: None,
1266 rev: None,
1267 ipfs: None,
1268 namespace: None,
1269 };
1270
1271 let dependency_details_tag = DependencyDetails {
1272 path: None,
1273 ..dependency_details_path_tag.clone()
1274 };
1275
1276 let dependency_details_ipfs_tag = DependencyDetails {
1277 path: None,
1278 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1279 ..dependency_details_path_branch.clone()
1280 };
1281
1282 let dependency_details_path_rev = DependencyDetails {
1283 version: None,
1284 path: Some("example_path/".to_string()),
1285 git: None,
1286 branch: None,
1287 tag: None,
1288 package: None,
1289 ipfs: None,
1290 rev: Some("9f35b8e".to_string()),
1291 namespace: None,
1292 };
1293
1294 let dependency_details_rev = DependencyDetails {
1295 path: None,
1296 ..dependency_details_path_rev.clone()
1297 };
1298
1299 let dependency_details_ipfs_rev = DependencyDetails {
1300 path: None,
1301 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1302 ..dependency_details_path_branch.clone()
1303 };
1304
1305 let expected_mismatch_error = "Details reserved for git sources used without a git field";
1306 assert_eq!(
1307 dependency_details_path_branch
1308 .validate()
1309 .err()
1310 .map(|e| e.to_string()),
1311 Some(expected_mismatch_error.to_string())
1312 );
1313 assert_eq!(
1314 dependency_details_ipfs_branch
1315 .validate()
1316 .err()
1317 .map(|e| e.to_string()),
1318 Some(expected_mismatch_error.to_string())
1319 );
1320 assert_eq!(
1321 dependency_details_path_tag
1322 .validate()
1323 .err()
1324 .map(|e| e.to_string()),
1325 Some(expected_mismatch_error.to_string())
1326 );
1327 assert_eq!(
1328 dependency_details_ipfs_tag
1329 .validate()
1330 .err()
1331 .map(|e| e.to_string()),
1332 Some(expected_mismatch_error.to_string())
1333 );
1334 assert_eq!(
1335 dependency_details_path_rev
1336 .validate()
1337 .err()
1338 .map(|e| e.to_string()),
1339 Some(expected_mismatch_error.to_string())
1340 );
1341 assert_eq!(
1342 dependency_details_ipfs_rev
1343 .validate()
1344 .err()
1345 .map(|e| e.to_string()),
1346 Some(expected_mismatch_error.to_string())
1347 );
1348 assert_eq!(
1349 dependency_details_branch
1350 .validate()
1351 .err()
1352 .map(|e| e.to_string()),
1353 Some(expected_mismatch_error.to_string())
1354 );
1355 assert_eq!(
1356 dependency_details_tag
1357 .validate()
1358 .err()
1359 .map(|e| e.to_string()),
1360 Some(expected_mismatch_error.to_string())
1361 );
1362 assert_eq!(
1363 dependency_details_rev
1364 .validate()
1365 .err()
1366 .map(|e| e.to_string()),
1367 Some(expected_mismatch_error.to_string())
1368 );
1369 }
1370 #[test]
1371 #[should_panic(expected = "Namespace can only be specified for sources with version")]
1372 fn test_error_namespace_without_version() {
1373 PackageManifest::from_dir("./tests/invalid/namespace_without_version").unwrap();
1374 }
1375
1376 #[test]
1377 #[should_panic(expected = "Both version and git details provided for same dependency")]
1378 fn test_error_version_with_git_for_same_dep() {
1379 PackageManifest::from_dir("./tests/invalid/version_and_git_same_dep").unwrap();
1380 }
1381
1382 #[test]
1383 #[should_panic(expected = "Both version and ipfs details provided for same dependency")]
1384 fn test_error_version_with_ipfs_for_same_dep() {
1385 PackageManifest::from_dir("./tests/invalid/version_and_ipfs_same_dep").unwrap();
1386 }
1387
1388 #[test]
1389 #[should_panic(expected = "duplicate key `foo` in table `dependencies`")]
1390 fn test_error_duplicate_deps_definition() {
1391 PackageManifest::from_dir("./tests/invalid/duplicate_keys").unwrap();
1392 }
1393
1394 #[test]
1395 fn test_error_duplicate_deps_definition_in_workspace() {
1396 let workspace =
1402 WorkspaceManifestFile::from_dir("./tests/invalid/patch_workspace_and_package").unwrap();
1403 let projects: Vec<_> = workspace
1404 .member_pkg_manifests()
1405 .unwrap()
1406 .collect::<Result<Vec<_>, _>>()
1407 .unwrap();
1408 assert_eq!(projects.len(), 1);
1409 let patches: Vec<_> = projects[0].resolve_patches().unwrap().collect();
1410 assert_eq!(patches.len(), 0);
1411
1412 let patches: Vec<_> = PackageManifestFile::from_dir("./tests/test_package")
1415 .unwrap()
1416 .resolve_patches()
1417 .unwrap()
1418 .collect();
1419 assert_eq!(patches.len(), 1);
1420 }
1421
1422 #[test]
1423 fn test_valid_dependency_details() {
1424 let dependency_details_path = DependencyDetails {
1425 version: None,
1426 path: Some("example_path/".to_string()),
1427 git: None,
1428 branch: None,
1429 tag: None,
1430 package: None,
1431 rev: None,
1432 ipfs: None,
1433 namespace: None,
1434 };
1435
1436 let git_source_string = "https://github.com/FuelLabs/sway".to_string();
1437 let dependency_details_git_tag = DependencyDetails {
1438 version: None,
1439 path: None,
1440 git: Some(git_source_string.clone()),
1441 branch: None,
1442 tag: Some("v0.1.0".to_string()),
1443 package: None,
1444 rev: None,
1445 ipfs: None,
1446 namespace: None,
1447 };
1448 let dependency_details_git_branch = DependencyDetails {
1449 version: None,
1450 path: None,
1451 git: Some(git_source_string.clone()),
1452 branch: Some("test_branch".to_string()),
1453 tag: None,
1454 package: None,
1455 rev: None,
1456 ipfs: None,
1457 namespace: None,
1458 };
1459 let dependency_details_git_rev = DependencyDetails {
1460 version: None,
1461 path: None,
1462 git: Some(git_source_string),
1463 branch: None,
1464 tag: None,
1465 package: None,
1466 rev: Some("9f35b8e".to_string()),
1467 ipfs: None,
1468 namespace: None,
1469 };
1470
1471 let dependency_details_ipfs = DependencyDetails {
1472 version: None,
1473 path: None,
1474 git: None,
1475 branch: None,
1476 tag: None,
1477 package: None,
1478 rev: None,
1479 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1480 namespace: None,
1481 };
1482
1483 assert!(dependency_details_path.validate().is_ok());
1484 assert!(dependency_details_git_tag.validate().is_ok());
1485 assert!(dependency_details_git_branch.validate().is_ok());
1486 assert!(dependency_details_git_rev.validate().is_ok());
1487 assert!(dependency_details_ipfs.validate().is_ok());
1488 }
1489
1490 #[test]
1491 fn test_project_with_null_metadata() {
1492 let project = Project {
1493 authors: Some(vec!["Test Author".to_string()]),
1494 name: "test-project".to_string(),
1495 version: Some(Version::parse("0.1.0").unwrap()),
1496 description: Some("test description".to_string()),
1497 homepage: None,
1498 documentation: None,
1499 categories: None,
1500 keywords: None,
1501 repository: None,
1502 organization: None,
1503 license: "Apache-2.0".to_string(),
1504 entry: "main.sw".to_string(),
1505 implicit_std: None,
1506 forc_version: None,
1507 experimental: HashMap::new(),
1508 metadata: Some(toml::Value::from(toml::value::Table::new())),
1509 force_dbg_in_release: None,
1510 };
1511
1512 let serialized = toml::to_string(&project).unwrap();
1513 let deserialized: Project = toml::from_str(&serialized).unwrap();
1514
1515 assert_eq!(project.name, deserialized.name);
1516 assert_eq!(project.metadata, deserialized.metadata);
1517 }
1518
1519 #[test]
1520 fn test_project_without_metadata() {
1521 let project = Project {
1522 authors: Some(vec!["Test Author".to_string()]),
1523 name: "test-project".to_string(),
1524 version: Some(Version::parse("0.1.0").unwrap()),
1525 description: Some("test description".to_string()),
1526 homepage: Some(Url::parse("https://example.com").unwrap()),
1527 documentation: Some(Url::parse("https://docs.example.com").unwrap()),
1528 categories: Some(vec!["test-category".to_string()]),
1529 keywords: Some(vec!["test-keyword".to_string()]),
1530 repository: Some(Url::parse("https://example.com").unwrap()),
1531 organization: None,
1532 license: "Apache-2.0".to_string(),
1533 entry: "main.sw".to_string(),
1534 implicit_std: None,
1535 forc_version: None,
1536 experimental: HashMap::new(),
1537 metadata: None,
1538 force_dbg_in_release: None,
1539 };
1540
1541 let serialized = toml::to_string(&project).unwrap();
1542 let deserialized: Project = toml::from_str(&serialized).unwrap();
1543
1544 assert_eq!(project.name, deserialized.name);
1545 assert_eq!(project.version, deserialized.version);
1546 assert_eq!(project.description, deserialized.description);
1547 assert_eq!(project.homepage, deserialized.homepage);
1548 assert_eq!(project.documentation, deserialized.documentation);
1549 assert_eq!(project.repository, deserialized.repository);
1550 assert_eq!(project.metadata, deserialized.metadata);
1551 assert_eq!(project.metadata, None);
1552 assert_eq!(project.categories, deserialized.categories);
1553 assert_eq!(project.keywords, deserialized.keywords);
1554 }
1555
1556 #[test]
1557 fn test_project_metadata_from_toml() {
1558 let toml_str = r#"
1559 name = "test-project"
1560 license = "Apache-2.0"
1561 entry = "main.sw"
1562 authors = ["Test Author"]
1563 description = "A test project"
1564 version = "1.0.0"
1565 keywords = ["test", "project"]
1566 categories = ["test"]
1567
1568 [metadata]
1569 mykey = "https://example.com"
1570 "#;
1571
1572 let project: Project = toml::from_str(toml_str).unwrap();
1573 assert!(project.metadata.is_some());
1574
1575 let metadata = project.metadata.unwrap();
1576 let table = metadata.as_table().unwrap();
1577
1578 assert_eq!(
1579 table.get("mykey").unwrap().as_str().unwrap(),
1580 "https://example.com"
1581 );
1582 }
1583
1584 #[test]
1585 fn test_project_with_invalid_metadata() {
1586 let invalid_toml = r#"
1588 name = "test-project"
1589 license = "Apache-2.0"
1590 entry = "main.sw"
1591
1592 [metadata
1593 description = "Invalid TOML"
1594 "#;
1595
1596 let result: Result<Project, _> = toml::from_str(invalid_toml);
1597 assert!(result.is_err());
1598
1599 let invalid_toml = r#"
1601 name = "test-project"
1602 license = "Apache-2.0"
1603 entry = "main.sw"
1604
1605 [metadata]
1606 ] = "Invalid key"
1607 "#;
1608
1609 let result: Result<Project, _> = toml::from_str(invalid_toml);
1610 assert!(result.is_err());
1611
1612 let invalid_toml = r#"
1614 name = "test-project"
1615 license = "Apache-2.0"
1616 entry = "main.sw"
1617
1618 [metadata]
1619 nested = { key = "value1" }
1620
1621 [metadata.nested]
1622 key = "value2"
1623 "#;
1624
1625 let result: Result<Project, _> = toml::from_str(invalid_toml);
1626 assert!(result.is_err());
1627 assert!(result
1628 .err()
1629 .unwrap()
1630 .to_string()
1631 .contains("duplicate key `nested` in table `metadata`"));
1632 }
1633
1634 #[test]
1635 fn test_metadata_roundtrip() {
1636 let original_toml = r#"
1637 name = "test-project"
1638 license = "Apache-2.0"
1639 entry = "main.sw"
1640
1641 [metadata]
1642 boolean = true
1643 integer = 42
1644 float = 3.12
1645 string = "value"
1646 array = [1, 2, 3]
1647 mixed_array = [1, "two", true]
1648
1649 [metadata.nested]
1650 key = "value2"
1651 "#;
1652
1653 let project: Project = toml::from_str(original_toml).unwrap();
1654 let serialized = toml::to_string(&project).unwrap();
1655 let deserialized: Project = toml::from_str(&serialized).unwrap();
1656
1657 assert_eq!(project.metadata, deserialized.metadata);
1659
1660 let table_val = project.metadata.unwrap();
1662 let table = table_val.as_table().unwrap();
1663 assert!(table.get("boolean").unwrap().as_bool().unwrap());
1664 assert_eq!(table.get("integer").unwrap().as_integer().unwrap(), 42);
1665 assert_eq!(table.get("float").unwrap().as_float().unwrap(), 3.12);
1666 assert_eq!(table.get("string").unwrap().as_str().unwrap(), "value");
1667 assert_eq!(table.get("array").unwrap().as_array().unwrap().len(), 3);
1668 assert!(table.get("nested").unwrap().as_table().is_some());
1669 }
1670
1671 #[test]
1672 fn test_workspace_with_metadata() {
1673 let toml_str = r#"
1674 [workspace]
1675 members = ["package1", "package2"]
1676
1677 [workspace.metadata]
1678 description = "A test workspace"
1679 version = "1.0.0"
1680 authors = ["Test Author"]
1681 homepage = "https://example.com"
1682
1683 [workspace.metadata.ci]
1684 workflow = "main"
1685 timeout = 3600
1686 "#;
1687
1688 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1689 assert!(manifest.workspace.metadata.is_some());
1690
1691 let metadata = manifest.workspace.metadata.unwrap();
1692 let table = metadata.as_table().unwrap();
1693
1694 assert_eq!(
1695 table.get("description").unwrap().as_str().unwrap(),
1696 "A test workspace"
1697 );
1698 assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0");
1699
1700 let ci = table.get("ci").unwrap().as_table().unwrap();
1701 assert_eq!(ci.get("workflow").unwrap().as_str().unwrap(), "main");
1702 assert_eq!(ci.get("timeout").unwrap().as_integer().unwrap(), 3600);
1703 }
1704
1705 #[test]
1706 fn test_workspace_without_metadata() {
1707 let toml_str = r#"
1708 [workspace]
1709 members = ["package1", "package2"]
1710 "#;
1711
1712 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1713 assert!(manifest.workspace.metadata.is_none());
1714 }
1715
1716 #[test]
1717 fn test_workspace_empty_metadata() {
1718 let toml_str = r#"
1719 [workspace]
1720 members = ["package1", "package2"]
1721
1722 [workspace.metadata]
1723 "#;
1724
1725 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1726 assert!(manifest.workspace.metadata.is_some());
1727 let metadata = manifest.workspace.metadata.unwrap();
1728 assert!(metadata.as_table().unwrap().is_empty());
1729 }
1730
1731 #[test]
1732 fn test_workspace_complex_metadata() {
1733 let toml_str = r#"
1734 [workspace]
1735 members = ["package1", "package2"]
1736
1737 [workspace.metadata]
1738 numbers = [1, 2, 3]
1739 strings = ["a", "b", "c"]
1740 mixed = [1, "two", true]
1741
1742 [workspace.metadata.nested]
1743 key = "value"
1744
1745 [workspace.metadata.nested.deep]
1746 another = "value"
1747 "#;
1748
1749 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1750 let metadata = manifest.workspace.metadata.unwrap();
1751 let table = metadata.as_table().unwrap();
1752
1753 assert!(table.get("numbers").unwrap().as_array().is_some());
1754 assert!(table.get("strings").unwrap().as_array().is_some());
1755 assert!(table.get("mixed").unwrap().as_array().is_some());
1756
1757 let nested = table.get("nested").unwrap().as_table().unwrap();
1758 assert_eq!(nested.get("key").unwrap().as_str().unwrap(), "value");
1759
1760 let deep = nested.get("deep").unwrap().as_table().unwrap();
1761 assert_eq!(deep.get("another").unwrap().as_str().unwrap(), "value");
1762 }
1763
1764 #[test]
1765 fn test_workspace_metadata_roundtrip() {
1766 let original = WorkspaceManifest {
1767 workspace: Workspace {
1768 members: vec![PathBuf::from("package1"), PathBuf::from("package2")],
1769 metadata: Some(toml::Value::Table({
1770 let mut table = toml::value::Table::new();
1771 table.insert("key".to_string(), toml::Value::String("value".to_string()));
1772 table
1773 })),
1774 },
1775 patch: None,
1776 };
1777
1778 let serialized = toml::to_string(&original).unwrap();
1779 let deserialized: WorkspaceManifest = toml::from_str(&serialized).unwrap();
1780
1781 assert_eq!(original.workspace.members, deserialized.workspace.members);
1782 assert_eq!(original.workspace.metadata, deserialized.workspace.metadata);
1783 }
1784
1785 #[test]
1786 fn test_dependency_alias_project_name_collision() {
1787 let original_toml = r#"
1788 [project]
1789 authors = ["Fuel Labs <contact@fuel.sh>"]
1790 entry = "main.sw"
1791 license = "Apache-2.0"
1792 name = "lib_contract_abi"
1793
1794 [dependencies]
1795 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1796 "#;
1797
1798 let project = PackageManifest::from_string(original_toml.to_string());
1799 let err = project.unwrap_err();
1800 assert_eq!(err.to_string(), format!("Dependency \"lib_contract\" declares an alias (\"package\" field) that is the same as project name"))
1801 }
1802
1803 #[test]
1804 fn test_dependency_name_project_name_collision() {
1805 let original_toml = r#"
1806 [project]
1807 authors = ["Fuel Labs <contact@fuel.sh>"]
1808 entry = "main.sw"
1809 license = "Apache-2.0"
1810 name = "lib_contract"
1811
1812 [dependencies]
1813 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1814 "#;
1815
1816 let project = PackageManifest::from_string(original_toml.to_string());
1817 let err = project.unwrap_err();
1818 assert_eq!(
1819 err.to_string(),
1820 format!("Dependency \"lib_contract\" collides with project name.")
1821 )
1822 }
1823}