flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/integrations/src/http.rs

71 lines2,626 bytesCodeBlame
1//! Calling outside systems. Every answer comes back as a status and a body,
2//! so each provider decides what a refusal means.
3
4use serde_json::Value;
5use worker::{Fetch, Headers, Method, Request, RequestInit, Result};
6
7pub struct Answer {
8 pub status: u16,
9 pub body: String,
10}
11
12impl Answer {
13 pub fn ok(&self) -> bool {
14 (200..300).contains(&self.status)
15 }
16
17 pub fn json(&self) -> Value {
18 serde_json::from_str(&self.body).unwrap_or(Value::Null)
19 }
20
21 /// What went wrong, briefly, for a person to read.
22 pub fn problem(&self, system: &str) -> String {
23 let said = self.json();
24 let message = said["message"]
25 .as_str()
26 .or_else(|| said["detail"].as_str())
27 .or_else(|| said["error"]["message"].as_str())
28 .or_else(|| said["errorMessages"][0].as_str())
29 .or_else(|| said["errors"][0]["message"].as_str())
30 .map(str::to_owned)
31 .unwrap_or_else(|| self.body.chars().take(200).collect());
32 match self.status {
33 401 => format!("{system} did not accept the credentials ({message})."),
34 403 => format!("{system} refused: the credentials cannot do this ({message})."),
35 404 => format!("{system} has nothing there ({message})."),
36 status => format!("{system} answered {status}: {message}"),
37 }
38 }
39}
40
41pub async fn send(method: Method, url: &str, headers: &[(&str, &str)], body: Option<String>) -> Result<Answer> {
42 let sent = Headers::new();
43 sent.set("user-agent", "g1t (+https://g1t.sh)")?;
44 sent.set("accept", "application/json")?;
45 for (name, value) in headers {
46 sent.set(name, value)?;
47 }
48 if body.is_some() && !headers.iter().any(|(name, _)| name.eq_ignore_ascii_case("content-type")) {
49 sent.set("content-type", "application/json")?;
50 }
51 let mut init = RequestInit::new();
52 init.with_method(method).with_headers(sent);
53 if let Some(body) = body {
54 init.with_body(Some(body.into()));
55 }
56 let mut response = Fetch::Request(Request::new_with_init(url, &init)?).send().await?;
57 Ok(Answer {
58 status: response.status_code(),
59 body: response.text().await.unwrap_or_default(),
60 })
61}
62
63/// `text`, cut to `max` characters at a line break where one is near.
64pub fn shorten(text: &str, max: usize) -> String {
65 if text.chars().count() <= max {
66 return text.to_owned();
67 }
68 let cut: String = text.chars().take(max).collect();
69 let at = cut.rfind('\n').filter(|at| *at > max / 2).unwrap_or(cut.len());
70 format!("{}\n\n…", cut[..at].trim_end())
71}