Skip to content

Commit 1dcc198

Browse files
committed
Add parse_to_url, TryFrom and GitProvider for Url
1 parent d2a4573 commit 1dcc198

File tree

5 files changed

+417
-314
lines changed

5 files changed

+417
-314
lines changed

src/types/mod.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,24 @@ impl TryFrom<GitUrl> for Url {
196196
}
197197
}
198198

199+
#[cfg(feature = "url")]
200+
impl TryFrom<&Url> for GitUrl {
201+
type Error = GitUrlParseError;
202+
fn try_from(value: &Url) -> Result<Self, Self::Error> {
203+
// Since we don't fully implement any spec, we'll rely on the url crate
204+
GitUrl::parse(value.as_str())
205+
}
206+
}
207+
208+
#[cfg(feature = "url")]
209+
impl TryFrom<Url> for GitUrl {
210+
type Error = GitUrlParseError;
211+
fn try_from(value: Url) -> Result<Self, Self::Error> {
212+
// Since we don't fully implement any spec, we'll rely on the url crate
213+
GitUrl::parse(value.as_str())
214+
}
215+
}
216+
199217
impl GitUrl {
200218
/// Returns `GitUrl` after removing all user info values
201219
pub fn trim_auth(&self) -> GitUrl {
@@ -219,8 +237,16 @@ impl GitUrl {
219237
/// # }
220238
/// ```
221239
pub fn parse(input: &str) -> Result<Self, GitUrlParseError> {
222-
let mut git_url_result = GitUrl::default();
240+
let git_url = Self::parse_to_git_url(input)?;
241+
242+
git_url.is_valid()?;
223243

244+
Ok(git_url)
245+
}
246+
247+
/// Internal parse to `GitUrl` without further validation
248+
fn parse_to_git_url(input: &str) -> Result<Self, GitUrlParseError> {
249+
let mut git_url_result = GitUrl::default();
224250
// Error if there are null bytes within the url
225251
// https://github.com/tjtelan/git-url-parse-rs/issues/16
226252
if input.contains('\0') {
@@ -294,6 +320,14 @@ impl GitUrl {
294320
Ok(git_url_result)
295321
}
296322

323+
/// Normalize input into form that can be used by [`Url::parse`](https://docs.rs/url/latest/url/struct.Url.html#method.parse)
324+
#[cfg(feature = "url")]
325+
pub fn parse_to_url(input: &str) -> Result<Url, GitUrlParseError> {
326+
let git_url = Self::parse_to_git_url(input)?;
327+
328+
Ok(Url::try_from(git_url)?)
329+
}
330+
297331
/// ```
298332
/// use git_url_parse::GitUrl;
299333
/// use git_url_parse::types::provider::GenericProvider;

src/types/provider/azure_devops.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use super::GitProvider;
2+
use crate::types::GitUrlParseHint;
3+
use crate::{GitUrl, GitUrlParseError};
4+
5+
use getset::Getters;
6+
use nom::Parser;
7+
use nom::bytes::complete::{is_not, tag, take_until};
8+
use nom::combinator::opt;
9+
use nom::sequence::{preceded, separated_pair, terminated};
10+
#[cfg(feature = "serde")]
11+
use serde::{Deserialize, Serialize};
12+
#[cfg(feature = "url")]
13+
use url::Url;
14+
15+
/// Azure DevOps repository provider
16+
/// ## Supported URL Formats
17+
///
18+
/// - `https://dev.azure.com/org/project/_git/repo`
19+
/// - `[email protected]:v3/org/project/repo`
20+
///
21+
/// Example:
22+
///
23+
/// ```
24+
/// use git_url_parse::{GitUrl, GitUrlParseError};
25+
/// use git_url_parse::types::provider::AzureDevOpsProvider;
26+
///
27+
/// let test_url = "https://[email protected]/CompanyName/ProjectName/_git/RepoName";
28+
/// let parsed = GitUrl::parse(test_url).expect("URL parse failed");
29+
///
30+
/// let provider_info: AzureDevOpsProvider = parsed.provider_info().unwrap();
31+
///
32+
/// assert_eq!(provider_info.org(), "CompanyName");
33+
/// assert_eq!(provider_info.project(), "ProjectName");
34+
/// assert_eq!(provider_info.repo(), "RepoName");
35+
/// assert_eq!(provider_info.fullname(), "CompanyName/ProjectName/RepoName");
36+
/// ```
37+
///
38+
#[derive(Debug, PartialEq, Eq, Clone, Getters)]
39+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40+
#[getset(get = "pub")]
41+
pub struct AzureDevOpsProvider {
42+
/// Azure Devops organization name
43+
org: String,
44+
/// Azure Devops project name
45+
project: String,
46+
/// Azure Devops repo name
47+
repo: String,
48+
}
49+
50+
impl AzureDevOpsProvider {
51+
/// Helper method to get the full name of a repo: `{org}/{project}/{repo}`
52+
pub fn fullname(&self) -> String {
53+
format!("{}/{}/{}", self.org, self.project, self.repo)
54+
}
55+
56+
/// Parse the path of a http url for Azure Devops patterns
57+
fn parse_http_path(input: &str) -> Result<(&str, AzureDevOpsProvider), GitUrlParseError> {
58+
// Handle optional leading /
59+
let (input, _) = opt(tag("/")).parse(input)?;
60+
61+
// Parse org/project/repo
62+
let (input, (org, (project, repo))) = separated_pair(
63+
is_not("/"),
64+
tag("/"),
65+
separated_pair(
66+
is_not("/"),
67+
tag("/"),
68+
preceded(opt(tag("_git/")), is_not("")),
69+
),
70+
)
71+
.parse(input)?;
72+
73+
Ok((
74+
input,
75+
AzureDevOpsProvider {
76+
org: org.to_string(),
77+
project: project.to_string(),
78+
repo: repo.to_string(),
79+
},
80+
))
81+
}
82+
83+
/// Parse the path of an ssh url for Azure Devops patterns
84+
fn parse_ssh_path(input: &str) -> Result<(&str, AzureDevOpsProvider), GitUrlParseError> {
85+
// Handle optional leading v3/ or other prefix
86+
let (input, _) = opt(take_until("/")).parse(input)?;
87+
let (input, _) = opt(tag("/")).parse(input)?;
88+
89+
// Parse org/project/repo
90+
let (input, (org, (project, repo))) = separated_pair(
91+
is_not("/"),
92+
tag("/"),
93+
separated_pair(
94+
is_not("/"),
95+
tag("/"),
96+
terminated(is_not("."), opt(tag(".git"))),
97+
),
98+
)
99+
.parse(input)?;
100+
101+
Ok((
102+
input,
103+
AzureDevOpsProvider {
104+
org: org.to_string(),
105+
project: project.to_string(),
106+
repo: repo.to_string(),
107+
},
108+
))
109+
}
110+
}
111+
112+
impl GitProvider<GitUrl, GitUrlParseError> for AzureDevOpsProvider {
113+
fn from_git_url(url: &GitUrl) -> Result<Self, GitUrlParseError> {
114+
let path = url.path();
115+
116+
let parsed = if url.hint() == GitUrlParseHint::Httplike {
117+
Self::parse_http_path(path)
118+
} else {
119+
Self::parse_ssh_path(path)
120+
};
121+
122+
parsed.map(|(_, provider)| provider)
123+
}
124+
}
125+
126+
#[cfg(feature = "url")]
127+
impl GitProvider<Url, GitUrlParseError> for AzureDevOpsProvider {
128+
fn from_git_url(url: &Url) -> Result<Self, GitUrlParseError> {
129+
let path = url.path();
130+
131+
let parsed = if url.scheme().contains("http") {
132+
Self::parse_http_path(path)
133+
} else {
134+
Self::parse_ssh_path(path)
135+
};
136+
137+
parsed.map(|(_, provider)| provider)
138+
}
139+
}

src/types/provider/generic.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use super::GitProvider;
2+
use crate::types::GitUrlParseHint;
3+
use crate::{GitUrl, GitUrlParseError};
4+
5+
use getset::Getters;
6+
use nom::Parser;
7+
use nom::bytes::complete::{is_not, tag, take_until};
8+
use nom::combinator::opt;
9+
use nom::sequence::separated_pair;
10+
#[cfg(feature = "serde")]
11+
use serde::{Deserialize, Serialize};
12+
#[cfg(feature = "url")]
13+
use url::Url;
14+
15+
/// Represents a generic Git repository provider
16+
///
17+
/// ## Typical Use Cases
18+
///
19+
/// - Common service hosting with `owner/repo` patterns (e.g. GitHub, Bitbucket)
20+
/// - Self-hosted repositories (e.g. Codeberg, Gitea)
21+
///
22+
/// Example:
23+
///
24+
/// ```
25+
/// use git_url_parse::{GitUrl, GitUrlParseError};
26+
/// use git_url_parse::types::provider::GenericProvider;
27+
///
28+
/// let test_url = "[email protected]:tjtelan/git-url-parse-rs.git";
29+
/// let parsed = GitUrl::parse(test_url).expect("URL parse failed");
30+
///
31+
/// let provider_info: GenericProvider = parsed.provider_info().unwrap();
32+
///
33+
/// assert_eq!(provider_info.owner(), "tjtelan");
34+
/// assert_eq!(provider_info.repo(), "git-url-parse-rs");
35+
/// assert_eq!(provider_info.fullname(), "tjtelan/git-url-parse-rs");
36+
/// ```
37+
///
38+
#[derive(Debug, PartialEq, Eq, Clone, Getters)]
39+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40+
#[getset(get = "pub")]
41+
pub struct GenericProvider {
42+
/// Repo owner
43+
owner: String,
44+
/// Repo name
45+
repo: String,
46+
}
47+
48+
impl GenericProvider {
49+
/// Parse the most common form of git url by offered by git providers
50+
fn parse_path(input: &str) -> Result<(&str, GenericProvider), GitUrlParseError> {
51+
let (input, _) = opt(tag("/")).parse(input)?;
52+
let (input, (user, repo)) = if input.ends_with(".git") {
53+
separated_pair(is_not("/"), tag("/"), take_until(".git")).parse(input)?
54+
} else {
55+
separated_pair(is_not("/"), tag("/"), is_not("/")).parse(input)?
56+
};
57+
Ok((
58+
input,
59+
GenericProvider {
60+
owner: user.to_string(),
61+
repo: repo.to_string(),
62+
},
63+
))
64+
}
65+
66+
/// Helper method to get the full name of a repo: `{owner}/{repo}`
67+
pub fn fullname(&self) -> String {
68+
format!("{}/{}", self.owner, self.repo)
69+
}
70+
}
71+
72+
impl GitProvider<GitUrl, GitUrlParseError> for GenericProvider {
73+
fn from_git_url(url: &GitUrl) -> Result<Self, GitUrlParseError> {
74+
if url.hint() == GitUrlParseHint::Filelike {
75+
return Err(GitUrlParseError::ProviderUnsupported);
76+
}
77+
78+
let path = url.path();
79+
Self::parse_path(path).map(|(_, provider)| provider)
80+
}
81+
}
82+
83+
#[cfg(feature = "url")]
84+
impl GitProvider<Url, GitUrlParseError> for GenericProvider {
85+
fn from_git_url(url: &Url) -> Result<Self, GitUrlParseError> {
86+
if url.scheme() == "file" {
87+
return Err(GitUrlParseError::ProviderUnsupported);
88+
}
89+
90+
let path = url.path();
91+
Self::parse_path(path).map(|(_, provider)| provider)
92+
}
93+
}

0 commit comments

Comments
 (0)