From 9076ab2be9cbc4b0ed13740bb00e2743cc4b1bf4 Mon Sep 17 00:00:00 2001 From: Bat Date: Sat, 15 Sep 2018 18:11:39 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ Cargo.toml | 7 +++++ README.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6aa9f6d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "canapi" +version = "0.1.0" +authors = ["Bat' "] + +[dependencies] +serde = "1.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6f2045 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Canapi + +Common Canapi traits. + +Canapi is a collection of Rust crate to make it possible to share a REST API definition between +clients and servers. You define your endpoints (`Endpoint` trait), bind them to your databse (`Provider` trait), +and you can re-use these endpoints in your client (`Fetch` trait). + +This requires three separate crates: one for your API defintion, one for your server, and one for the client. + +## Example + +`my-api`, where you define the API. + +```rust +use canapi::Endpoint; + +#[derive(Default, Serialize, Deserialize)] +struct UserEndpoint { + id: Option, + name: Option, + bio: Option, +} + +impl Endpoint for PostEndpoint { + type Id = i32; + + fn endpoint() -> &'static str { + "/api/v1/users" + } +} +``` + +`my-server`, to bind endpoints to database (and expose them to the rest of the world) + +```rust +use canapi::Provider; +use diesel::*; // Example with Diesel +use my_api::UserEndpoint; + +/// Define the User model… + +impl Provider for User { + type Data = UserEndpoint; + + fn get(conn: &PgConnection, id: i32) -> Option { + posts::table.filter(posts::id.eq(id)) + .limit(1) + .get_result::(conn) + .ok() + } +} + +// Use rocket, actix-web, iron, gotham or whatever you want to expose your endpoints… +``` + +`my-client`, to use your API (here with a potential WASM `Fetch` impl) + +```rust +use canapi_wasm::WasmFetch; +use plume_api; + +#[derive(Default)] +pub struct Api { + users: UserEndpoint +} + +fn fetch_first_post() { + let api = Api::default(); + let post = api.users.get::(1); + assert_eq(post.id, Some(1)); +} + +fn find_hello() { + let api = Api::default(); + let post = api.users.find::(PostEndpoint { + name: Some(String::new("Jane")), + ..PostEndpoint::default() + }); + assert_eq!(post.title, Some(String::new("Jane"))); +} +``` diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..95b5a8b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,75 @@ +extern crate serde; + +use serde::{Serialize, de::DeserializeOwned}; +use std::fmt::Display; + +pub enum Error { + Fetch(String), + SerDe(String), + NotFound(String), + Authorization(String), +} + +/// API to DB link +/// +/// `E` is the endpoint (API) +/// `P` is the provider (DB) +pub trait Provider

{ + type Data: Endpoint; + + /// Get a single result by ID + fn get(provider: &P, id: ::Id) -> Result; + + /// List all matching results + fn list(provider: &P, query: Self::Data) -> Vec; + + /// Save a new object, and returns it + fn create(provider: &P, query: Self::Data) -> Result; + + /// Update an object + fn update(provider: &P, id: ::Id, new_data: Self::Data) -> Result; + + /// Delete an object + fn delete(provider: &P, id: ::Id); +} + +/// API Endpoint, common to the server and the front +pub trait Endpoint: Default + Serialize + DeserializeOwned { + type Id: Display; + + /// The URL on which this endpoint is mounted. + /// + /// It should start with a /, and end without. + fn endpoint() -> &'static str; + + fn get(&self, id: Self::Id) -> Result { + F::fetch("GET", format!("{}/{}", Self::endpoint(), id), None) + } + + fn list(&self) -> Result { + F::fetch("GET", format!("{}", Self::endpoint()), None) + } + + fn find(&self, query: Self) -> Result { + F::fetch("GET", format!("{}", Self::endpoint()), Some(query)) + } + + fn create(&self, new: Self) -> Result { + F::fetch("POST", format!("{}", Self::endpoint()), Some(new)) + } + + fn update(&self, id: Self::Id, data: Self) -> Result { + F::fetch("PUT", format!("{}/{}", Self::endpoint(), id), Some(data)) + } + + fn delete(&self, id: Self::Id) -> Result { + F::fetch("DELETE", format!("{}/{}", Self::endpoint(), id), None) + } +} + +/// Anything that can perform a network request to fetch an endpoint +pub trait Fetch { + + /// Fetch a given endpoint + fn fetch(method: &'static str, url: String, query: Option) -> Result; +}