Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http Operation traits and structures #167

Merged
merged 12 commits into from
Jan 25, 2021
1 change: 1 addition & 0 deletions design/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
book
6 changes: 6 additions & 0 deletions design/book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[book]
authors = ["Russell Cohen"]
language = "en"
multilingual = false
src = "src"
title = "AWS Rust SDK Design"
3 changes: 3 additions & 0 deletions design/src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Summary

- [Http Operations](./operation.md)
74 changes: 74 additions & 0 deletions design/src/operation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# HTTP-based Operations
The Smithy code generator for Rust (and by extension), the AWS SDK use an `Operation` abstraction to provide a unified
interface for dispatching requests. `Operation`s contain:
* A base HTTP request (with a potentially streaming body)
* A typed property bag of configuration options
* A fully generic response handler

In the typical case, these configuration options include things like a `CredentialsProvider`, however, they can also be
full middleware layers that will get added by the dispatch stack.

## Operation Phases
This section details the flow of a request through the SDK until a response is returned to the user.

### Input Construction

A customer interacts with the SDK builders to construct an input. The `build()` method on an input returns
an `Operation<Output>`. This codifies the base HTTP request & all the configuration and middleware layers required to modify and dispatch the request.

```rust,ignore
pub struct Operation<H> {
request: Request,
response_handler: Box<H>,
}

pub struct Request {
base: http::Request<SdkBody>,
configuration: PropertyBag,
}
```

For most requests, `.build()` will NOT consume the input. A user can call `.build()` multiple times to produce multiple operations from the same input.

By using a property bag, we can define the `Operation` in Smithy core. AWS specific configuration can be added later in the stack.

### Operation Construction
In order to construct an operation, the generated code injects appropriate middleware & configuration via the configuration property bag. It does this by reading the configuration properties out of the service
config, copying them as necessary, and loading them into the `Request`:

```rust,ignore
// This is approximately the generated code, I've cleaned a few things up for readability.
pub fn build(self, config: &dynamodb::config::Config) -> Operation<BatchExecuteStatement> {
let op = BatchExecuteStatement::new(BatchExecuteStatementInput {
statements: self.statements,
});
let mut request = operation::Request::new(
op.build_http_request()
.map(|body| operation::SdkBody::from(body)),
);

use operation::signing_middleware::SigningConfigExt;
request
.config
.insert_signingconfig(SigningConfig::default_config(
auth::ServiceConfig {
service: config.signing_service().into(),
region: config.region.clone().into(),
},
auth::RequestConfig {
request_ts: || std::time::SystemTime::now(),
},
));
use operation::signing_middleware::CredentialProviderExt;
request
.config
.insert_credentials_provider(config.credentials_provider.clone());

use operation::endpoint::EndpointProviderExt;
request
.config
.insert_endpoint_provider(config.endpoint_provider.clone());

Operation::new(request, op)
}
```
3 changes: 3 additions & 0 deletions rust-runtime/smithy-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ edition = "2018"

[dependencies]
smithy-types = { path = "../smithy-types" }
bytes = "1"
http-body = "0.4.0"
http = "0.2.3"

[dev-dependencies]
proptest = "0.10.1"
Expand Down
94 changes: 94 additions & 0 deletions rust-runtime/smithy-http/src/body.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

use bytes::Bytes;
use http::{HeaderMap, HeaderValue};
use std::error::Error;
use std::pin::Pin;
use std::task::{Context, Poll};

type BodyError = Box<dyn Error + Send + Sync>;

/// SdkBody type
///
/// This is the Body used for dispatching all HTTP Requests.
/// For handling responses, the type of the body will be controlled
/// by the HTTP stack.
///
/// TODO: Consider renaming to simply `Body`, although I'm concerned about naming headaches
/// between hyper::Body and our Body
pub enum SdkBody {
Once(Option<Bytes>),
// TODO: tokio::sync::mpsc based streaming body
}

impl SdkBody {
fn poll_inner(&mut self) -> Poll<Option<Result<Bytes, BodyError>>> {
match self {
SdkBody::Once(ref mut opt) => {
let data = opt.take();
match data {
Some(bytes) => Poll::Ready(Some(Ok(bytes))),
None => Poll::Ready(None),
}
}
}
}

/// If possible, return a reference to this body as `&[u8]`
///
/// If this SdkBody is NOT streaming, this will return the byte slab
/// If this SdkBody is streaming, this will return `None`
pub fn bytes(&self) -> Option<&[u8]> {
match self {
SdkBody::Once(Some(b)) => Some(&b),
SdkBody::Once(None) => Some(&[]),
// In the future, streaming variants will return `None`
}
}

pub fn try_clone(&self) -> Option<Self> {
match self {
SdkBody::Once(bytes) => Some(SdkBody::Once(bytes.clone())),
}
}
}

impl From<&str> for SdkBody {
fn from(s: &str) -> Self {
SdkBody::Once(Some(Bytes::copy_from_slice(s.as_bytes())))
}
}

impl From<Bytes> for SdkBody {
fn from(bytes: Bytes) -> Self {
SdkBody::Once(Some(bytes))
}
}

impl From<Vec<u8>> for SdkBody {
fn from(data: Vec<u8>) -> Self {
Self::from(Bytes::from(data))
}
}

impl http_body::Body for SdkBody {
type Data = Bytes;
type Error = BodyError;

fn poll_data(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
self.poll_inner()
}

fn poll_trailers(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<Option<HeaderMap<HeaderValue>>, Self::Error>> {
Poll::Ready(Ok(None))
}
}
6 changes: 4 additions & 2 deletions rust-runtime/smithy-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
* SPDX-License-Identifier: Apache-2.0.
*/

// TODO: there is no compelling reason to have this be a shared crate—we should vendor this
// module into the individual crates
pub mod base64;
pub mod body;
pub mod label;
pub mod operation;
pub mod property_bag;
pub mod query;
pub mod response;
127 changes: 127 additions & 0 deletions rust-runtime/smithy-http/src/operation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use crate::body::SdkBody;
use crate::property_bag::PropertyBag;
use std::cell::{Ref, RefCell, RefMut};
use std::rc::Rc;

pub struct Operation<H, R> {
request: Request,
response_handler: H,
_retry_policy: R,
}

impl<H> Operation<H, ()> {
pub fn into_request_response(self) -> (Request, H) {
(self.request, self.response_handler)
}

pub fn new(request: Request, response_handler: H) -> Self {
Operation {
request,
response_handler,
_retry_policy: (),
}
}
}

pub struct Request {
/// The underlying HTTP Request
inner: http::Request<SdkBody>,

/// Property bag of configuration options
///
/// Middleware can read and write from the property bag and use its
/// contents to augment the request (see `Request::augment`)
///
/// configuration is stored in an `Rc<RefCell>>` to facilitate cloning requests during retries
/// We should consider if this should instead be an `Arc<Mutex>`. I'm not aware of times where
/// we'd need to modify the request concurrently, but perhaps such a thing may some day exist.
configuration: Rc<RefCell<PropertyBag>>,
}

impl Request {
pub fn new(base: http::Request<SdkBody>) -> Self {
Request {
inner: base,
configuration: Rc::new(RefCell::new(PropertyBag::new())),
}
}

pub fn augment<T>(
self,
f: impl FnOnce(http::Request<SdkBody>, &mut PropertyBag) -> Result<http::Request<SdkBody>, T>,
) -> Result<Request, T> {
let inner = {
let configuration: &mut PropertyBag = &mut self.configuration.as_ref().borrow_mut();
f(self.inner, configuration)?
};
Ok(Request {
inner,
configuration: self.configuration,
})
}

pub fn config_mut(&mut self) -> RefMut<'_, PropertyBag> {
self.configuration.as_ref().borrow_mut()
}

pub fn config(&self) -> Ref<'_, PropertyBag> {
self.configuration.as_ref().borrow()
}

pub fn try_clone(&self) -> Option<Request> {
let cloned_body = self.inner.body().try_clone()?;
let mut cloned_request = http::Request::builder()
.uri(self.inner.uri().clone())
.method(self.inner.method());
*cloned_request
.headers_mut()
.expect("builder has not been modified, headers must be valid") =
self.inner.headers().clone();
let inner = cloned_request
.body(cloned_body)
.expect("a clone of a valid request should be a valid request");
Some(Request {
inner,
configuration: self.configuration.clone(),
})
}

pub fn into_parts(self) -> (http::Request<SdkBody>, Rc<RefCell<PropertyBag>>) {
(self.inner, self.configuration)
}
}

#[cfg(test)]
mod test {
use crate::body::SdkBody;
use crate::operation::Request;
use http::header::{AUTHORIZATION, CONTENT_LENGTH};
use http::Uri;

#[test]
fn try_clone_clones_all_data() {
let mut request = Request::new(
http::Request::builder()
.uri(Uri::from_static("http://www.amazon.com"))
.method("POST")
.header(CONTENT_LENGTH, 456)
.header(AUTHORIZATION, "Token: hello")
.body(SdkBody::from("hello world!"))
.expect("valid request"),
);
request.config_mut().insert("hello");
let cloned = request.try_clone().expect("request is cloneable");

let (request, config) = cloned.into_parts();
assert_eq!(request.uri(), &Uri::from_static("http://www.amazon.com"));
assert_eq!(request.method(), "POST");
assert_eq!(request.headers().len(), 2);
assert_eq!(
request.headers().get(AUTHORIZATION).unwrap(),
"Token: hello"
);
assert_eq!(request.headers().get(CONTENT_LENGTH).unwrap(), "456");
assert_eq!(request.body().bytes().unwrap(), "hello world!".as_bytes());
assert_eq!(config.as_ref().borrow().get::<&str>(), Some(&"hello"));
}
}
Loading