From b62b0feb5bf30de8ae7c05108b764a3cb7cc873b Mon Sep 17 00:00:00 2001 From: Sophie <47993817+sdankel@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:36:49 -0800 Subject: [PATCH 1/5] feat: Add support for additional package manifest keys --- forc-pkg/src/manifest/mod.rs | 64 ++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index c20effec0fb..1299ac487d6 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -194,8 +194,13 @@ pub struct PackageManifest { pub struct Project { pub authors: Option>, pub name: String, + pub version: Option, + pub description: Option, pub organization: Option, pub license: String, + pub homepage: Option, + pub repository: Option, + pub documentation: Option, #[serde(default = "default_entry")] pub entry: String, pub implicit_std: Option, @@ -318,6 +323,14 @@ impl Dependency { Self::Detailed(ref det) => det.package.as_deref(), } } + + /// The string of the `version` field if specified. + pub fn version(&self) -> Option<&str> { + match *self { + Self::Simple(ref version) => Some(version), + Self::Detailed(ref det) => det.version.as_deref(), + } + } } impl PackageManifestFile { @@ -571,10 +584,25 @@ impl PackageManifest { // package or a workspace. While doing so, we should be printing the warnings if the given // file parses so that we only see warnings for the correct type of manifest. let path = path.as_ref(); - let mut warnings = vec![]; - let manifest_str = std::fs::read_to_string(path) + let contents = std::fs::read_to_string(path) .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?; - let toml_de = toml::de::Deserializer::new(&manifest_str); + Self::from_string(contents) + } + + /// Given a path to a `Forc.toml`, read it and construct a `PackageManifest`. + /// + /// This also `validate`s the manifest, returning an `Err` in the case that invalid names, + /// fields were used. + /// + /// If `core` and `std` are unspecified, `std` will be added to the `dependencies` table + /// implicitly. In this case, the git tag associated with the version of this crate is used to + /// specify the pinned commit at which we fetch `std`. + pub fn from_string(contents: String) -> Result { + // While creating a `ManifestFile` we need to check if the given path corresponds to a + // package or a workspace. While doing so, we should be printing the warnings if the given + // file parses so that we only see warnings for the correct type of manifest. + let mut warnings = vec![]; + let toml_de = toml::de::Deserializer::new(&contents); let mut manifest: Self = serde_ignored::deserialize(toml_de, |path| { let warning = format!("unused manifest key: {path}"); warnings.push(warning); @@ -1338,6 +1366,11 @@ mod tests { let project = Project { authors: Some(vec!["Test Author".to_string()]), name: "test-project".to_string(), + version: Some("0.1.0".to_string()), + description: Some("test description".to_string()), + homepage: None, + documentation: None, + repository: None, organization: None, license: "Apache-2.0".to_string(), entry: "main.sw".to_string(), @@ -1359,6 +1392,11 @@ mod tests { let project = Project { authors: Some(vec!["Test Author".to_string()]), name: "test-project".to_string(), + version: Some("0.1.0".to_string()), + description: Some("test description".to_string()), + homepage: Some("https://example.com".to_string()), + documentation: Some("https://docs.example.com".to_string()), + repository: Some("https://example.com".to_string()), organization: None, license: "Apache-2.0".to_string(), entry: "main.sw".to_string(), @@ -1372,6 +1410,11 @@ mod tests { let deserialized: Project = toml::from_str(&serialized).unwrap(); assert_eq!(project.name, deserialized.name); + assert_eq!(project.version, deserialized.version); + assert_eq!(project.description, deserialized.description); + assert_eq!(project.homepage, deserialized.homepage); + assert_eq!(project.documentation, deserialized.documentation); + assert_eq!(project.repository, deserialized.repository); assert_eq!(project.metadata, deserialized.metadata); assert_eq!(project.metadata, None); } @@ -1383,13 +1426,11 @@ mod tests { license = "Apache-2.0" entry = "main.sw" authors = ["Test Author"] - - [metadata] description = "A test project" version = "1.0.0" - homepage = "https://example.com" - documentation = "https://docs.example.com" - repository = "https://github.com/example/test-project" + + [metadata] + mykey = "https://example.com" keywords = ["test", "project"] categories = ["test"] "#; @@ -1401,12 +1442,7 @@ mod tests { let table = metadata.as_table().unwrap(); assert_eq!( - table.get("description").unwrap().as_str().unwrap(), - "A test project" - ); - assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0"); - assert_eq!( - table.get("homepage").unwrap().as_str().unwrap(), + table.get("mykey").unwrap().as_str().unwrap(), "https://example.com" ); From 1d8861204f7b9a504e96041f1191064717893f61 Mon Sep 17 00:00:00 2001 From: Sophie <47993817+sdankel@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:02:12 -0800 Subject: [PATCH 2/5] validation --- docs/book/src/forc/manifest_reference.md | 17 ++++++++- forc-pkg/src/manifest/mod.rs | 48 ++++++++++++++++++++---- forc-util/src/restricted.rs | 6 +++ 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/docs/book/src/forc/manifest_reference.md b/docs/book/src/forc/manifest_reference.md index 0757f13e213..ecedae6c8f5 100644 --- a/docs/book/src/forc/manifest_reference.md +++ b/docs/book/src/forc/manifest_reference.md @@ -4,9 +4,14 @@ The `Forc.toml` (the _manifest_ file) is a compulsory file for each package and * [`[project]`](#the-project-section) — Defines a sway project. * `name` — The name of the project. + * `version` — The version of the project. + * `description` — A description of the project. * `authors` — The authors of the project. * `organization` — The organization of the project. - * `license`— The project license. + * `license` — The project license. + * `homepage` — URL of the project homepage. + * `repository` — URL of the project source repository. + * `documentation` — URL of the project documentation. * `entry` — The entry point for the compiler to start parsing from. * For the recommended way of selecting an entry point of large libraries please take a look at: [Libraries](./../sway-program-types/libraries.md) * `implicit-std` - Controls whether provided `std` version (with the current `forc` version) will get added as a dependency _implicitly_. _Unless you know what you are doing, leave this as default._ @@ -29,6 +34,11 @@ An example `Forc.toml` is shown below. Under `[project]` the following fields ar * `authors` * `organization` +* `version` +* `description` +* `homepage` +* `repository` +* `documentation` Also for the following fields, a default value is provided so omitting them is allowed: @@ -39,6 +49,11 @@ Also for the following fields, a default value is provided so omitting them is a [project] authors = ["user"] entry = "main.sw" +description = "Wallet contract" +version = "1.0.0" +homepage = "https://example.com/" +repository = "https://example.com/" +documentation = "https://example.com/" organization = "Fuel_Labs" license = "Apache-2.0" name = "wallet_contract" diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index 1299ac487d6..f1be22f5a6d 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -3,8 +3,8 @@ pub mod build_profile; use crate::pkg::{manifest_file_missing, parsing_failed, wrong_program_type}; use anyhow::{anyhow, bail, Context, Result}; use forc_tracing::println_warning; -use forc_util::{validate_name, validate_project_name}; -use serde::{Deserialize, Serialize}; +use forc_util::{restricted::is_valid_package_version, validate_name, validate_project_name}; +use serde::{de, Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use std::{ collections::{BTreeMap, HashMap}, @@ -19,6 +19,7 @@ use sway_utils::{ constants, find_nested_manifest_dir, find_parent_manifest_dir, find_parent_manifest_dir_with_check, }; +use url::Url; use self::build_profile::BuildProfile; @@ -193,14 +194,16 @@ pub struct PackageManifest { #[serde(rename_all = "kebab-case")] pub struct Project { pub authors: Option>, + #[serde(deserialize_with = "validate_package_name")] pub name: String, + #[serde(deserialize_with = "validate_package_version")] pub version: Option, pub description: Option, pub organization: Option, pub license: String, - pub homepage: Option, - pub repository: Option, - pub documentation: Option, + pub homepage: Option, + pub repository: Option, + pub documentation: Option, #[serde(default = "default_entry")] pub entry: String, pub implicit_std: Option, @@ -210,6 +213,35 @@ pub struct Project { pub metadata: Option, } +// Validation function for the `name` field +fn validate_package_name<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let name: String = Deserialize::deserialize(deserializer)?; + match validate_project_name(&name) { + Ok(_) => Ok(name), + Err(e) => Err(de::Error::custom(e.to_string())), + } +} + +// Validation function for `version` +fn validate_package_version<'de, D>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, +{ + let version: Option = Deserialize::deserialize(deserializer)?; + if let Some(ref version_str) = version { + if is_valid_package_version(version_str) { + return Err(de::Error::custom(format!( + "Invalid semantic version: '{}'", + version_str + ))); + } + } + Ok(version) +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct Network { @@ -1394,9 +1426,9 @@ mod tests { name: "test-project".to_string(), version: Some("0.1.0".to_string()), description: Some("test description".to_string()), - homepage: Some("https://example.com".to_string()), - documentation: Some("https://docs.example.com".to_string()), - repository: Some("https://example.com".to_string()), + homepage: Some(Url::parse("https://example.com").unwrap()), + documentation: Some(Url::parse("https://docs.example.com").unwrap()), + repository: Some(Url::parse("https://example.com").unwrap()), organization: None, license: "Apache-2.0".to_string(), entry: "main.sw".to_string(), diff --git a/forc-util/src/restricted.rs b/forc-util/src/restricted.rs index 08cf7d4901b..c2a4c71270d 100644 --- a/forc-util/src/restricted.rs +++ b/forc-util/src/restricted.rs @@ -107,6 +107,12 @@ pub fn is_valid_project_name_format(name: &str) -> Result<()> { Ok(()) } +pub fn is_valid_package_version(vers: &str) -> bool { + // Must start with an alphanumeric character, can contain only letters, numbers, underscores, dots and hyphens + let re = Regex::new(r"^[a-zA-Z0-9][\w.-]*$").unwrap(); + re.is_match(vers) +} + #[test] fn test_invalid_char() { assert_eq!( From 065b3c9fe65b9541463d5525387bb82a03f0a736 Mon Sep 17 00:00:00 2001 From: Sophie <47993817+sdankel@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:08:15 -0800 Subject: [PATCH 3/5] Add default value for version --- forc-pkg/src/manifest/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index f1be22f5a6d..02df03b9638 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -196,7 +196,7 @@ pub struct Project { pub authors: Option>, #[serde(deserialize_with = "validate_package_name")] pub name: String, - #[serde(deserialize_with = "validate_package_version")] + #[serde(default, deserialize_with = "validate_package_version")] pub version: Option, pub description: Option, pub organization: Option, From d602a0c6bd8d520ffc0f0f4a52f9284f79790eac Mon Sep 17 00:00:00 2001 From: Sophie <47993817+sdankel@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:34:49 -0800 Subject: [PATCH 4/5] not is_valid_package_version --- forc-pkg/src/manifest/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index 02df03b9638..0f7b8fc539f 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -232,7 +232,7 @@ where { let version: Option = Deserialize::deserialize(deserializer)?; if let Some(ref version_str) = version { - if is_valid_package_version(version_str) { + if !is_valid_package_version(version_str) { return Err(de::Error::custom(format!( "Invalid semantic version: '{}'", version_str From b86b44002fae7af4814ac89e4c3826a64f612de8 Mon Sep 17 00:00:00 2001 From: Sophie <47993817+sdankel@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:17:32 -0800 Subject: [PATCH 5/5] use version::Version --- forc-pkg/src/manifest/mod.rs | 27 +++++---------------------- forc-util/src/restricted.rs | 6 ------ 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index 0f7b8fc539f..8f77091b518 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -3,7 +3,8 @@ pub mod build_profile; use crate::pkg::{manifest_file_missing, parsing_failed, wrong_program_type}; use anyhow::{anyhow, bail, Context, Result}; use forc_tracing::println_warning; -use forc_util::{restricted::is_valid_package_version, validate_name, validate_project_name}; +use forc_util::{validate_name, validate_project_name}; +use semver::Version; use serde::{de, Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use std::{ @@ -196,8 +197,7 @@ pub struct Project { pub authors: Option>, #[serde(deserialize_with = "validate_package_name")] pub name: String, - #[serde(default, deserialize_with = "validate_package_version")] - pub version: Option, + pub version: Option, pub description: Option, pub organization: Option, pub license: String, @@ -225,23 +225,6 @@ where } } -// Validation function for `version` -fn validate_package_version<'de, D>(deserializer: D) -> Result, D::Error> -where - D: de::Deserializer<'de>, -{ - let version: Option = Deserialize::deserialize(deserializer)?; - if let Some(ref version_str) = version { - if !is_valid_package_version(version_str) { - return Err(de::Error::custom(format!( - "Invalid semantic version: '{}'", - version_str - ))); - } - } - Ok(version) -} - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct Network { @@ -1398,7 +1381,7 @@ mod tests { let project = Project { authors: Some(vec!["Test Author".to_string()]), name: "test-project".to_string(), - version: Some("0.1.0".to_string()), + version: Some(Version::parse("0.1.0").unwrap()), description: Some("test description".to_string()), homepage: None, documentation: None, @@ -1424,7 +1407,7 @@ mod tests { let project = Project { authors: Some(vec!["Test Author".to_string()]), name: "test-project".to_string(), - version: Some("0.1.0".to_string()), + version: Some(Version::parse("0.1.0").unwrap()), description: Some("test description".to_string()), homepage: Some(Url::parse("https://example.com").unwrap()), documentation: Some(Url::parse("https://docs.example.com").unwrap()), diff --git a/forc-util/src/restricted.rs b/forc-util/src/restricted.rs index c2a4c71270d..08cf7d4901b 100644 --- a/forc-util/src/restricted.rs +++ b/forc-util/src/restricted.rs @@ -107,12 +107,6 @@ pub fn is_valid_project_name_format(name: &str) -> Result<()> { Ok(()) } -pub fn is_valid_package_version(vers: &str) -> bool { - // Must start with an alphanumeric character, can contain only letters, numbers, underscores, dots and hyphens - let re = Regex::new(r"^[a-zA-Z0-9][\w.-]*$").unwrap(); - re.is_match(vers) -} - #[test] fn test_invalid_char() { assert_eq!(