[go: up one dir, main page]

uv_auth/
service.rs

1use serde::{Deserialize, Serialize};
2use std::str::FromStr;
3use thiserror::Error;
4use url::Url;
5use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
6
7#[derive(Error, Debug)]
8pub enum ServiceParseError {
9    #[error(transparent)]
10    InvalidUrl(#[from] DisplaySafeUrlError),
11    #[error("Unsupported scheme: {0}")]
12    UnsupportedScheme(String),
13    #[error("HTTPS is required for non-local hosts")]
14    HttpsRequired,
15}
16
17/// A service URL that wraps [`DisplaySafeUrl`] for CLI usage.
18///
19/// This type provides automatic URL parsing and validation when used as a CLI argument,
20/// eliminating the need for manual parsing in command functions.
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
22#[serde(transparent)]
23pub struct Service(DisplaySafeUrl);
24
25impl Service {
26    /// Get the underlying [`DisplaySafeUrl`].
27    pub fn url(&self) -> &DisplaySafeUrl {
28        &self.0
29    }
30
31    /// Convert into the underlying [`DisplaySafeUrl`].
32    pub fn into_url(self) -> DisplaySafeUrl {
33        self.0
34    }
35
36    /// Validate that the URL scheme is supported.
37    fn check_scheme(url: &Url) -> Result<(), ServiceParseError> {
38        match url.scheme() {
39            "https" => Ok(()),
40            "http" if matches!(url.host_str(), Some("localhost" | "127.0.0.1")) => Ok(()),
41            "http" => Err(ServiceParseError::HttpsRequired),
42            value => Err(ServiceParseError::UnsupportedScheme(value.to_string())),
43        }
44    }
45}
46
47impl FromStr for Service {
48    type Err = ServiceParseError;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        // First try parsing as-is
52        let url = match DisplaySafeUrl::parse(s) {
53            Ok(url) => url,
54            Err(DisplaySafeUrlError::Url(url::ParseError::RelativeUrlWithoutBase)) => {
55                // If it's a relative URL, try prepending https://
56                let with_https = format!("https://{s}");
57                DisplaySafeUrl::parse(&with_https)?
58            }
59            Err(err) => return Err(err.into()),
60        };
61
62        Self::check_scheme(&url)?;
63
64        Ok(Self(url))
65    }
66}
67
68impl std::fmt::Display for Service {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        self.0.fmt(f)
71    }
72}
73
74impl TryFrom<String> for Service {
75    type Error = ServiceParseError;
76
77    fn try_from(value: String) -> Result<Self, Self::Error> {
78        Self::from_str(&value)
79    }
80}
81
82impl From<Service> for String {
83    fn from(service: Service) -> Self {
84        service.to_string()
85    }
86}
87
88impl TryFrom<DisplaySafeUrl> for Service {
89    type Error = ServiceParseError;
90
91    fn try_from(value: DisplaySafeUrl) -> Result<Self, Self::Error> {
92        Self::check_scheme(&value)?;
93        Ok(Self(value))
94    }
95}