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/crates/kit/src/lib.rs

192 lines6,781 bytesCodeBlame
1//! Plumbing shared by g1t services that run on Workers.
2//!
3//! Services talk to each other over service bindings with a small JSON
4//! protocol: `POST /rpc/<method>` with the method's arguments as the body,
5//! answered with the method's return value.
6
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9use worker::{Date, Fetcher, Headers, Method, Request, RequestInit, Response, Result};
10
11/// The current time in milliseconds since the epoch.
12pub fn now_ms() -> u64 {
13 Date::now().as_millis()
14}
15
16/// The method name of an RPC request, or `None` if it is not one.
17pub fn rpc_method(request: &Request) -> Option<String> {
18 if request.method() != Method::Post {
19 return None;
20 }
21 request
22 .path()
23 .strip_prefix("/rpc/")
24 .map(|method| method.to_owned())
25}
26
27/// Deserializes a method's arguments.
28pub fn args<A: DeserializeOwned>(body: serde_json::Value) -> Result<A> {
29 serde_json::from_value(body)
30 .map_err(|error| worker::Error::RustError(format!("bad arguments: {error}")))
31}
32
33/// Serializes a method's return value as the response body.
34pub fn reply<R: Serialize>(value: &R) -> Result<Response> {
35 Response::from_json(value)
36}
37
38/// Calls `method` on another service through its binding.
39pub async fn call<A: Serialize, R: DeserializeOwned>(
40 service: &Fetcher,
41 method: &str,
42 arguments: &A,
43) -> Result<R> {
44 let headers = Headers::new();
45 headers.set("content-type", "application/json")?;
46 let mut init = RequestInit::new();
47 init.with_method(Method::Post)
48 .with_headers(headers)
49 .with_body(Some(serde_json::to_string(arguments)?.into()));
50 // The hostname is ignored; a service binding always reaches its service.
51 let request = Request::new_with_init(&format!("https://service/rpc/{method}"), &init)?;
52 let mut response = service.fetch_request(request).await?;
53 if response.status_code() != 200 {
54 return Err(worker::Error::RustError(format!(
55 "{method} failed with status {}: {}",
56 response.status_code(),
57 response.text().await.unwrap_or_default()
58 )));
59 }
60 response.json().await
61}
62
63/// Helpers for bindings that workers-rs has no typed wrapper for, such as
64/// Artifacts and Email Sending. Values cross the boundary as JSON.
65pub mod js {
66 use std::fmt;
67
68 use serde::Serialize;
69 use serde::de::DeserializeOwned;
70 use worker::js_sys::{Array, Function, JSON, Promise, Reflect};
71 use worker::wasm_bindgen::{JsCast, JsValue};
72 use worker::wasm_bindgen_futures::JsFuture;
73 use worker::{Env, Error, Result};
74
75 /// An exception thrown by JavaScript, with its `code` if it had one.
76 #[derive(Debug)]
77 pub struct Thrown {
78 pub code: Option<String>,
79 pub message: String,
80 }
81
82 impl Thrown {
83 fn from_value(value: JsValue) -> Self {
84 let property = |name: &str| {
85 Reflect::get(&value, &name.into())
86 .ok()
87 .and_then(|property| property.as_string())
88 };
89 Thrown {
90 code: property("code"),
91 message: property("message").unwrap_or_else(|| format!("{value:?}")),
92 }
93 }
94
95 pub fn is(&self, code: &str) -> bool {
96 self.code.as_deref() == Some(code)
97 }
98 }
99
100 impl fmt::Display for Thrown {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match &self.code {
103 Some(code) => write!(f, "{code}: {}", self.message),
104 None => f.write_str(&self.message),
105 }
106 }
107 }
108
109 impl From<Thrown> for Error {
110 fn from(thrown: Thrown) -> Self {
111 Error::RustError(thrown.to_string())
112 }
113 }
114
115 /// The binding called `name`, as a raw JavaScript value.
116 pub fn binding(env: &Env, name: &str) -> Result<JsValue> {
117 let value = Reflect::get(env.as_ref(), &name.into()).map_err(Thrown::from_value)?;
118 if value.is_undefined() {
119 return Err(Error::RustError(format!(
120 "binding {name} is not configured"
121 )));
122 }
123 Ok(value)
124 }
125
126 /// Reads a property of a JavaScript object.
127 pub fn get(target: &JsValue, name: &str) -> JsValue {
128 Reflect::get(target, &name.into()).unwrap_or(JsValue::UNDEFINED)
129 }
130
131 /// Sets a property on an object.
132 pub fn set(target: &JsValue, name: &str, value: &JsValue) {
133 let _ = Reflect::set(target, &name.into(), value);
134 }
135
136 pub fn to_js<T: Serialize>(value: &T) -> Result<JsValue> {
137 Ok(JSON::parse(&serde_json::to_string(value)?).map_err(Thrown::from_value)?)
138 }
139
140 pub fn from_js<T: DeserializeOwned>(value: &JsValue) -> Result<T> {
141 let text = if value.is_undefined() {
142 None
143 } else {
144 JSON::stringify(value)
145 .map_err(Thrown::from_value)?
146 .as_string()
147 };
148 let text = text.as_deref().unwrap_or("null");
149 serde_json::from_str(text).map_err(|error| {
150 // Say what arrived; a bare serde error is useless in a log.
151 let seen: String = text.chars().take(300).collect();
152 Error::RustError(format!(
153 "unexpected value from JavaScript ({error}): {seen}"
154 ))
155 })
156 }
157
158 /// Calls `target[method](...args)` and awaits the result if it is a
159 /// thenable. `method` may be a name or a symbol.
160 pub async fn call_key(
161 target: &JsValue,
162 method: &JsValue,
163 args: &[JsValue],
164 ) -> std::result::Result<JsValue, Thrown> {
165 let function: Function = Reflect::get(target, method)
166 .map_err(Thrown::from_value)?
167 .dyn_into()
168 .map_err(|_| Thrown {
169 code: None,
170 message: format!("{method:?} is not a function"),
171 })?;
172 let arguments: Array = args.iter().collect();
173 // An RPC stub treats every property access as a remote method, so
174 // `function.apply(...)` would be sent over the wire as a call to
175 // "apply". Reflect.apply invokes the function without touching it.
176 let returned = Reflect::apply(&function, target, &arguments).map_err(Thrown::from_value)?;
177 // Worker RPC returns its own thenable rather than a Promise, so
178 // resolve whatever came back instead of testing its type.
179 JsFuture::from(Promise::resolve(&returned))
180 .await
181 .map_err(Thrown::from_value)
182 }
183
184 /// Calls `target.method(...args)`; see [`call_key`].
185 pub async fn call(
186 target: &JsValue,
187 method: &str,
188 args: &[JsValue],
189 ) -> std::result::Result<JsValue, Thrown> {
190 call_key(target, &method.into(), args).await
191 }
192}