diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..543ff6d7 --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/example/main.rs b/example/main.rs index e920424d..23c8dbc5 100644 --- a/example/main.rs +++ b/example/main.rs @@ -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 = fn(&ArgMatches, &ApiClientType); diff --git a/src/account.rs b/src/endpoints/account.rs similarity index 100% rename from src/account.rs rename to src/endpoints/account.rs diff --git a/src/dns.rs b/src/endpoints/dns.rs similarity index 97% rename from src/dns.rs rename to src/endpoints/dns.rs index 860b19d4..110710ea 100644 --- a/src/dns.rs +++ b/src/endpoints/dns.rs @@ -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}; diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs new file mode 100644 index 00000000..1b0359b0 --- /dev/null +++ b/src/endpoints/mod.rs @@ -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; diff --git a/src/plan.rs b/src/endpoints/plan.rs similarity index 100% rename from src/plan.rs rename to src/endpoints/plan.rs diff --git a/src/workerskv/create_namespace.rs b/src/endpoints/workerskv/create_namespace.rs similarity index 94% rename from src/workerskv/create_namespace.rs rename to src/endpoints/workerskv/create_namespace.rs index bed9a3cf..c7cc3403 100644 --- a/src/workerskv/create_namespace.rs +++ b/src/endpoints/workerskv/create_namespace.rs @@ -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. diff --git a/src/workerskv/list_namespace_keys.rs b/src/endpoints/workerskv/list_namespace_keys.rs similarity index 94% rename from src/workerskv/list_namespace_keys.rs rename to src/endpoints/workerskv/list_namespace_keys.rs index a9dc2da8..28f0cb44 100644 --- a/src/workerskv/list_namespace_keys.rs +++ b/src/endpoints/workerskv/list_namespace_keys.rs @@ -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 diff --git a/src/workerskv/list_namespaces.rs b/src/endpoints/workerskv/list_namespaces.rs similarity index 92% rename from src/workerskv/list_namespaces.rs rename to src/endpoints/workerskv/list_namespaces.rs index fff51eda..c6d3db88 100644 --- a/src/workerskv/list_namespaces.rs +++ b/src/endpoints/workerskv/list_namespaces.rs @@ -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 diff --git a/src/workerskv/mod.rs b/src/endpoints/workerskv/mod.rs similarity index 95% rename from src/workerskv/mod.rs rename to src/endpoints/workerskv/mod.rs index 78431a3b..1a2decaf 100644 --- a/src/workerskv/mod.rs +++ b/src/endpoints/workerskv/mod.rs @@ -1,4 +1,4 @@ -use crate::response::ApiResult; +use crate::framework::response::ApiResult; use chrono::offset::Utc; use chrono::DateTime; diff --git a/src/workerskv/remove_namespace.rs b/src/endpoints/workerskv/remove_namespace.rs similarity index 91% rename from src/workerskv/remove_namespace.rs rename to src/endpoints/workerskv/remove_namespace.rs index b64cf3a1..6b5a7b0e 100644 --- a/src/workerskv/remove_namespace.rs +++ b/src/endpoints/workerskv/remove_namespace.rs @@ -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. diff --git a/src/workerskv/rename_namespace.rs b/src/endpoints/workerskv/rename_namespace.rs similarity index 93% rename from src/workerskv/rename_namespace.rs rename to src/endpoints/workerskv/rename_namespace.rs index bdf6ee8e..e8a327f8 100644 --- a/src/workerskv/rename_namespace.rs +++ b/src/endpoints/workerskv/rename_namespace.rs @@ -1,4 +1,4 @@ -use crate::endpoint::{Endpoint, Method}; +use crate::framework::endpoint::{Endpoint, Method}; /// Rename a Namespace /// Modifies a namespace's title. diff --git a/src/zone.rs b/src/endpoints/zone.rs similarity index 96% rename from src/zone.rs rename to src/endpoints/zone.rs index e1b89a28..4c6a4ecf 100644 --- a/src/zone.rs +++ b/src/endpoints/zone.rs @@ -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; diff --git a/src/apiclient.rs b/src/framework/apiclient.rs similarity index 77% rename from src/apiclient.rs rename to src/framework/apiclient.rs index fd9ed96f..5f30a917 100644 --- a/src/apiclient.rs +++ b/src/framework/apiclient.rs @@ -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 { diff --git a/src/auth.rs b/src/framework/auth.rs similarity index 100% rename from src/auth.rs rename to src/framework/auth.rs diff --git a/src/endpoint.rs b/src/framework/endpoint.rs similarity index 89% rename from src/endpoint.rs rename to src/framework/endpoint.rs index 3ecd3874..80712216 100644 --- a/src/endpoint.rs +++ b/src/framework/endpoint.rs @@ -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; diff --git a/src/mock.rs b/src/framework/mock.rs similarity index 84% rename from src/mock.rs rename to src/framework/mock.rs index aa74a921..dd68217e 100644 --- a/src/mock.rs +++ b/src/framework/mock.rs @@ -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; diff --git a/src/framework/mod.rs b/src/framework/mod.rs new file mode 100644 index 00000000..528494dd --- /dev/null +++ b/src/framework/mod.rs @@ -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( + &self, + endpoint: &dyn endpoint::Endpoint, + ) -> response::ApiResponse + 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) + } +} diff --git a/src/response/apifail.rs b/src/framework/response/apifail.rs similarity index 99% rename from src/response/apifail.rs rename to src/framework/response/apifail.rs index 5b105f7f..9d8c6448 100644 --- a/src/response/apifail.rs +++ b/src/framework/response/apifail.rs @@ -46,7 +46,6 @@ impl fmt::Display for ApiError { pub trait ApiResult: DeserializeOwned + Debug {} - #[derive(Debug)] pub enum ApiFailure { Error(reqwest::StatusCode, ApiErrors), @@ -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 { @@ -93,4 +91,4 @@ impl From for ApiFailure { fn from(error: reqwest::Error) -> Self { ApiFailure::Invalid(error) } -} \ No newline at end of file +} diff --git a/src/response/mod.rs b/src/framework/response/mod.rs similarity index 100% rename from src/response/mod.rs rename to src/framework/response/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 9a897baf..7d21c0de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,109 +7,5 @@ extern crate serde_json; extern crate serde_qs; extern crate url; -mod account; -pub mod apiclient; -pub mod auth; -pub mod dns; -pub mod endpoint; -pub mod mock; -mod plan; -pub mod response; -pub mod workerskv; -pub mod zone; - -use crate::apiclient::ApiClient; -use crate::auth::{AuthClient, Credentials}; -use crate::endpoint::{Endpoint, Method}; -use crate::response::{ApiResponse, ApiResult}; -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: Credentials, - http_client: reqwest::Client, -} - -impl HttpApiClient { - pub fn new(credentials: 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( - &self, - endpoint: &dyn Endpoint, - ) -> ApiResponse - where - ResultType: 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()?; - - response::map_api_response(response) - } -} +pub mod endpoints; +pub mod framework;