use std::fmt;
use std::str::FromStr;
use serde::de::{self, Deserializer, Visitor};
use serde::{Deserialize, Serialize, Serializer};
include!("generated/services.rs");
pub const DOMAIN: &str = "origincloudapis.eu";
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FullName {
full: String,
service_len: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum FullNameError {
#[error("an empty string names no resource")]
Empty,
#[error(
"{0:?} is a URN; a resource is named by its full resource name, //{{service}}.{DOMAIN}/{{name}}"
)]
Urn(String),
#[error("{0:?} is a URL; a full resource name has no scheme, //{{service}}.{DOMAIN}/{{name}}")]
Scheme(String),
#[error("{0:?} is a relative resource name; a full one starts with //{{service}}.{DOMAIN}/")]
Relative(String),
#[error(
"{0:?} is an id; a resource is named by its full resource name, //{{service}}.{DOMAIN}/{{name}}"
)]
BareId(String),
#[error("{name:?} names no resource: {host} is not the host of one of the platform's APIs")]
UnknownService {
name: String,
host: String,
},
#[error(
"{name:?} names {host}; a full resource name has the service's global host, {service}.{DOMAIN}"
)]
NotGlobalHost {
name: String,
host: String,
service: String,
},
#[error("{name:?} carries the API version {version:?}; a full resource name has none")]
Version {
name: String,
version: String,
},
#[error("{0:?} has no relative name after its service")]
NoRelativeName(String),
#[error("{0:?} is a full resource name; a relative one is expected here")]
NotRelative(String),
#[error("{0:?} ends with a '/'")]
TrailingSlash(String),
#[error("{0:?} has an empty segment")]
EmptySegment(String),
#[error("{name:?} has the segment {segment:?}, which a resource name can't")]
DotSegment {
name: String,
segment: String,
},
#[error(
"{name:?} has the segment {segment:?}: a segment is letters, digits and '-', '.', '_' or '~'"
)]
InvalidSegment {
name: String,
segment: String,
},
#[error("{0:?} is not an API version, such as v1 or v1beta1")]
NotAVersion(String),
}
impl FullName {
pub fn parse(name: &str) -> Result<FullName, FullNameError> {
let Some(rest) = name.strip_prefix("//") else {
return Err(not_full(name));
};
let Some((host, relative)) = rest.split_once('/') else {
return Err(match service_of_host(name, rest) {
Ok(_) => FullNameError::NoRelativeName(name.to_owned()),
Err(e) => e,
});
};
let service = service_of_host(name, host)?;
if relative.is_empty() {
return Err(FullNameError::NoRelativeName(name.to_owned()));
}
check_relative(name, relative)?;
Ok(FullName {
full: name.to_owned(),
service_len: service.len(),
})
}
pub fn from_relative(service: &str, relative: &str) -> Result<FullName, FullNameError> {
if relative.starts_with("//") {
return Err(FullNameError::NotRelative(relative.to_owned()));
}
let host = format!("{service}.{DOMAIN}");
if !RESOURCE_SERVICES.contains(&service) {
return Err(FullNameError::UnknownService {
name: relative.to_owned(),
host,
});
}
if relative.is_empty() {
return Err(FullNameError::NoRelativeName(relative.to_owned()));
}
check_relative(relative, relative)?;
Ok(FullName {
full: format!("//{host}/{relative}"),
service_len: service.len(),
})
}
pub fn service(&self) -> &str {
&self.full[2..2 + self.service_len]
}
pub fn host(&self) -> &str {
&self.full[2..self.host_end()]
}
pub fn relative(&self) -> &str {
&self.full[self.host_end() + 1..]
}
pub fn as_str(&self) -> &str {
&self.full
}
pub fn url(&self, version: &str) -> Result<String, FullNameError> {
if !is_version(version) {
return Err(FullNameError::NotAVersion(version.to_owned()));
}
Ok(format!(
"https://{}/{version}/{}",
self.host(),
self.relative()
))
}
pub fn principal(&self) -> Option<Principal<'_>> {
if self.service() != "iam" {
return None;
}
let segments: Vec<&str> = self.relative().split('/').collect();
match segments.as_slice() {
["users", user] => Some(Principal::User { user }),
[
"organizations",
organization,
"serviceAccounts",
service_account,
] => Some(Principal::ServiceAccount {
organization,
service_account,
}),
["serviceAgents", service_agent] => Some(Principal::ServiceAgent { service_agent }),
["services", service] => Some(Principal::Service { service }),
_ => None,
}
}
pub fn role(&self) -> Option<&str> {
if self.service() != "iam" {
return None;
}
self.relative()
.strip_prefix("roles/")
.filter(|role| !role.contains('/'))
}
pub fn of_role(role: &str) -> Result<FullName, FullNameError> {
if role.contains('/') {
return Err(FullNameError::InvalidSegment {
name: role.to_owned(),
segment: role.to_owned(),
});
}
FullName::from_relative("iam", &format!("roles/{role}"))
}
fn host_end(&self) -> usize {
2 + self.service_len + 1 + DOMAIN.len()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Principal<'a> {
User {
user: &'a str,
},
ServiceAccount {
organization: &'a str,
service_account: &'a str,
},
ServiceAgent {
service_agent: &'a str,
},
Service {
service: &'a str,
},
}
impl Principal<'_> {
pub fn full_name(&self) -> Result<FullName, FullNameError> {
let (relative, parts) = match *self {
Principal::User { user } => (format!("users/{user}"), vec![user]),
Principal::ServiceAccount {
organization,
service_account,
} => (
format!("organizations/{organization}/serviceAccounts/{service_account}"),
vec![organization, service_account],
),
Principal::ServiceAgent { service_agent } => (
format!("serviceAgents/{service_agent}"),
vec![service_agent],
),
Principal::Service { service } => (format!("services/{service}"), vec![service]),
};
if let Some(part) = parts.iter().find(|p| p.contains('/')) {
return Err(FullNameError::InvalidSegment {
name: relative.clone(),
segment: (*part).to_owned(),
});
}
FullName::from_relative("iam", &relative)
}
}
fn service_of_host<'h>(name: &str, host: &'h str) -> Result<&'h str, FullNameError> {
let code = host.strip_suffix(DOMAIN).and_then(|h| h.strip_suffix('.'));
if let Some(code) = code.filter(|c| RESOURCE_SERVICES.contains(c)) {
return Ok(code);
}
let front_door = code
.and_then(|h| h.split_once('.'))
.filter(|(_, location)| !location.is_empty())
.map(|(code, _)| code)
.filter(|code| RESOURCE_SERVICES.contains(code));
Err(match front_door {
Some(service) => FullNameError::NotGlobalHost {
name: name.to_owned(),
host: host.to_owned(),
service: service.to_owned(),
},
None => FullNameError::UnknownService {
name: name.to_owned(),
host: host.to_owned(),
},
})
}
fn not_full(name: &str) -> FullNameError {
let owned = name.to_owned();
if name.is_empty() {
FullNameError::Empty
} else if name
.get(..4)
.is_some_and(|p| p.eq_ignore_ascii_case("urn:"))
{
FullNameError::Urn(owned)
} else if name.contains("://") {
FullNameError::Scheme(owned)
} else if name.contains('/') {
FullNameError::Relative(owned)
} else {
FullNameError::BareId(owned)
}
}
fn check_relative(name: &str, relative: &str) -> Result<(), FullNameError> {
if relative.ends_with('/') {
return Err(FullNameError::TrailingSlash(name.to_owned()));
}
let mut segments = relative.split('/').peekable();
if let Some(first) = segments.peek()
&& is_version(first)
{
return Err(FullNameError::Version {
name: name.to_owned(),
version: (*first).to_owned(),
});
}
for segment in segments {
check_segment(name, segment)?;
}
Ok(())
}
fn check_segment(name: &str, segment: &str) -> Result<(), FullNameError> {
if segment.is_empty() {
return Err(FullNameError::EmptySegment(name.to_owned()));
}
if segment == "." || segment == ".." {
return Err(FullNameError::DotSegment {
name: name.to_owned(),
segment: segment.to_owned(),
});
}
let unreserved = |c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_' | '~');
if !segment.chars().all(unreserved) {
return Err(FullNameError::InvalidSegment {
name: name.to_owned(),
segment: segment.to_owned(),
});
}
Ok(())
}
fn is_version(segment: &str) -> bool {
let Some(rest) = segment.strip_prefix('v') else {
return false;
};
let major_len = rest.chars().take_while(char::is_ascii_digit).count();
if major_len == 0 || rest.starts_with('0') {
return false;
}
let stability = &rest[major_len..];
let number = stability
.strip_prefix("alpha")
.or_else(|| stability.strip_prefix("beta"));
match number {
None => stability.is_empty(),
Some(number) => number.chars().all(|c| c.is_ascii_digit()),
}
}
pub const ANY_SERVICE: &str = "*";
pub(crate) fn path_name<'v>(
own: &str,
services: &[&str],
field: &str,
value: &'v str,
) -> Result<&'v str, String> {
let own = [own];
let services = if services.is_empty() {
&own[..]
} else {
services
};
let refused = |e: FullNameError| format!("{field} must be a name on {}: {e}", hosts(services));
if !value.starts_with("//") {
return match not_full(value) {
e @ (FullNameError::Urn(_) | FullNameError::Scheme(_)) => Err(refused(e)),
_ => Ok(value),
};
}
let name = FullName::parse(value).map_err(refused)?;
if !services.contains(&ANY_SERVICE) && !services.contains(&name.service()) {
return Err(format!(
"{field} must be a name on {}, got {value:?}, a name on {}",
hosts(services),
name.host()
));
}
Ok(&value[value.len() - name.relative().len()..])
}
fn hosts(services: &[&str]) -> String {
if services.contains(&ANY_SERVICE) {
return "the host of one of the platform's APIs".to_owned();
}
let hosts: Vec<String> = services.iter().map(|s| format!("{s}.{DOMAIN}")).collect();
match hosts.as_slice() {
[one] => one.clone(),
_ => format!("one of {}", hosts.join(", ")),
}
}
impl fmt::Display for FullName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.full)
}
}
impl fmt::Debug for FullName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("FullName").field(&self.full).finish()
}
}
impl FromStr for FullName {
type Err = FullNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
FullName::parse(s)
}
}
impl AsRef<str> for FullName {
fn as_ref(&self) -> &str {
&self.full
}
}
impl From<FullName> for String {
fn from(name: FullName) -> String {
name.full
}
}
impl Serialize for FullName {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.full)
}
}
impl<'de> Deserialize<'de> for FullName {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct FullNameVisitor;
impl Visitor<'_> for FullNameVisitor {
type Value = FullName;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a full resource name, //{service}.origincloudapis.eu/{name}")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<FullName, E> {
FullName::parse(value).map_err(E::custom)
}
}
deserializer.deserialize_str(FullNameVisitor)
}
}
#[cfg(test)]
mod tests;