azure_functions/bindings/
blob_trigger.rs

1use crate::{
2    bindings::Blob,
3    blob::Properties,
4    rpc::{typed_data::Data, TypedData},
5    util::convert_from,
6};
7use serde_json::from_str;
8use std::collections::HashMap;
9
10const PATH_KEY: &str = "BlobTrigger";
11const URI_KEY: &str = "Uri";
12const PROPERTIES_KEY: &str = "Properties";
13const METADATA_KEY: &str = "Metadata";
14
15/// Represents an Azure Storage blob trigger binding.
16///
17/// The following binding attributes are supported:
18///
19/// | Name         | Description                                                                                                                        |
20/// |--------------|------------------------------------------------------------------------------------------------------------------------------------|
21/// | `name`       | The name of the parameter being bound.                                                                                             |
22/// | `path`       | The container to monitor. May be a blob name pattern.                                                                              |
23/// | `connection` | The name of an app setting that contains the Storage connection string to use for this binding. Defaults to `AzureWebJobsStorage`. |
24///
25/// # Examples
26///
27/// A function that runs when a blob is created in the `example` container:
28///
29/// ```rust
30/// use azure_functions::bindings::BlobTrigger;
31/// use azure_functions::func;
32/// use log::info;
33///
34/// #[func]
35/// #[binding(name = "trigger", path = "example/")]
36/// pub fn print_blob(trigger: BlobTrigger) {
37///     info!("Blob (as string): {}", trigger.blob.as_str().unwrap());
38/// }
39/// ```
40#[derive(Debug)]
41pub struct BlobTrigger {
42    /// The blob that triggered the function.
43    pub blob: Blob,
44    /// The path of the blob.
45    pub path: String,
46    /// The URI of the blob.
47    pub uri: String,
48    /// The properties of the blob.
49    pub properties: Properties,
50    /// The metadata of the blob.
51    pub metadata: HashMap<String, String>,
52}
53
54impl BlobTrigger {
55    #[doc(hidden)]
56    pub fn new(data: TypedData, mut metadata: HashMap<String, TypedData>) -> Self {
57        BlobTrigger {
58            blob: data.into(),
59            path: metadata
60                .remove(PATH_KEY)
61                .map(|data| match data.data {
62                    Some(Data::String(s)) => s,
63                    _ => panic!("expected a string for 'path' metadata key"),
64                })
65                .expect("expected a blob path"),
66            uri: metadata.get(URI_KEY).map_or(String::new(), |data| {
67                convert_from(data).unwrap_or_else(|| panic!("failed to convert uri"))
68            }),
69            properties: metadata
70                .remove(PROPERTIES_KEY)
71                .map_or(Properties::default(), |data| match data.data {
72                    Some(Data::Json(s)) => {
73                        from_str(&s).expect("failed to deserialize blob properties")
74                    }
75                    _ => panic!("expected a string for properties"),
76                }),
77            metadata: metadata
78                .remove(METADATA_KEY)
79                .map_or(HashMap::new(), |data| match data.data {
80                    Some(Data::Json(s)) => {
81                        from_str(&s).expect("failed to deserialize blob metadata")
82                    }
83                    _ => panic!("expected a string for metadata"),
84                }),
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::blob::*;
93    use chrono::Utc;
94    use matches::matches;
95    use serde_json::{json, to_string};
96
97    #[test]
98    fn it_constructs() {
99        const BLOB: &'static str = "blob";
100        const PATH: &'static str = "foo/bar";
101        const URI: &'static str = "https://example.com/blob";
102        const CACHE_CONTROL: &'static str = "cache-control";
103        const CONTENT_DISPOSITION: &'static str = "content-disposition";
104        const CONTENT_ENCODING: &'static str = "content-encoding";
105        const CONTENT_LANGUAGE: &'static str = "content-language";
106        const CONTENT_LENGTH: u32 = 1234;
107        const CONTENT_MD5: &'static str = "abcdef";
108        const CONTENT_TYPE: &'static str = "text/plain";
109        const ETAG: &'static str = "12345";
110        const IS_SERVER_ENCRYPTED: bool = true;
111        const IS_INCREMENTAL_COPY: bool = false;
112        const BLOB_TIER_INFERRED: bool = false;
113        const USER_METADAT_KEY: &'static str = "key";
114        const USER_METADATA_VALUE: &'static str = "value";
115
116        let now = Utc::now();
117
118        let properties = json!({
119            "CacheControl": CACHE_CONTROL,
120            "ContentDisposition": CONTENT_DISPOSITION,
121            "ContentEncoding": CONTENT_ENCODING,
122            "ContentLanguage": CONTENT_LANGUAGE,
123            "Length": CONTENT_LENGTH,
124            "ContentMD5": CONTENT_MD5,
125            "ContentType": CONTENT_TYPE,
126            "ETag": ETAG,
127            "LastModified": now.to_rfc3339(),
128            "BlobType": 2,
129            "LeaseStatus": 2,
130            "LeaseState": 1,
131            "LeaseDuration": 0,
132            "PageBlobSequenceNumber": null,
133            "AppendBlobCommittedBlockCount": null,
134            "IsServerEncrypted": IS_SERVER_ENCRYPTED,
135            "IsIncrementalCopy": IS_INCREMENTAL_COPY,
136            "StandardBlobTier": 0,
137            "RehydrationStatus": null,
138            "PremiumPageBlobTier": null,
139            "BlobTierInferred": BLOB_TIER_INFERRED,
140            "BlobTierLastModifiedTime": null
141        });
142
143        let data = TypedData {
144            data: Some(Data::String(BLOB.to_string())),
145        };
146
147        let mut user_metadata = HashMap::new();
148        user_metadata.insert(
149            USER_METADAT_KEY.to_string(),
150            USER_METADATA_VALUE.to_string(),
151        );
152
153        let mut metadata = HashMap::new();
154        metadata.insert(
155            PATH_KEY.to_string(),
156            TypedData {
157                data: Some(Data::String(PATH.to_string())),
158            },
159        );
160        metadata.insert(
161            URI_KEY.to_string(),
162            TypedData {
163                data: Some(Data::Json("\"".to_string() + URI + "\"")),
164            },
165        );
166        metadata.insert(
167            PROPERTIES_KEY.to_string(),
168            TypedData {
169                data: Some(Data::Json(properties.to_string())),
170            },
171        );
172        metadata.insert(
173            METADATA_KEY.to_string(),
174            TypedData {
175                data: Some(Data::Json(to_string(&user_metadata).unwrap())),
176            },
177        );
178
179        let trigger = BlobTrigger::new(data, metadata);
180        assert_eq!(trigger.path, PATH);
181        assert_eq!(trigger.uri, URI);
182
183        assert!(trigger
184            .properties
185            .append_blob_committed_block_count
186            .is_none());
187        assert_eq!(
188            *trigger.properties.blob_tier_inferred.as_ref().unwrap(),
189            BLOB_TIER_INFERRED
190        );
191        assert!(trigger.properties.blob_tier_last_modified_time.is_none());
192        assert!(matches!(trigger.properties.blob_type, BlobType::BlockBlob));
193        assert_eq!(
194            trigger.properties.cache_control.as_ref().unwrap(),
195            CACHE_CONTROL
196        );
197        assert_eq!(
198            trigger.properties.content_disposition.as_ref().unwrap(),
199            CONTENT_DISPOSITION
200        );
201        assert_eq!(
202            trigger.properties.content_encoding.as_ref().unwrap(),
203            CONTENT_ENCODING
204        );
205        assert_eq!(
206            trigger.properties.content_language.as_ref().unwrap(),
207            CONTENT_LANGUAGE
208        );
209        assert_eq!(
210            trigger.properties.content_md5.as_ref().unwrap(),
211            CONTENT_MD5
212        );
213        assert_eq!(
214            trigger.properties.content_type.as_ref().unwrap(),
215            CONTENT_TYPE
216        );
217        assert!(trigger.properties.created.is_none());
218        assert!(trigger.properties.deleted_time.is_none());
219        assert_eq!(trigger.properties.etag.as_ref().unwrap(), ETAG);
220        assert_eq!(trigger.properties.is_incremental_copy, IS_INCREMENTAL_COPY);
221        assert_eq!(trigger.properties.is_server_encrypted, IS_SERVER_ENCRYPTED);
222        assert_eq!(
223            trigger
224                .properties
225                .last_modified
226                .as_ref()
227                .unwrap()
228                .to_rfc3339(),
229            now.to_rfc3339()
230        );
231        assert!(matches!(
232            trigger.properties.lease_duration,
233            LeaseDuration::Unspecified
234        ));
235        assert!(matches!(
236            trigger.properties.lease_state,
237            LeaseState::Available
238        ));
239        assert!(matches!(
240            trigger.properties.lease_status,
241            LeaseStatus::Unlocked
242        ));
243        assert_eq!(trigger.properties.length, CONTENT_LENGTH as i64);
244        assert!(trigger.properties.page_blob_sequence_number.is_none());
245        assert!(trigger.properties.premium_page_blob_tier.is_none());
246        assert!(trigger.properties.rehydration_status.is_none());
247        assert!(trigger
248            .properties
249            .remaining_days_before_permanent_delete
250            .is_none());
251        assert!(matches!(
252            trigger.properties.standard_blob_tier.as_ref().unwrap(),
253            StandardBlobTier::Unknown
254        ));
255
256        assert_eq!(trigger.metadata.len(), 1);
257        assert_eq!(
258            trigger.metadata.get(USER_METADAT_KEY).unwrap(),
259            USER_METADATA_VALUE
260        );
261    }
262}