use std::{
cmp::Ordering,
collections::{BTreeMap, HashSet},
marker::PhantomData,
};
use itertools::Itertools;
use serde::{Serialize, Serializer};
use serde_with::{serde_as, SerializeAs};
use url::Url;
use crate::{
file_format_version::FileFormatVersion,
parse::{models::v7, V7},
Channel, CondaPackageData, EnvironmentData, EnvironmentPackageData, LockFile, LockFileInner,
PlatformData, PypiIndexes, PypiPackageData, SolveOptions, SourceIdentifier, UrlOrPath,
Verbatim,
};
#[serde_as]
#[derive(Serialize)]
#[serde(bound(serialize = "V: SerializeAs<PackageData<'a>>"))]
struct SerializableLockFile<'a, V> {
version: FileFormatVersion,
platforms: Vec<SerializablePlatform<'a>>,
environments: BTreeMap<&'a String, SerializableEnvironment<'a>>,
#[serde_as(as = "Vec<V>")]
packages: Vec<PackageData<'a>>,
#[serde(skip)]
_version: PhantomData<V>,
}
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
struct SerializablePlatform<'a> {
name: &'a str,
#[serde(default, skip_serializing_if = "Option::is_none")]
subdir: Option<&'static str>,
#[serde(default, skip_serializing_if = "<[String]>::is_empty")]
virtual_packages: &'a [String],
}
impl<'a> SerializablePlatform<'a> {
fn from_platform(platform: &'a PlatformData) -> Self {
let subdir = (platform.subdir.as_str() != platform.name.as_str())
.then_some(platform.subdir.as_str());
Self {
name: platform.name.as_str(),
subdir,
virtual_packages: &platform.virtual_packages,
}
}
}
#[derive(Serialize)]
struct SerializableEnvironment<'a> {
channels: &'a [Channel],
#[serde(flatten)]
indexes: Option<&'a PypiIndexes>,
#[serde(default, skip_serializing_if = "crate::utils::serde::is_default")]
options: SolveOptions,
packages: BTreeMap<String, Vec<SerializablePackageSelector<'a>>>,
}
impl<'a> SerializableEnvironment<'a> {
fn from_environment(
inner: &'a LockFileInner,
env_data: &'a EnvironmentData,
used_pypi_packages: &HashSet<usize>,
) -> Self {
SerializableEnvironment {
channels: &env_data.channels,
indexes: env_data.indexes.as_ref(),
options: env_data.options.clone(),
packages: env_data
.packages
.iter()
.map(|(platform, packages)| {
let platform_name = inner
.platforms
.get(*platform)
.expect("Platform indices are valid")
.name
.to_string();
(
platform_name,
packages
.iter()
.map(|&package_data| {
SerializablePackageSelector::from_lock_file(
inner,
package_data,
used_pypi_packages,
)
})
.sorted()
.collect(),
)
})
.collect(),
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Eq, PartialEq)]
#[serde(untagged)]
enum SerializablePackageDataV7<'a> {
Conda(v7::CondaPackageDataModel<'a>),
Source(v7::SourcePackageDataModel<'a>),
Pypi(v7::PypiPackageDataModel<'a>),
}
impl<'a> From<PackageData<'a>> for SerializablePackageDataV7<'a> {
fn from(package: PackageData<'a>) -> Self {
match package {
PackageData::Conda(CondaPackageData::Binary(binary)) => {
Self::Conda(binary.as_ref().into())
}
PackageData::Conda(CondaPackageData::Source(source)) => {
Self::Source(source.as_ref().into())
}
PackageData::Pypi(p) => Self::Pypi(p.into()),
}
}
}
#[derive(Serialize, Eq, PartialEq)]
#[serde(untagged, rename_all = "snake_case")]
enum SerializablePackageSelector<'a> {
Conda {
conda: &'a UrlOrPath,
},
CondaSource {
conda_source: SourceIdentifier,
},
Pypi {
pypi: &'a Verbatim<UrlOrPath>,
},
}
impl<'a> SerializablePackageSelector<'a> {
fn from_lock_file(
inner: &'a LockFileInner,
package: EnvironmentPackageData,
used_pypi_packages: &HashSet<usize>,
) -> Self {
match package {
EnvironmentPackageData::Conda(idx) => Self::from_conda(&inner.conda_packages[idx]),
EnvironmentPackageData::Pypi(pkg_data_idx) => Self::from_pypi(
inner,
&inner.pypi_packages[pkg_data_idx],
used_pypi_packages,
),
}
}
fn from_conda(package: &'a CondaPackageData) -> Self {
match package {
CondaPackageData::Source(source_data) => Self::CondaSource {
conda_source: SourceIdentifier::from_source_data(source_data),
},
CondaPackageData::Binary(binary_data) => Self::Conda {
conda: &binary_data.location,
},
}
}
fn from_pypi(
_inner: &'a LockFileInner,
package: &'a PypiPackageData,
_used_pypi_packages: &HashSet<usize>,
) -> Self {
Self::Pypi {
pypi: package.location(),
}
}
}
impl PartialOrd for SerializablePackageSelector<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SerializablePackageSelector<'_> {
fn cmp(&self, other: &Self) -> Ordering {
fn type_order(selector: &SerializablePackageSelector<'_>) -> u8 {
match selector {
SerializablePackageSelector::Conda { .. } => 0,
SerializablePackageSelector::CondaSource { .. } => 1,
SerializablePackageSelector::Pypi { .. } => 2,
}
}
let type_cmp = type_order(self).cmp(&type_order(other));
if type_cmp != Ordering::Equal {
return type_cmp;
}
match (self, other) {
(
SerializablePackageSelector::CondaSource { conda_source: a },
SerializablePackageSelector::CondaSource { conda_source: b },
) => {
a.name()
.cmp(b.name())
.then_with(|| a.hash().cmp(b.hash()))
.then_with(|| compare_url_by_location(a.location(), b.location()))
}
(
SerializablePackageSelector::Conda { conda: a },
SerializablePackageSelector::Conda { conda: b },
) => compare_url_by_location(a, b),
(
SerializablePackageSelector::Pypi { pypi: a },
SerializablePackageSelector::Pypi { pypi: b },
) => compare_url_by_location(a, b),
_ => unreachable!(),
}
}
}
fn compare_url_by_filename(a: &Url, b: &Url) -> Ordering {
if let (Some(a), Some(b)) = (
a.path_segments()
.and_then(Iterator::last)
.map(str::to_lowercase),
b.path_segments()
.and_then(Iterator::last)
.map(str::to_lowercase),
) {
match a.cmp(&b) {
Ordering::Equal => {}
ordering => return ordering,
}
}
a.cmp(b)
}
fn compare_url_by_location(a: &UrlOrPath, b: &UrlOrPath) -> Ordering {
match (a, b) {
(UrlOrPath::Url(a), UrlOrPath::Url(b)) => compare_url_by_filename(a, b),
(UrlOrPath::Url(_), UrlOrPath::Path(_)) => Ordering::Less,
(UrlOrPath::Path(_), UrlOrPath::Url(_)) => Ordering::Greater,
(UrlOrPath::Path(a), UrlOrPath::Path(b)) => a.as_str().cmp(b.as_str()),
}
}
impl<'a> SerializeAs<PackageData<'a>> for V7 {
fn serialize_as<S>(source: &PackageData<'a>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
SerializablePackageDataV7::from(*source).serialize(serializer)
}
}
impl Serialize for LockFile {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let inner = self.inner.as_ref();
let mut used_conda_packages = HashSet::new();
let mut used_pypi_packages = HashSet::new();
for env in inner.environments.iter() {
for packages in env.packages.values() {
for package in packages {
match package {
EnvironmentPackageData::Conda(idx) => {
used_conda_packages.insert(*idx);
}
EnvironmentPackageData::Pypi(pkg_idx) => {
used_pypi_packages.insert(*pkg_idx);
}
}
}
}
}
let environments = inner
.environment_lookup
.iter()
.map(|(name, env_idx)| {
(
name,
SerializableEnvironment::from_environment(
inner,
&inner.environments[*env_idx],
&used_pypi_packages,
),
)
})
.collect::<BTreeMap<_, _>>();
let mut seen_binary_locations = HashSet::new();
let conda_packages = inner
.conda_packages
.iter()
.enumerate()
.filter(|(idx, _)| used_conda_packages.contains(idx))
.filter(|(_, p)| {
match p {
CondaPackageData::Binary(binary) => {
seen_binary_locations.insert(binary.location.clone())
}
CondaPackageData::Source(_) => true,
}
})
.map(|(_, p)| PackageData::Conda(p));
let pypi_packages = inner
.pypi_packages
.iter()
.enumerate()
.filter(|(idx, _)| used_pypi_packages.contains(idx))
.map(|(_, p)| PackageData::Pypi(p));
let packages = itertools::chain!(conda_packages, pypi_packages).sorted();
let platforms = {
let mut tmp: Vec<_> = inner
.platforms
.iter()
.map(SerializablePlatform::from_platform)
.collect();
tmp.sort_by_key(|p| p.name);
tmp
};
let raw = SerializableLockFile {
version: FileFormatVersion::LATEST,
platforms,
environments,
packages: packages.collect(),
_version: PhantomData::<V7>,
};
raw.serialize(serializer)
}
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum PackageData<'a> {
Conda(&'a CondaPackageData),
Pypi(&'a PypiPackageData),
}
impl PackageData<'_> {
fn source_name(&self) -> &str {
match self {
PackageData::Conda(p) => p.name().as_source(),
PackageData::Pypi(p) => p.name().as_ref(),
}
}
}
impl PartialOrd<Self> for PackageData<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PackageData<'_> {
fn cmp(&self, other: &Self) -> Ordering {
use PackageData::{Conda, Pypi};
self.source_name()
.cmp(other.source_name())
.then_with(|| match (self, other) {
(Conda(a), Conda(b)) => a.cmp(b),
(Pypi(a), Pypi(b)) => a.cmp(b),
(Pypi(_), _) => Ordering::Less,
(_, Pypi(_)) => Ordering::Greater,
})
}
}
impl Serialize for CondaPackageData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
CondaPackageData::Binary(binary) => {
SerializablePackageDataV7::Conda(v7::CondaPackageDataModel::from(binary.as_ref()))
.serialize(serializer)
}
CondaPackageData::Source(source) => {
SerializablePackageDataV7::Source(v7::SourcePackageDataModel::from(source.as_ref()))
.serialize(serializer)
}
}
}
}
impl Serialize for PypiPackageData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
SerializablePackageDataV7::Pypi(v7::PypiPackageDataModel::from(self)).serialize(serializer)
}
}