diff --git a/locator-codegen/Cargo.toml b/locator-codegen/Cargo.toml index 99fb206..34a372f 100644 --- a/locator-codegen/Cargo.toml +++ b/locator-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "locator-codegen" -version = "4.0.2" +version = "4.1.0" edition = "2024" [lints] diff --git a/locator-codegen/src/ecosystem.rs b/locator-codegen/src/ecosystem.rs index 9bf70b6..265f3a8 100644 --- a/locator-codegen/src/ecosystem.rs +++ b/locator-codegen/src/ecosystem.rs @@ -1,4 +1,5 @@ //! Types and helpers for the `ecosystem` macro. +//! Note that this macro is not meant to be invoked from outside the `locator` crate. use std::collections::HashMap; @@ -30,17 +31,47 @@ impl Entry { /// /// The `#name` ecosystem. /// #derives /// pub struct #name; + /// + /// impl #name { + /// /// Parse an instance from a string. + /// pub fn parse(input: impl AsRef) -> Result; + /// + /// /// View the instance as a string. + /// pub fn as_str(&self) -> &str { + /// } /// ``` fn struct_variant(&self) -> TokenStream { let name = &self.name; let serialized = &self.serialized; let docs = &self.docs; let derives = Invocation::struct_derives(); + let fromstrings = mk_tryfromstr_parse(&name); + let displays = mk_display_from_str(&name); + let deserialize = mk_deserialize_parse(&name); + let serialize = mk_serialize_from_str(&name); quote! { #(#docs)* #derives - #[serde(rename = #serialized)] - pub struct #name + pub struct #name; + impl #name { + /// Parse an instance from a string. + pub fn parse(input: impl AsRef) -> Result { + let input = input.as_ref(); + if input == #serialized { + Ok(#name) + } else { + Err(locator::ParseError::new_literal_exact(input, #serialized).into()) + } + } + /// View the instance as a string. + pub fn as_str(&self) -> &str { + #serialized + } + } + #fromstrings + #displays + #deserialize + #serialize } } @@ -62,6 +93,32 @@ impl Entry { } } + /// Creates the enum variant for serializing this entry. + /// + /// ```ignore + /// #name => #serialized + /// ``` + fn enum_variant_ser(&self, parent: &Ident) -> TokenStream { + let variant = &self.name; + let serialized = &self.serialized; + quote! { + #parent::#variant => #serialized + } + } + + /// Creates the enum variant for deserializing this entry. + /// + /// ```ignore + /// #serialized => #name + /// ``` + fn enum_variant_de(&self, parent: &Ident) -> TokenStream { + let variant = &self.name; + let serialized = &self.serialized; + quote! { + #serialized => Ok(#parent::#variant) + } + } + /// Create an enum match fragment matching the name of this entry /// from one enum ident to another. /// @@ -97,8 +154,15 @@ impl Invocation { pub fn mk_ecosystem_enum(&self) -> TokenStream { let ident = Self::ecosystem(); let derives = Self::enum_derives(); - let variants = self.entries.iter().map(|entry| entry.enum_variant()); - let iter = self.entries.iter().map(|entry| &entry.name); + let entries = &self.entries; + let variants = entries.iter().map(|entry| entry.enum_variant()); + let names = entries.iter().map(|entry| &entry.name); + + let options = entries.iter().map(|entry| &entry.serialized); + let variants_serialized = entries.iter().map(|e| e.enum_variant_ser(&ident)); + let variants_deserialized = entries.iter().map(|e| e.enum_variant_de(&ident)); + let fromstrings = mk_tryfromstr_parse(&ident); + let displays = mk_display_from_str(&ident); quote! { /// Identifies supported code host ecosystems. @@ -109,11 +173,27 @@ impl Invocation { #(#variants),*, } impl #ident { + /// Parse an instance from a string. + pub fn parse(input: impl AsRef) -> Result { + let input = input.as_ref(); + match input { + #(#variants_deserialized),*, + _ => Err(locator::ParseError::new_literal_oneof(input, [#(#options),*]).into()), + } + } + /// View the instance as a string. + pub fn as_str(&self) -> &str { + match self { + #(#variants_serialized),* + } + } /// Iterate over all variants. pub fn iter() -> impl Iterator { - [#(#ident::#iter),*].into_iter() + [#(#ident::#names),*].into_iter() } } + #fromstrings + #displays impl From<&#ident> for #ident { fn from(value: &#ident) -> Self { *value @@ -189,7 +269,7 @@ impl Invocation { }); quote! { - #(#variants);*; + #(#variants)* #(#conversions)* #(#variant_conversions)* } @@ -204,14 +284,15 @@ impl Invocation { let categories = self.category_entries(); let enums = categories.iter().map(|(category, entries)| { let category = Self::ecosystem_category(category); - let matches_to = entries - .iter() - .map(|entry| entry.enum_match_fragment(&category, &ecosystem)); - let matches_from = entries - .iter() - .map(|entry| entry.enum_match_fragment(&ecosystem, &category)); + let matches_to = entries.iter().map(|entry| entry.enum_match_fragment(&category, &ecosystem)); + let matches_from = entries.iter().map(|entry| entry.enum_match_fragment(&ecosystem, &category)); let variants = entries.iter().map(|entry| entry.enum_variant()); - let iter = entries.iter().map(|entry| &entry.name); + let names = entries.iter().map(|entry| &entry.name); + let options = entries.iter().map(|entry| &entry.serialized); + let variants_serialized = entries.iter().map(|e| e.enum_variant_ser(&category)); + let variants_deserialized = entries.iter().map(|e| e.enum_variant_de(&category)); + let fromstrings = mk_tryfromstr_parse(&category); + let displays = mk_display_from_str(&category); let doc = format!("Identifies `{category}` supported code host ecosystems."); quote! { #[doc = #doc] @@ -222,11 +303,27 @@ impl Invocation { #(#variants),*, } impl #category { + /// Parse an instance from a string. + pub fn parse(input: impl AsRef) -> Result { + let input = input.as_ref(); + match input { + #(#variants_deserialized),*, + _ => Err(locator::ParseError::new_literal_oneof(input, [#(#options),*]).into()), + } + } + /// View the instance as a string. + pub fn as_str(&self) -> &str { + match self { + #(#variants_serialized),* + } + } /// Iterate over all variants. pub fn iter() -> impl Iterator { - [#(#category::#iter),*].into_iter() + [#(#category::#names),*].into_iter() } } + #fromstrings + #displays impl From<#category> for #ecosystem { fn from(value: #category) -> Self { match value { @@ -288,8 +385,6 @@ impl Invocation { quote! { #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, - ::serde::Serialize, - ::serde::Deserialize, ::documented::Documented, ::utoipa::ToSchema, )] @@ -347,3 +442,72 @@ impl Parse for Invocation { Ok(Self { entries }) } } + +/// Implement `Deserialize` using `Self::parse`. +fn mk_deserialize_parse(ident: &Ident) -> TokenStream { + quote! { + impl<'de> serde::de::Deserialize<'de> for #ident { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::parse(&s).map_err(serde::de::Error::custom) + } + } + } +} + +/// Implement `Serialize` using `Self::as_str`. +fn mk_serialize_from_str(ident: &Ident) -> TokenStream { + quote! { + impl serde::ser::Serialize for #ident { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(self.as_str()) + } + } + } +} + +/// Construct `TryFrom` conversions for `String`, `&String`, and `&str` using `Self::parse`. +fn mk_tryfromstr_parse(ident: &Ident) -> TokenStream { + quote! { + impl TryFrom<&str> for #ident { + type Error = locator::Error; + fn try_from(value: &str) -> Result { + Self::parse(value) + } + } + impl TryFrom<&String> for #ident { + type Error = locator::Error; + fn try_from(value: &String) -> Result { + Self::parse(value) + } + } + impl TryFrom for #ident { + type Error = locator::Error; + fn try_from(value: String) -> Result { + Self::parse(value) + } + } + } +} + +/// Construct `Display` and `AsRef` conversions using `Self::as_str`. +fn mk_display_from_str(ident: &Ident) -> TokenStream { + quote! { + impl std::fmt::Display for #ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } + } + impl std::convert::AsRef for #ident { + fn as_ref(&self) -> &str { + self.as_str() + } + } + } +} diff --git a/locator-codegen/src/lib.rs b/locator-codegen/src/lib.rs index ebe93d8..435c36a 100644 --- a/locator-codegen/src/lib.rs +++ b/locator-codegen/src/lib.rs @@ -254,6 +254,7 @@ pub fn ecosystems(attr: TokenStream, item: TokenStream) -> TokenStream { TokenStream::from(quote! { #(#docs)* #vis mod #name { + use crate as locator; #invalid_conversion_err; #ecosystems #subsets diff --git a/locator-codegen/src/locator_parts.rs b/locator-codegen/src/locator_parts.rs index d6528a6..2285257 100644 --- a/locator-codegen/src/locator_parts.rs +++ b/locator-codegen/src/locator_parts.rs @@ -59,11 +59,11 @@ impl Invocation { locator::LocatorParts::parse(input).map(Self) } /// Extract the instance to its parts. - pub(crate) fn into_parts(self) -> locator::LocatorParts<#ecosystem, #organization, #package, #revision> { + pub fn into_parts(self) -> locator::LocatorParts<#ecosystem, #organization, #package, #revision> { self.0 } /// Construct an instance from its parts. - pub(crate) fn from_parts(parts: locator::LocatorParts<#ecosystem, #organization, #package, #revision>) -> Self { + pub fn from_parts(parts: locator::LocatorParts<#ecosystem, #organization, #package, #revision>) -> Self { Self(parts) } } @@ -88,7 +88,6 @@ impl Invocation { package, revision, } = &self.types; - quote! { #[locator::macro_support::bon::bon] impl #struct_name { diff --git a/locator/Cargo.toml b/locator/Cargo.toml index f37b0a4..0142410 100644 --- a/locator/Cargo.toml +++ b/locator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "locator" -version = "4.0.2" +version = "4.1.0" edition = "2024" [lints] @@ -39,8 +39,9 @@ assert_matches = "1.5.0" color-eyre = "0.6.5" impls = "1.0.3" itertools = "0.14.0" +maplit = "1.0.2" monostate = "0.1.14" -pretty_assertions = "1.4.1" +pretty_assertions = { version = "1.4.1", features = ["unstable"] } proptest = "1.6.0" simple_test_case = "1.3.0" static_assertions = "1.1.0" diff --git a/locator/src/error.rs b/locator/src/error.rs index cb74732..51acdac 100644 --- a/locator/src/error.rs +++ b/locator/src/error.rs @@ -18,6 +18,38 @@ pub enum ParseError { #[error("input was empty, which is invalid for this type")] Empty, + /// The input was expected to exactly match the target value, + /// but was something else. + #[error("input literal '{input}' did not match the target value '{expected}'")] + LiteralExact { + /// The input originally provided. + #[source_code] + input: String, + + /// The expected value. + expected: String, + + /// The location of the error. + #[label("field")] + span: SourceSpan, + }, + + /// The input was expected to be one of the provided options, + /// but was something else. + #[error("input literal '{input}' did not match one of: {}", options.join(", "))] + LiteralOneOf { + /// The input originally provided. + #[source_code] + input: String, + + /// The possible accepted options. + options: Vec, + + /// The location of the error. + #[label("field")] + span: SourceSpan, + }, + /// The input did not match the required syntax. #[error("input '{input}' did not match required syntax: {error}")] Syntax { @@ -54,6 +86,31 @@ pub enum ParseError { }, } +impl ParseError { + /// Create a new `LiteralOneOf` variant with the provided input and options. + pub(crate) fn new_literal_exact(input: impl Into, expected: impl Into) -> Self { + let input = input.into(); + Self::LiteralExact { + expected: expected.into(), + span: (0, input.len()).into(), + input, + } + } + + /// Create a new `LiteralOneOf` variant with the provided input and options. + pub(crate) fn new_literal_oneof( + input: impl Into, + options: impl IntoIterator>, + ) -> Self { + let input = input.into(); + Self::LiteralOneOf { + options: options.into_iter().map(Into::into).collect(), + span: (0, input.len()).into(), + input, + } + } +} + /// Return the span of `substr` inside `text`. pub fn span(text: &str, substr: &str) -> (usize, usize) { text.find(substr) diff --git a/locator/src/lib.rs b/locator/src/lib.rs index 64ce17d..6741ef4 100644 --- a/locator/src/lib.rs +++ b/locator/src/lib.rs @@ -49,6 +49,7 @@ pub mod macro_support { pub use bon; pub use non_empty_string; pub use semver; + pub use serde_plain; pub use versions; } @@ -169,10 +170,6 @@ pub mod macro_support { )] pub struct ecosystems; -serde_plain::derive_display_from_serialize!(ecosystems::Ecosystem); -serde_plain::derive_display_from_serialize!(ecosystems::EcosystemPublic); -serde_plain::derive_display_from_serialize!(ecosystems::EcosystemPrivate); - /// This field indicates no value: it's the equivalent of an always-`None` `Option<()>`. /// /// - Unconditionally parses from any value. diff --git a/locator/tests/it/ecosystem.rs b/locator/tests/it/ecosystem.rs index 5e9d21a..4612212 100644 --- a/locator/tests/it/ecosystem.rs +++ b/locator/tests/it/ecosystem.rs @@ -1,4 +1,7 @@ -use locator::{Ecosystem, EcosystemPrivate, EcosystemPublic}; +use std::collections::HashSet; + +use locator::{Ecosystem, EcosystemPrivate, EcosystemPublic, Error, ParseError}; +use maplit::hashset; use simple_test_case::test_case; #[test_case(Ecosystem::Archive, "archive"; "archive")] @@ -33,7 +36,12 @@ use simple_test_case::test_case; #[test_case(Ecosystem::User, "user"; "user")] #[test] fn render_all(ecosystem: Ecosystem, target: &str) { - pretty_assertions::assert_eq!(&ecosystem.to_string(), target, "render {ecosystem:?}"); + pretty_assertions::assert_eq!(&ecosystem.to_string(), target, "to_string {ecosystem:?}"); + pretty_assertions::assert_eq!(ecosystem.as_str(), target, "as_str {ecosystem:?}"); + pretty_assertions::assert_eq!(ecosystem.as_ref(), target, "as_ref {ecosystem:?}"); + + let serialized = serde_plain::to_string(&ecosystem).expect(&format!("serialize {ecosystem:?}")); + pretty_assertions::assert_eq!(serialized, target, "serialized {ecosystem:?}"); } #[test_case(EcosystemPrivate::Archive, "archive"; "archive")] @@ -44,7 +52,12 @@ fn render_all(ecosystem: Ecosystem, target: &str) { #[test_case(EcosystemPrivate::User, "user"; "user")] #[test] fn render_private(ecosystem: EcosystemPrivate, target: &str) { - pretty_assertions::assert_eq!(&ecosystem.to_string(), target, "render {ecosystem:?}"); + pretty_assertions::assert_eq!(&ecosystem.to_string(), target, "to_string {ecosystem:?}"); + pretty_assertions::assert_eq!(ecosystem.as_str(), target, "as_str {ecosystem:?}"); + pretty_assertions::assert_eq!(ecosystem.as_ref(), target, "as_ref {ecosystem:?}"); + + let serialized = serde_plain::to_string(&ecosystem).expect(&format!("serialize {ecosystem:?}")); + pretty_assertions::assert_eq!(serialized, target, "serialized {ecosystem:?}"); } #[test_case(EcosystemPublic::Bower, "bower"; "bower")] @@ -73,7 +86,12 @@ fn render_private(ecosystem: EcosystemPrivate, target: &str) { #[test_case(EcosystemPublic::Url, "url"; "url")] #[test] fn render_public(ecosystem: EcosystemPublic, target: &str) { - pretty_assertions::assert_eq!(&ecosystem.to_string(), target, "render {ecosystem:?}"); + pretty_assertions::assert_eq!(&ecosystem.to_string(), target, "to_string {ecosystem:?}"); + pretty_assertions::assert_eq!(ecosystem.as_str(), target, "as_str {ecosystem:?}"); + pretty_assertions::assert_eq!(ecosystem.as_ref(), target, "as_ref {ecosystem:?}"); + + let serialized = serde_plain::to_string(&ecosystem).expect(&format!("serialize {ecosystem:?}")); + pretty_assertions::assert_eq!(serialized, target, "serialized {ecosystem:?}"); } #[test_case(EcosystemPublic::Bower, "bower"; "bower")] @@ -102,7 +120,10 @@ fn render_public(ecosystem: EcosystemPublic, target: &str) { #[test_case(EcosystemPublic::Url, "url"; "url")] #[test] fn parse_public(ecosystem: EcosystemPublic, target: &str) { - let parsed = serde_plain::from_str::(target).expect("parse ecosystem"); + let parsed = serde_plain::from_str::(target).expect("deserialize"); + pretty_assertions::assert_eq!(parsed, ecosystem, "deserialize {ecosystem:?}"); + + let parsed = EcosystemPublic::parse(target).expect("parse"); pretty_assertions::assert_eq!(parsed, ecosystem, "parse {ecosystem:?}"); } @@ -138,7 +159,10 @@ fn parse_public(ecosystem: EcosystemPublic, target: &str) { #[test_case(Ecosystem::User, "user"; "user")] #[test] fn parse_all(ecosystem: Ecosystem, target: &str) { - let parsed = serde_plain::from_str::(target).expect("parse ecosystem"); + let parsed = serde_plain::from_str::(target).expect("deserialize"); + pretty_assertions::assert_eq!(parsed, ecosystem, "deserialize {ecosystem:?}"); + + let parsed = Ecosystem::parse(target).expect("parse"); pretty_assertions::assert_eq!(parsed, ecosystem, "parse {ecosystem:?}"); } @@ -150,10 +174,115 @@ fn parse_all(ecosystem: Ecosystem, target: &str) { #[test_case(EcosystemPrivate::User, "user"; "user")] #[test] fn parse_private(ecosystem: EcosystemPrivate, target: &str) { - let parsed = serde_plain::from_str::(target).expect("parse ecosystem"); + let parsed = serde_plain::from_str::(target).expect("deserialize"); + pretty_assertions::assert_eq!(parsed, ecosystem, "deserialize {ecosystem:?}"); + + let parsed = EcosystemPrivate::parse(target).expect("parse"); pretty_assertions::assert_eq!(parsed, ecosystem, "parse {ecosystem:?}"); } +#[test] +fn parse_fail_all() { + let parsed = Ecosystem::parse("_does_not_exist").expect_err("parse"); + let expected = hashset! { + String::from("archive"), + String::from("bower"), + String::from("cart"), + String::from("cargo"), + String::from("csbinary"), + String::from("comp"), + String::from("conan"), + String::from("conda"), + String::from("cpan"), + String::from("cran"), + String::from("custom"), + String::from("gem"), + String::from("git"), + String::from("go"), + String::from("hackage"), + String::from("hex"), + String::from("apk"), + String::from("deb"), + String::from("rpm-generic"), + String::from("mvn"), + String::from("npm"), + String::from("nuget"), + String::from("pip"), + String::from("pod"), + String::from("pub"), + String::from("swift"), + String::from("rpm"), + String::from("upath"), + String::from("url"), + String::from("user"), + }; + let options = match parsed { + Error::Parse(ParseError::LiteralOneOf { options, .. }) => { + options.into_iter().collect::>() + } + _ => panic!("expected `Error::Parse(ParseError::LiteralOneOf {{ .. }})`, got: {parsed:?}"), + }; + pretty_assertions::assert_eq!(options, expected); +} + +#[test] +fn parse_fail_private() { + let parsed = EcosystemPrivate::parse("_does_not_exist").expect_err("parse"); + let expected = hashset! { + String::from("archive"), + String::from("csbinary"), + String::from("custom"), + String::from("rpm"), + String::from("upath"), + String::from("user"), + }; + let options = match parsed { + Error::Parse(ParseError::LiteralOneOf { options, .. }) => { + options.into_iter().collect::>() + } + _ => panic!("expected `Error::Parse(ParseError::LiteralOneOf {{ .. }})`, got: {parsed:?}"), + }; + pretty_assertions::assert_eq!(options, expected); +} + +#[test] +fn parse_fail_public() { + let parsed = EcosystemPublic::parse("_does_not_exist").expect_err("parse"); + let expected = hashset! { + String::from("bower"), + String::from("cart"), + String::from("cargo"), + String::from("comp"), + String::from("conan"), + String::from("conda"), + String::from("cpan"), + String::from("cran"), + String::from("gem"), + String::from("git"), + String::from("go"), + String::from("hackage"), + String::from("hex"), + String::from("apk"), + String::from("deb"), + String::from("rpm-generic"), + String::from("mvn"), + String::from("npm"), + String::from("nuget"), + String::from("pip"), + String::from("pod"), + String::from("pub"), + String::from("swift"), + String::from("url"), + }; + let options = match parsed { + Error::Parse(ParseError::LiteralOneOf { options, .. }) => { + options.into_iter().collect::>() + } + _ => panic!("expected `Error::Parse(ParseError::LiteralOneOf {{ .. }})`, got: {parsed:?}"), + }; + pretty_assertions::assert_eq!(options, expected); +} + #[test] fn iter_all() { let expected = vec![ @@ -237,3 +366,106 @@ fn iter_private() { let iterated = EcosystemPrivate::iter().collect::>(); pretty_assertions::assert_eq!(iterated, expected); } + +// Can't use the nice `test_case` macro here since these are all distinct types; +// the following is basically a more verbose recreation of it. +pub mod struct_variants { + use locator::{Error, ParseError, ecosystems}; + + #[duplicate::duplicate_item( + _name _rendered _test_name; + [ ecosystems::Archive ] [ "archive" ] [ render_archive ]; + [ ecosystems::Bower ] [ "bower" ] [ render_bower ]; + [ ecosystems::Cart ] [ "cart" ] [ render_cart ]; + [ ecosystems::Cargo ] [ "cargo" ] [ render_cargo ]; + [ ecosystems::CodeSentry ] [ "csbinary" ] [ render_code_sentry ]; + [ ecosystems::Comp ] [ "comp" ] [ render_comp ]; + [ ecosystems::Conan ] [ "conan" ] [ render_conan ]; + [ ecosystems::Conda ] [ "conda" ] [ render_conda ]; + [ ecosystems::Cpan ] [ "cpan" ] [ render_cpan ]; + [ ecosystems::Cran ] [ "cran" ] [ render_cran ]; + [ ecosystems::Custom ] [ "custom" ] [ render_custom ]; + [ ecosystems::Gem ] [ "gem" ] [ render_gem ]; + [ ecosystems::Git ] [ "git" ] [ render_git ]; + [ ecosystems::Go ] [ "go" ] [ render_go ]; + [ ecosystems::Hackage ] [ "hackage" ] [ render_hackage ]; + [ ecosystems::Hex ] [ "hex" ] [ render_hex ]; + [ ecosystems::LinuxAlpine ] [ "apk" ] [ render_linux_alpine ]; + [ ecosystems::LinuxDebian ] [ "deb" ] [ render_linux_debian ]; + [ ecosystems::LinuxRpm ] [ "rpm-generic" ] [ render_linux_rpm ]; + [ ecosystems::Maven ] [ "mvn" ] [ render_maven ]; + [ ecosystems::Npm ] [ "npm" ] [ render_npm ]; + [ ecosystems::Nuget ] [ "nuget" ] [ render_nuget ]; + [ ecosystems::Pip ] [ "pip" ] [ render_pip ]; + [ ecosystems::Pod ] [ "pod" ] [ render_pod ]; + [ ecosystems::Pub ] [ "pub" ] [ render_dart ]; + [ ecosystems::Swift ] [ "swift" ] [ render_swift ]; + [ ecosystems::Rpm ] [ "rpm" ] [ render_rpm ]; + [ ecosystems::UnresolvedPath ] [ "upath" ] [ render_unresolved_path ]; + [ ecosystems::Url ] [ "url" ] [ render_url ]; + [ ecosystems::User ] [ "user" ] [ render_user ]; + )] + #[test] + fn _test_name() { + let name = _name; + let rendered = _rendered; + pretty_assertions::assert_eq!(&name.to_string(), rendered, "to_string {name:?}"); + pretty_assertions::assert_eq!(name.as_str(), rendered, "as_str {name:?}"); + pretty_assertions::assert_eq!(name.as_ref(), rendered, "as_ref {name:?}"); + + let serialized = serde_plain::to_string(&name).expect(&format!("serialize {name:?}")); + pretty_assertions::assert_eq!(serialized, rendered, "serialized {name:?}"); + } + + #[duplicate::duplicate_item( + _name _rendered _test_name; + [ ecosystems::Archive ] [ "archive" ] [ parse_archive ]; + [ ecosystems::Bower ] [ "bower" ] [ parse_bower ]; + [ ecosystems::Cart ] [ "cart" ] [ parse_cart ]; + [ ecosystems::Cargo ] [ "cargo" ] [ parse_cargo ]; + [ ecosystems::CodeSentry ] [ "csbinary" ] [ parse_code_sentry ]; + [ ecosystems::Comp ] [ "comp" ] [ parse_comp ]; + [ ecosystems::Conan ] [ "conan" ] [ parse_conan ]; + [ ecosystems::Conda ] [ "conda" ] [ parse_conda ]; + [ ecosystems::Cpan ] [ "cpan" ] [ parse_cpan ]; + [ ecosystems::Cran ] [ "cran" ] [ parse_cran ]; + [ ecosystems::Custom ] [ "custom" ] [ parse_custom ]; + [ ecosystems::Gem ] [ "gem" ] [ parse_gem ]; + [ ecosystems::Git ] [ "git" ] [ parse_git ]; + [ ecosystems::Go ] [ "go" ] [ parse_go ]; + [ ecosystems::Hackage ] [ "hackage" ] [ parse_hackage ]; + [ ecosystems::Hex ] [ "hex" ] [ parse_hex ]; + [ ecosystems::LinuxAlpine ] [ "apk" ] [ parse_linux_alpine ]; + [ ecosystems::LinuxDebian ] [ "deb" ] [ parse_linux_debian ]; + [ ecosystems::LinuxRpm ] [ "rpm-generic" ] [ parse_linux_rpm ]; + [ ecosystems::Maven ] [ "mvn" ] [ parse_maven ]; + [ ecosystems::Npm ] [ "npm" ] [ parse_npm ]; + [ ecosystems::Nuget ] [ "nuget" ] [ parse_nuget ]; + [ ecosystems::Pip ] [ "pip" ] [ parse_pip ]; + [ ecosystems::Pod ] [ "pod" ] [ parse_pod ]; + [ ecosystems::Pub ] [ "pub" ] [ parse_dart ]; + [ ecosystems::Swift ] [ "swift" ] [ parse_swift ]; + [ ecosystems::Rpm ] [ "rpm" ] [ parse_rpm ]; + [ ecosystems::UnresolvedPath ] [ "upath" ] [ parse_unresolved_path ]; + [ ecosystems::Url ] [ "url" ] [ parse_url ]; + [ ecosystems::User ] [ "user" ] [ parse_user ]; + )] + #[test] + fn _test_name() { + let input = _rendered; + _name::parse(input).expect(&format!("parse {input:?}")); + _name::try_from(input).expect(&format!("try_from {input:?}")); + _name::try_from(String::from(input)).expect(&format!("try_from_string {input:?}")); + _name::try_from(&String::from(input)).expect(&format!("try_from_refstring {input:?}")); + serde_plain::from_str::<_name>(input).expect(&format!("deserialize {input:?}")); + + let parsed = _name::parse("_does_not_exist").expect_err(&format!("parse nonexistent")); + let expected = match parsed { + Error::Parse(ParseError::LiteralExact { expected, .. }) => expected, + _ => panic!( + "expected `Error::Parse(ParseError::LiteralExact {{ .. }})`, got: {parsed:?}" + ), + }; + pretty_assertions::assert_eq!(expected, input); + } +} diff --git a/locator/tests/it/locator_package.rs b/locator/tests/it/locator_package.rs index d842632..e6c1185 100644 --- a/locator/tests/it/locator_package.rs +++ b/locator/tests/it/locator_package.rs @@ -97,6 +97,14 @@ fn roundtrip_serialization() { assert_eq!(input, deserialized); } +#[test] +fn roundtrip_fromparts() { + let input = package!(org 1 => Custom, "foo"); + let parts = input.clone().into_parts(); + let constructed = PackageLocator::from_parts(parts); + assert_eq!(input, constructed); +} + #[test] fn serde_deserialization() { #[derive(Debug, Deserialize, PartialEq)] diff --git a/locator/tests/it/locator_strict.rs b/locator/tests/it/locator_strict.rs index d42f3c6..c9249ab 100644 --- a/locator/tests/it/locator_strict.rs +++ b/locator/tests/it/locator_strict.rs @@ -140,6 +140,14 @@ fn roundtrip_serialization() { assert_eq!(input, deserialized); } +#[test] +fn roundtrip_fromparts() { + let input = strict!(org 1 => Custom, "foo", "bar"); + let parts = input.clone().into_parts(); + let constructed = StrictLocator::from_parts(parts); + assert_eq!(input, constructed); +} + #[test] fn serde_deserialization() { #[derive(Debug, Deserialize, PartialEq)] diff --git a/locator/tests/it/locator_t.rs b/locator/tests/it/locator_t.rs index baf15b7..7a574a8 100644 --- a/locator/tests/it/locator_t.rs +++ b/locator/tests/it/locator_t.rs @@ -157,6 +157,14 @@ fn roundtrip_serialization() { assert_eq!(input, deserialized); } +#[test] +fn roundtrip_fromparts() { + let input = locator!(org 1 => Custom, "foo", "bar"); + let parts = input.clone().into_parts(); + let constructed = Locator::from_parts(parts); + assert_eq!(input, constructed); +} + #[test] fn serde_deserialization() { #[derive(Debug, Deserialize, PartialEq)] @@ -188,6 +196,32 @@ fn promotes_strict() { assert_eq!(expected, promoted, "promote {input}"); } +#[test] +fn build_with_macro() { + locator::locator!(org 10 => Npm, "lodash", "1.0"); + locator::locator!(Npm, "lodash", "1.0"); + locator::locator!(Npm, "lodash"); +} + +#[test] +fn build_with_builder() { + locator::Locator::builder() + .organization(10) + .ecosystem(Ecosystem::Npm) + .package("lodash") + .revision(locator::revision!("1.0")) + .build(); + locator::Locator::builder() + .ecosystem(Ecosystem::Npm) + .package("lodash") + .revision(locator::revision!("1.0")) + .build(); + locator::Locator::builder() + .ecosystem(Ecosystem::Npm) + .package("lodash") + .build(); +} + #[test] fn ordering() { let locators = vec![ @@ -274,12 +308,12 @@ proptest! { /// Regular expression that matches any unicode string that is: /// - Prefixed with `custom+` -/// - Contains zero or more digits +/// - Contains zero or more digits that do not begin with `0` /// - Contains a literal `/` /// - Contains at least one character that is not a control character, space, or the literal `$` /// - Contains a literal `$` /// - Contains at least one character that is not a control character, space, or the literal `$` -const VALID_INPUTS_CUSTOM_WITH_ORG: &str = r"custom\+\d*/[^\pC\s$]+\$[^\pC\s$]+"; +const VALID_INPUTS_CUSTOM_WITH_ORG: &str = r"custom\+([1-9]\d*)?/[^\pC\s$]+\$[^\pC\s$]+"; proptest! { /// Tests randomly generated strings that match the provided regular expression against the parser.