Skip to content

Commit

Permalink
Adds base structure for handling endpoints and implements support for…
Browse files Browse the repository at this point in the history
… KV store
  • Loading branch information
jmgilman committed Sep 29, 2021
1 parent 4e98e72 commit dcb81d6
Show file tree
Hide file tree
Showing 17 changed files with 780 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/consulrs_derive/target
/target
Cargo.lock
14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ edition = "2018"

[dependencies]
async-trait = "0.1.51"
bytes = "1.1.0"
base64 = "0.13.0"
consulrs_derive = { version = "0.1.0", path = "consulrs_derive" }
derive_builder = "0.10.2"
http = "0.2.5"
reqwest = { version = "0.11.4", default-features = false, features = ["rustls-tls"] }
rustify = "0.4.4"
rustify = "0.5.1"
rustify_derive = "0.5.1"
serde = "1.0.130"
serde_json = "1.0.66"
serde_with = "1.10.0"
thiserror = "1.0.29"
tracing = "0.1.28"
url = "2.2.2"

[dev-dependencies]
dockertest-server = { version = "0.1.3", features=["hashi"] }
env_logger = "0.9.0"
test-env-log = { version = "0.2.7", features = ["trace"] }
tracing-subscriber = {version = "0.2.17", default-features = false, features = ["env-filter", "fmt"]}
19 changes: 19 additions & 0 deletions consulrs_derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "consulrs_derive"
version = "0.1.0"
authors = ["Joshua Gilman <joshuagilman@gmail.com>"]
description = "A derive macro for implementing query options for Consul endpoints"
license = "MIT"
repository = "https://github.com/jmgilman/consulrs"
edition = "2018"

[lib]
proc-macro = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
syn = "1.0"
quote = "1.0"
synstructure = "0.12.5"
proc-macro2 = "1.0.28"
31 changes: 31 additions & 0 deletions consulrs_derive/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use proc_macro2::Span;

/// The general error object returned by functions in this crate.
///
/// The error object can be directly converted from a [syn::Error] as well as
/// be converted directly into a [proc_macro2::TokenStream] to be returned to
/// the compiler.
#[derive(Debug)]
pub struct Error(proc_macro2::TokenStream);

impl Error {
/// Returns a new instance of [Error] using the given [Span] and message.
///
/// This uses [quote_spanned!] in order to provide more accurate information
/// to the compiler about the exact location of the error.
pub fn new(span: Span, message: &str) -> Error {
Error(quote_spanned! { span =>
compile_error!(#message);
})
}

pub fn into_tokens(self) -> proc_macro2::TokenStream {
self.0
}
}

impl From<syn::Error> for Error {
fn from(e: syn::Error) -> Error {
Error(e.to_compile_error())
}
}
43 changes: 43 additions & 0 deletions consulrs_derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#[macro_use]
extern crate synstructure;
extern crate proc_macro;

mod error;

use error::Error;
use proc_macro2::Span;

const FIELD_NAME: &str = "features";

/// Returns field names of the given struct.
fn fields(data: &syn::DataStruct) -> Vec<String> {
data.fields
.iter()
.map(|f| f.ident.clone().unwrap().to_string())
.collect()
}

fn endpoint_derive(s: synstructure::Structure) -> proc_macro2::TokenStream {
// Validate the required field exists
if let syn::Data::Struct(data) = &s.ast().data {
let fields = fields(data);
if !fields.contains(&FIELD_NAME.into()) {
return Error::new(data.struct_token.span, "Missing required `features` field")
.into_tokens();
}
} else {
return Error::new(Span::call_site(), "May only be used on with structs").into_tokens();
}

s.gen_impl(quote! {
use crate::api::features::{FeaturedEndpoint, Features};

gen impl FeaturedEndpoint for @Self {
fn features(&self) -> Option<Features> {
self.features.clone()
}
}
})
}

synstructure::decl_derive!([QueryEndpoint] => endpoint_derive);
168 changes: 163 additions & 5 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,63 @@
use std::str::FromStr;

use rustify::endpoint::{Endpoint, MiddleWare};
use crate::api::features::FeaturedEndpoint;
use crate::client::Client;
use crate::error::ClientError;
use derive_builder::Builder;
use rustify::endpoint::{Endpoint, EndpointResult, MiddleWare};
use rustify::errors::ClientError as RestClientError;
use serde::de::DeserializeOwned;

pub use crate::api::features::Features;

pub mod features;
pub mod kv;

#[derive(Builder, Debug)]
#[builder(pattern = "owned")]
pub struct ApiResponse<T> {
#[builder(setter(into, strip_option), default)]
pub cache: Option<String>,
#[builder(setter(into, strip_option), default)]
pub content_hash: Option<String>,
#[builder(setter(into, strip_option), default)]
pub default_acl_policy: Option<String>,
#[builder(setter(into, strip_option), default)]
pub index: Option<String>,
#[builder(setter(into, strip_option), default)]
pub known_leader: Option<String>,
#[builder(setter(into, strip_option), default)]
pub last_contact: Option<String>,
#[builder(setter(into, strip_option), default)]
pub query_backend: Option<String>,
pub response: T,
}

impl<T> ApiResponse<T> {
pub fn builder() -> ApiResponseBuilder<T> {
ApiResponseBuilder::default()
}
}

/// A [MiddleWare] for adding version and token information to all requests.
///
/// Implements [MiddleWare] to provide support for prepending API version
/// information to all requests and adding an ACL token to the header of all
/// requests. This is automatically passed by the API functions when an endpoint
/// is executed.
/// requests. Additionally, any API features specified in the endpoint are
/// appended to the request. This is passed by the API functions when an
/// endpoint is executed.
#[derive(Debug, Clone)]
pub struct EndpointMiddleware {
pub features: Option<Features>,
pub token: Option<String>,
pub version: String,
}
impl MiddleWare for EndpointMiddleware {
#[instrument(skip(self, req), err)]
fn request<E: Endpoint>(
&self,
_: &E,
req: &mut http::Request<bytes::Bytes>,
req: &mut http::Request<Vec<u8>>,
) -> Result<(), rustify::errors::ClientError> {
// Prepend API version to all requests
debug!(
Expand All @@ -41,14 +81,132 @@ impl MiddleWare for EndpointMiddleware {
);
}

// Add optional API features
if let Some(f) = &self.features {
f.process(req);
}

Ok(())
}

fn response<E: Endpoint>(
&self,
_: &E,
_: &mut http::Response<bytes::Bytes>,
_: &mut http::Response<Vec<u8>>,
) -> Result<(), rustify::errors::ClientError> {
Ok(())
}
}

/// Executes an [Endpoint] and returns the raw response body.
///
/// Any errors which occur in execution are wrapped in a
/// [ClientError::RestClientError] and propagated.
pub async fn exec_with_raw<E>(
client: &impl Client,
endpoint: E,
) -> Result<ApiResponse<Vec<u8>>, ClientError>
where
E: Endpoint + FeaturedEndpoint,
{
info!("Executing {} and expecting no response", endpoint.path());
let features = endpoint.features();
endpoint
.with_middleware(&client.middle(features))
.exec(client.http())
.await
.map_err(parse_err)
.map(parse_raw)?
}

/// Executes an [Endpoint] and returns the result.
///
/// The result from the executed endpoint has a few operations performed on it:
///
/// * Any potential API error responses from the execution are searched for and,
/// if found, converted to a [ClientError::APIError]
/// * All other errors are mapped from [rustify::errors::ClientError] to
/// [ClientError::RestClientError]
pub async fn exec_with_result<E>(
client: &impl Client,
endpoint: E,
) -> Result<ApiResponse<E::Response>, ClientError>
where
E: Endpoint + FeaturedEndpoint,
{
info!("Executing {} and expecting a response", endpoint.path());
let features = endpoint.features();
endpoint
.with_middleware(&client.middle(features))
.exec(client.http())
.await
.map_err(parse_err)
.map(parse)?
}

/// Parses an [EndpointResult], turning it into an [ApiResponse].
fn parse<T>(result: EndpointResult<T>) -> Result<ApiResponse<T>, ClientError>
where
T: DeserializeOwned + Send + Sync,
{
let mut builder = parse_headers(result.response.headers());

let response = result.parse().map_err(ClientError::from)?;
builder = builder.response(response);
Ok(builder.build().unwrap())
}

/// Parses an [EndpointResult], turning it into an [ApiResponse].
fn parse_raw<T>(result: EndpointResult<T>) -> Result<ApiResponse<Vec<u8>>, ClientError>
where
T: DeserializeOwned + Send + Sync,
{
let mut builder = parse_headers(result.response.headers());

let response = result.raw();
builder = builder.response(response);
Ok(builder.build().unwrap())
}

/// Parses commonly found header fields out of response headers.
fn parse_headers<T>(headers: &http::HeaderMap) -> ApiResponseBuilder<T>
where
T: DeserializeOwned + Send + Sync,
{
let mut builder = ApiResponse::builder();

if headers.contains_key("X-Cache") {
builder = builder.cache(headers["X-Cache"].to_str().unwrap());
}
if headers.contains_key("X-Consul-ContentHash") {
builder = builder.content_hash(headers["X-Consul-ContentHash"].to_str().unwrap())
}
if headers.contains_key("X-Consul-Default-ACL-Policy") {
builder =
builder.default_acl_policy(headers["X-Consul-Default-ACL-Policy"].to_str().unwrap())
}
if headers.contains_key("X-Consul-Index") {
builder = builder.index(headers["X-Consul-Index"].to_str().unwrap())
}
if headers.contains_key("X-Consul-KnownLeader") {
builder = builder.known_leader(headers["X-Consul-KnownLeader"].to_str().unwrap())
}
if headers.contains_key("X-Consul-LastContact") {
builder = builder.last_contact(headers["X-Consul-LastContact"].to_str().unwrap())
}
if headers.contains_key("X-Consul-Query-Backend") {
builder = builder.query_backend(headers["X-Consul-Query-Backend"].to_str().unwrap())
}

builder
}

/// Extracts any API errors found and converts them to [ClientError::APIError].
fn parse_err(e: RestClientError) -> ClientError {
if let RestClientError::ServerResponseError { code, content } = &e {
dbg!(content);
ClientError::APIError { code: *code }
} else {
ClientError::from(e)
}
}
Loading

0 comments on commit dcb81d6

Please sign in to comment.