1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3
4use url::Url;
5
6pub const JSON_CONTENT_TYPE: &str = "application/json; charset=utf-8";
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum Route {
12 Observe,
14 FetchInterface,
16 CallOperation,
18}
19
20impl Route {
21 #[must_use]
23 pub const fn relative_reference(self) -> &'static str {
24 match self {
25 Self::Observe => "v1/observe",
26 Self::FetchInterface => "v1/fetch_interface",
27 Self::CallOperation => "v1/call_operation",
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Endpoint(Url);
35
36impl Endpoint {
37 pub fn parse(input: &str) -> Result<Self, EndpointError> {
45 let mut url = Url::parse(input).map_err(EndpointError::InvalidUrl)?;
46 if url.cannot_be_a_base() || url.host_str().is_none() {
47 return Err(EndpointError::NotAbsoluteBase);
48 }
49 if url.query().is_some() {
50 return Err(EndpointError::HasQuery);
51 }
52 if url.fragment().is_some() {
53 return Err(EndpointError::HasFragment);
54 }
55 if !url.path().ends_with('/') {
56 let path = format!("{}/", url.path());
57 url.set_path(&path);
58 }
59 Ok(Self(url))
60 }
61
62 #[must_use]
64 pub fn as_url(&self) -> &Url {
65 &self.0
66 }
67
68 #[must_use]
70 pub fn route(&self, route: Route) -> Url {
71 self.0
72 .join(route.relative_reference())
73 .expect("fixed relative WIP routes are valid URL references")
74 }
75
76 #[must_use]
81 pub fn recognize(&self, candidate: &Url) -> Option<Route> {
82 const ROUTES: [Route; 3] = [Route::Observe, Route::FetchInterface, Route::CallOperation];
83 if candidate.query().is_some() || candidate.fragment().is_some() {
84 return None;
85 }
86 ROUTES
87 .into_iter()
88 .find(|route| self.route(*route) == *candidate)
89 }
90}
91
92impl Display for Endpoint {
93 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
94 self.0.fmt(formatter)
95 }
96}
97
98impl TryFrom<&str> for Endpoint {
99 type Error = EndpointError;
100
101 fn try_from(value: &str) -> Result<Self, Self::Error> {
102 Self::parse(value)
103 }
104}
105
106#[derive(Debug)]
108pub enum EndpointError {
109 InvalidUrl(url::ParseError),
111 NotAbsoluteBase,
113 HasQuery,
115 HasFragment,
117}
118
119impl Display for EndpointError {
120 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::InvalidUrl(error) => write!(formatter, "invalid endpoint URL: {error}"),
123 Self::NotAbsoluteBase => {
124 formatter.write_str("WIP endpoint must be an absolute base URL")
125 }
126 Self::HasQuery => formatter.write_str("WIP endpoint must not contain a query"),
127 Self::HasFragment => formatter.write_str("WIP endpoint must not contain a fragment"),
128 }
129 }
130}
131
132impl Error for EndpointError {
133 fn source(&self) -> Option<&(dyn Error + 'static)> {
134 match self {
135 Self::InvalidUrl(error) => Some(error),
136 _ => None,
137 }
138 }
139}