azure_functions/bindings/
blob.rs

1use crate::{
2    http::Body,
3    rpc::{typed_data::Data, TypedData},
4};
5use serde::de::Error;
6use serde::Deserialize;
7use serde_json::{from_str, Result, Value};
8use std::borrow::Cow;
9use std::fmt;
10use std::str::from_utf8;
11
12/// Represents an Azure Storage blob input or output binding.
13///
14/// The following binding attributes are supported:
15///
16/// | Name         | Description                                                                                                                        |
17/// |--------------|------------------------------------------------------------------------------------------------------------------------------------|
18/// | `name`       | The name of the parameter being bound.                                                                                             |
19/// | `path`       | The path to the blob.                                                                                                              |
20/// | `connection` | The name of an app setting that contains the Storage connection string to use for this binding. Defaults to `AzureWebJobsStorage`. |
21///
22/// # Examples
23///
24/// Creating a blob from a string:
25///
26/// ```rust
27/// use azure_functions::bindings::{HttpRequest, Blob};
28/// use azure_functions::func;
29///
30/// #[func]
31/// #[binding(name = "output1", path = "example")]
32/// pub fn create_blob(_req: HttpRequest) -> ((), Blob) {
33///     ((), "Hello world!".into())
34/// }
35/// ```
36///
37/// Creating a blob from a JSON value (see the [json! macro](https://docs.serde.rs/serde_json/macro.json.html) from the `serde_json` crate):
38///
39/// ```rust
40/// use azure_functions::bindings::{HttpRequest, Blob};
41/// use azure_functions::func;
42/// use serde_json::json;
43///
44/// #[func]
45/// #[binding(name = "output1", path = "example")]
46/// pub fn create_blob(_req: HttpRequest) -> ((), Blob) {
47///     ((), json!({ "hello": "world!" }).into())
48/// }
49/// ```
50///
51/// Creating a blob from a sequence of bytes:
52///
53/// ```rust
54/// use azure_functions::bindings::{HttpRequest, Blob};
55/// use azure_functions::func;
56///
57/// #[func]
58/// #[binding(name = "output1", path = "example")]
59/// pub fn create_blob(_req: HttpRequest) -> ((), Blob) {
60///     ((), [1, 2, 3][..].into())
61/// }
62/// ```
63#[derive(Debug, Clone)]
64pub struct Blob(TypedData);
65
66impl Blob {
67    /// Gets the content of the blob as a string.
68    ///
69    /// Returns None if there is no valid string representation of the blob.
70    pub fn as_str(&self) -> Option<&str> {
71        match &self.0.data {
72            Some(Data::String(s)) => Some(s),
73            Some(Data::Json(s)) => Some(s),
74            Some(Data::Bytes(b)) => from_utf8(b).ok(),
75            Some(Data::Stream(s)) => from_utf8(s).ok(),
76            _ => None,
77        }
78    }
79
80    /// Gets the content of the blob as a slice of bytes.
81    pub fn as_bytes(&self) -> &[u8] {
82        match &self.0.data {
83            Some(Data::String(s)) => s.as_bytes(),
84            Some(Data::Json(s)) => s.as_bytes(),
85            Some(Data::Bytes(b)) => b,
86            Some(Data::Stream(s)) => s,
87            _ => panic!("unexpected data for blob content"),
88        }
89    }
90
91    /// Deserializes the blob as JSON to the requested type.
92    pub fn as_json<'b, T>(&'b self) -> Result<T>
93    where
94        T: Deserialize<'b>,
95    {
96        from_str(
97            self.as_str()
98                .ok_or_else(|| ::serde_json::Error::custom("blob is not valid UTF-8"))?,
99        )
100    }
101}
102
103impl fmt::Display for Blob {
104    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105        write!(f, "{}", self.as_str().unwrap_or(""))
106    }
107}
108
109impl<'a> From<&'a str> for Blob {
110    fn from(content: &'a str) -> Self {
111        Blob(TypedData {
112            data: Some(Data::String(content.to_owned())),
113        })
114    }
115}
116
117impl From<String> for Blob {
118    fn from(content: String) -> Self {
119        Blob(TypedData {
120            data: Some(Data::String(content)),
121        })
122    }
123}
124
125impl From<&Value> for Blob {
126    fn from(content: &Value) -> Self {
127        Blob(TypedData {
128            data: Some(Data::Json(content.to_string())),
129        })
130    }
131}
132
133impl From<Value> for Blob {
134    fn from(content: Value) -> Self {
135        Blob(TypedData {
136            data: Some(Data::Json(content.to_string())),
137        })
138    }
139}
140
141impl<'a> From<&'a [u8]> for Blob {
142    fn from(content: &'a [u8]) -> Self {
143        Blob(TypedData {
144            data: Some(Data::Bytes(content.to_owned())),
145        })
146    }
147}
148
149impl From<Vec<u8>> for Blob {
150    fn from(content: Vec<u8>) -> Self {
151        Blob(TypedData {
152            data: Some(Data::Bytes(content)),
153        })
154    }
155}
156
157#[doc(hidden)]
158impl From<TypedData> for Blob {
159    fn from(data: TypedData) -> Self {
160        Blob(data)
161    }
162}
163
164impl Into<String> for Blob {
165    fn into(self) -> String {
166        match self.0.data {
167            Some(Data::String(s)) => s,
168            Some(Data::Json(s)) => s,
169            Some(Data::Bytes(b)) => {
170                String::from_utf8(b).expect("blob does not contain valid UTF-8 bytes")
171            }
172            Some(Data::Stream(s)) => {
173                String::from_utf8(s).expect("blob does not contain valid UTF-8 bytes")
174            }
175            _ => panic!("unexpected data for blob content"),
176        }
177    }
178}
179
180impl Into<Value> for Blob {
181    fn into(self) -> Value {
182        from_str(
183            self.as_str()
184                .expect("blob does not contain valid UTF-8 data"),
185        )
186        .expect("blob does not contain valid JSON data")
187    }
188}
189
190impl Into<Vec<u8>> for Blob {
191    fn into(self) -> Vec<u8> {
192        match self.0.data {
193            Some(Data::String(s)) => s.into_bytes(),
194            Some(Data::Json(s)) => s.into_bytes(),
195            Some(Data::Bytes(b)) => b,
196            Some(Data::Stream(s)) => s,
197            _ => panic!("unexpected data for blob content"),
198        }
199    }
200}
201
202impl<'a> Into<Body<'a>> for Blob {
203    fn into(self) -> Body<'a> {
204        match self.0.data {
205            Some(Data::String(s)) => s.into(),
206            Some(Data::Json(s)) => Body::Json(Cow::from(s)),
207            Some(Data::Bytes(b)) => b.into(),
208            Some(Data::Stream(s)) => s.into(),
209            _ => panic!("unexpected data for blob content"),
210        }
211    }
212}
213
214#[doc(hidden)]
215impl Into<TypedData> for Blob {
216    fn into(self) -> TypedData {
217        self.0
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use serde::{Deserialize, Serialize};
225    use serde_json::json;
226    use serde_json::to_value;
227    use std::fmt::Write;
228
229    #[test]
230    fn it_has_string_content() {
231        const BLOB: &'static str = "test blob";
232
233        let blob: Blob = BLOB.into();
234        assert_eq!(blob.as_str().unwrap(), BLOB);
235
236        let data: TypedData = blob.into();
237        assert_eq!(data.data, Some(Data::String(BLOB.to_string())));
238    }
239
240    #[test]
241    fn it_has_json_content() {
242        #[derive(Serialize, Deserialize)]
243        struct SerializedData {
244            message: String,
245        };
246
247        const MESSAGE: &'static str = "test";
248
249        let data = SerializedData {
250            message: MESSAGE.to_string(),
251        };
252
253        let blob: Blob = ::serde_json::to_value(data).unwrap().into();
254        assert_eq!(blob.as_json::<SerializedData>().unwrap().message, MESSAGE);
255
256        let data: TypedData = blob.into();
257        assert_eq!(
258            data.data,
259            Some(Data::Json(r#"{"message":"test"}"#.to_string()))
260        );
261    }
262
263    #[test]
264    fn it_has_bytes_content() {
265        const BLOB: &'static [u8] = &[1, 2, 3];
266
267        let blob: Blob = BLOB.into();
268        assert_eq!(blob.as_bytes(), BLOB);
269
270        let data: TypedData = blob.into();
271        assert_eq!(data.data, Some(Data::Bytes(BLOB.to_owned())));
272    }
273
274    #[test]
275    fn it_displays_as_a_string() {
276        const BLOB: &'static str = "test";
277
278        let blob: Blob = BLOB.into();
279
280        let mut s = String::new();
281        write!(s, "{}", blob).unwrap();
282
283        assert_eq!(s, BLOB);
284    }
285
286    #[test]
287    fn it_converts_from_str() {
288        let blob: Blob = "test".into();
289        assert_eq!(blob.as_str().unwrap(), "test");
290    }
291
292    #[test]
293    fn it_converts_from_string() {
294        let blob: Blob = "test".to_string().into();
295        assert_eq!(blob.as_str().unwrap(), "test");
296    }
297
298    #[test]
299    fn it_converts_from_json() {
300        let blob: Blob = to_value("hello world").unwrap().into();
301        assert_eq!(blob.as_str().unwrap(), r#""hello world""#);
302    }
303
304    #[test]
305    fn it_converts_from_u8_slice() {
306        let blob: Blob = [0, 1, 2][..].into();
307        assert_eq!(blob.as_bytes(), [0, 1, 2]);
308    }
309
310    #[test]
311    fn it_converts_from_u8_vec() {
312        let blob: Blob = vec![0, 1, 2].into();
313        assert_eq!(blob.as_bytes(), [0, 1, 2]);
314    }
315
316    #[test]
317    fn it_converts_from_typed_data() {
318        const BLOB: &'static str = "hello world!";
319
320        let data = TypedData {
321            data: Some(Data::String(BLOB.to_string())),
322        };
323
324        let blob: Blob = data.into();
325        assert_eq!(blob.as_str().unwrap(), BLOB);
326    }
327
328    #[test]
329    fn it_converts_to_string() {
330        let blob: Blob = "hello world!".into();
331        let s: String = blob.into();
332        assert_eq!(s, "hello world!");
333    }
334
335    #[test]
336    fn it_converts_to_json() {
337        let blob: Blob = json!({"hello": "world"}).into();
338        let value: Value = blob.into();
339        assert_eq!(value.to_string(), r#"{"hello":"world"}"#);
340    }
341
342    #[test]
343    fn it_converts_to_bytes() {
344        let blob: Blob = vec![1, 2, 3].into();
345        let bytes: Vec<u8> = blob.into();
346        assert_eq!(bytes, [1, 2, 3]);
347    }
348
349    #[test]
350    fn it_converts_to_body() {
351        let blob: Blob = "hello world!".into();
352        let body: Body = blob.into();
353        assert_eq!(body.as_str().unwrap(), "hello world!");
354
355        let blob: Blob = json!({"hello": "world"}).into();
356        let body: Body = blob.into();
357        assert_eq!(body.as_str().unwrap(), r#"{"hello":"world"}"#);
358
359        let blob: Blob = vec![1, 2, 3].into();
360        let body: Body = blob.into();
361        assert_eq!(body.as_bytes(), [1, 2, 3]);
362    }
363
364    #[test]
365    fn it_converts_to_typed_data() {
366        let blob: Blob = "test".into();
367        let data: TypedData = blob.into();
368        assert_eq!(data.data, Some(Data::String("test".to_string())));
369
370        let blob: Blob = to_value("test").unwrap().into();
371        let data: TypedData = blob.into();
372        assert_eq!(data.data, Some(Data::Json(r#""test""#.to_string())));
373
374        let blob: Blob = vec![1, 2, 3].into();
375        let data: TypedData = blob.into();
376        assert_eq!(data.data, Some(Data::Bytes(vec![1, 2, 3])));
377    }
378}