azure_functions/bindings/
cosmos_db_document.rs

1use crate::{
2    http::Body,
3    rpc::{typed_data::Data, TypedData},
4    util::convert_from,
5    FromVec, IntoVec,
6};
7use serde_json::{from_str, Map, Value};
8use std::borrow::Cow;
9use std::fmt;
10
11/// Represents the input or output binding for a Cosmos DB document.
12///
13/// The following binding attributes are supported:
14///
15/// | Name                    | Description                                                                                                                                                                                               |
16/// |-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
17/// | `name`                  | The name of the parameter being bound.                                                                                                                                                                    |
18/// | `database_name`         | The database containing the document.                                                                                                                                                                     |
19/// | `collection_name`       | The name of the collection that contains the document.                                                                                                                                                    |
20/// | `id`                    | The identifier of the document to retrieve. This attribute supports binding expressions. Cannot be used with `sql_query`. If neither are specified, the entire collection is retrieved.                   |
21/// | `sql_query`             | An Azure Cosmos DB SQL query used for retrieving multiple documents. Cannot be used with `id`. If neither are specified, the entire collection is retrieved.                                              |
22/// | `connection`            | The name of the app setting containing your Azure Cosmos DB connection string.                                                                                                                            |
23/// | `partition_key`         | Specifies the partition key value for the lookup; may include binding parameters (input only). When `create_collection` is true, defines the partition key path for the created collection (output only). |
24/// | `create_collection`     | Specifies if the collection should be created (output only).                                                                                                                                              |
25/// | `collection_throughput` | When `create_collection` is true, defines the throughput of the created collection (output only).                                                                                                         |
26///
27/// # Examples
28///
29/// Using `CosmosDbDocument` as an input binding with a SQL query:
30///
31/// ```rust
32/// use azure_functions::{
33///     bindings::{CosmosDbDocument, HttpRequest, HttpResponse},
34///     func,
35/// };
36///
37/// #[func]
38/// #[binding(
39///     name = "documents",
40///     connection = "myconnection",
41///     database_name = "mydb",
42///     collection_name = "mycollection",
43///     sql_query = "select * from mycollection c where startswith(c.name, 'peter')",
44/// )]
45/// pub fn read_documents(_req: HttpRequest, documents: Vec<CosmosDbDocument>) -> HttpResponse {
46///     documents.into()
47/// }
48/// ```
49///
50/// Using `CosmosDbDocument` as an input binding for a specific document:
51///
52/// ```rust
53/// use azure_functions::{
54///     bindings::{CosmosDbDocument, HttpRequest, HttpResponse},
55///     func,
56/// };
57///
58/// #[func]
59/// #[binding(name = "_req", route = "read/{id}")]
60/// #[binding(
61///     name = "document",
62///     connection = "myconnection",
63///     database_name = "mydb",
64///     collection_name = "mycollection",
65///     id = "{id}",
66/// )]
67/// pub fn read_document(_req: HttpRequest, document: CosmosDbDocument) -> HttpResponse {
68///     document.into()
69/// }
70/// ```
71///
72/// Using `CosmosDbDocument` as an output binding:
73///
74/// ```rust
75/// # use serde_json::json;
76/// use azure_functions::{
77///     bindings::{CosmosDbDocument, HttpRequest, HttpResponse},
78///     func,
79/// };
80///
81/// #[func]
82/// #[binding(
83///     name = "output1",
84///     connection = "myconnection",
85///     database_name = "mydb",
86///     collection_name = "mycollection"
87/// )]
88/// pub fn create_document(_req: HttpRequest) -> (HttpResponse, CosmosDbDocument) {
89///     (
90///         "Document created.".into(),
91///         json!({
92///             "id": "myid",
93///             "name": "Peter",
94///             "subject": "example"
95///         }).into()
96///     )
97/// }
98/// ```
99#[derive(Debug, Clone)]
100pub struct CosmosDbDocument(Value);
101
102impl CosmosDbDocument {
103    /// Creates a new `CosmosDbDocument` from a JSON object value.
104    ///
105    /// The value must be a JSON object.
106    pub fn new(value: Value) -> CosmosDbDocument {
107        if !value.is_object() {
108            panic!("expected a single object for a Cosmos DB document");
109        }
110        CosmosDbDocument(value)
111    }
112
113    /// Gets whether or not the Cosmos DB document is null.
114    ///
115    /// A Cosmos DB document can be null as a result of a query that returned no documents.
116    pub fn is_null(&self) -> bool {
117        self.0.is_null()
118    }
119
120    /// Gets the JSON object for the Cosmos DB document
121    ///
122    /// Returns None if the document is null.
123    pub fn as_object(&self) -> Option<&Map<String, Value>> {
124        self.0.as_object()
125    }
126
127    /// Gets a mutable JSON object for the Cosmos DB document
128    ///
129    /// Returns None if the document is null.
130    pub fn as_object_mut(&mut self) -> Option<&mut Map<String, Value>> {
131        self.0.as_object_mut()
132    }
133}
134
135impl fmt::Display for CosmosDbDocument {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        self.0.fmt(f)
138    }
139}
140
141impl<'a> From<&'a str> for CosmosDbDocument {
142    fn from(json: &'a str) -> Self {
143        CosmosDbDocument::new(from_str(json).unwrap())
144    }
145}
146
147impl From<String> for CosmosDbDocument {
148    fn from(json: String) -> Self {
149        CosmosDbDocument::new(from_str(&json).unwrap())
150    }
151}
152
153impl From<Value> for CosmosDbDocument {
154    fn from(value: Value) -> Self {
155        CosmosDbDocument::new(value)
156    }
157}
158
159#[doc(hidden)]
160impl IntoVec<CosmosDbDocument> for TypedData {
161    fn into_vec(self) -> Vec<CosmosDbDocument> {
162        if self.data.is_none() {
163            return vec![];
164        }
165
166        match convert_from(&self).expect("expected JSON data for Cosmos DB document") {
167            Value::Null => vec![],
168            Value::Array(arr) => arr.into_iter().map(CosmosDbDocument::new).collect(),
169            Value::Object(obj) => vec![CosmosDbDocument(Value::Object(obj))],
170            _ => panic!("expected array or object for Cosmos DB document data"),
171        }
172    }
173}
174
175#[doc(hidden)]
176impl FromVec<CosmosDbDocument> for TypedData {
177    fn from_vec(vec: Vec<CosmosDbDocument>) -> Self {
178        TypedData {
179            data: Some(Data::Json(
180                Value::Array(vec.into_iter().map(|d| d.0).collect()).to_string(),
181            )),
182        }
183    }
184}
185
186#[doc(hidden)]
187impl From<TypedData> for CosmosDbDocument {
188    fn from(data: TypedData) -> Self {
189        if data.data.is_none() {
190            return CosmosDbDocument(Value::Null);
191        }
192
193        let value: Value = convert_from(&data).expect("expected JSON data for Cosmos DB document");
194
195        match value {
196            Value::Null => CosmosDbDocument(Value::Null),
197            Value::Array(mut arr) => {
198                if arr.is_empty() {
199                    CosmosDbDocument(Value::Null)
200                } else {
201                    CosmosDbDocument::new(arr.swap_remove(0))
202                }
203            }
204            Value::Object(obj) => CosmosDbDocument(Value::Object(obj)),
205            _ => panic!("expected an array or object for Cosmos DB document data"),
206        }
207    }
208}
209
210impl Into<String> for CosmosDbDocument {
211    fn into(self) -> String {
212        self.0.to_string()
213    }
214}
215
216impl Into<Value> for CosmosDbDocument {
217    fn into(self) -> Value {
218        self.0
219    }
220}
221
222impl<'a> Into<Body<'a>> for CosmosDbDocument {
223    fn into(self) -> Body<'a> {
224        self.0.into()
225    }
226}
227
228impl<'a> Into<Body<'a>> for Vec<CosmosDbDocument> {
229    fn into(self) -> Body<'a> {
230        Body::Json(Cow::from(
231            Value::Array(self.into_iter().map(|d| d.0).collect()).to_string(),
232        ))
233    }
234}
235
236#[doc(hidden)]
237impl Into<TypedData> for CosmosDbDocument {
238    fn into(self) -> TypedData {
239        TypedData {
240            data: Some(Data::Json(self.0.to_string())),
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use serde_json::json;
249
250    #[test]
251    fn it_constructs_from_an_object_value() {
252        let document = CosmosDbDocument::new(json!({ "id": "foo", "key": "value"}));
253        let data = document.as_object().unwrap();
254        assert_eq!(data["id"].as_str().unwrap(), "foo");
255        assert_eq!(data["key"].as_str().unwrap(), "value");
256    }
257
258    #[test]
259    #[should_panic(expected = "expected a single object for a Cosmos DB document")]
260    fn it_panics_if_constructed_without_an_object_or_array() {
261        CosmosDbDocument::new(json!(5));
262    }
263
264    #[test]
265    fn it_displays_as_json() {
266        let document = CosmosDbDocument::new(json!({ "foo": "bar"}));
267        assert_eq!(format!("{}", document), r#"{"foo":"bar"}"#);
268    }
269
270    #[test]
271    fn it_converts_from_str() {
272        let document: CosmosDbDocument = r#"{ "foo": "bar" }"#.into();
273        let data = document.as_object().unwrap();
274        assert_eq!(data["foo"].as_str().unwrap(), "bar");
275    }
276
277    #[test]
278    fn it_converts_from_string() {
279        let document: CosmosDbDocument = r#"{ "foo": "bar" }"#.to_string().into();
280        let data = document.as_object().unwrap();
281        assert_eq!(data["foo"].as_str().unwrap(), "bar");
282    }
283
284    #[test]
285    fn it_converts_from_value() {
286        let document: CosmosDbDocument = json!({ "foo": "bar" }).into();
287        let data = document.as_object().unwrap();
288        assert_eq!(data["foo"].as_str().unwrap(), "bar");
289    }
290
291    #[test]
292    fn it_converts_to_string() {
293        let document: CosmosDbDocument = json!({ "foo": "bar" }).into();
294        let string: String = document.into();
295        assert_eq!(string, r#"{"foo":"bar"}"#);
296    }
297
298    #[test]
299    fn it_converts_to_value() {
300        let document: CosmosDbDocument = json!({ "foo": "bar" }).into();
301        let data = document.as_object().unwrap();
302        assert_eq!(data["foo"].as_str().unwrap(), "bar");
303
304        let value: Value = document.into();
305        assert!(value.is_object());
306        assert_eq!(value.as_object().unwrap()["foo"].as_str().unwrap(), "bar");
307    }
308
309    #[test]
310    fn it_converts_to_body() {
311        let document: CosmosDbDocument = r#"{ "foo": "bar" }"#.into();
312        let body: Body = document.into();
313        assert_eq!(body.as_str().unwrap(), r#"{"foo":"bar"}"#);
314
315        let document: CosmosDbDocument = json!({"hello": "world"}).into();
316        let body: Body = document.into();
317        assert_eq!(body.as_str().unwrap(), r#"{"hello":"world"}"#);
318    }
319
320    #[test]
321    fn it_converts_from_typed_data() {
322        let document: CosmosDbDocument = TypedData {
323            data: Some(Data::Json(r#"{ "foo": "bar" }"#.to_string())),
324        }
325        .into();
326
327        let data = document.as_object().unwrap();
328        assert_eq!(data["foo"].as_str().unwrap(), "bar");
329    }
330
331    #[test]
332    fn it_converts_to_typed_data() {
333        let document: CosmosDbDocument = json!({ "foo": "bar" }).into();
334        let data = document.as_object().unwrap();
335        assert_eq!(data["foo"].as_str().unwrap(), "bar");
336
337        let data: TypedData = document.into();
338        assert_eq!(data.data, Some(Data::Json(r#"{"foo":"bar"}"#.to_string())));
339    }
340}