Skip to content

Commit

Permalink
Merge pull request #24 from cloudflare/achalmers/23
Browse files Browse the repository at this point in the history
fix #23: separate modules for endpoints and framework
  • Loading branch information
adamchalmers authored Aug 16, 2019
2 parents 2685244 + 260150a commit 54b21d4
Show file tree
Hide file tree
Showing 21 changed files with 205 additions and 137 deletions.
55 changes: 55 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
Every PR should have a corresponding issue, and the issue number should appear in the PR's description.

PRs should be squashed to one commit before merging.

There are two root-level modules in `cloudflare-rs`. Most PRs will only touch one of them. If you're
working on the API framework itself, all the relevant code lives under the `framework/`
module. If you're looking to add or edit an endpoint, read on.

# Adding New Endpoints

Every Cloudflare product should have its own directory under `endpoints/`. For example, all the
DNS endpoints live under `endpoints/dns`.

If your product's module gets big enough, we suggest structuring it like so:

```
src/
endpoints/
myproduct/
data_structures.rs
endpoint_a.rs
endpoint_b.rs
mod.rs
```

In this structure, every endpoint gets its own module, which includes

* Endpoint struct
* Request struct
* Response struct (if necessary)
* Params struct (if necessary)

Common data structures which are used in multiple endpoints should be put in `data_structures.rs`.
`mod.rs` should then make all its submodules public, like so:

```rust
mod data_structures;
mod endpoint_a;
mod endpoint_b;

pub use data_structures;
pub use endpoint_a;
pub use endpoint_b;
```

## Documentation

Endpoint structs should have a docstring with a link to the [Cloudflare API docs](https://api.cloudflare.com).

Fields which represent endpoint parameters should be commented with their description in the
Cloudflare API docs.

If in doubt, follow the docstring structure for modules like `dns`. Ideally, someone reading your
endpoint code shouldn't need to open up api.cloudflare.com for documentation. Your comments should
be documentation enough.
15 changes: 8 additions & 7 deletions example/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ extern crate clap;
extern crate cloudflare;

use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use cloudflare::apiclient::ApiClient;
use cloudflare::auth::Credentials;
use cloudflare::dns;
use cloudflare::mock::{MockApiClient, NoopEndpoint};
use cloudflare::response::{ApiFailure, ApiResponse, ApiResult};
use cloudflare::zone;
use cloudflare::{HttpApiClient, OrderDirection};
use cloudflare::endpoints::{dns, zone};
use cloudflare::framework::{
apiclient::ApiClient,
auth::Credentials,
mock::{MockApiClient, NoopEndpoint},
response::{ApiFailure, ApiResponse, ApiResult},
HttpApiClient, OrderDirection,
};

type SectionFunction<ApiClientType> = fn(&ArgMatches, &ApiClientType);

Expand Down
File renamed without changes.
8 changes: 5 additions & 3 deletions src/dns.rs → src/endpoints/dns.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::framework::{
endpoint::{Endpoint, Method},
response::ApiResult,
};
/// https://api.cloudflare.com/#dns-records-for-a-zone-properties
use super::{OrderDirection, SearchMatch};
use crate::endpoint::{Endpoint, Method};
use crate::response::ApiResult;
use crate::framework::{OrderDirection, SearchMatch};
use chrono::offset::Utc;
use chrono::DateTime;
use std::net::{Ipv4Addr, Ipv6Addr};
Expand Down
10 changes: 10 additions & 0 deletions src/endpoints/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*!
Implementations of the Endpoint trait for individual Cloudflare API endpoints, e.g. DNS or Workers.
If you want to add a new Cloudflare API to this crate, simply add a new submodule of this `endpoints`
module.
*/
pub mod account;
pub mod dns;
pub mod plan;
pub mod workerskv;
pub mod zone;
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::WorkersKvNamespace;

use crate::endpoint::{Endpoint, Method};
use crate::framework::endpoint::{Endpoint, Method};

/// Create a Namespace
/// Creates a namespace under the given title.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::Key;

use crate::endpoint::{Endpoint, Method};
use crate::framework::endpoint::{Endpoint, Method};

/// List a Namespace's Keys
/// https://api.cloudflare.com/#workers-kv-namespace-list-a-namespace-s-keys
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::WorkersKvNamespace;

use crate::endpoint::{Endpoint, Method};
use crate::framework::endpoint::{Endpoint, Method};

/// List Namespaces
/// Returns the namespaces owned by an account
Expand Down
2 changes: 1 addition & 1 deletion src/workerskv/mod.rs → src/endpoints/workerskv/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::response::ApiResult;
use crate::framework::response::ApiResult;
use chrono::offset::Utc;
use chrono::DateTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::endpoint::{Endpoint, Method};
use crate::framework::endpoint::{Endpoint, Method};

/// Remove a Namespace
/// Deletes the namespace corresponding to the given ID.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::endpoint::{Endpoint, Method};
use crate::framework::endpoint::{Endpoint, Method};

/// Rename a Namespace
/// Modifies a namespace's title.
Expand Down
11 changes: 6 additions & 5 deletions src/zone.rs → src/endpoints/zone.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use super::{OrderDirection, SearchMatch};
use crate::account::Account;
use crate::endpoint::{Endpoint, Method};
use crate::plan::Plan;
use crate::response::ApiResult;
use crate::endpoints::{account::Account, plan::Plan};
use crate::framework::{
endpoint::{Endpoint, Method},
response::ApiResult,
};
use crate::framework::{OrderDirection, SearchMatch};
use chrono::offset::Utc;
use chrono::DateTime;

Expand Down
6 changes: 4 additions & 2 deletions src/apiclient.rs → src/framework/apiclient.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::endpoint::Endpoint;
use crate::response::{ApiResponse, ApiResult};
use crate::framework::{
endpoint::Endpoint,
response::{ApiResponse, ApiResult},
};
use serde::Serialize;

pub trait ApiClient {
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/endpoint.rs → src/framework/endpoint.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::Environment;
use crate::response::ApiResult;
use crate::framework::response::ApiResult;
use crate::framework::Environment;
use serde::Serialize;
use url::Url;

Expand Down
6 changes: 3 additions & 3 deletions src/mock.rs → src/framework/mock.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::apiclient::ApiClient;
use crate::endpoint::{Endpoint, Method};
use crate::response::{ApiErrors, ApiFailure, ApiResponse, ApiResult, ApiError};
use crate::framework::apiclient::ApiClient;
use crate::framework::endpoint::{Endpoint, Method};
use crate::framework::response::{ApiError, ApiErrors, ApiFailure, ApiResponse, ApiResult};
use reqwest;
use std::collections::HashMap;

Expand Down
103 changes: 103 additions & 0 deletions src/framework/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*!
This module controls how requests are sent to Cloudflare's API, and how responses are parsed from it.
*/
pub mod apiclient;
pub mod auth;
pub mod endpoint;
pub mod mock;
pub mod response;

use crate::framework::{
apiclient::ApiClient, auth::AuthClient, endpoint::Method, response::map_api_response,
};
use serde::Serialize;

#[derive(Serialize, Clone, Debug)]
pub enum OrderDirection {
#[serde(rename = "asc")]
Ascending,
#[serde(rename = "desc")]
Descending,
}

#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum SearchMatch {
All,
Any,
}

#[derive(Debug)]
pub enum Environment {
Production,
}

impl<'a> From<&'a Environment> for url::Url {
fn from(environment: &Environment) -> Self {
match environment {
Environment::Production => {
url::Url::parse("https://api.cloudflare.com/client/v4/").unwrap()
}
}
}
}

pub struct HttpApiClient {
environment: Environment,
credentials: auth::Credentials,
http_client: reqwest::Client,
}

impl HttpApiClient {
pub fn new(credentials: auth::Credentials) -> HttpApiClient {
HttpApiClient {
environment: Environment::Production,
credentials,
http_client: reqwest::Client::new(),
}
}
}

// TODO: This should probably just implement request for the Reqwest client itself :)
// TODO: It should also probably be called `ReqwestApiClient` rather than `HttpApiClient`.
impl<'a> ApiClient for HttpApiClient {
fn request<ResultType, QueryType, BodyType>(
&self,
endpoint: &dyn endpoint::Endpoint<ResultType, QueryType, BodyType>,
) -> response::ApiResponse<ResultType>
where
ResultType: response::ApiResult,
QueryType: Serialize,
BodyType: Serialize,
{
fn match_reqwest_method(method: Method) -> reqwest::Method {
match method {
Method::Get => reqwest::Method::GET,
Method::Post => reqwest::Method::POST,
Method::Delete => reqwest::Method::DELETE,
Method::Put => reqwest::Method::PUT,
Method::Patch => reqwest::Method::PATCH,
}
}

// Build the request
let mut request = self
.http_client
.request(
match_reqwest_method(endpoint.method()),
endpoint.url(&self.environment),
)
.query(&endpoint.query());

if let Some(body) = endpoint.body() {
request = request.body(serde_json::to_string(&body).unwrap());
request = request.header(reqwest::header::CONTENT_TYPE, endpoint.content_type());
}

request = request.auth(&self.credentials);

let response = request.send()?;

map_api_response(response)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ impl fmt::Display for ApiError {

pub trait ApiResult: DeserializeOwned + Debug {}


#[derive(Debug)]
pub enum ApiFailure {
Error(reqwest::StatusCode, ApiErrors),
Expand All @@ -70,7 +69,6 @@ impl fmt::Display for ApiFailure {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ApiFailure::Error(status, api_errors) => {

let mut output = "".to_owned();
output.push_str(&format!("HTTP {}", status));
for err in &api_errors.errors {
Expand All @@ -93,4 +91,4 @@ impl From<reqwest::Error> for ApiFailure {
fn from(error: reqwest::Error) -> Self {
ApiFailure::Invalid(error)
}
}
}
File renamed without changes.
Loading

0 comments on commit 54b21d4

Please sign in to comment.