use std::fmt;
use once_cell::sync::Lazy;
use serde::{Serialize, Deserialize, Deserializer, Serializer};
use url::Url;
use crate::Error;
use crate::version::PackageVersion;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Registry(url::Url);
impl Registry {
fn try_from_segments(segments: &[&str]) -> Option<Self> {
if segments.is_empty() {
return None;
}
let mut reconstructed: String = segments.join("/");
if !reconstructed.ends_with('/') {
reconstructed.push('/');
}
let registry_url = url::Url::parse(&reconstructed).ok()?;
let registry = Registry::from(registry_url);
Some(registry)
}
}
static DEFAULT_REGISTRY: Lazy<Registry> = Lazy::new(|| {
let url = url::Url::parse("https://packages.fluvio.io/v1/").unwrap();
Registry::from(url)
});
static DEFAULT_GROUP: Lazy<GroupName> = Lazy::new(|| GroupName("fluvio".to_owned()));
impl fmt::Display for Registry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Default for Registry {
fn default() -> Self {
DEFAULT_REGISTRY.clone()
}
}
impl std::str::FromStr for Registry {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let url = url::Url::parse(s).map_err(Error::FailedToParseRegistry)?;
Ok(Self(url))
}
}
impl From<url::Url> for Registry {
fn from(url: url::Url) -> Self {
Self(url)
}
}
impl AsRef<url::Url> for Registry {
fn as_ref(&self) -> &Url {
&self.0
}
}
macro_rules! deserialize_no_slash_string {
($mod:ident, $id:ident, $err:expr, $string_name:expr $(,)?) => {
mod $mod {
use super::$id;
use super::Error;
impl std::str::FromStr for $id {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.find('/').is_some() {
return Err($err("cannot contain '/'".to_string()));
}
if s.find(':').is_some() {
return Err($err("cannot contain ':'".to_string()));
}
return Ok(Self(s.to_string()));
}
}
impl $id {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> serde::Deserialize<'de> for $id {
fn deserialize<D>(
deserializer: D,
) -> Result<Self, <D as serde::Deserializer<'de>>::Error>
where
D: serde::Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
if let Err(e) = string.parse::<$id>() {
use serde::de::{Unexpected, Error as DeserializeError};
return Err(DeserializeError::invalid_value(
Unexpected::Other(&e.to_string()),
&&*format!("valid {}", $string_name),
));
}
Ok(Self(string))
}
}
}
};
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct GroupName(String);
impl fmt::Display for GroupName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
deserialize_no_slash_string!(group, GroupName, Error::InvalidGroupName, "index group");
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct PackageName(String);
impl fmt::Display for PackageName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
deserialize_no_slash_string!(
package_name,
PackageName,
Error::InvalidPackageName,
"package name"
);
pub type WithVersion = PackageVersion;
pub type MaybeVersion = Option<PackageVersion>;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PackageId<V = MaybeVersion> {
registry: Option<Registry>,
group: Option<GroupName>,
name: PackageName,
version: V,
}
impl<T> PackageId<T> {
pub fn registry(&self) -> &Registry {
match self.registry.as_ref() {
Some(registry) => registry,
None => &DEFAULT_REGISTRY,
}
}
pub fn group(&self) -> &GroupName {
match self.group.as_ref() {
Some(group) => group,
None => &DEFAULT_GROUP,
}
}
pub fn name(&self) -> &PackageName {
&self.name
}
pub fn pretty(&self) -> impl fmt::Display {
let prefix = match (self.registry.as_ref(), self.group.as_ref()) {
(Some(reg), _) => format!("{}{}/", reg, self.group()),
(None, Some(group)) => format!("{group}/"),
(None, None) => "".to_string(),
};
format!("{}{}", prefix, self.name)
}
pub fn uid(&self) -> String {
format!("{}{}/{}", self.registry(), self.group(), self.name)
}
}
impl PackageId<WithVersion> {
pub fn version(&self) -> &PackageVersion {
&self.version
}
pub fn into_version(self) -> PackageVersion {
self.version
}
pub fn into_maybe_versioned(self) -> PackageId<MaybeVersion> {
PackageId {
name: self.name,
group: self.group,
registry: self.registry,
version: Some(self.version),
}
}
pub fn into_unversioned(self) -> PackageId<MaybeVersion> {
PackageId {
name: self.name,
group: self.group,
registry: self.registry,
version: None,
}
}
}
impl PackageId<MaybeVersion> {
pub(crate) fn new_unversioned(name: PackageName, group: GroupName) -> Self {
PackageId {
registry: None,
group: Some(group),
name,
version: None,
}
}
pub fn into_versioned(self, version: PackageVersion) -> PackageId<WithVersion> {
PackageId {
registry: self.registry,
name: self.name,
group: self.group,
version,
}
}
}
impl PackageId<MaybeVersion> {
pub fn maybe_version(&self) -> Option<&PackageVersion> {
self.version.as_ref()
}
}
impl std::str::FromStr for PackageId<WithVersion> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut segments: Vec<&str> = s.split('/').collect();
let name_version_segment = segments.pop().unwrap();
let name_version_segments: Vec<&str> = name_version_segment.split(':').collect();
let (name_string, version_string) = match &name_version_segments[..] {
[name_string, version_string] => (name_string, version_string),
_ => return Err(Error::InvalidNameVersionSegment),
};
let version = version_string.parse::<PackageVersion>()?;
let name: PackageName = name_string.parse()?;
let maybe_group_segment = segments.pop();
let group: Option<GroupName> = match maybe_group_segment {
None | Some("fluvio") => None,
Some(group_string) => Some(group_string.parse()?),
};
let registry = Registry::try_from_segments(&segments);
let package_id = PackageId {
registry,
group,
name,
version,
};
Ok(package_id)
}
}
impl std::str::FromStr for PackageId<MaybeVersion> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut segments: Vec<&str> = s.split('/').collect();
let name_version_segment = segments.pop().unwrap();
let name_version_segments: Vec<&str> = name_version_segment.split(':').collect();
let (name_string, version_string) = match &name_version_segments[..] {
[name_string] => (name_string, None),
[name_string, version_string] => (name_string, Some(version_string)),
_ => return Err(Error::InvalidNameVersionSegment),
};
let name: PackageName = name_string.parse()?;
let version = match version_string {
Some(version_string) => Some(version_string.parse::<PackageVersion>()?),
None => None,
};
let maybe_group_string = segments.pop();
let group: Option<GroupName> = match maybe_group_string {
None | Some("fluvio") => None,
Some(group_string) => Some(group_string.parse()?),
};
let registry = Registry::try_from_segments(&segments);
let package_id = PackageId {
registry,
group,
name,
version,
};
Ok(package_id)
}
}
impl fmt::Display for PackageId<WithVersion> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let registry = self.registry.as_ref().map(|it| it.0.as_str()).unwrap_or("");
write!(
f,
"{registry}{group}/{name}:{version}",
registry = registry,
group = self.group(),
name = self.name,
version = self.version,
)
}
}
impl fmt::Display for PackageId<MaybeVersion> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let registry = self.registry.as_ref().map(|it| it.0.as_str()).unwrap_or("");
let version = self
.version
.as_ref()
.map(|it| format!(":{it}"))
.unwrap_or_default();
write!(
f,
"{registry}{group}/{name}{version}",
registry = registry,
group = self.group(),
name = self.name.as_str(),
version = version,
)
}
}
impl<'de> Deserialize<'de> for PackageId<WithVersion> {
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
let package_id = match string.parse::<PackageId<WithVersion>>() {
Ok(pid) => pid,
Err(e) => {
use serde::de::{Unexpected, Error as DeserializeError};
return Err(DeserializeError::invalid_value(
Unexpected::Other("Invalid PackageId"),
&&*format!("A PackageId, <registry>/<group>/<name>:<version>, where <registry> is optional: {e}"),
));
}
};
Ok(package_id)
}
}
impl<'de> Deserialize<'de> for PackageId<MaybeVersion> {
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
let package_id = match string.parse::<PackageId<MaybeVersion>>() {
Ok(pid) => pid,
Err(e) => {
use serde::de::{Unexpected, Error as DeserializeError};
return Err(DeserializeError::invalid_value(
Unexpected::Other("Invalid PackageId"),
&&*format!("A PackageId, <registry>/<group>/<name>:<version>, where <registry> is optional: {e}"),
));
}
};
Ok(package_id)
}
}
impl Serialize for PackageId<WithVersion> {
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
self.to_string().serialize(serializer)
}
}
impl Serialize for PackageId<MaybeVersion> {
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
self.to_string().serialize(serializer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package_id::{Registry, PackageId};
use semver::Version;
#[test]
fn test_deserialize_group() {
let group: GroupName = "fluvio.io".parse().unwrap();
#[derive(Debug, Deserialize, Serialize)]
struct JsonGroup {
group: GroupName,
}
let json_group = JsonGroup {
group: group.clone(),
};
let group_string = serde_json::to_string(&json_group).unwrap();
let read_json_group: JsonGroup = serde_json::from_str(&group_string).unwrap();
let read_group = read_json_group.group;
assert_eq!(group, read_group);
assert!("fluvio/io".parse::<GroupName>().is_err());
let bad_json_group = JsonGroup {
group: GroupName("fluvio/io".to_string()),
};
let bad_group_string = serde_json::to_string(&bad_json_group).unwrap();
let read_bad_group_result = serde_json::from_str::<JsonGroup>(&bad_group_string);
assert!(read_bad_group_result.is_err());
}
#[test]
fn test_parse_package_id_default_registry() {
let package_id: PackageId<WithVersion> = "fluvio.io/fluvio:0.6.0".parse().unwrap();
assert_eq!(package_id.registry(), &Registry::default());
assert_eq!(package_id.group().as_str(), "fluvio.io");
assert_eq!(package_id.name().as_str(), "fluvio");
assert_eq!(
package_id.version(),
&PackageVersion::Semver(Version::parse("0.6.0").unwrap())
);
}
#[test]
fn test_parse_package_id_custom_registry() {
let registry_url = "https://other.registry.io/v2/";
let package_id: PackageId<WithVersion> = format!("{registry_url}/fluvio.io/fluvio:0.6.0")
.parse()
.unwrap();
assert_eq!(package_id.registry(), ®istry_url.parse().unwrap());
assert_eq!(package_id.group().as_str(), "fluvio.io");
assert_eq!(package_id.name().as_str(), "fluvio");
assert_eq!(
package_id.version(),
&PackageVersion::Semver(Version::parse("0.6.0").unwrap())
);
}
#[test]
fn test_parse_package_id_default_group() {
let package_id: PackageId<WithVersion> = "fluvio-cloud:0.1.4".parse().unwrap();
assert_eq!(package_id.registry(), &Registry::default());
assert_eq!(package_id.group().as_str(), "fluvio");
assert_eq!(package_id.name().as_str(), "fluvio-cloud");
assert_eq!(
package_id.version(),
&PackageVersion::Semver(Version::parse("0.1.4").unwrap())
);
}
#[test]
fn test_parse_package_id_default_group_maybe_version() {
let package_id: PackageId = "fluvio-cloud".parse().unwrap();
let package_id: PackageId<MaybeVersion> = package_id;
assert_eq!(package_id.registry(), &Registry::default());
assert_eq!(package_id.group().as_str(), "fluvio");
assert_eq!(package_id.name().as_str(), "fluvio-cloud");
assert!(package_id.maybe_version().is_none());
}
#[test]
fn test_package_id_display() {
let package_id_with_version = "fluvio/fluvio:1.2.3-alpha"
.parse::<PackageId<WithVersion>>()
.unwrap();
assert_eq!(
"fluvio/fluvio:1.2.3-alpha",
format!("{package_id_with_version}")
);
let package_id_maybe_with_version: PackageId<MaybeVersion> =
"fluvio/fluvio:3.4.5-beta".parse().unwrap();
assert_eq!(
"fluvio/fluvio:3.4.5-beta",
format!("{package_id_maybe_with_version}")
);
let package_id_maybe_without_version =
PackageId::new_unversioned("fluvio".parse().unwrap(), "fluvio".parse().unwrap());
assert_eq!(
"fluvio/fluvio",
format!("{package_id_maybe_without_version}")
);
}
#[test]
fn test_pretty_no_group() {
let package_id: PackageId = "fluvio:1.2.3".parse().unwrap();
let pretty = format!("{}", package_id.pretty());
assert_eq!(pretty, "fluvio");
}
#[test]
fn test_pretty_group() {
let package_id: PackageId = "fluvio/fluvio:1.2.3".parse().unwrap();
let pretty = format!("{}", package_id.pretty());
assert_eq!(pretty, "fluvio");
}
#[test]
fn test_pretty_maybe_id_no_group() {
let package_id: PackageId<MaybeVersion> = "fluvio".parse().unwrap();
let pretty = format!("{}", package_id.pretty());
assert_eq!(pretty, "fluvio");
}
#[test]
fn test_pretty_maybe_id_group() {
let package_id: PackageId<MaybeVersion> = "fluvio/fluvio".parse().unwrap();
let pretty = format!("{}", package_id.pretty());
assert_eq!(pretty, "fluvio");
}
#[test]
fn test_serialize_package_id_unversioned() {
let package_id_without_version =
PackageId::new_unversioned("fluvio".parse().unwrap(), "fluvio".parse().unwrap());
let json = serde_json::to_string(&package_id_without_version).unwrap();
assert_eq!(json, r#""fluvio/fluvio""#);
}
#[test]
fn test_serialize_package_id_versioned() {
let package_id_with_version = "fluvio/fluvio:1.2.3"
.parse::<PackageId<WithVersion>>()
.unwrap();
let json = serde_json::to_string(&package_id_with_version).unwrap();
assert_eq!(json, r#""fluvio/fluvio:1.2.3""#);
}
}