Skip to content
/ aisil Public

Typeful rust framework for defining simple APIs

License

Notifications You must be signed in to change notification settings

sirewix/aisil

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

aisil

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

  • A method is defined as Request → (method name, Response) dependency in the context of an API (See HasMethod trait).

  • An API is defined as ApiMetaType[*] dependency (See IsApi trait), 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>;
} }

Implement service

#[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(())
  }
}

Expose service

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);

Make client calls

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);

Generate spec

OpenAPI for HTTP POST /<method_name>:

println!("{}", gen_openapi_yaml::<SomeAPI>());

OpenRPC for JsonRPC:

println!("{}", gen_openrpc_yaml::<SomeAPI>());

Generate TS types

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')
  }
}

Things to implement/improve

  • Allow for non-inlined TS types generation
  • Debug ts feature
  • no-std feature

About

Typeful rust framework for defining simple APIs

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages