#http #request #client

feignhttp

Declarative HTTP client for rust

10 unstable releases (3 breaking)

new 0.3.1 Apr 19, 2021
0.3.0 Apr 19, 2021
0.2.3 Apr 9, 2021
0.1.2 Mar 21, 2021
0.0.0 Mar 16, 2021

#24 in HTTP client

Download history 68/week @ 2021-03-16 9/week @ 2021-03-23 37/week @ 2021-03-30 47/week @ 2021-04-06

65 downloads per month

MIT license

44KB
618 lines

FeignHttp

crates.io Documentation MIT licensed

FeignHttp is a declarative HTTP client. Based on rust macros.

Features

  • Easy to use
  • Asynchronous request
  • Configurable timeout settings
  • Supports form, plain text and JSON
  • Selectable HTTP backends (reqwest or isahc)

Usage

FeignHttp mark macros on asynchronous functions, you need a runtime for support async/await. You can use async-std or tokio.

async-std:

[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }

The feature tokio1 is need when use reqwest as the HTTP backend.

tokio:

[dependencies]
tokio = { version = "1", features = ["full"] }

Add feignhttp in your Cargo.toml and use default feature:

feignhttp = { version = "0.3" }

Then add the following code:

use feignhttp::get;

#[get("https://api.github.com")]
async fn github() -> feignhttp::Result<String> {}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let r = github().await?;
    println!("result: {}", r);

    Ok(())
}

The get attribute macro specifies get request, feignhttp::Result<String> specifies the return result. It will send get request to https://api.github.com and receive a plain text body.

Using non-default HTTP backend:

feignhttp = { version = "0.3", default-features = false, features = ["isahc-client"] }

The default-features = false option disable default reqwest.

Making a POST request

For a post request, you should use the post attribute macro to specify request method and use a body attribute to specify a request body.

use feignhttp::post;

#[post(url = "https://httpbin.org/anything")]
async fn post_data(#[body] text: String) -> feignhttp::Result<String> {}

The #[body] mark a request body. Function parameter text is a String type, it will put in the request body as plain text. String and &str will be put as plain text into the request body.

Paths

Using path to specify path value:

use feignhttp::get;

#[get("https://api.github.com/repos/{owner}/{repo}")]
async fn repository(
    #[path] owner: &str,
    #[path] repo: String,
) -> feignhttp::Result<String> {}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let r = repository("dxx", "feignhttp".to_string()).await?;
    println!("repository result: {}", r);

    Ok(())
}

dxx will replace {owner} and feignhttp will replace {repo} , the url to be send will be https://api.github.com/repos/dxx/feignhttp. You can specify a path name like #[path("owner")].

Query Parameters

Using query to specify query parameter:

use feignhttp::get;

#[get("https://api.github.com/repos/{owner}/{repo}/contributors")]
async fn contributors(
    #[path("owner")] user: &str,
    #[path] repo: &str,
    #[query] page: u32,
) -> feignhttp::Result<String> {}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let r = contributors (
        "dxx",
        "feignhttp",
        1,
    ).await?;
    println!("contributors result: {}", r);

    Ok(())
}

The page parameter will as query parameter in the url. An url which will be send is https://api.github.com/repos/dxx/feignhttp?page=1.

Note: A function parameter without query attribute will as a query parameter by default.

Headers

Using header to specify request header:

use feignhttp::get;

#[get("https://api.github.com/repos/{owner}/{repo}/commits")]
async fn commits(
    #[header] accept: &str,
    #[path] owner: &str,
    #[path] repo: &str,
    #[query] page: u32,
    #[query] per_page: u32,
) -> feignhttp::Result<String> {}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let r = commits(
        "application/vnd.github.v3+json",
        "dxx",
        "feignhttp",
        1,
        5,
    )
    .await?;
    println!("commits result: {}", r);

    Ok(())
}

A header accept:application/vnd.github.v3+json will be send.

Form

Using form to specify form parameter:

use feignhttp::post;

#[post(url = "https://httpbin.org/anything")]
async fn post_user(
    #[form] id: i32,
    #[form] name: &str,
) -> feignhttp::Result<String> {}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let r = post_user(1, "jack").await?;
    println!("{}", r);

    Ok(())
}

See here for more examples.

URL constant

We can use constant to maintain all urls of request:

use feignhttp::get;

const GITHUB_URL: &str = "https://api.github.com";

#[get(GITHUB_URL, path = "/repos/{owner}/{repo}/languages")]
async fn languages(
    #[path] owner: &str,
    #[path] repo: &str,
) -> feignhttp::Result<String> {}

Url constant must be the first metadata in get attribute macro. You also can specify metadata key:

#[get(url = GITHUB_URL, path = "/repos/{owner}/{repo}/languages")]
async fn languages(
    #[path] owner: &str,
    #[path] repo: &str,
) -> feignhttp::Result<String> {}

JSON

Serde is a framework for serializing and deserializing Rust data structures. When use json, you should add serde in Cargo.toml:

[dependencies]
serde = { version = "1", features = ["derive"] }

Here is an example of getting json:

use feignhttp::get;
use serde::Deserialize;

// Deserialize: Specifies deserialization
#[derive(Debug, Deserialize)]
struct IssueItem {
    pub id: u32,
    pub number: u32,
    pub title: String,
    pub url: String,
    pub repository_url: String,
    pub state: String,
    pub body: Option<String>,
}


const GITHUB_URL: &str = "https://api.github.com";

#[get(url = GITHUB_URL, path = "/repos/{owner}/{repo}/issues")]
async fn issues(
    #[path] owner: &str,
    #[path] repo: &str,
    page: u32,
    per_page: u32,
) -> feignhttp::Result<Vec<IssueItem>> {}


#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let r = issues("octocat", "hello-world", 1, 2).await?;
    println!("issues: {:#?}", r);

    Ok(())
}

This issues function return Vec<IssueItem>, it is deserialized according to the content of the response.

Send a json request:

use feignhttp::post;
use serde::Serialize;

#[derive(Debug, Serialize)]
struct User {
    id: i32,
    name: String,
}

#[post(url = "https://httpbin.org/anything")]
async fn post_user(#[body] user: User) -> feignhttp::Result<String> {}

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let user = User {
        id: 1,
        name: "jack".to_string(),
    };
    let r = post_user(user).await?;
    println!("{}", r);

    Ok(())
}

See here for a complete example.

Using structure

Structure is a good way to manage requests. Define a structure and then define a large number of request methods:

use feignhttp::feign;

const GITHUB_URL: &str = "https://api.github.com";

struct Github;

#[feign(url = GITHUB_URL)]
impl Github {
    #[get]
    async fn home() -> feignhttp::Result<String> {}

    #[get("/repos/{owner}/{repo}")]
    async fn repository(
        #[path] owner: &str,
        #[path] repo: &str,
    ) -> feignhttp::Result<String> {}

    // ...
    
    // Structure method still send request
    #[get(path = "/repos/{owner}/{repo}/languages")]
    async fn languages(
        &self,
        #[path] owner: &str,
        #[path] repo: &str,
    ) -> feignhttp::Result<String> {}
}

See here for a complete example.

Timeout configuration

If you need to configure the timeout, use connect_timeout and timeout to specify connect timeout and read timeout.

Connect timeout:

#[get(url = "http://xxx.com", connect_timeout = 3000)]
async fn connect_timeout() -> feignhttp::Result<String> {}

Read timeout:

#[get(url = "https://httpbin.org/delay/5", timeout = 3000)]
async fn timeout() -> feignhttp::Result<String> {}

License

FeignHttp is provided under the MIT license. See LICENSE.

Dependencies

~2.8–8.5MB
~197K SLoC