azure-functions-rs/azure-functions/src/bindings/http_response.rs

291 lines
8.3 KiB
Rust

use crate::{
http::{Body, ResponseBuilder, Status},
rpc::{typed_data::Data, RpcHttp, TypedData},
};
use std::collections::HashMap;
/// Represents a HTTP output binding.
///
/// The following binding attributes are supported:
///
/// | Name | Description |
/// |--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
/// | `name` | The name of the parameter being bound. |
///
/// Responses can be created for any type that implements `Into<Body>`.
///
/// # Examples
///
/// Creating a response from a string:
///
/// ```rust
/// use azure_functions::bindings::{HttpRequest, HttpResponse};
/// use azure_functions::func;
///
/// #[func]
/// pub fn example(_req: HttpRequest) -> HttpResponse {
/// "Hello world!".into()
/// }
/// ```
///
/// Creating a response from a JSON value (see the [json! macro](https://docs.serde.rs/serde_json/macro.json.html) from the `serde_json` crate):
///
/// ```rust
/// use azure_functions::bindings::{HttpRequest, HttpResponse};
/// use azure_functions::func;
/// use serde_json::json;
///
/// #[func]
/// pub fn example(_req: HttpRequest) -> HttpResponse {
/// json!({ "hello": "world" }).into()
/// }
/// ```
///
/// Creating a response from a sequence of bytes:
///
/// ```rust
/// use azure_functions::bindings::{HttpRequest, HttpResponse};
/// use azure_functions::func;
///
/// #[func]
/// pub fn example(_req: HttpRequest) -> HttpResponse {
/// [1, 2, 3][..].into()
/// }
/// ```
///
/// Building a custom response:
///
/// ```rust
/// use azure_functions::bindings::{HttpRequest, HttpResponse};
/// use azure_functions::func;
/// use azure_functions::http::Status;
///
/// #[func]
/// pub fn example(_req: HttpRequest) -> HttpResponse {
/// HttpResponse::build()
/// .status(Status::MovedPermanently)
/// .header("Location", "http://example.com")
/// .body("The requested resource has moved to: http://example.com")
/// .into()
/// }
/// ```
#[derive(Default, Debug)]
pub struct HttpResponse {
pub(crate) data: RpcHttp,
pub(crate) status: Status,
}
impl HttpResponse {
pub(crate) fn new() -> Self {
HttpResponse {
data: RpcHttp::default(),
status: Status::Ok,
}
}
/// Creates a new [ResponseBuilder](../http/struct.ResponseBuilder.html) for building a response.
///
/// # Examples
///
/// ```rust
/// use azure_functions::bindings::HttpResponse;
/// use azure_functions::http::Status;
///
/// let response: HttpResponse = HttpResponse::build().status(Status::NotFound).into();
/// assert_eq!(response.status(), Status::NotFound);
/// ```
pub fn build() -> ResponseBuilder {
ResponseBuilder::new()
}
/// Gets the status code for the response.
///
/// # Examples
///
/// ```rust
/// use azure_functions::bindings::HttpResponse;
/// use azure_functions::http::Status;
///
/// let response: HttpResponse = HttpResponse::build().status(Status::BadRequest).into();
/// assert_eq!(response.status(), Status::BadRequest);
/// ```
pub fn status(&self) -> Status {
self.status
}
/// Gets the body of the response.
///
/// # Examples
///
/// ```rust
/// use azure_functions::bindings::HttpResponse;
///
/// let response: HttpResponse = HttpResponse::build().body("example").into();
/// assert_eq!(response.body().as_str().unwrap(), "example");
/// ```
pub fn body(&self) -> Body {
self.data
.body
.as_ref()
.map(|b| Body::from(&**b))
.unwrap_or(Body::Empty)
}
/// Gets the headers of the response.
///
/// # Examples
///
/// ```rust
/// use azure_functions::bindings::HttpResponse;
///
/// let response: HttpResponse = HttpResponse::build().header("Content-Type", "text/plain").into();
/// assert_eq!(response.headers().get("Content-Type").unwrap(), "text/plain");
/// ```
pub fn headers(&self) -> &HashMap<String, String> {
&self.data.headers
}
}
impl<'a, T> From<T> for HttpResponse
where
T: Into<Body<'a>>,
{
fn from(data: T) -> Self {
HttpResponse::build().body(data).into()
}
}
impl From<ResponseBuilder> for HttpResponse {
fn from(builder: ResponseBuilder) -> Self {
builder.0
}
}
#[doc(hidden)]
impl Into<TypedData> for HttpResponse {
fn into(mut self) -> TypedData {
self.data.status_code = self.status.to_string();
TypedData {
data: Some(Data::Http(Box::new(self.data))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use matches::matches;
use serde::{Deserialize, Serialize};
#[test]
fn it_is_empty_by_default() {
let response = HttpResponse::new();
assert_eq!(response.status(), Status::Ok);
assert!(matches!(response.body(), Body::Empty));
}
#[test]
fn it_is_empty_from_a_builder() {
let response: HttpResponse = HttpResponse::build().into();
assert_eq!(response.status(), Status::Ok);
assert!(matches!(response.body(), Body::Empty));
}
#[test]
fn it_builds_with_a_status() {
let response: HttpResponse = HttpResponse::build().status(Status::Continue).into();
assert_eq!(response.status(), Status::Continue);
}
#[test]
fn it_builds_with_a_string_body() {
const BODY: &'static str = "test body";
let response: HttpResponse = HttpResponse::build().body(BODY).into();
assert_eq!(
response.headers().get("Content-Type").unwrap(),
"text/plain"
);
assert_eq!(response.body().as_str().unwrap(), BODY);
}
#[test]
fn it_builds_with_a_json_body() {
#[derive(Serialize, Deserialize)]
struct Data {
message: String,
};
const MESSAGE: &'static str = "test";
let data = Data {
message: MESSAGE.to_string(),
};
let response: HttpResponse = HttpResponse::build()
.body(::serde_json::to_value(data).unwrap())
.into();
assert_eq!(
response.headers().get("Content-Type").unwrap(),
"application/json"
);
assert_eq!(response.body().as_json::<Data>().unwrap().message, MESSAGE);
}
#[test]
fn it_builds_with_a_bytes_body() {
const BODY: &'static [u8] = &[1, 2, 3];
let response: HttpResponse = HttpResponse::build().body(BODY).into();
assert_eq!(
response.headers().get("Content-Type").unwrap(),
"application/octet-stream"
);
assert_eq!(response.body().as_bytes(), BODY);
}
#[test]
fn it_builds_with_headers() {
let response: HttpResponse = HttpResponse::build()
.header("header1", "value1")
.header("header2", "value2")
.header("header3", "value3")
.into();
assert_eq!(response.headers().get("header1").unwrap(), "value1");
assert_eq!(response.headers().get("header2").unwrap(), "value2");
assert_eq!(response.headers().get("header3").unwrap(), "value3");
}
#[test]
fn it_converts_to_typed_data() {
let response: HttpResponse = HttpResponse::build()
.status(Status::BadRequest)
.header("header", "value")
.body("body")
.into();
let data: TypedData = response.into();
match data.data {
Some(Data::Http(http)) => {
assert_eq!(http.status_code, "400");
assert_eq!(http.headers.get("header").unwrap(), "value");
assert_eq!(
http.body,
Some(Box::new(TypedData {
data: Some(Data::String("body".to_string()))
}))
);
}
_ => assert!(false),
}
}
}