pr_01m47d24b0e6n91zwymwxg0vpx/crates/kit/src/lib.rs

132 lines4,870 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API and MCP server, Rust identity service, registration, site redesign1//! 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}
Email verification, password reset, and Git for AI scale positioning62
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 serde::Serialize;
67 use serde::de::DeserializeOwned;
68 use worker::js_sys::{Function, JSON, Promise, Reflect};
69 use worker::wasm_bindgen::{JsCast, JsValue};
70 use worker::wasm_bindgen_futures::JsFuture;
71 use worker::{Env, Error, Result};
72
73 fn error(context: &str, value: JsValue) -> Error {
74 let message = JSON::stringify(&value)
75 .ok()
76 .and_then(|text| text.as_string())
77 .filter(|text| text != "{}")
78 .or_else(|| {
79 Reflect::get(&value, &"message".into())
80 .ok()
81 .and_then(|message| message.as_string())
82 })
83 .unwrap_or_else(|| format!("{value:?}"));
84 Error::RustError(format!("{context}: {message}"))
85 }
86
87 /// The binding called `name`, as a raw JavaScript value.
88 pub fn binding(env: &Env, name: &str) -> Result<JsValue> {
89 let value = Reflect::get(env.as_ref(), &name.into()).map_err(|e| error(name, e))?;
90 if value.is_undefined() {
91 return Err(Error::RustError(format!(
92 "binding {name} is not configured"
93 )));
94 }
95 Ok(value)
96 }
97
98 pub fn to_js<T: Serialize>(value: &T) -> Result<JsValue> {
99 JSON::parse(&serde_json::to_string(value)?).map_err(|e| error("to_js", e))
100 }
101
102 pub fn from_js<T: DeserializeOwned>(value: &JsValue) -> Result<T> {
103 if value.is_undefined() {
104 return Ok(serde_json::from_value(serde_json::Value::Null)?);
105 }
106 let text = JSON::stringify(value)
107 .map_err(|e| error("from_js", e))?
108 .as_string()
109 .unwrap_or_else(|| "null".to_owned());
110 Ok(serde_json::from_str(&text)?)
111 }
112
113 /// Calls `target.method(...args)` and awaits the result if it is a
114 /// promise.
115 pub async fn call(target: &JsValue, method: &str, args: &[JsValue]) -> Result<JsValue> {
116 let function: Function = Reflect::get(target, &method.into())
117 .map_err(|e| error(method, e))?
118 .dyn_into()
119 .map_err(|_| Error::RustError(format!("{method} is not a function")))?;
120 let arguments = worker::js_sys::Array::new();
121 for arg in args {
122 arguments.push(arg);
123 }
124 let returned = function
125 .apply(target, &arguments)
126 .map_err(|e| error(method, e))?;
127 match returned.dyn_into::<Promise>() {
128 Ok(promise) => JsFuture::from(promise).await.map_err(|e| error(method, e)),
129 Err(value) => Ok(value),
130 }
131 }
132}