azure_functions/bindings/http_response.rs
1use crate::{
2 http::{Body, ResponseBuilder, Status},
3 rpc::{typed_data::Data, RpcHttp, TypedData},
4};
5use std::collections::HashMap;
6
7/// Represents a HTTP output binding.
8///
9/// The following binding attributes are supported:
10///
11/// | Name | Description |
12/// |--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
13/// | `name` | The name of the parameter being bound. |
14///
15/// Responses can be created for any type that implements `Into<Body>`.
16///
17/// # Examples
18///
19/// Creating a response from a string:
20///
21/// ```rust
22/// use azure_functions::bindings::{HttpRequest, HttpResponse};
23/// use azure_functions::func;
24///
25/// #[func]
26/// pub fn example(_req: HttpRequest) -> HttpResponse {
27/// "Hello world!".into()
28/// }
29/// ```
30///
31/// 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):
32///
33/// ```rust
34/// use azure_functions::bindings::{HttpRequest, HttpResponse};
35/// use azure_functions::func;
36/// use serde_json::json;
37///
38/// #[func]
39/// pub fn example(_req: HttpRequest) -> HttpResponse {
40/// json!({ "hello": "world" }).into()
41/// }
42/// ```
43///
44/// Creating a response from a sequence of bytes:
45///
46/// ```rust
47/// use azure_functions::bindings::{HttpRequest, HttpResponse};
48/// use azure_functions::func;
49///
50/// #[func]
51/// pub fn example(_req: HttpRequest) -> HttpResponse {
52/// [1, 2, 3][..].into()
53/// }
54/// ```
55///
56/// Building a custom response:
57///
58/// ```rust
59/// use azure_functions::bindings::{HttpRequest, HttpResponse};
60/// use azure_functions::func;
61/// use azure_functions::http::Status;
62///
63/// #[func]
64/// pub fn example(_req: HttpRequest) -> HttpResponse {
65/// HttpResponse::build()
66/// .status(Status::MovedPermanently)
67/// .header("Location", "http://example.com")
68/// .body("The requested resource has moved to: http://example.com")
69/// .finish()
70/// }
71/// ```
72#[derive(Default, Debug)]
73pub struct HttpResponse {
74 pub(crate) data: RpcHttp,
75 pub(crate) status: Status,
76}
77
78impl HttpResponse {
79 pub(crate) fn new() -> Self {
80 HttpResponse {
81 data: RpcHttp::default(),
82 status: Status::Ok,
83 }
84 }
85
86 /// Creates a new [ResponseBuilder](../http/struct.ResponseBuilder.html) for building a response.
87 ///
88 /// # Examples
89 ///
90 /// ```rust
91 /// # use azure_functions::bindings::HttpResponse;
92 /// use azure_functions::http::Status;
93 ///
94 /// let response = HttpResponse::build().status(Status::NotFound).finish();
95 /// assert_eq!(response.status(), Status::NotFound);
96 /// ```
97 pub fn build() -> ResponseBuilder {
98 ResponseBuilder::new()
99 }
100
101 /// Gets the status code for the response.
102 ///
103 /// # Examples
104 ///
105 /// ```rust
106 /// # use azure_functions::bindings::HttpResponse;
107 /// use azure_functions::http::Status;
108 ///
109 /// let response = HttpResponse::build().status(Status::BadRequest).finish();
110 /// assert_eq!(response.status(), Status::BadRequest);
111 /// ```
112 pub fn status(&self) -> Status {
113 self.status
114 }
115
116 /// Gets the body of the response.
117 ///
118 /// # Examples
119 ///
120 /// ```rust
121 /// # use azure_functions::bindings::HttpResponse;
122 /// let response = HttpResponse::build().body("example").finish();
123 ///
124 /// assert_eq!(response.body().as_str().unwrap(), "example");
125 /// ```
126 pub fn body(&self) -> Body {
127 self.data
128 .body
129 .as_ref()
130 .map(|b| Body::from(&**b))
131 .unwrap_or(Body::Empty)
132 }
133
134 /// Gets the headers of the response.
135 ///
136 /// # Examples
137 ///
138 /// ```rust
139 /// # use azure_functions::bindings::HttpResponse;
140 /// let response = HttpResponse::build().header("Content-Type", "text/plain").finish();
141 ///
142 /// assert_eq!(response.headers().get("Content-Type").unwrap(), "text/plain");
143 /// ```
144 pub fn headers(&self) -> &HashMap<String, String> {
145 &self.data.headers
146 }
147}
148
149impl<'a, T> From<T> for HttpResponse
150where
151 T: Into<Body<'a>>,
152{
153 fn from(data: T) -> Self {
154 HttpResponse::build().body(data).finish()
155 }
156}
157
158#[doc(hidden)]
159impl Into<TypedData> for HttpResponse {
160 fn into(mut self) -> TypedData {
161 self.data.status_code = self.status.to_string();
162
163 TypedData {
164 data: Some(Data::Http(Box::new(self.data))),
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use matches::matches;
173 use serde::{Deserialize, Serialize};
174
175 #[test]
176 fn it_is_empty_by_default() {
177 let response = HttpResponse::new();
178
179 assert_eq!(response.status(), Status::Ok);
180 assert!(matches!(response.body(), Body::Empty));
181 }
182
183 #[test]
184 fn it_is_empty_from_a_builder() {
185 let response: HttpResponse = HttpResponse::build().finish();
186
187 assert_eq!(response.status(), Status::Ok);
188 assert!(matches!(response.body(), Body::Empty));
189 }
190
191 #[test]
192 fn it_builds_with_a_status() {
193 let response: HttpResponse = HttpResponse::build().status(Status::Continue).finish();
194
195 assert_eq!(response.status(), Status::Continue);
196 }
197
198 #[test]
199 fn it_builds_with_a_string_body() {
200 const BODY: &'static str = "test body";
201
202 let response: HttpResponse = HttpResponse::build().body(BODY).finish();
203
204 assert_eq!(
205 response.headers().get("Content-Type").unwrap(),
206 "text/plain"
207 );
208 assert_eq!(response.body().as_str().unwrap(), BODY);
209 }
210
211 #[test]
212 fn it_builds_with_a_json_body() {
213 #[derive(Serialize, Deserialize)]
214 struct Data {
215 message: String,
216 };
217
218 const MESSAGE: &'static str = "test";
219
220 let data = Data {
221 message: MESSAGE.to_string(),
222 };
223
224 let response = HttpResponse::build()
225 .body(::serde_json::to_value(data).unwrap())
226 .finish();
227
228 assert_eq!(
229 response.headers().get("Content-Type").unwrap(),
230 "application/json"
231 );
232 assert_eq!(response.body().as_json::<Data>().unwrap().message, MESSAGE);
233 }
234
235 #[test]
236 fn it_builds_with_a_bytes_body() {
237 const BODY: &'static [u8] = &[1, 2, 3];
238
239 let response: HttpResponse = HttpResponse::build().body(BODY).finish();
240
241 assert_eq!(
242 response.headers().get("Content-Type").unwrap(),
243 "application/octet-stream"
244 );
245 assert_eq!(response.body().as_bytes(), BODY);
246 }
247
248 #[test]
249 fn it_builds_with_headers() {
250 let response: HttpResponse = HttpResponse::build()
251 .header("header1", "value1")
252 .header("header2", "value2")
253 .header("header3", "value3")
254 .finish();
255
256 assert_eq!(response.headers().get("header1").unwrap(), "value1");
257 assert_eq!(response.headers().get("header2").unwrap(), "value2");
258 assert_eq!(response.headers().get("header3").unwrap(), "value3");
259 }
260
261 #[test]
262 fn it_converts_to_typed_data() {
263 let response: HttpResponse = HttpResponse::build()
264 .status(Status::BadRequest)
265 .header("header", "value")
266 .body("body")
267 .finish();
268
269 let data: TypedData = response.into();
270 match data.data {
271 Some(Data::Http(http)) => {
272 assert_eq!(http.status_code, "400");
273 assert_eq!(http.headers.get("header").unwrap(), "value");
274 assert_eq!(
275 http.body,
276 Some(Box::new(TypedData {
277 data: Some(Data::String("body".to_string()))
278 }))
279 );
280 }
281 _ => assert!(false),
282 }
283 }
284}