Skip to content
This repository has been archived by the owner on Dec 14, 2021. It is now read-only.

feat!: Prepare for v1.0.0 release #8

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions examples/pagination.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use futures::StreamExt;
use rest_client::Client;
use rest_client::{PaginatedRequest, Paginator, QueryPaginator, Request, RequestBody};
use rest_client::{PaginationState, PaginationType};
use rest_client::pagination::{PaginatedRequest, PaginationState, PaginationType, QueryPaginator};
use rest_client::{Client, Request, RequestData};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use stream_flatten_iters::TryStreamExt;
Expand Down Expand Up @@ -50,21 +49,23 @@ struct PassengersWrapper {
}

impl Request for GetPassengers {
type Body = Self;
type Data = Self;
type Response = PassengersWrapper;

fn endpoint(&self) -> Cow<str> {
"/v1/passenger".into()
}

fn body(&self) -> RequestBody<&Self> {
RequestBody::Query(self)
fn data(&self) -> RequestData<&Self> {
RequestData::Query(self)
}
}

impl PaginatedRequest for GetPassengers {
fn paginator(&self) -> Box<dyn Paginator<Self::Response>> {
Box::new(QueryPaginator::new(get_next_url))
type Paginator = QueryPaginator<Self::Response>;

fn paginator(&self) -> Self::Paginator {
QueryPaginator::new(get_next_url)
}
}

Expand Down
30 changes: 15 additions & 15 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::error::{Error, Result};
use crate::pagination::{PaginatedRequest, PaginationState, PaginationType};
use crate::pagination::{PaginatedRequest, PaginationState, PaginationType, Paginator};
use crate::request::{Request, RequestBuilderExt};
use futures::prelude::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
Expand All @@ -8,7 +8,7 @@ use std::convert::TryFrom;
use std::sync::Arc;

#[derive(Clone)]
enum Authentication {
enum Authorization {
Bearer(String),
Basic(String, String),
Query(Vec<(String, String)>),
Expand All @@ -18,12 +18,12 @@ enum Authentication {
/// The main client used for making requests.
///
/// `Client` stores an async Reqwest client as well as the associated
/// base url for the REST server.
/// base url and possible authorization details for the REST server.
#[derive(Clone)]
pub struct Client {
inner: Arc<ReqwestClient>,
base_url: String,
auth: Option<Authentication>,
auth: Option<Authorization>,
}

impl Client {
Expand All @@ -40,13 +40,13 @@ impl Client {

/// Enable bearer authentication for the client
pub fn bearer_auth<S: ToString>(mut self, token: S) -> Self {
self.auth = Some(Authentication::Bearer(token.to_string()));
self.auth = Some(Authorization::Bearer(token.to_string()));
self
}

/// Enable basic authentication for the client
pub fn basic_auth<S: ToString>(mut self, user: S, pass: S) -> Self {
self.auth = Some(Authentication::Basic(user.to_string(), pass.to_string()));
self.auth = Some(Authorization::Basic(user.to_string(), pass.to_string()));
self
}

Expand All @@ -56,7 +56,7 @@ impl Client {
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
self.auth = Some(Authentication::Query(pairs));
self.auth = Some(Authorization::Query(pairs));
self
}

Expand All @@ -71,7 +71,7 @@ impl Client {
HeaderValue::from_str(&v).expect("Failed to create HeaderValue"),
);
}
self.auth = Some(Authentication::Header(map));
self.auth = Some(Authorization::Header(map));
self
}

Expand All @@ -84,14 +84,14 @@ impl Client {
.inner
.request(R::METHOD, &url)
.headers(request.headers())
.request_body(request.body());
.request_data(request.data());

let req = match &self.auth {
None => req,
Some(Authentication::Bearer(token)) => req.bearer_auth(token),
Some(Authentication::Basic(user, pass)) => req.basic_auth(user, Some(pass)),
Some(Authentication::Query(pairs)) => req.query(&pairs),
Some(Authentication::Header(pairs)) => req.headers(pairs.clone()),
Some(Authorization::Bearer(token)) => req.bearer_auth(token),
Some(Authorization::Basic(user, pass)) => req.basic_auth(user, Some(pass)),
Some(Authorization::Query(pairs)) => req.query(&pairs),
Some(Authorization::Header(pairs)) => req.headers(pairs.clone()),
};
req.build().map_err(From::from)
}
Expand Down Expand Up @@ -127,11 +127,11 @@ impl Client {
requests: I,
) -> impl Stream<Item = Result<R::Response>> + Unpin + 'a
where
I: Iterator<Item = &'a R> + 'a,
I: IntoIterator<Item = &'a R> + 'a,
R: Request + 'a,
{
Box::pin(
stream::iter(requests)
stream::iter(requests.into_iter())
.map(move |r| self.send(r).map_into())
.filter_map(|x| x),
)
Expand Down
7 changes: 4 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! rest-client is a library for building strongly typed REST clients, with built-in capabilites
//! for authentication, various request and response types and pagination.
//!
//! Inspired heavily by [ring-api](https://github.com/H2CO3/ring_api)
//! Originally inspired by [ring-api](https://github.com/H2CO3/ring_api)
mod client;
mod error;
mod pagination;
pub mod pagination;
mod request;

pub use client::Client;
pub use error::Error;
pub use pagination::*;
pub use request::*;
pub use reqwest::header;
pub use reqwest::Method;
pub use reqwest::StatusCode;
46 changes: 28 additions & 18 deletions src/pagination.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use crate::Request;

#[derive(Debug, Clone)]
/// The type of pagination used for the resource.
pub enum PaginationType {
/// Pagination by one or multiple query parameters.
Query(Vec<(String, String)>),
}

/// Base trait for paginators. Paginators can use the previous pagination state
/// and the response from the previous request to create a new pagination state.
pub trait Paginator<T> {
fn next(
&self,
Expand All @@ -13,47 +17,53 @@ pub trait Paginator<T> {
) -> PaginationState<PaginationType>;
}

/// Trait for any request that requires pagination.
pub trait PaginatedRequest: Request {
fn paginator(&self) -> Box<dyn Paginator<Self::Response>>;
/// The paginator used for the request.
type Paginator: Paginator<Self::Response>;

/// Return the associated paginator.
fn paginator(&self) -> Self::Paginator;
}

#[derive(Clone, Debug)]
pub enum PaginationState<T: Clone> {
/// The current pagination state.
pub enum PaginationState<T> {
/// State associated with the initial request.
Start(Option<T>),
/// State associated with continuing pagination.
Next(T),
/// State denoting that the last page has been reached.
End,
}

impl<T: Clone> Default for PaginationState<T> {
impl<T> Default for PaginationState<T> {
fn default() -> PaginationState<T> {
PaginationState::Start(None)
}
}

pub struct QueryPaginator<F, T>
where
F: Fn(&PaginationState<PaginationType>, &T) -> Option<Vec<(String, String)>>,
{
f: F,
/// A paginator that implements pagination through one or more query parameters.
#[allow(clippy::type_complexity)]
pub struct QueryPaginator<T> {
f: Box<dyn Fn(&PaginationState<PaginationType>, &T) -> Option<Vec<(String, String)>>>,
_phantom: std::marker::PhantomData<T>,
}

impl<F, T> QueryPaginator<F, T>
where
F: Fn(&PaginationState<PaginationType>, &T) -> Option<Vec<(String, String)>>,
{
pub fn new(f: F) -> Self {
impl<T> QueryPaginator<T> {
pub fn new<
F: 'static + Fn(&PaginationState<PaginationType>, &T) -> Option<Vec<(String, String)>>,
>(
f: F,
) -> Self {
Self {
f,
f: Box::new(f),
_phantom: std::marker::PhantomData,
}
}
}

impl<F, T> Paginator<T> for QueryPaginator<F, T>
where
F: Fn(&PaginationState<PaginationType>, &T) -> Option<Vec<(String, String)>>,
{
impl<T> Paginator<T> for QueryPaginator<T> {
fn next(
&self,
prev: &PaginationState<PaginationType>,
Expand Down
42 changes: 30 additions & 12 deletions src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,52 @@ use reqwest::{header::HeaderMap, Method, RequestBuilder};
use serde::{Deserialize, Deserializer, Serialize};
use std::borrow::Cow;

pub enum RequestBody<T> {
None,
Query(T),
/// Additional data to be sent along with the request.
pub enum RequestData<T> {
/// No additional data.
Empty,
/// HTTP form data.
Form(T),
/// JSON data.
Json(T),
/// Query data.
Query(T),
}

impl<T> Default for RequestBody<T> {
impl<T> Default for RequestData<T> {
fn default() -> Self {
RequestBody::None
RequestData::Empty
}
}

/// The base-trait for requests sent by the client. The trait specifies the full life-cycle of the
/// request, including the endpoint, headers, data, method and eventual response.
pub trait Request {
type Body: Serialize;
/// The type of additional data sent with the request. Usually, this will be `()` or `Self`.
type Data: Serialize;
/// The type of the response from the server.
type Response: for<'de> Deserialize<'de> + Unpin;
/// The HTTP method for the request.
const METHOD: Method = Method::GET;

/// The endpoint to which the request will be sent. The base url is set in the client, and the
/// endpoint method returns the specific resource endpoint.
fn endpoint(&self) -> Cow<str>;

/// Any additional headers that should be sent with the request. Note that common headers such
/// as authorization headers should be set on the client directly.
fn headers(&self) -> HeaderMap {
Default::default()
}

fn body(&self) -> RequestBody<&Self::Body> {
/// The formatted request data.
fn data(&self) -> RequestData<&Self::Data> {
Default::default()
}
}

#[derive(Debug)]
/// Struct symbolizing an empty response from the server.
pub struct EmptyResponse;
impl<'de> Deserialize<'de> for EmptyResponse {
fn deserialize<D>(_deserializer: D) -> Result<EmptyResponse, D::Error>
Expand All @@ -42,15 +59,16 @@ impl<'de> Deserialize<'de> for EmptyResponse {
}

pub(crate) trait RequestBuilderExt: Sized {
fn request_body<T: Serialize>(self, body: RequestBody<T>) -> Self;
fn request_data<T: Serialize>(self, body: RequestData<T>) -> Self;
}

impl RequestBuilderExt for RequestBuilder {
fn request_body<T: Serialize>(self, body: RequestBody<T>) -> Self {
fn request_data<T: Serialize>(self, body: RequestData<T>) -> Self {
match body {
RequestBody::None => self,
RequestBody::Json(value) => self.json(&value),
RequestBody::Query(value) => self.query(&value),
RequestData::Empty => self,
RequestData::Form(value) => self.form(&value),
RequestData::Json(value) => self.json(&value),
RequestData::Query(value) => self.query(&value),
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
use rest_client::{Client, EmptyResponse, Request};
use std::borrow::Cow;
use crate::utils::EmptyHello;
use rest_client::Client;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

struct EmptyHello;

impl Request for EmptyHello {
type Body = ();
type Response = EmptyResponse;

fn endpoint(&self) -> Cow<str> {
"/hello".into()
}
}

#[tokio::test]
async fn basic_auth() {
let server = MockServer::start().await;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
use rest_client::{Client, EmptyResponse, Request};
use std::borrow::Cow;
use crate::utils::EmptyHello;
use rest_client::Client;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

struct EmptyHello;

impl Request for EmptyHello {
type Body = ();
type Response = EmptyResponse;

fn endpoint(&self) -> Cow<str> {
"/hello".into()
}
}

#[tokio::test]
async fn bearer_auth() {
let server = MockServer::start().await;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
use rest_client::{Client, EmptyResponse, Request};
use std::borrow::Cow;
use crate::utils::EmptyHello;
use rest_client::Client;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

struct EmptyHello;

impl Request for EmptyHello {
type Body = ();
type Response = EmptyResponse;

fn endpoint(&self) -> Cow<str> {
"/hello".into()
}
}

#[tokio::test]
async fn header_auth() {
let server = MockServer::start().await;
Expand Down
Loading