Lightweight framework to define APIs as types.
aisil is designed to be transport and protocol agnostic. At the moment,
however, only one transport protocol is supported (HTTP's POST /<method_name>
with json bodies). Feel free to extend the base framework with whatever fits
your requirements.
See docs at docs.rs/aisil.
- Define API
- Implement service
- Expose service
- Make client calls
- Generate spec
- Generate TS types
- Things to implement/improve
-
A method is defined as
Request→ (method name,Response) dependency in the context of an API (SeeHasMethodtrait). -
An API is defined as
ApiMetaType→[*]dependency (SeeIsApitrait), where[*]is a heterogeneous list of request types that belong to the API.
An example of an API definition with two methods:
/// Get A
#[derive(Serialize, Deserialize, JsonSchema, TS, DocumentedOpt)]
pub struct GetA;
#[derive(Serialize, Deserialize, JsonSchema, TS)]
pub struct PostA(pub bool);
/// Some example api
#[derive(DocumentedOpt)]
pub struct SomeAPI;
define_api! { SomeAPI => {
// <method_name>, <RequestType> => <ResponseType>;
// documentation for this method will be taken from DocumentedOpt
get_a, GetA => bool;
/// Post A
post_a, PostA => Result<(), String>;
} }#[derive(Clone, Default)]
struct SomeBackend {
a: Arc<Mutex<bool>>,
}
impl ImplsMethod<SomeAPI, GetA> for SomeBackend {
async fn call_api(&self, _: GetA) -> bool {
self.a.lock().await.clone()
}
}
impl ImplsMethod<SomeAPI, PostA> for SomeBackend {
async fn call_api(&self, PostA(new_a): PostA) -> Result<(), String> {
let mut a = self.a.lock().await;
(!*a).then_some(()).ok_or("can't post `a` anymore".to_owned())?;
*a = new_a;
Ok(())
}
}As HTTP POST /<method_name>:
pub fn router() -> axum::Router {
let backend = SomeBackend::default();
aisil::post_json::mk_post_json_router::<SomeAPI, SomeBackend>().with_state(backend)
}or as JsonRPC:
let backend = SomeBackend::default();
Router::new().route(
"/rpc",
post(async move |State(svc), Json(request): Json<server::JsonRpcRequest>| {
Json(aisil::server::json_rpc::json_rpc_router::<SomeAPI, SomeBackend>(&svc, request).await)
}),
).with_state(state);Use that API to make type safe client calls:
Either HTTP POST /<method_name>:
let client = PostJsonClient::new(Url::parse(client_url)?, reqwest::Client::new());
client.call_api(PostA(true)).await?.unwrap();
let new_a = client.call_api(GetA).await?;
assert_eq!(new_a, true);or as JsonRPC:
let client = JsonRpcClient::new(Method::POST, Url::parse(client_url)?, reqwest::Client::new());
client.call_api(PostA(true)).await?.unwrap();
let new_a = client.call_api(GetA).await?;
assert_eq!(new_a, true);OpenAPI for HTTP POST /<method_name>:
println!("{}", gen_openapi_yaml::<SomeAPI>());OpenRPC for JsonRPC:
println!("{}", gen_openrpc_yaml::<SomeAPI>());println!("{}", gen_ts_api::<SomeAPI>());Current implementation works by inlining everything, which is probably undesirable:
type Request<M> =
M extends 'get_a' ? null :
M extends 'post_a' ? boolean :
void;
type Response<M> =
M extends 'get_a' ? Result<boolean, number> :
M extends 'post_a' ? Result<null, number> :
void;TS boilerplate would look something like this:
const callSomeApi<M> = async (req: Request<M>) => {
const raw_response = await fetch(`http://example.com/{method}`, {
method: 'POST',
body: req,
headers: { 'Content-Type': 'application/json' }
});
const json = await raw_response.json();
json as Response<M>
}And to unwrap rust's Result:
function unwrapResult<R, E>(a: Result<R, E>): R {
if ('Ok' in a) {
return a.Ok;
} else if ('Err' in a) {
throw Error(JSON.stringify(a.Err))
} else {
throw Error('non api error')
}
}- Allow for non-inlined TS types generation
- Debug
tsfeature - no-std feature