Skip to content

Commit

Permalink
feat: introduce condaurl to enable better channel comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
baszalmstra committed Nov 12, 2024
1 parent cc379ae commit e05d2ad
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 70 deletions.
76 changes: 76 additions & 0 deletions crates/rattler_conda_types/src/channel/conda_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::fmt::{Display, Formatter};

use serde::{Deserialize, Deserializer, Serialize};
use url::Url;

use crate::Platform;

/// Represents a channel base url. This is a wrapper around an url that is
/// normalized:
///
/// * The URL always contains a trailing `/`.
///
/// This is useful to be able to compare different channels.
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize)]
#[serde(transparent)]
pub struct CondaUrl(Url);

impl CondaUrl {
/// Returns the base Url of the channel.
pub fn url(&self) -> &Url {
&self.0
}

/// Returns the string representation of the url.
pub fn as_str(&self) -> &str {
self.0.as_str()
}

/// Append the platform to the base url.
pub fn platform_url(&self, platform: Platform) -> Url {
self.0
.join(&format!("{}/", platform.as_str())) // trailing slash is important here as this signifies a directory
.expect("platform is a valid url fragment")
}
}

impl<'de> Deserialize<'de> for CondaUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let url = Url::deserialize(deserializer)?;
Ok(url.into())
}
}

impl From<Url> for CondaUrl {
fn from(url: Url) -> Self {
let path = url.path();
if path.ends_with('/') {
Self(url)
} else {
let mut url = url.clone();
url.set_path(&format!("{path}/"));
Self(url)
}
}
}

impl From<CondaUrl> for Url {
fn from(value: CondaUrl) -> Self {
value.0
}
}

impl AsRef<Url> for CondaUrl {
fn as_ref(&self) -> &Url {
&self.0
}
}

impl Display for CondaUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
95 changes: 41 additions & 54 deletions crates/rattler_conda_types/src/channel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ use typed_path::{Utf8NativePathBuf, Utf8TypedPath, Utf8TypedPathBuf};
use url::Url;

use super::{ParsePlatformError, Platform};
use crate::utils::{
path::is_path,
url::{add_trailing_slash, parse_scheme},
};
use crate::utils::{path::is_path, url::parse_scheme};

mod conda_url;

pub use conda_url::CondaUrl;

const DEFAULT_CHANNEL_ALIAS: &str = "https://conda.anaconda.org";

Expand Down Expand Up @@ -105,7 +106,7 @@ impl NamedChannelOrUrl {

/// Converts the channel to a base url using the given configuration.
/// This method ensures that the base url always ends with a `/`.
pub fn into_base_url(self, config: &ChannelConfig) -> Result<Url, ParseChannelError> {
pub fn into_base_url(self, config: &ChannelConfig) -> Result<CondaUrl, ParseChannelError> {
let url = match self {
NamedChannelOrUrl::Name(name) => {
let mut base_url = config.channel_alias.clone();
Expand All @@ -114,16 +115,17 @@ impl NamedChannelOrUrl {
segments.push(segment);
}
}
base_url
base_url.into()
}
NamedChannelOrUrl::Url(url) => url,
NamedChannelOrUrl::Url(url) => url.into(),
NamedChannelOrUrl::Path(path) => {
let absolute_path = absolute_path(path.as_str(), &config.root_dir)?;
directory_path_to_url(absolute_path.to_path())
.map_err(|_err| ParseChannelError::InvalidPath(path.to_string()))?
.into()
}
};
Ok(add_trailing_slash(&url).into_owned())
Ok(url)
}

/// Converts this instance into a channel.
Expand All @@ -136,7 +138,8 @@ impl NamedChannelOrUrl {
let base_url = self.into_base_url(config)?;
Ok(Channel {
name,
..Channel::from_url(base_url)
base_url,
platforms: None,
})
}
}
Expand Down Expand Up @@ -189,7 +192,7 @@ pub struct Channel {
pub platforms: Option<Vec<Platform>>,

/// Base URL of the channel, everything is relative to this url.
pub base_url: Url,
pub base_url: CondaUrl,

/// The name of the channel
pub name: Option<String>,
Expand Down Expand Up @@ -221,7 +224,7 @@ impl Channel {
.map_err(|_err| ParseChannelError::InvalidPath(channel.to_owned()))?;
Self {
platforms,
base_url: url,
base_url: url.into(),
name: Some(channel.to_owned()),
}
}
Expand Down Expand Up @@ -252,39 +255,30 @@ impl Channel {
// Get the path part of the URL but trim the directory suffix
let path = url.path().trim_end_matches('/');

// Ensure that the base_url does always ends in a `/`
let base_url = if url.path().ends_with('/') {
url.clone()
} else {
let mut url = url.clone();
url.set_path(&format!("{path}/"));
url
};

// Case 1: No path give, channel name is ""

// Case 2: migrated_custom_channels
// Case 3: migrated_channel_aliases
// Case 4: custom_channels matches
// Case 5: channel_alias match

if base_url.has_host() {
if url.has_host() {
// Case 7: Fallback
let name = path.trim_start_matches('/');
Self {
platforms: None,
name: (!name.is_empty()).then_some(name).map(str::to_owned),
base_url,
base_url: url.into(),
}
} else {
// Case 6: non-otherwise-specified file://-type urls
let name = path
.rsplit_once('/')
.map_or_else(|| base_url.path(), |(_, path_part)| path_part);
.map_or_else(|| path, |(_, path_part)| path_part);
Self {
platforms: None,
name: (!name.is_empty()).then_some(name).map(str::to_owned),
base_url,
base_url: url.into(),
}
}
}
Expand All @@ -305,7 +299,8 @@ impl Channel {
base_url: config
.channel_alias
.join(dir_name.as_ref())
.expect("name is not a valid Url"),
.expect("name is not a valid Url")
.into(),
name: (!name.is_empty()).then_some(name).map(str::to_owned),
}
}
Expand All @@ -329,14 +324,14 @@ impl Channel {
let url = Url::from_directory_path(path).expect("path is a valid url");
Self {
platforms: None,
base_url: url,
base_url: url.into(),
name: None,
}
}

/// Returns the name of the channel
pub fn name(&self) -> &str {
match self.base_url().scheme() {
match self.base_url.url().scheme() {
// The name of the channel is only defined for http and https channels.
// If the name is not defined we return the base url.
"https" | "http" => self
Expand All @@ -347,17 +342,9 @@ impl Channel {
}
}

/// Returns the base Url of the channel. This does not include the platform
/// part.
pub fn base_url(&self) -> &Url {
&self.base_url
}

/// Returns the Urls for the given platform
pub fn platform_url(&self, platform: Platform) -> Url {
self.base_url()
.join(&format!("{}/", platform.as_str())) // trailing slash is important here as this signifies a directory
.expect("platform is a valid url fragment")
self.base_url.platform_url(platform)
}

/// Returns the Urls for all the supported platforms of this package.
Expand All @@ -380,7 +367,7 @@ impl Channel {

/// Returns the canonical name of the channel
pub fn canonical_name(&self) -> String {
self.base_url.clone().redact().to_string()
self.base_url.url().clone().redact().to_string()
}
}

Expand Down Expand Up @@ -579,7 +566,7 @@ mod tests {

let channel = Channel::from_str("conda-forge", &config).unwrap();
assert_eq!(
channel.base_url,
channel.base_url.url().clone(),
Url::from_str("https://conda.anaconda.org/conda-forge/").unwrap()
);
assert_eq!(channel.name.as_deref(), Some("conda-forge"));
Expand All @@ -596,14 +583,14 @@ mod tests {
let channel =
Channel::from_str("https://conda.anaconda.org/conda-forge/", &config).unwrap();
assert_eq!(
channel.base_url,
channel.base_url.url().clone(),
Url::from_str("https://conda.anaconda.org/conda-forge/").unwrap()
);
assert_eq!(channel.name.as_deref(), Some("conda-forge"));
assert_eq!(channel.name(), "conda-forge");
assert_eq!(channel.platforms, None);
assert_eq!(
channel.base_url().to_string(),
channel.base_url.to_string(),
"https://conda.anaconda.org/conda-forge/"
);

Expand All @@ -622,12 +609,12 @@ mod tests {
assert_eq!(channel.name.as_deref(), Some("conda-forge"));
assert_eq!(channel.name(), "file:///var/channels/conda-forge/");
assert_eq!(
channel.base_url,
channel.base_url.url().clone(),
Url::from_str("file:///var/channels/conda-forge/").unwrap()
);
assert_eq!(channel.platforms, None);
assert_eq!(
channel.base_url().to_string(),
channel.base_url.to_string(),
"file:///var/channels/conda-forge/"
);

Expand All @@ -643,7 +630,7 @@ mod tests {
);
assert_eq!(channel.platforms, None);
assert_eq!(
channel.base_url().to_file_path().unwrap(),
channel.base_url.url().to_file_path().unwrap(),
current_dir.join("dir/does/not_exist")
);
}
Expand All @@ -654,7 +641,7 @@ mod tests {

let channel = Channel::from_str("http://localhost:1234", &config).unwrap();
assert_eq!(
channel.base_url,
channel.base_url.url().clone(),
Url::from_str("http://localhost:1234/").unwrap()
);
assert_eq!(channel.name, None);
Expand All @@ -681,7 +668,7 @@ mod tests {
)
.unwrap();
assert_eq!(
channel.base_url,
channel.base_url.url().clone(),
Url::from_str("https://conda.anaconda.org/conda-forge/").unwrap()
);
assert_eq!(channel.name.as_deref(), Some("conda-forge"));
Expand All @@ -693,15 +680,15 @@ mod tests {
)
.unwrap();
assert_eq!(
channel.base_url,
channel.base_url.url().clone(),
Url::from_str("https://conda.anaconda.org/pkgs/main/").unwrap()
);
assert_eq!(channel.name.as_deref(), Some("pkgs/main"));
assert_eq!(channel.platforms, Some(vec![platform]));

let channel = Channel::from_str("conda-forge/label/rust_dev", &config).unwrap();
assert_eq!(
channel.base_url,
channel.base_url.url().clone(),
Url::from_str("https://conda.anaconda.org/conda-forge/label/rust_dev/").unwrap()
);
assert_eq!(channel.name.as_deref(), Some("conda-forge/label/rust_dev"));
Expand Down Expand Up @@ -785,8 +772,8 @@ mod tests {

for channel_str in test_channels {
let channel = Channel::from_str(channel_str, &channel_config).unwrap();
assert!(channel.base_url().as_str().ends_with('/'));
assert!(!channel.base_url().as_str().ends_with("//"));
assert!(channel.base_url.as_str().ends_with('/'));
assert!(!channel.base_url.as_str().ends_with("//"));

let named_channel = NamedChannelOrUrl::from_str(channel_str).unwrap();
let base_url = named_channel
Expand All @@ -798,8 +785,8 @@ mod tests {
assert!(!base_url_str.ends_with("//"));

let channel = named_channel.into_channel(&channel_config).unwrap();
assert!(channel.base_url().as_str().ends_with('/'));
assert!(!channel.base_url().as_str().ends_with("//"));
assert!(channel.base_url.as_str().ends_with('/'));
assert!(!channel.base_url.as_str().ends_with("//"));
}
}

Expand All @@ -813,14 +800,14 @@ mod tests {
let channel = Channel::from_str("conda-forge", &channel_config).unwrap();
assert_eq!(
&channel.base_url,
named.into_channel(&channel_config).unwrap().base_url()
&named.into_channel(&channel_config).unwrap().base_url
);

let named = NamedChannelOrUrl::Name("nvidia/label/cuda-11.8.0".to_string());
let channel = Channel::from_str("nvidia/label/cuda-11.8.0", &channel_config).unwrap();
assert_eq!(
channel.base_url(),
named.into_channel(&channel_config).unwrap().base_url()
channel.base_url,
named.into_channel(&channel_config).unwrap().base_url
);
}
}
2 changes: 1 addition & 1 deletion crates/rattler_conda_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub mod prefix_record;
use std::path::{Path, PathBuf};

pub use build_spec::{BuildNumber, BuildNumberSpec, ParseBuildNumberSpecError};
pub use channel::{Channel, ChannelConfig, NamedChannelOrUrl, ParseChannelError};
pub use channel::{Channel, ChannelConfig, CondaUrl, NamedChannelOrUrl, ParseChannelError};
pub use channel_data::{ChannelData, ChannelDataPackage};
pub use environment_yaml::{EnvironmentYaml, MatchSpecOrSubSection};
pub use explicit_environment_spec::{
Expand Down
Loading

0 comments on commit e05d2ad

Please sign in to comment.