liminal_server/services/
lys_http.rs1use super::ServiceEffectError;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct LysAnswer {
21 pub status: u16,
23 pub body: serde_json::Value,
25}
26
27impl LysAnswer {
28 #[must_use]
30 pub fn succeeded(&self) -> bool {
31 (200..300).contains(&self.status)
32 }
33
34 #[must_use]
37 pub fn refusal(&self) -> String {
38 self.body
39 .get("refusal")
40 .or_else(|| self.body.get("error"))
41 .and_then(serde_json::Value::as_str)
42 .map_or_else(|| format!("lys_status_{}", self.status), str::to_owned)
43 }
44
45 #[must_use]
47 pub fn refused(&self, route: &str) -> ServiceEffectError {
48 ServiceEffectError(format!("Lys refused {route}: {}", self.refusal()))
49 }
50}
51
52pub trait LysHttp: Send + Sync + std::fmt::Debug {
54 fn get(&self, bearer: &str, path: &str) -> Result<LysAnswer, ServiceEffectError>;
60
61 fn post(
67 &self,
68 bearer: &str,
69 path: &str,
70 body: &serde_json::Value,
71 ) -> Result<LysAnswer, ServiceEffectError>;
72}
73
74fn could_not_be_asked(route: &str, error: reqwest::Error) -> ServiceEffectError {
76 ServiceEffectError(format!(
77 "lys_could_not_be_asked: {route}: {}",
78 error.without_url()
79 ))
80}
81
82pub struct HttpLys {
89 http: reqwest::Client,
90 issuer: reqwest::Url,
91 handle: tokio::runtime::Handle,
92}
93
94impl std::fmt::Debug for HttpLys {
95 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 formatter
97 .debug_struct("HttpLys")
98 .field("issuer", &self.issuer.as_str())
99 .finish_non_exhaustive()
100 }
101}
102
103impl HttpLys {
104 pub fn new(issuer: &str, handle: tokio::runtime::Handle) -> Result<Self, ServiceEffectError> {
109 let mut issuer = reqwest::Url::parse(issuer)
110 .map_err(|error| ServiceEffectError(format!("Lys issuer is not a URL: {error}")))?;
111 if !matches!(issuer.scheme(), "http" | "https")
112 || issuer.host_str().is_none()
113 || !issuer.username().is_empty()
114 || issuer.password().is_some()
115 || issuer.query().is_some()
116 || issuer.fragment().is_some()
117 {
118 return Err(ServiceEffectError(
119 "Lys issuer is not an HTTP origin".to_owned(),
120 ));
121 }
122 if !issuer.path().ends_with('/') {
123 let path = format!("{}/", issuer.path());
124 issuer.set_path(&path);
125 }
126 Ok(Self {
127 http: reqwest::Client::new(),
128 issuer,
129 handle,
130 })
131 }
132
133 fn endpoint(&self, path: &str) -> Result<reqwest::Url, ServiceEffectError> {
134 self.issuer
135 .join(path.trim_start_matches('/'))
136 .map_err(|error| ServiceEffectError(format!("Lys route {path} refused: {error}")))
137 }
138
139 fn run(
140 &self,
141 route: &str,
142 request: reqwest::RequestBuilder,
143 ) -> Result<LysAnswer, ServiceEffectError> {
144 let handle = &self.handle;
145 let future = async move {
146 let response = request
147 .send()
148 .await
149 .map_err(|error| could_not_be_asked(route, error))?;
150 let status = response.status().as_u16();
151 let bytes = response
152 .bytes()
153 .await
154 .map_err(|error| could_not_be_asked(route, error))?;
155 let body = if bytes.is_empty() {
156 serde_json::Value::Null
157 } else {
158 serde_json::from_slice(&bytes).map_err(|error| {
159 ServiceEffectError(format!(
160 "lys_could_not_be_asked: {route}: the answer is not JSON: {error}"
161 ))
162 })?
163 };
164 Ok(LysAnswer { status, body })
165 };
166 std::thread::scope(|scope| scope.spawn(move || handle.block_on(future)).join())
167 .unwrap_or_else(|_| {
168 Err(ServiceEffectError(format!(
169 "lys_could_not_be_asked: {route}: the request did not complete"
170 )))
171 })
172 }
173}
174
175impl LysHttp for HttpLys {
176 fn get(&self, bearer: &str, path: &str) -> Result<LysAnswer, ServiceEffectError> {
177 let url = self.endpoint(path)?;
178 self.run(path, self.http.get(url).bearer_auth(bearer))
179 }
180
181 fn post(
182 &self,
183 bearer: &str,
184 path: &str,
185 body: &serde_json::Value,
186 ) -> Result<LysAnswer, ServiceEffectError> {
187 let url = self.endpoint(path)?;
188 self.run(path, self.http.post(url).bearer_auth(bearer).json(body))
189 }
190}
191
192#[must_use]
195pub fn segment(value: &str) -> String {
196 const HEX: &[u8; 16] = b"0123456789ABCDEF";
197 let mut encoded = String::with_capacity(value.len());
198 for byte in value.bytes() {
199 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'~') {
200 encoded.push(char::from(byte));
201 } else {
202 encoded.push('%');
203 for nibble in [byte >> 4, byte & 0x0f] {
204 if let Some(digit) = HEX.get(usize::from(nibble)) {
205 encoded.push(char::from(*digit));
206 }
207 }
208 }
209 }
210 encoded
211}