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")
912 .ok()
913 .unwrap_or_else(|| format!("v{}", env!("CARGO_PKG_VERSION")));
914 const SWAY_GIT_REPO_URL: &str = "https://github.com/fuellabs/sway";
915
916 let branch = std::env::var("FORC_IMPLICIT_STD_GIT_BRANCH").ok();
918 let tag = branch.as_ref().map_or_else(|| Some(tag), |_| None);
919
920 let mut det = DependencyDetails {
921 git: std::env::var("FORC_IMPLICIT_STD_GIT")
922 .ok()
923 .or_else(|| Some(SWAY_GIT_REPO_URL.to_string())),
924 tag,
925 branch,
926 ..Default::default()
927 };
928
929 if let Some((_, build_metadata)) = det.tag.as_ref().and_then(|tag| tag.split_once('+')) {
930 let rev = build_metadata.split('.').next_back().map(|r| r.to_string());
932
933 det.tag = None;
936 det.rev = rev;
937 };
938
939 Dependency::Detailed(det)
940}
941
942fn default_entry() -> String {
943 PackageManifest::DEFAULT_ENTRY_FILE_NAME.to_string()
944}
945
946fn default_url() -> String {
947 constants::DEFAULT_NODE_URL.into()
948}
949
950#[derive(Clone, Debug)]
952pub struct WorkspaceManifestFile {
953 manifest: WorkspaceManifest,
955 path: PathBuf,
957}
958
959#[derive(Serialize, Deserialize, Clone, Debug)]
961#[serde(rename_all = "kebab-case")]
962pub struct WorkspaceManifest {
963 workspace: Workspace,
964 patch: Option<BTreeMap<String, PatchMap>>,
965}
966
967#[derive(Serialize, Deserialize, Clone, Debug)]
968#[serde(rename_all = "kebab-case")]
969pub struct Workspace {
970 pub members: Vec<PathBuf>,
971 pub metadata: Option<toml::Value>,
972}
973
974impl WorkspaceManifestFile {
975 pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
977 self.patch
978 .as_ref()
979 .into_iter()
980 .flat_map(|patches| patches.iter())
981 }
982
983 pub fn members(&self) -> impl Iterator<Item = &PathBuf> + '_ {
985 self.workspace.members.iter()
986 }
987
988 pub fn member_paths(&self) -> Result<impl Iterator<Item = PathBuf> + '_> {
992 Ok(self
993 .workspace
994 .members
995 .iter()
996 .map(|member| self.dir().join(member)))
997 }
998
999 pub fn member_pkg_manifests(
1001 &self,
1002 ) -> Result<impl Iterator<Item = Result<PackageManifestFile>> + '_> {
1003 let member_paths = self.member_paths()?;
1004 let member_pkg_manifests = member_paths.map(PackageManifestFile::from_dir);
1005 Ok(member_pkg_manifests)
1006 }
1007
1008 pub fn is_member_path(&self, path: &Path) -> Result<bool> {
1010 Ok(self.member_paths()?.any(|member_path| member_path == path))
1011 }
1012}
1013
1014impl GenericManifestFile for WorkspaceManifestFile {
1015 fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
1020 let path = path.as_ref().canonicalize()?;
1021 let parent = path
1022 .parent()
1023 .ok_or_else(|| anyhow!("Cannot get parent dir of {:?}", path))?;
1024 let manifest = WorkspaceManifest::from_file(&path)?;
1025 manifest.validate(parent)?;
1026 Ok(Self { manifest, path })
1027 }
1028
1029 fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
1035 let manifest_dir = manifest_dir.as_ref();
1036 let dir = find_parent_manifest_dir_with_check(manifest_dir, |possible_manifest_dir| {
1037 let possible_path = possible_manifest_dir.join(constants::MANIFEST_FILE_NAME);
1040 Self::from_file(possible_path)
1049 .err()
1050 .map(|e| !e.to_string().contains("missing field `workspace`"))
1051 .unwrap_or_else(|| true)
1052 })
1053 .ok_or_else(|| manifest_file_missing(manifest_dir))?;
1054 let path = dir.join(constants::MANIFEST_FILE_NAME);
1055 Self::from_file(path)
1056 }
1057
1058 fn path(&self) -> &Path {
1059 &self.path
1060 }
1061
1062 fn lock_path(&self) -> Result<PathBuf> {
1066 Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
1067 }
1068
1069 fn member_manifests(&self) -> Result<MemberManifestFiles> {
1070 let mut member_manifest_files = BTreeMap::new();
1071 for member_manifest in self.member_pkg_manifests()? {
1072 let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
1073 member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
1074 }
1075
1076 Ok(member_manifest_files)
1077 }
1078}
1079
1080impl WorkspaceManifest {
1081 pub fn from_file(path: &Path) -> Result<Self> {
1083 let mut warnings = vec![];
1087 let manifest_str = std::fs::read_to_string(path)
1088 .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
1089 let toml_de = toml::de::Deserializer::new(&manifest_str);
1090 let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
1091 let warning = format!("unused manifest key: {path}");
1092 warnings.push(warning);
1093 })
1094 .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
1095 for warning in warnings {
1096 println_warning(&warning);
1097 }
1098 Ok(manifest)
1099 }
1100
1101 pub fn validate(&self, path: &Path) -> Result<()> {
1105 let mut pkg_name_to_paths: HashMap<String, Vec<PathBuf>> = HashMap::new();
1106 for member in &self.workspace.members {
1107 let member_path = path.join(member).join("Forc.toml");
1108 if !member_path.exists() {
1109 bail!(
1110 "{:?} is listed as a member of the workspace but {:?} does not exists",
1111 &member,
1112 member_path
1113 );
1114 }
1115 if Self::from_file(&member_path).is_ok() {
1116 bail!("Unexpected nested workspace '{}'. Workspaces are currently only allowed in the project root.", member.display());
1117 };
1118
1119 let member_manifest_file = PackageManifestFile::from_file(member_path.clone())?;
1120 let pkg_name = member_manifest_file.manifest.project.name;
1121 pkg_name_to_paths
1122 .entry(pkg_name)
1123 .or_default()
1124 .push(member_path);
1125 }
1126
1127 let duplicate_pkg_lines = pkg_name_to_paths
1129 .iter()
1130 .filter_map(|(pkg_name, paths)| {
1131 if paths.len() > 1 {
1132 let duplicate_paths = pkg_name_to_paths
1133 .get(pkg_name)
1134 .expect("missing duplicate paths");
1135 Some(format!("{pkg_name}: {duplicate_paths:#?}"))
1136 } else {
1137 None
1138 }
1139 })
1140 .collect::<Vec<_>>();
1141
1142 if !duplicate_pkg_lines.is_empty() {
1143 let error_message = duplicate_pkg_lines.join("\n");
1144 bail!(
1145 "Duplicate package names detected in the workspace:\n\n{}",
1146 error_message
1147 );
1148 }
1149 Ok(())
1150 }
1151}
1152
1153impl std::ops::Deref for WorkspaceManifestFile {
1154 type Target = WorkspaceManifest;
1155 fn deref(&self) -> &Self::Target {
1156 &self.manifest
1157 }
1158}
1159
1160pub fn find_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1164 use crate::source::reg::REG_DIR_NAME;
1165 use sway_types::constants::STD;
1166 const SWAY_STD_FOLDER: &str = "sway-lib-std";
1167 walkdir::WalkDir::new(dir)
1168 .into_iter()
1169 .filter_map(|entry| {
1170 entry
1171 .ok()
1172 .filter(|entry| entry.path().ends_with(constants::MANIFEST_FILE_NAME))
1173 })
1174 .find_map(|entry| {
1175 let path = entry.path();
1176 let manifest = PackageManifest::from_file(path).ok()?;
1177 if (manifest.project.name == pkg_name && pkg_name != STD)
1181 || (manifest.project.name == STD
1182 && path.components().any(|comp| {
1183 comp.as_os_str() == SWAY_STD_FOLDER || comp.as_os_str() == REG_DIR_NAME
1184 }))
1185 {
1186 Some(path.to_path_buf())
1187 } else {
1188 None
1189 }
1190 })
1191}
1192
1193pub fn find_dir_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1195 find_within(dir, pkg_name).and_then(|path| path.parent().map(Path::to_path_buf))
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use std::str::FromStr;
1201
1202 use super::*;
1203
1204 #[test]
1205 fn deserialize_contract_dependency() {
1206 let contract_dep_str = r#"{"path": "../", "salt": "0x1111111111111111111111111111111111111111111111111111111111111111" }"#;
1207
1208 let contract_dep_expected: ContractDependency =
1209 serde_json::from_str(contract_dep_str).unwrap();
1210
1211 let dependency_det = DependencyDetails {
1212 path: Some("../".to_owned()),
1213 ..Default::default()
1214 };
1215 let dependency = Dependency::Detailed(dependency_det);
1216 let contract_dep = ContractDependency {
1217 dependency,
1218 salt: HexSalt::from_str(
1219 "0x1111111111111111111111111111111111111111111111111111111111111111",
1220 )
1221 .unwrap(),
1222 };
1223 assert_eq!(contract_dep, contract_dep_expected)
1224 }
1225 #[test]
1226 fn test_invalid_dependency_details_mixed_together() {
1227 let dependency_details_path_branch = DependencyDetails {
1228 version: None,
1229 path: Some("example_path/".to_string()),
1230 git: None,
1231 branch: Some("test_branch".to_string()),
1232 tag: None,
1233 package: None,
1234 rev: None,
1235 ipfs: None,
1236 namespace: None,
1237 };
1238
1239 let dependency_details_branch = DependencyDetails {
1240 path: None,
1241 ..dependency_details_path_branch.clone()
1242 };
1243
1244 let dependency_details_ipfs_branch = DependencyDetails {
1245 path: None,
1246 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1247 ..dependency_details_path_branch.clone()
1248 };
1249
1250 let dependency_details_path_tag = DependencyDetails {
1251 version: None,
1252 path: Some("example_path/".to_string()),
1253 git: None,
1254 branch: None,
1255 tag: Some("v0.1.0".to_string()),
1256 package: None,
1257 rev: None,
1258 ipfs: None,
1259 namespace: None,
1260 };
1261
1262 let dependency_details_tag = DependencyDetails {
1263 path: None,
1264 ..dependency_details_path_tag.clone()
1265 };
1266
1267 let dependency_details_ipfs_tag = DependencyDetails {
1268 path: None,
1269 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1270 ..dependency_details_path_branch.clone()
1271 };
1272
1273 let dependency_details_path_rev = DependencyDetails {
1274 version: None,
1275 path: Some("example_path/".to_string()),
1276 git: None,
1277 branch: None,
1278 tag: None,
1279 package: None,
1280 ipfs: None,
1281 rev: Some("9f35b8e".to_string()),
1282 namespace: None,
1283 };
1284
1285 let dependency_details_rev = DependencyDetails {
1286 path: None,
1287 ..dependency_details_path_rev.clone()
1288 };
1289
1290 let dependency_details_ipfs_rev = DependencyDetails {
1291 path: None,
1292 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1293 ..dependency_details_path_branch.clone()
1294 };
1295
1296 let expected_mismatch_error = "Details reserved for git sources used without a git field";
1297 assert_eq!(
1298 dependency_details_path_branch
1299 .validate()
1300 .err()
1301 .map(|e| e.to_string()),
1302 Some(expected_mismatch_error.to_string())
1303 );
1304 assert_eq!(
1305 dependency_details_ipfs_branch
1306 .validate()
1307 .err()
1308 .map(|e| e.to_string()),
1309 Some(expected_mismatch_error.to_string())
1310 );
1311 assert_eq!(
1312 dependency_details_path_tag
1313 .validate()
1314 .err()
1315 .map(|e| e.to_string()),
1316 Some(expected_mismatch_error.to_string())
1317 );
1318 assert_eq!(
1319 dependency_details_ipfs_tag
1320 .validate()
1321 .err()
1322 .map(|e| e.to_string()),
1323 Some(expected_mismatch_error.to_string())
1324 );
1325 assert_eq!(
1326 dependency_details_path_rev
1327 .validate()
1328 .err()
1329 .map(|e| e.to_string()),
1330 Some(expected_mismatch_error.to_string())
1331 );
1332 assert_eq!(
1333 dependency_details_ipfs_rev
1334 .validate()
1335 .err()
1336 .map(|e| e.to_string()),
1337 Some(expected_mismatch_error.to_string())
1338 );
1339 assert_eq!(
1340 dependency_details_branch
1341 .validate()
1342 .err()
1343 .map(|e| e.to_string()),
1344 Some(expected_mismatch_error.to_string())
1345 );
1346 assert_eq!(
1347 dependency_details_tag
1348 .validate()
1349 .err()
1350 .map(|e| e.to_string()),
1351 Some(expected_mismatch_error.to_string())
1352 );
1353 assert_eq!(
1354 dependency_details_rev
1355 .validate()
1356 .err()
1357 .map(|e| e.to_string()),
1358 Some(expected_mismatch_error.to_string())
1359 );
1360 }
1361 #[test]
1362 #[should_panic(expected = "Namespace can only be specified for sources with version")]
1363 fn test_error_namespace_without_version() {
1364 PackageManifest::from_dir("./tests/invalid/namespace_without_version").unwrap();
1365 }
1366
1367 #[test]
1368 #[should_panic(expected = "Both version and git details provided for same dependency")]
1369 fn test_error_version_with_git_for_same_dep() {
1370 PackageManifest::from_dir("./tests/invalid/version_and_git_same_dep").unwrap();
1371 }
1372
1373 #[test]
1374 #[should_panic(expected = "Both version and ipfs details provided for same dependency")]
1375 fn test_error_version_with_ipfs_for_same_dep() {
1376 PackageManifest::from_dir("./tests/invalid/version_and_ipfs_same_dep").unwrap();
1377 }
1378
1379 #[test]
1380 #[should_panic(expected = "duplicate key `foo` in table `dependencies`")]
1381 fn test_error_duplicate_deps_definition() {
1382 PackageManifest::from_dir("./tests/invalid/duplicate_keys").unwrap();
1383 }
1384
1385 #[test]
1386 fn test_error_duplicate_deps_definition_in_workspace() {
1387 let workspace =
1393 WorkspaceManifestFile::from_dir("./tests/invalid/patch_workspace_and_package").unwrap();
1394 let projects: Vec<_> = workspace
1395 .member_pkg_manifests()
1396 .unwrap()
1397 .collect::<Result<Vec<_>, _>>()
1398 .unwrap();
1399 assert_eq!(projects.len(), 1);
1400 let patches: Vec<_> = projects[0].resolve_patches().unwrap().collect();
1401 assert_eq!(patches.len(), 0);
1402
1403 let patches: Vec<_> = PackageManifestFile::from_dir("./tests/test_package")
1406 .unwrap()
1407 .resolve_patches()
1408 .unwrap()
1409 .collect();
1410 assert_eq!(patches.len(), 1);
1411 }
1412
1413 #[test]
1414 fn test_valid_dependency_details() {
1415 let dependency_details_path = DependencyDetails {
1416 version: None,
1417 path: Some("example_path/".to_string()),
1418 git: None,
1419 branch: None,
1420 tag: None,
1421 package: None,
1422 rev: None,
1423 ipfs: None,
1424 namespace: None,
1425 };
1426
1427 let git_source_string = "https://github.com/FuelLabs/sway".to_string();
1428 let dependency_details_git_tag = DependencyDetails {
1429 version: None,
1430 path: None,
1431 git: Some(git_source_string.clone()),
1432 branch: None,
1433 tag: Some("v0.1.0".to_string()),
1434 package: None,
1435 rev: None,
1436 ipfs: None,
1437 namespace: None,
1438 };
1439 let dependency_details_git_branch = DependencyDetails {
1440 version: None,
1441 path: None,
1442 git: Some(git_source_string.clone()),
1443 branch: Some("test_branch".to_string()),
1444 tag: None,
1445 package: None,
1446 rev: None,
1447 ipfs: None,
1448 namespace: None,
1449 };
1450 let dependency_details_git_rev = DependencyDetails {
1451 version: None,
1452 path: None,
1453 git: Some(git_source_string),
1454 branch: None,
1455 tag: None,
1456 package: None,
1457 rev: Some("9f35b8e".to_string()),
1458 ipfs: None,
1459 namespace: None,
1460 };
1461
1462 let dependency_details_ipfs = DependencyDetails {
1463 version: None,
1464 path: None,
1465 git: None,
1466 branch: None,
1467 tag: None,
1468 package: None,
1469 rev: None,
1470 ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1471 namespace: None,
1472 };
1473
1474 assert!(dependency_details_path.validate().is_ok());
1475 assert!(dependency_details_git_tag.validate().is_ok());
1476 assert!(dependency_details_git_branch.validate().is_ok());
1477 assert!(dependency_details_git_rev.validate().is_ok());
1478 assert!(dependency_details_ipfs.validate().is_ok());
1479 }
1480
1481 #[test]
1482 fn test_project_with_null_metadata() {
1483 let project = Project {
1484 authors: Some(vec!["Test Author".to_string()]),
1485 name: "test-project".to_string(),
1486 version: Some(Version::parse("0.1.0").unwrap()),
1487 description: Some("test description".to_string()),
1488 homepage: None,
1489 documentation: None,
1490 categories: None,
1491 keywords: None,
1492 repository: None,
1493 organization: None,
1494 license: "Apache-2.0".to_string(),
1495 entry: "main.sw".to_string(),
1496 implicit_std: None,
1497 forc_version: None,
1498 experimental: HashMap::new(),
1499 metadata: Some(toml::Value::from(toml::value::Table::new())),
1500 force_dbg_in_release: None,
1501 };
1502
1503 let serialized = toml::to_string(&project).unwrap();
1504 let deserialized: Project = toml::from_str(&serialized).unwrap();
1505
1506 assert_eq!(project.name, deserialized.name);
1507 assert_eq!(project.metadata, deserialized.metadata);
1508 }
1509
1510 #[test]
1511 fn test_project_without_metadata() {
1512 let project = Project {
1513 authors: Some(vec!["Test Author".to_string()]),
1514 name: "test-project".to_string(),
1515 version: Some(Version::parse("0.1.0").unwrap()),
1516 description: Some("test description".to_string()),
1517 homepage: Some(Url::parse("https://example.com").unwrap()),
1518 documentation: Some(Url::parse("https://docs.example.com").unwrap()),
1519 categories: Some(vec!["test-category".to_string()]),
1520 keywords: Some(vec!["test-keyword".to_string()]),
1521 repository: Some(Url::parse("https://example.com").unwrap()),
1522 organization: None,
1523 license: "Apache-2.0".to_string(),
1524 entry: "main.sw".to_string(),
1525 implicit_std: None,
1526 forc_version: None,
1527 experimental: HashMap::new(),
1528 metadata: None,
1529 force_dbg_in_release: None,
1530 };
1531
1532 let serialized = toml::to_string(&project).unwrap();
1533 let deserialized: Project = toml::from_str(&serialized).unwrap();
1534
1535 assert_eq!(project.name, deserialized.name);
1536 assert_eq!(project.version, deserialized.version);
1537 assert_eq!(project.description, deserialized.description);
1538 assert_eq!(project.homepage, deserialized.homepage);
1539 assert_eq!(project.documentation, deserialized.documentation);
1540 assert_eq!(project.repository, deserialized.repository);
1541 assert_eq!(project.metadata, deserialized.metadata);
1542 assert_eq!(project.metadata, None);
1543 assert_eq!(project.categories, deserialized.categories);
1544 assert_eq!(project.keywords, deserialized.keywords);
1545 }
1546
1547 #[test]
1548 fn test_project_metadata_from_toml() {
1549 let toml_str = r#"
1550 name = "test-project"
1551 license = "Apache-2.0"
1552 entry = "main.sw"
1553 authors = ["Test Author"]
1554 description = "A test project"
1555 version = "1.0.0"
1556 keywords = ["test", "project"]
1557 categories = ["test"]
1558
1559 [metadata]
1560 mykey = "https://example.com"
1561 "#;
1562
1563 let project: Project = toml::from_str(toml_str).unwrap();
1564 assert!(project.metadata.is_some());
1565
1566 let metadata = project.metadata.unwrap();
1567 let table = metadata.as_table().unwrap();
1568
1569 assert_eq!(
1570 table.get("mykey").unwrap().as_str().unwrap(),
1571 "https://example.com"
1572 );
1573 }
1574
1575 #[test]
1576 fn test_project_with_invalid_metadata() {
1577 let invalid_toml = r#"
1579 name = "test-project"
1580 license = "Apache-2.0"
1581 entry = "main.sw"
1582
1583 [metadata
1584 description = "Invalid TOML"
1585 "#;
1586
1587 let result: Result<Project, _> = toml::from_str(invalid_toml);
1588 assert!(result.is_err());
1589
1590 let invalid_toml = r#"
1592 name = "test-project"
1593 license = "Apache-2.0"
1594 entry = "main.sw"
1595
1596 [metadata]
1597 ] = "Invalid key"
1598 "#;
1599
1600 let result: Result<Project, _> = toml::from_str(invalid_toml);
1601 assert!(result.is_err());
1602
1603 let invalid_toml = r#"
1605 name = "test-project"
1606 license = "Apache-2.0"
1607 entry = "main.sw"
1608
1609 [metadata]
1610 nested = { key = "value1" }
1611
1612 [metadata.nested]
1613 key = "value2"
1614 "#;
1615
1616 let result: Result<Project, _> = toml::from_str(invalid_toml);
1617 assert!(result.is_err());
1618 assert!(result
1619 .err()
1620 .unwrap()
1621 .to_string()
1622 .contains("duplicate key `nested` in table `metadata`"));
1623 }
1624
1625 #[test]
1626 fn test_metadata_roundtrip() {
1627 let original_toml = r#"
1628 name = "test-project"
1629 license = "Apache-2.0"
1630 entry = "main.sw"
1631
1632 [metadata]
1633 boolean = true
1634 integer = 42
1635 float = 3.12
1636 string = "value"
1637 array = [1, 2, 3]
1638 mixed_array = [1, "two", true]
1639
1640 [metadata.nested]
1641 key = "value2"
1642 "#;
1643
1644 let project: Project = toml::from_str(original_toml).unwrap();
1645 let serialized = toml::to_string(&project).unwrap();
1646 let deserialized: Project = toml::from_str(&serialized).unwrap();
1647
1648 assert_eq!(project.metadata, deserialized.metadata);
1650
1651 let table_val = project.metadata.unwrap();
1653 let table = table_val.as_table().unwrap();
1654 assert!(table.get("boolean").unwrap().as_bool().unwrap());
1655 assert_eq!(table.get("integer").unwrap().as_integer().unwrap(), 42);
1656 assert_eq!(table.get("float").unwrap().as_float().unwrap(), 3.12);
1657 assert_eq!(table.get("string").unwrap().as_str().unwrap(), "value");
1658 assert_eq!(table.get("array").unwrap().as_array().unwrap().len(), 3);
1659 assert!(table.get("nested").unwrap().as_table().is_some());
1660 }
1661
1662 #[test]
1663 fn test_workspace_with_metadata() {
1664 let toml_str = r#"
1665 [workspace]
1666 members = ["package1", "package2"]
1667
1668 [workspace.metadata]
1669 description = "A test workspace"
1670 version = "1.0.0"
1671 authors = ["Test Author"]
1672 homepage = "https://example.com"
1673
1674 [workspace.metadata.ci]
1675 workflow = "main"
1676 timeout = 3600
1677 "#;
1678
1679 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1680 assert!(manifest.workspace.metadata.is_some());
1681
1682 let metadata = manifest.workspace.metadata.unwrap();
1683 let table = metadata.as_table().unwrap();
1684
1685 assert_eq!(
1686 table.get("description").unwrap().as_str().unwrap(),
1687 "A test workspace"
1688 );
1689 assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0");
1690
1691 let ci = table.get("ci").unwrap().as_table().unwrap();
1692 assert_eq!(ci.get("workflow").unwrap().as_str().unwrap(), "main");
1693 assert_eq!(ci.get("timeout").unwrap().as_integer().unwrap(), 3600);
1694 }
1695
1696 #[test]
1697 fn test_workspace_without_metadata() {
1698 let toml_str = r#"
1699 [workspace]
1700 members = ["package1", "package2"]
1701 "#;
1702
1703 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1704 assert!(manifest.workspace.metadata.is_none());
1705 }
1706
1707 #[test]
1708 fn test_workspace_empty_metadata() {
1709 let toml_str = r#"
1710 [workspace]
1711 members = ["package1", "package2"]
1712
1713 [workspace.metadata]
1714 "#;
1715
1716 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1717 assert!(manifest.workspace.metadata.is_some());
1718 let metadata = manifest.workspace.metadata.unwrap();
1719 assert!(metadata.as_table().unwrap().is_empty());
1720 }
1721
1722 #[test]
1723 fn test_workspace_complex_metadata() {
1724 let toml_str = r#"
1725 [workspace]
1726 members = ["package1", "package2"]
1727
1728 [workspace.metadata]
1729 numbers = [1, 2, 3]
1730 strings = ["a", "b", "c"]
1731 mixed = [1, "two", true]
1732
1733 [workspace.metadata.nested]
1734 key = "value"
1735
1736 [workspace.metadata.nested.deep]
1737 another = "value"
1738 "#;
1739
1740 let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1741 let metadata = manifest.workspace.metadata.unwrap();
1742 let table = metadata.as_table().unwrap();
1743
1744 assert!(table.get("numbers").unwrap().as_array().is_some());
1745 assert!(table.get("strings").unwrap().as_array().is_some());
1746 assert!(table.get("mixed").unwrap().as_array().is_some());
1747
1748 let nested = table.get("nested").unwrap().as_table().unwrap();
1749 assert_eq!(nested.get("key").unwrap().as_str().unwrap(), "value");
1750
1751 let deep = nested.get("deep").unwrap().as_table().unwrap();
1752 assert_eq!(deep.get("another").unwrap().as_str().unwrap(), "value");
1753 }
1754
1755 #[test]
1756 fn test_workspace_metadata_roundtrip() {
1757 let original = WorkspaceManifest {
1758 workspace: Workspace {
1759 members: vec![PathBuf::from("package1"), PathBuf::from("package2")],
1760 metadata: Some(toml::Value::Table({
1761 let mut table = toml::value::Table::new();
1762 table.insert("key".to_string(), toml::Value::String("value".to_string()));
1763 table
1764 })),
1765 },
1766 patch: None,
1767 };
1768
1769 let serialized = toml::to_string(&original).unwrap();
1770 let deserialized: WorkspaceManifest = toml::from_str(&serialized).unwrap();
1771
1772 assert_eq!(original.workspace.members, deserialized.workspace.members);
1773 assert_eq!(original.workspace.metadata, deserialized.workspace.metadata);
1774 }
1775
1776 #[test]
1777 fn test_dependency_alias_project_name_collision() {
1778 let original_toml = r#"
1779 [project]
1780 authors = ["Fuel Labs <contact@fuel.sh>"]
1781 entry = "main.sw"
1782 license = "Apache-2.0"
1783 name = "lib_contract_abi"
1784
1785 [dependencies]
1786 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1787 "#;
1788
1789 let project = PackageManifest::from_string(original_toml.to_string());
1790 let err = project.unwrap_err();
1791 assert_eq!(err.to_string(), format!("Dependency \"lib_contract\" declares an alias (\"package\" field) that is the same as project name"))
1792 }
1793
1794 #[test]
1795 fn test_dependency_name_project_name_collision() {
1796 let original_toml = r#"
1797 [project]
1798 authors = ["Fuel Labs <contact@fuel.sh>"]
1799 entry = "main.sw"
1800 license = "Apache-2.0"
1801 name = "lib_contract"
1802
1803 [dependencies]
1804 lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1805 "#;
1806
1807 let project = PackageManifest::from_string(original_toml.to_string());
1808 let err = project.unwrap_err();
1809 assert_eq!(
1810 err.to_string(),
1811 format!("Dependency \"lib_contract\" collides with project name.")
1812 )
1813 }
1814}