#![forbid(unsafe_code)]
use super::error::DomainError;
use crate::validation::{MAX_FIELD_LEN, MAX_TAG_LEN, MAX_TAGS};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
#[repr(transparent)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct VpsName(String);
impl VpsName {
pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
let s = raw.as_ref();
crate::paths::validate_name(s).map_err(|e| DomainError::new("vps_name", e.to_string()))?;
let nfc = crate::paths::normalize_nfc(s);
if nfc.len() > MAX_FIELD_LEN {
return Err(DomainError::new(
"vps_name",
format!("name must be 1..={MAX_FIELD_LEN} chars"),
));
}
Ok(Self(nfc))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for VpsName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for VpsName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for VpsName {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::try_new(s).map_err(serde::de::Error::custom)
}
}
#[repr(transparent)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct SshHost(String);
impl SshHost {
pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
let t = raw.as_ref().trim();
if t.is_empty() {
return Err(DomainError::new("ssh_host", "host must not be empty"));
}
if t.len() > MAX_FIELD_LEN {
return Err(DomainError::new(
"ssh_host",
format!("host must be 1..={MAX_FIELD_LEN} chars"),
));
}
if t.chars().any(|c| c.is_control()) {
return Err(DomainError::new(
"ssh_host",
"host must not contain control characters",
));
}
Ok(Self(t.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for SshHost {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for SshHost {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for SshHost {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::try_new(s).map_err(serde::de::Error::custom)
}
}
#[repr(transparent)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct SshUser(String);
impl SshUser {
pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
let t = raw.as_ref().trim();
if t.is_empty() {
return Err(DomainError::new("ssh_user", "username must not be empty"));
}
if t.len() > MAX_FIELD_LEN {
return Err(DomainError::new(
"ssh_user",
format!("username must be 1..={MAX_FIELD_LEN} chars"),
));
}
if t.chars().any(|c| c.is_control()) {
return Err(DomainError::new(
"ssh_user",
"username must not contain control characters",
));
}
Ok(Self(t.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for SshUser {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for SshUser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for SshUser {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::try_new(s).map_err(serde::de::Error::custom)
}
}
#[repr(transparent)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct HostTag(String);
impl HostTag {
pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
let t = raw.as_ref().trim();
if t.is_empty() || t.len() > MAX_TAG_LEN {
return Err(DomainError::new(
"host_tag",
format!("each tag must be 1..={MAX_TAG_LEN} chars"),
));
}
if t.chars().any(|c| c.is_control() || c == '/' || c == '\\') {
return Err(DomainError::new(
"host_tag",
"tag must not contain control chars or path separators",
));
}
Ok(Self(t.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for HostTag {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for HostTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for HostTag {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::try_new(s).map_err(serde::de::Error::custom)
}
}
pub fn try_tags(
raw: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<Vec<HostTag>, DomainError> {
let mut out = Vec::new();
for t in raw {
out.push(HostTag::try_new(t)?);
if out.len() > MAX_TAGS {
return Err(DomainError::new(
"tags_count",
format!("at most {MAX_TAGS} tags allowed"),
));
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn host_user_name() {
assert!(SshHost::try_new(" ").is_err());
assert!(SshUser::try_new("").is_err());
assert_eq!(SshHost::try_new(" a.b ").unwrap().as_str(), "a.b");
assert!(VpsName::try_new("lab-01").is_ok());
assert!(VpsName::try_new("../x").is_err());
}
#[test]
fn tags() {
assert!(HostTag::try_new("prod").is_ok());
assert!(HostTag::try_new("a/b").is_err());
}
}