From f6be7c4b1b96032573b358ce8636898b742ea1f0 Mon Sep 17 00:00:00 2001 From: "T.J. Telan" Date: Wed, 17 Sep 2025 13:30:44 -0700 Subject: [PATCH] Remove lifetime from GitUrl Fixes #69 --- src/lib.rs | 2 +- src/types/mod.rs | 126 +++++++++++++++++++++++--------------- src/types/provider/mod.rs | 109 ++++++++++++++++++++++----------- src/types/spec.rs | 82 +++++++++++++------------ tests/provider.rs | 2 +- 5 files changed, 197 insertions(+), 124 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f97944a..c981c92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ //! #[derive(Debug, Clone, PartialEq, Eq)] //! struct CustomProvider; //! -//! impl GitProvider, GitUrlParseError> for CustomProvider { +//! impl GitProvider for CustomProvider { //! fn from_git_url(_url: &GitUrl) -> Result { //! // Your custom provider parsing here //! Ok(Self) diff --git a/src/types/mod.rs b/src/types/mod.rs index 0dcbbf0..f0189c1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -14,7 +14,7 @@ use core::str; use std::fmt; use url::Url; -use getset::{CloneGetters, CopyGetters, Setters}; +use getset::{CopyGetters, Getters, Setters}; #[cfg(feature = "log")] use log::debug; use nom::Finish; @@ -42,37 +42,33 @@ pub(crate) enum GitUrlParseHint { /// GitUrl is an input url used by git. /// Parsing of the url inspired by rfc3986, but does not strictly cover the spec /// Optional, but by default, uses the `url` crate to perform a final validation of the parsing effort -#[derive(Clone, CopyGetters, CloneGetters, Debug, Default, Setters, PartialEq, Eq)] +#[derive(Clone, CopyGetters, Getters, Debug, Default, Setters, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct GitUrl<'url> { +#[getset(set = "pub(crate)")] +pub struct GitUrl { /// scheme name (i.e. `scheme://`) - #[getset(get_copy = "pub", set = "pub(crate)")] - scheme: Option<&'url str>, + scheme: Option, /// user name userinfo - #[getset(get_copy = "pub", set = "pub(crate)")] - user: Option<&'url str>, + user: Option, /// password userinfo provided with `user` (i.e. `user`:`password`@...) - #[getset(get_copy = "pub", set = "pub(crate)")] - password: Option<&'url str>, + password: Option, /// The hostname or IP of the repo host - #[getset(get_copy = "pub")] - host: Option<&'url str>, + host: Option, /// The port number of the repo host, if specified #[getset(get_copy = "pub")] port: Option, /// File or network path to repo - #[getset(get_copy = "pub", set = "pub(crate)")] - path: &'url str, + path: String, /// If we should print `scheme://` from input or derived during parsing - #[getset(get_copy = "pub", set = "pub(crate)")] + #[getset(get_copy = "pub")] print_scheme: bool, /// Pattern style of url derived during parsing - #[getset(get_copy = "pub(crate)")] + #[getset(get_copy = "pub")] hint: GitUrlParseHint, } /// Build the printable GitUrl from its components -impl fmt::Display for GitUrl<'_> { +impl fmt::Display for GitUrl { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let git_url_str = self.display(); @@ -80,7 +76,48 @@ impl fmt::Display for GitUrl<'_> { } } -impl<'url> GitUrl<'url> { +impl GitUrl { + /// scheme name (i.e. `scheme://`) + pub fn scheme(&self) -> Option<&str> { + if let Some(s) = &self.scheme { + Some(&s[..]) + } else { + None + } + } + + /// user name userinfo + pub fn user(&self) -> Option<&str> { + if let Some(u) = &self.user { + Some(&u[..]) + } else { + None + } + } + + /// password userinfo provided with `user` (i.e. `user`:`password`@...) + pub fn password(&self) -> Option<&str> { + if let Some(p) = &self.password { + Some(&p[..]) + } else { + None + } + } + + /// The hostname or IP of the repo host + pub fn host(&self) -> Option<&str> { + if let Some(h) = &self.host { + Some(&h[..]) + } else { + None + } + } + + /// File or network path to repo + pub fn path(&self) -> &str { + &self.path[..] + } + /// Wrapper function for the default output mode via [`Display`](std::fmt::Display) trait fn display(&self) -> String { self.build_string(false) @@ -142,7 +179,7 @@ impl<'url> GitUrl<'url> { } #[cfg(feature = "url")] -impl<'url> TryFrom<&GitUrl<'url>> for Url { +impl TryFrom<&GitUrl> for Url { type Error = url::ParseError; fn try_from(value: &GitUrl) -> Result { // Since we don't fully implement any spec, we'll rely on the url crate @@ -151,7 +188,7 @@ impl<'url> TryFrom<&GitUrl<'url>> for Url { } #[cfg(feature = "url")] -impl<'url> TryFrom> for Url { +impl TryFrom for Url { type Error = url::ParseError; fn try_from(value: GitUrl) -> Result { // Since we don't fully implement any spec, we'll rely on the url crate @@ -159,7 +196,7 @@ impl<'url> TryFrom> for Url { } } -impl<'url> GitUrl<'url> { +impl GitUrl { /// Returns `GitUrl` after removing all user info values pub fn trim_auth(&self) -> GitUrl { let mut new_giturl = self.clone(); @@ -181,7 +218,9 @@ impl<'url> GitUrl<'url> { /// # Ok(()) /// # } /// ``` - pub fn parse(input: &'url str) -> Result { + pub fn parse(input: &str) -> Result { + let mut git_url_result = GitUrl::default(); + // Error if there are null bytes within the url // https://github.com/tjtelan/git-url-parse-rs/issues/16 if input.contains('\0') { @@ -190,19 +229,26 @@ impl<'url> GitUrl<'url> { let (_input, url_spec_parser) = UrlSpecParser::parse(input).finish().unwrap_or_default(); - let mut scheme = url_spec_parser.scheme(); + let scheme = url_spec_parser.scheme(); let user = url_spec_parser.hier_part().authority().userinfo().user(); let password = url_spec_parser.hier_part().authority().userinfo().token(); let host = url_spec_parser.hier_part().authority().host(); let port = url_spec_parser.hier_part().authority().port(); - let mut path = url_spec_parser.hier_part().path(); + let path = url_spec_parser.hier_part().path(); + + git_url_result.set_scheme(scheme.clone()); + git_url_result.set_user(user.clone()); + git_url_result.set_password(password.clone()); + git_url_result.set_host(host.clone()); + git_url_result.set_port(*port); + git_url_result.set_path(path.clone()); // We will respect whether scheme was initially set let print_scheme = scheme.is_some(); // Take a moment to identify the type of url we have // We use the GitUrlParseHint to validate or adjust formatting path, if necessary - let hint = if let Some(scheme) = scheme { + let hint = if let Some(scheme) = scheme.as_ref() { if scheme.contains("ssh") { GitUrlParseHint::Sshlike } else { @@ -232,36 +278,20 @@ impl<'url> GitUrl<'url> { // If we found an ssh url, we should adjust the path. // Skip the first character if hint == GitUrlParseHint::Sshlike { - if let Some(scheme) = scheme.as_mut() { - *scheme = "ssh"; - } else { - scheme = Some("ssh") - } - path = &path[1..]; + git_url_result.set_scheme(Some("ssh".to_string())); + git_url_result.set_path(path[1..].to_string()); } if hint == GitUrlParseHint::Filelike { - if let Some(scheme) = scheme.as_mut() { - *scheme = "file"; - } else { - scheme = Some("file") - } + git_url_result.set_scheme(Some("file".to_string())); } - let git_url = GitUrl { - scheme, - user, - password, - host, - port, - path, - print_scheme, - hint, - }; + git_url_result.set_print_scheme(print_scheme); + git_url_result.set_hint(hint); - git_url.is_valid()?; + git_url_result.is_valid()?; - Ok(git_url) + Ok(git_url_result) } /// ``` @@ -278,7 +308,7 @@ impl<'url> GitUrl<'url> { /// # } pub fn provider_info(&self) -> Result where - T: provider::GitProvider, GitUrlParseError>, + T: provider::GitProvider, { T::from_git_url(self) } diff --git a/src/types/provider/mod.rs b/src/types/provider/mod.rs index 54c4407..309da1d 100644 --- a/src/types/provider/mod.rs +++ b/src/types/provider/mod.rs @@ -12,7 +12,7 @@ use crate::types::GitUrlParseHint; use crate::{GitUrl, GitUrlParseError}; -use getset::{CloneGetters, CopyGetters}; +use getset::{CloneGetters, Getters}; use nom::Parser; use nom::bytes::complete::{is_not, tag, take_until}; use nom::combinator::opt; @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Debug, Clone, PartialEq, Eq)] /// struct MyCustomProvider; /// -/// impl GitProvider, GitUrlParseError> for MyCustomProvider { +/// impl GitProvider for MyCustomProvider { /// fn from_git_url(_url: &GitUrl) -> Result { /// // Do your custom parsing here with your GitUrl /// Ok(Self) @@ -76,17 +76,17 @@ pub trait GitProvider: Clone + std::fmt::Debug { /// assert_eq!(provider_info.fullname(), "tjtelan/git-url-parse-rs"); /// ``` /// -#[derive(Debug, PartialEq, Eq, Clone, CopyGetters)] +#[derive(Debug, PartialEq, Eq, Clone, Getters)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct GenericProvider<'a> { +#[getset(get = "pub")] +pub struct GenericProvider { /// Repo owner - owner: &'a str, + owner: String, /// Repo name - repo: &'a str, + repo: String, } -impl<'a> GenericProvider<'a> { +impl GenericProvider { /// Parse the most common form of git url by offered by git providers fn parse_path(input: &str) -> Result<(&str, GenericProvider), GitUrlParseError> { let (input, _) = opt(tag("/")).parse(input)?; @@ -95,7 +95,13 @@ impl<'a> GenericProvider<'a> { } else { separated_pair(is_not("/"), tag("/"), is_not("/")).parse(input)? }; - Ok((input, GenericProvider { owner: user, repo })) + Ok(( + input, + GenericProvider { + owner: user.to_string(), + repo: repo.to_string(), + }, + )) } /// Helper method to get the full name of a repo: `{owner}/{repo}` @@ -104,8 +110,8 @@ impl<'a> GenericProvider<'a> { } } -impl<'a> GitProvider, GitUrlParseError> for GenericProvider<'a> { - fn from_git_url(url: &GitUrl<'a>) -> Result { +impl GitProvider for GenericProvider { + fn from_git_url(url: &GitUrl) -> Result { if url.hint() == GitUrlParseHint::Filelike { return Err(GitUrlParseError::ProviderUnsupported); } @@ -138,19 +144,19 @@ impl<'a> GitProvider, GitUrlParseError> for GenericProvider<'a> { /// assert_eq!(provider_info.fullname(), "CompanyName/ProjectName/RepoName"); /// ``` /// -#[derive(Debug, PartialEq, Eq, Clone, CopyGetters)] +#[derive(Debug, PartialEq, Eq, Clone, Getters)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[getset(get_copy = "pub")] -pub struct AzureDevOpsProvider<'a> { +#[getset(get = "pub")] +pub struct AzureDevOpsProvider { /// Azure Devops organization name - org: &'a str, + org: String, /// Azure Devops project name - project: &'a str, + project: String, /// Azure Devops repo name - repo: &'a str, + repo: String, } -impl<'a> AzureDevOpsProvider<'a> { +impl AzureDevOpsProvider { /// Helper method to get the full name of a repo: `{org}/{project}/{repo}` pub fn fullname(&self) -> String { format!("{}/{}/{}", self.org, self.project, self.repo) @@ -173,7 +179,14 @@ impl<'a> AzureDevOpsProvider<'a> { ) .parse(input)?; - Ok((input, AzureDevOpsProvider { org, project, repo })) + Ok(( + input, + AzureDevOpsProvider { + org: org.to_string(), + project: project.to_string(), + repo: repo.to_string(), + }, + )) } /// Parse the path of an ssh url for Azure Devops patterns @@ -194,12 +207,19 @@ impl<'a> AzureDevOpsProvider<'a> { ) .parse(input)?; - Ok((input, AzureDevOpsProvider { org, project, repo })) + Ok(( + input, + AzureDevOpsProvider { + org: org.to_string(), + project: project.to_string(), + repo: repo.to_string(), + }, + )) } } -impl<'a> GitProvider, GitUrlParseError> for AzureDevOpsProvider<'a> { - fn from_git_url(url: &GitUrl<'a>) -> Result { +impl GitProvider for AzureDevOpsProvider { + fn from_git_url(url: &GitUrl) -> Result { let path = url.path(); let parsed = if url.hint() == GitUrlParseHint::Httplike { @@ -248,21 +268,32 @@ impl<'a> GitProvider, GitUrlParseError> for AzureDevOpsProvider<'a> { /// } /// ``` /// -#[derive(Clone, Debug, PartialEq, Eq, Default, CopyGetters, CloneGetters)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Getters, CloneGetters)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct GitLabProvider<'a> { +pub struct GitLabProvider { /// Repo owner - #[getset(get_copy = "pub")] - owner: &'a str, + #[getset(get = "pub")] + owner: String, /// Gitlab subgroups - #[getset(get_clone = "pub")] - subgroup: Option>, + //#[getset(get_clone = "pub")] + subgroup: Option>, /// Repo name - #[getset(get_copy = "pub")] - repo: &'a str, + #[getset(get = "pub")] + repo: String, } -impl<'a> GitLabProvider<'a> { +impl GitLabProvider { + /// Repo owner + /// Gitlab subgroups + pub fn subgroup(&self) -> Option> { + if let Some(s) = &self.subgroup { + let subgroup_vec: Vec<&str> = s.iter().map(|s| s.as_str()).collect(); + Some(subgroup_vec) + } else { + None + } + } + /// Helper method to get the full name of a repo: `{owner}/{repo}` or `{owner}/{subgroups}/{repo}` pub fn fullname(&self) -> String { if let Some(subgroup) = self.subgroup() { @@ -293,13 +324,19 @@ impl<'a> GitLabProvider<'a> { } // Last part is the repo - let repo = parts[parts.len() - 1]; + let repo = parts[parts.len() - 1].to_string(); // Everything before the last part is the owner/subgroups let (owner, subgroup) = if parts.len() > 2 { - (parts[0], Some(parts[1..parts.len() - 1].to_vec())) + let subgroup: Vec = parts[1..(parts.len() - 1)] + .iter() + .copied() + .map(|s| s.to_string()) + .collect(); + + (parts[0].to_string(), Some(subgroup)) } else { - (parts[0], None) + (parts[0].to_string(), None) }; Ok(( @@ -313,8 +350,8 @@ impl<'a> GitLabProvider<'a> { } } -impl<'a> GitProvider, GitUrlParseError> for GitLabProvider<'a> { - fn from_git_url(url: &GitUrl<'a>) -> Result { +impl GitProvider for GitLabProvider { + fn from_git_url(url: &GitUrl) -> Result { let path = url.path(); Self::parse_path(path).map(|(_, provider)| provider) } diff --git a/src/types/spec.rs b/src/types/spec.rs index 02a85f2..5ec76cb 100644 --- a/src/types/spec.rs +++ b/src/types/spec.rs @@ -3,7 +3,7 @@ //! Internal structs with RFC 3968 parsing logic for Git urls //! -use getset::CopyGetters; +use getset::Getters; #[cfg(feature = "log")] use log::debug; use nom::Finish; @@ -17,16 +17,16 @@ use nom::sequence::{pair, preceded, separated_pair, terminated}; use nom::{IResult, Parser, combinator::opt}; /// Top-level struct for RFC 3986 spec parser -#[derive(Debug, Default, Clone, Copy, CopyGetters)] -#[getset(get_copy = "pub")] -pub(crate) struct UrlSpecParser<'url> { +#[derive(Debug, Default, Clone, Getters)] +#[getset(get = "pub")] +pub(crate) struct UrlSpecParser { /// RFC 3986 scheme - pub(crate) scheme: Option<&'url str>, + pub(crate) scheme: Option, /// RFC 3986 hier-part - pub(crate) hier_part: UrlHierPart<'url>, + pub(crate) hier_part: UrlHierPart, } -impl<'url> UrlSpecParser<'url> { +impl UrlSpecParser { /// https://datatracker.ietf.org/doc/html/rfc3986 /// Based on rfc3986, but does not strictly cover the spec /// * No support for: @@ -36,7 +36,7 @@ impl<'url> UrlSpecParser<'url> { /// * parsing ssh git urls which use ":" as a delimiter between the authority and path /// * parsing userinfo into user:token (but its officially deprecated, per #section-3.2.1) /// * some limited support for windows/linux filepaths - pub(crate) fn parse(input: &'url str) -> IResult<&'url str, Self> { + pub(crate) fn parse(input: &str) -> IResult<&str, Self> { let (input, scheme) = Self::parse_scheme.parse(input).finish().unwrap_or_default(); let (input, heir_part) = Self::parse_hier_part(input).finish().unwrap_or_default(); @@ -49,7 +49,7 @@ impl<'url> UrlSpecParser<'url> { } /// RFC 3986 scheme - fn parse_scheme(input: &'url str) -> IResult<&'url str, Option<&'url str>> { + fn parse_scheme(input: &str) -> IResult<&str, Option> { #[cfg(feature = "log")] { debug!("Looking ahead before parsing for scheme"); @@ -76,7 +76,7 @@ impl<'url> UrlSpecParser<'url> { if Self::short_git_scheme_check(input) { // return early if we are normalizing 'git:' (short git) if let Ok((input, scheme)) = Self::short_git_scheme_parser().parse(input) { - return Ok((input, scheme)); + return Ok((input, scheme.map(|s| s.to_string()))); } } @@ -122,7 +122,7 @@ impl<'url> UrlSpecParser<'url> { debug!("{scheme:?}"); } - Ok((input, scheme)) + Ok((input, scheme.map(|s| s.to_string()))) } /// RFC 3986 hier-part @@ -130,7 +130,7 @@ impl<'url> UrlSpecParser<'url> { // The rfc says parsing the "//" part of the uri belongs to the hier-part parsing // but we only support common internet protocols, file paths, but not other "baseless" ones // so it is sensible for this move it with scheme parsing to support git user service urls - fn parse_hier_part(input: &'url str) -> IResult<&'url str, UrlHierPart<'url>> { + fn parse_hier_part(input: &str) -> IResult<&str, UrlHierPart> { #[cfg(feature = "log")] { debug!("Parsing for heir-part"); @@ -152,7 +152,10 @@ impl<'url> UrlSpecParser<'url> { ) .parse(input)?; - let hier_part = UrlHierPart { authority, path }; + let hier_part = UrlHierPart { + authority, + path: path.to_string(), + }; #[cfg(feature = "log")] { @@ -164,7 +167,7 @@ impl<'url> UrlSpecParser<'url> { } /// RFC 3986 authority - fn parse_authority(input: &'url str) -> IResult<&'url str, UrlAuthority<'url>> { + fn parse_authority(input: &str) -> IResult<&str, UrlAuthority> { #[cfg(feature = "log")] { debug!("Parsing for Authority"); @@ -229,7 +232,7 @@ impl<'url> UrlSpecParser<'url> { let authority = UrlAuthority { userinfo, - host, + host: host.map(|h| h.to_string()), port, }; @@ -243,7 +246,7 @@ impl<'url> UrlSpecParser<'url> { } /// RFC 3986 userinfo - fn parse_userinfo(authority_input: &'url str) -> IResult<&'url str, UrlUserInfo<'url>> { + fn parse_userinfo(authority_input: &str) -> IResult<&str, UrlUserInfo> { // Peek for username@ #[cfg(feature = "log")] { @@ -320,7 +323,10 @@ impl<'url> UrlSpecParser<'url> { (None, None) }; - let userinfo = UrlUserInfo { user, token }; + let userinfo = UrlUserInfo { + user: user.map(|u| u.to_string()), + token: token.map(|u| u.to_string()), + }; #[cfg(feature = "log")] { @@ -332,7 +338,7 @@ impl<'url> UrlSpecParser<'url> { } /// RFC 3986 port - fn parse_port(authority_input: &'url str) -> IResult<&'url str, Option> { + fn parse_port(authority_input: &str) -> IResult<&str, Option> { #[cfg(feature = "log")] { debug!("Parsing port"); @@ -364,7 +370,7 @@ impl<'url> UrlSpecParser<'url> { } /// RFC 3986 path-abempty - fn path_abempty_parser( + fn path_abempty_parser<'url>( ) -> impl Parser< &'url str, Output = > as Parser< @@ -388,7 +394,7 @@ impl<'url> UrlSpecParser<'url> { } /// Not part of RFC 3986 - ssh-based url path - fn path_ssh_parser( + fn path_ssh_parser<'url>( ) -> impl Parser< &'url str, Output = > as Parser< @@ -412,7 +418,7 @@ impl<'url> UrlSpecParser<'url> { } /// RFC 3986 path-rootless - fn path_rootless_parser( + fn path_rootless_parser<'url>( ) -> impl Parser< &'url str, Output = > as Parser< @@ -435,7 +441,7 @@ impl<'url> UrlSpecParser<'url> { } /// consuming parser for `git:` (short git) as scheme for normalizing - fn short_git_scheme_parser() -> impl Parser< + fn short_git_scheme_parser<'url>() -> impl Parser< &'url str, Output = UrlSpecParser<'url> { } /// Non-consuming check for `git:` (short git) as scheme for normalizing - fn short_git_scheme_check(input: &'url str) -> bool { + fn short_git_scheme_check(input: &str) -> bool { context( "short git validate", peek(terminated( @@ -473,35 +479,35 @@ impl<'url> UrlSpecParser<'url> { } /// RFC 3986 userinfo -#[derive(Debug, Default, Clone, Copy, CopyGetters)] -#[getset(get_copy = "pub")] -pub(crate) struct UrlUserInfo<'url> { +#[derive(Debug, Default, Clone, Getters)] +#[getset(get = "pub")] +pub(crate) struct UrlUserInfo { /// RFC 3986 Userinfo - pub(crate) user: Option<&'url str>, + pub(crate) user: Option, /// Non-spec, deprecated - pub(crate) token: Option<&'url str>, + pub(crate) token: Option, } /// RFC 3986 authority -#[derive(Debug, Default, Clone, Copy, CopyGetters)] -#[getset(get_copy = "pub")] -pub(crate) struct UrlAuthority<'url> { +#[derive(Debug, Default, Clone, Getters)] +#[getset(get = "pub")] +pub(crate) struct UrlAuthority { /// RFC 3986 Username, non-spec token - pub(crate) userinfo: UrlUserInfo<'url>, + pub(crate) userinfo: UrlUserInfo, /// RFC 3986 Host - pub(crate) host: Option<&'url str>, + pub(crate) host: Option, /// RFC 3986 Port pub(crate) port: Option, } /// RFC 3986 hier-part -#[derive(Debug, Default, Clone, Copy, CopyGetters)] -#[getset(get_copy = "pub")] -pub(crate) struct UrlHierPart<'url> { +#[derive(Debug, Default, Clone, Getters)] +#[getset(get = "pub")] +pub(crate) struct UrlHierPart { /// RFC 3986 authority - pub(crate) authority: UrlAuthority<'url>, + pub(crate) authority: UrlAuthority, /// RFC 3986 relative-part - pub(crate) path: &'url str, + pub(crate) path: String, } /// RFC 3986 pchar diff --git a/tests/provider.rs b/tests/provider.rs index 19aea9d..7d3bc46 100644 --- a/tests/provider.rs +++ b/tests/provider.rs @@ -46,7 +46,7 @@ fn ssh_generic_git() { fn custom_provider() { #[derive(Debug, Clone, PartialEq, Eq)] struct TestProvider; - impl GitProvider, GitUrlParseError> for TestProvider { + impl GitProvider for TestProvider { fn from_git_url(_url: &GitUrl) -> Result { Ok(Self) }