firestore_db_and_auth/
dto.rs

1//! # Data Transfer Object definitions
2//! In this module only those Data Transfer Objects (DTO) are defined, which are used by the firebase API
3//! to access, alter documents or firebase users.
4//!
5//! Domain specific DTOs for OAuth or session management are defined in [`crate::session`].
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Default, Clone, Debug, Serialize, Deserialize)]
12pub struct GoogleFirestoreAdminv1IndexField {
13    #[serde(rename = "fieldPath")]
14    pub field_path: Option<String>,
15    pub mode: Option<String>,
16}
17
18#[derive(Default, Clone, Debug, Serialize, Deserialize)]
19pub struct ListenResponse {
20    pub filter: Option<ExistenceFilter>,
21    #[serde(rename = "targetChange")]
22    pub target_change: Option<TargetChange>,
23    #[serde(rename = "documentDelete")]
24    pub document_delete: Option<DocumentDelete>,
25    #[serde(rename = "documentChange")]
26    pub document_change: Option<DocumentChange>,
27    #[serde(rename = "documentRemove")]
28    pub document_remove: Option<DocumentRemove>,
29}
30
31#[derive(Default, Clone, Debug, Serialize, Deserialize)]
32pub struct BeginTransactionResponse {
33    pub transaction: Option<String>,
34}
35
36#[derive(Default, Clone, Debug, Serialize, Deserialize)]
37pub struct Write {
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub delete: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    #[serde(rename = "currentDocument")]
42    pub current_document: Option<Precondition>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub update: Option<Document>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub transform: Option<DocumentTransform>,
47    #[serde(rename = "updateMask")]
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub update_mask: Option<DocumentMask>,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53#[allow(non_camel_case_types)]
54pub enum FieldOperator {
55    OPERATOR_UNSPECIFIED,  //	Unspecified. This value must not be used.
56    LESS_THAN,             //	Less than. Requires that the field come first in orderBy.
57    LESS_THAN_OR_EQUAL,    //	Less than or equal. Requires that the field come first in orderBy.
58    GREATER_THAN,          //	Greater than. Requires that the field come first in orderBy.
59    GREATER_THAN_OR_EQUAL, //	Greater than or equal. Requires that the field come first in orderBy.
60    EQUAL,                 //	Equal.
61    ARRAY_CONTAINS,        //	Contains. Requires that the field is an array.
62}
63
64impl Default for FieldOperator {
65    fn default() -> Self {
66        FieldOperator::OPERATOR_UNSPECIFIED
67    }
68}
69
70#[derive(Default, Clone, Debug, Serialize, Deserialize)]
71pub struct FieldFilter {
72    pub field: FieldReference,
73    pub value: Value,
74    pub op: FieldOperator,
75}
76
77#[derive(Default, Clone, Debug, Serialize, Deserialize)]
78pub struct GoogleFirestoreAdminv1ImportDocumentsRequest {
79    #[serde(rename = "inputUriPrefix")]
80    pub input_uri_prefix: Option<String>,
81    #[serde(rename = "collectionIds")]
82    pub collection_ids: Option<Vec<String>>,
83}
84
85#[derive(Default, Clone, Debug, Serialize, Deserialize)]
86pub struct Document {
87    pub fields: Option<HashMap<String, Value>>,
88    #[serde(rename = "updateTime")]
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub update_time: Option<String>,
91    #[serde(rename = "createTime")]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub create_time: Option<String>,
94    pub name: String,
95}
96
97#[derive(Default, Clone, Debug, Serialize, Deserialize)]
98pub struct GoogleFirestoreAdminv1ListIndexesResponse {
99    #[serde(rename = "nextPageToken")]
100    pub next_page_token: Option<String>,
101    pub indexes: Option<Vec<GoogleFirestoreAdminv1Index>>,
102}
103
104#[derive(Default, Clone, Debug, Serialize, Deserialize)]
105pub struct BatchGetDocumentsResponse {
106    pub found: Option<Document>,
107    pub transaction: Option<String>,
108    #[serde(rename = "readTime")]
109    pub read_time: Option<String>,
110    pub missing: Option<String>,
111}
112
113#[derive(Default, Clone, Debug, Serialize, Deserialize)]
114pub struct Status {
115    pub message: Option<String>,
116    pub code: Option<i32>,
117    pub details: Option<Vec<HashMap<String, String>>>,
118}
119
120#[derive(Default, Clone, Debug, Serialize, Deserialize)]
121pub struct ListenRequest {
122    pub labels: Option<HashMap<String, String>>,
123    #[serde(rename = "addTarget")]
124    pub add_target: Option<Target>,
125    #[serde(rename = "removeTarget")]
126    pub remove_target: Option<i32>,
127}
128
129#[derive(Default, Clone, Debug, Serialize, Deserialize)]
130pub struct RunQueryRequest {
131    #[serde(rename = "newTransaction")]
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub new_transaction: Option<TransactionOptions>,
134    pub transaction: Option<String>,
135    #[serde(rename = "structuredQuery")]
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub structured_query: Option<StructuredQuery>,
138    #[serde(rename = "readTime")]
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub read_time: Option<String>,
141}
142
143#[derive(Default, Clone, Debug, Serialize, Deserialize)]
144pub struct FieldReference {
145    #[serde(rename = "fieldPath")]
146    pub field_path: String,
147}
148
149#[derive(Default, Clone, Debug, Serialize, Deserialize)]
150pub struct UnaryFilter {
151    pub field: FieldReference,
152    pub op: String,
153}
154
155#[derive(Default, Clone, Debug, Serialize, Deserialize)]
156pub struct ArrayValue {
157    pub values: Option<Vec<Value>>,
158}
159
160#[derive(Default, Clone, Debug, Serialize, Deserialize)]
161pub struct DocumentMask {
162    #[serde(rename = "fieldPaths")]
163    pub field_paths: Vec<String>,
164}
165
166#[derive(Default, Clone, Debug, Serialize, Deserialize)]
167pub struct CompositeFilter {
168    pub filters: Vec<Filter>,
169    pub op: String,
170}
171
172#[derive(Default, Clone, Debug, Serialize, Deserialize)]
173pub struct Empty {
174    _never_set: Option<bool>,
175}
176
177#[derive(Default, Clone, Debug, Serialize, Deserialize)]
178pub struct Filter {
179    #[serde(rename = "unaryFilter")]
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub unary_filter: Option<UnaryFilter>,
182    #[serde(rename = "fieldFilter")]
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub field_filter: Option<FieldFilter>,
185    #[serde(rename = "compositeFilter")]
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub composite_filter: Option<CompositeFilter>,
188}
189
190#[derive(Default, Clone, Debug, Serialize, Deserialize)]
191pub struct WriteResponse {
192    #[serde(rename = "writeResults")]
193    pub write_results: Option<Vec<WriteResult>>,
194    #[serde(rename = "streamToken")]
195    pub stream_token: Option<String>,
196    #[serde(rename = "commitTime")]
197    pub commit_time: Option<String>,
198    #[serde(rename = "streamId")]
199    pub stream_id: Option<String>,
200}
201
202#[derive(Default, Clone, Debug, Serialize, Deserialize)]
203pub struct ListCollectionIdsRequest {
204    #[serde(rename = "pageToken")]
205    pub page_token: Option<String>,
206    #[serde(rename = "pageSize")]
207    pub page_size: Option<i32>,
208}
209
210#[derive(Default, Clone, Debug, Serialize, Deserialize)]
211pub struct BatchGetDocumentsRequest {
212    #[serde(rename = "newTransaction")]
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub new_transaction: Option<TransactionOptions>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub transaction: Option<String>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub mask: Option<DocumentMask>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub documents: Option<Vec<String>>,
221    #[serde(rename = "readTime")]
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub read_time: Option<String>,
224}
225
226#[derive(Default, Clone, Debug, Serialize, Deserialize)]
227pub struct MapValue {
228    pub fields: Option<HashMap<String, Value>>,
229}
230
231#[derive(Default, Clone, Debug, Serialize, Deserialize)]
232pub struct TransactionOptions {
233    #[serde(rename = "readWrite")]
234    pub read_write: Option<ReadWrite>,
235    #[serde(rename = "readOnly")]
236    pub read_only: Option<ReadOnly>,
237}
238
239#[derive(Default, Clone, Debug, Serialize, Deserialize)]
240pub struct CommitResponse {
241    #[serde(rename = "writeResults")]
242    pub write_results: Option<Vec<WriteResult>>,
243    #[serde(rename = "commitTime")]
244    pub commit_time: Option<String>,
245}
246
247#[derive(Default, Clone, Debug, Serialize, Deserialize)]
248pub struct Target {
249    pub documents: Option<DocumentsTarget>,
250    pub once: Option<bool>,
251    pub query: Option<QueryTarget>,
252    #[serde(rename = "resumeToken")]
253    pub resume_token: Option<String>,
254    #[serde(rename = "targetId")]
255    pub target_id: Option<i32>,
256    #[serde(rename = "readTime")]
257    pub read_time: Option<String>,
258}
259
260#[derive(Default, Clone, Debug, Serialize, Deserialize)]
261pub struct ExistenceFilter {
262    pub count: Option<i32>,
263    #[serde(rename = "targetId")]
264    pub target_id: Option<i32>,
265}
266
267#[derive(Default, Clone, Debug, Serialize, Deserialize)]
268pub struct DocumentsTarget {
269    pub documents: Option<Vec<String>>,
270}
271
272#[derive(Default, Clone, Debug, Serialize, Deserialize)]
273pub struct Precondition {
274    #[serde(rename = "updateTime")]
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub update_time: Option<String>,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub exists: Option<bool>,
279}
280
281#[derive(Default, Clone, Debug, Serialize, Deserialize)]
282pub struct Value {
283    #[serde(rename = "bytesValue")]
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub bytes_value: Option<String>,
286
287    #[serde(rename = "timestampValue")]
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub timestamp_value: Option<String>,
290
291    #[serde(rename = "geoPointValue")]
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub geo_point_value: Option<LatLng>,
294
295    #[serde(rename = "referenceValue")]
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub reference_value: Option<String>,
298
299    #[serde(rename = "doubleValue")]
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub double_value: Option<f64>,
302
303    #[serde(rename = "mapValue")]
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub map_value: Option<MapValue>,
306
307    #[serde(rename = "stringValue")]
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub string_value: Option<String>,
310
311    #[serde(rename = "booleanValue")]
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub boolean_value: Option<bool>,
314
315    #[serde(rename = "arrayValue")]
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub array_value: Option<ArrayValue>,
318
319    #[serde(rename = "integerValue")]
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub integer_value: Option<String>,
322
323    #[serde(rename = "nullValue")]
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub null_value: Option<String>,
326}
327
328#[derive(Default, Clone, Debug, Serialize, Deserialize)]
329pub struct Cursor {
330    pub values: Option<Vec<Value>>,
331    pub before: Option<bool>,
332}
333
334#[derive(Default, Clone, Debug, Serialize, Deserialize)]
335pub struct CollectionSelector {
336    #[serde(rename = "allDescendants")]
337    pub all_descendants: Option<bool>,
338    #[serde(rename = "collectionId")]
339    pub collection_id: Option<String>,
340}
341
342#[derive(Default, Clone, Debug, Serialize, Deserialize)]
343pub struct GoogleFirestoreAdminv1Index {
344    pub fields: Option<Vec<GoogleFirestoreAdminv1IndexField>>,
345    pub state: Option<String>,
346    pub name: Option<String>,
347    #[serde(rename = "collectionId")]
348    pub collection_id: Option<String>,
349}
350
351#[derive(Default, Clone, Debug, Serialize, Deserialize)]
352pub struct StructuredQuery {
353    #[serde(rename = "orderBy")]
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub order_by: Option<Vec<Order>>,
356    #[serde(rename = "startAt")]
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub start_at: Option<Cursor>,
359    #[serde(rename = "endAt")]
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub end_at: Option<Cursor>,
362    pub limit: Option<i32>,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub offset: Option<i32>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub from: Option<Vec<CollectionSelector>>,
367    #[serde(rename = "where")]
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub where_: Option<Filter>,
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub select: Option<Projection>,
372}
373
374#[derive(Default, Clone, Debug, Serialize, Deserialize)]
375pub struct FieldTransform {
376    #[serde(rename = "fieldPath")]
377    pub field_path: Option<String>,
378    #[serde(rename = "appendMissingElements")]
379    pub append_missing_elements: Option<ArrayValue>,
380    #[serde(rename = "setToServerValue")]
381    pub set_to_server_value: Option<String>,
382    #[serde(rename = "removeAllFromArray")]
383    pub remove_all_from_array: Option<ArrayValue>,
384}
385
386#[derive(Default, Clone, Debug, Serialize, Deserialize)]
387pub struct DocumentDelete {
388    #[serde(rename = "removedTargetIds")]
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub removed_target_ids: Option<Vec<i32>>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub document: Option<String>,
393    #[serde(rename = "readTime")]
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub read_time: Option<String>,
396}
397
398#[derive(Default, Clone, Debug, Serialize, Deserialize)]
399pub struct GoogleFirestoreAdminv1ExportDocumentsRequest {
400    #[serde(rename = "outputUriPrefix")]
401    pub output_uri_prefix: Option<String>,
402    #[serde(rename = "collectionIds")]
403    pub collection_ids: Option<Vec<String>>,
404}
405
406#[derive(Default, Clone, Debug, Serialize, Deserialize)]
407pub struct Order {
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub field: Option<FieldReference>,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub direction: Option<String>,
412}
413
414#[derive(Default, Clone, Debug, Serialize, Deserialize)]
415pub struct TargetChange {
416    #[serde(rename = "resumeToken")]
417    pub resume_token: Option<String>,
418    #[serde(rename = "targetChangeType")]
419    pub target_change_type: Option<String>,
420    pub cause: Option<Status>,
421    #[serde(rename = "targetIds")]
422    pub target_ids: Option<Vec<i32>>,
423    #[serde(rename = "readTime")]
424    pub read_time: Option<String>,
425}
426
427#[derive(Default, Clone, Debug, Serialize, Deserialize)]
428pub struct RunQueryResponse {
429    #[serde(rename = "skippedResults")]
430    pub skipped_results: Option<i32>,
431    pub transaction: Option<String>,
432    pub document: Option<Document>,
433    #[serde(rename = "readTime")]
434    pub read_time: Option<String>,
435}
436
437#[derive(Default, Clone, Debug, Serialize, Deserialize)]
438pub struct ListCollectionIdsResponse {
439    #[serde(rename = "nextPageToken")]
440    pub next_page_token: Option<String>,
441    #[serde(rename = "collectionIds")]
442    pub collection_ids: Option<Vec<String>>,
443}
444
445#[derive(Default, Clone, Debug, Serialize, Deserialize)]
446pub struct CommitRequest {
447    pub writes: Option<Vec<Write>>,
448    pub transaction: Option<String>,
449}
450
451#[derive(Default, Clone, Debug, Serialize, Deserialize)]
452pub struct Projection {
453    pub fields: Option<Vec<FieldReference>>,
454}
455
456#[derive(Default, Clone, Debug, Serialize, Deserialize)]
457pub struct ListDocumentsResponse {
458    #[serde(rename = "nextPageToken")]
459    pub next_page_token: Option<String>,
460    pub documents: Option<Vec<Document>>,
461}
462
463#[derive(Default, Clone, Debug, Serialize, Deserialize)]
464pub struct ReadWrite {
465    #[serde(rename = "retryTransaction")]
466    pub retry_transaction: Option<String>,
467}
468
469#[derive(Default, Clone, Debug, Serialize, Deserialize)]
470pub struct GoogleLongrunningOperation {
471    pub error: Option<Status>,
472    pub done: Option<bool>,
473    pub response: Option<HashMap<String, String>>,
474    pub name: Option<String>,
475    pub metadata: Option<HashMap<String, String>>,
476}
477
478#[derive(Default, Clone, Debug, Serialize, Deserialize)]
479pub struct LatLng {
480    pub latitude: Option<f64>,
481    pub longitude: Option<f64>,
482}
483
484#[derive(Default, Clone, Debug, Serialize, Deserialize)]
485pub struct DocumentChange {
486    #[serde(rename = "removedTargetIds")]
487    pub removed_target_ids: Option<Vec<i32>>,
488    pub document: Option<Document>,
489    #[serde(rename = "targetIds")]
490    pub target_ids: Option<Vec<i32>>,
491}
492
493#[derive(Default, Clone, Debug, Serialize, Deserialize)]
494pub struct DocumentRemove {
495    #[serde(rename = "removedTargetIds")]
496    pub removed_target_ids: Option<Vec<i32>>,
497    pub document: Option<String>,
498    #[serde(rename = "readTime")]
499    pub read_time: Option<String>,
500}
501
502#[derive(Default, Clone, Debug, Serialize, Deserialize)]
503pub struct RollbackRequest {
504    pub transaction: Option<String>,
505}
506
507#[derive(Default, Clone, Debug, Serialize, Deserialize)]
508pub struct ReadOnly {
509    #[serde(rename = "readTime")]
510    pub read_time: Option<String>,
511}
512
513#[derive(Default, Clone, Debug, Serialize, Deserialize)]
514pub struct BeginTransactionRequest {
515    pub options: Option<TransactionOptions>,
516}
517
518#[derive(Default, Clone, Debug, Serialize, Deserialize)]
519pub struct DocumentTransform {
520    pub document: Option<String>,
521    #[serde(rename = "fieldTransforms")]
522    pub field_transforms: Option<Vec<FieldTransform>>,
523}
524
525#[derive(Default, Clone, Debug, Serialize, Deserialize)]
526pub struct WriteResult {
527    #[serde(rename = "updateTime")]
528    pub update_time: Option<String>,
529    #[serde(rename = "transformResults")]
530    pub transform_results: Option<Vec<Value>>,
531}
532
533#[derive(Default, Clone, Debug, Serialize, Deserialize)]
534pub struct QueryTarget {
535    #[serde(rename = "structuredQuery")]
536    pub structured_query: Option<StructuredQuery>,
537    pub parent: Option<String>,
538}
539
540#[derive(Default, Clone, Debug, Serialize, Deserialize)]
541pub struct WriteRequest {
542    pub writes: Option<Vec<Write>>,
543    pub labels: Option<HashMap<String, String>>,
544    #[serde(rename = "streamToken")]
545    pub stream_token: Option<String>,
546    #[serde(rename = "streamId")]
547    pub stream_id: Option<String>,
548}
549
550#[derive(Serialize, Debug)]
551pub struct SignInWithIdpRequest {
552    pub post_body: String,
553    pub request_uri: String,
554    pub return_idp_credential: bool,
555    pub return_secure_token: bool,
556}
557
558#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
559#[serde(rename_all = "camelCase")]
560pub struct OAuthResponse {
561    pub federated_id: String,
562    pub provider_id: String,
563    pub local_id: String,
564    pub email_verified: bool,
565    pub email: String,
566    pub oauth_access_token: String,
567    pub first_name: String,
568    pub last_name: String,
569    pub full_name: String,
570    pub display_name: String,
571    pub id_token: String,
572    pub photo_url: String,
573    pub refresh_token: String,
574    pub expires_in: String,
575    pub raw_user_info: String,
576}
577
578#[cfg(test)]
579mod tests {
580    #[test]
581    fn it_deserializes_a_document_with_empty_mapvalue() {
582        let doc = r#"{
583        "name": "projects/firestore-db-and-auth/databases/(default)/documents/user/1",
584        "fields": {
585            "gender": {
586                "stringValue": "male"
587            },
588            "age": {
589                "integerValue": "35"
590            },
591            "profile": {
592                "mapValue": {}
593            }
594        },
595        "createTime": "2020-04-28T14:52:51.250511Z",
596        "updateTime": "2020-04-28T14:52:51.250511Z"
597        }"#;
598        let document: Result<crate::dto::Document, serde_json::Error> = serde_json::from_str(&doc);
599        assert!(document.is_ok());
600    }
601
602    #[test]
603    fn it_deserializes_a_document_with_every_data_type() {
604        let doc = r#"{
605        "name": "projects/firestore-db-and-auth/databases/(default)/documents/user/1",
606        "fields": {
607            "exampleArray": {
608                "arrayValue": {
609                    "values": [
610                        {"stringValue": "string-example"},
611                        {"integerValue": "456"}
612                    ]
613                }
614            },
615            "exampleBytes": {
616                "bytesValue": "YWJj"
617            },
618            "exampleBoolean": {
619                "booleanValue": false
620            },
621            "exampleDoubleValue": {
622                "doubleValue": 3.85185988877447170611195588516985463707620329643077639047987759113311767578125
623            },
624            "exampleInteger": {
625                "integerValue": "1024"
626            },
627            "exampleMap": {
628                "mapValue": {
629                    "fields": {
630                        "age": {
631                            "integerValue": "1"
632                        },
633                        "name": {
634                            "stringValue": "Bobby Seale"
635                        }
636                    }
637                }
638            },
639            "exampleNull": {
640                "nullValue": null
641            },
642            "exampleTimestamp": {
643                "timestampValue": "2014-10-02T15:01:23Z"
644            },
645            "exampleString": {
646                "stringValue": "abc-def"
647            },
648            "exampleReferenceValue": {
649                "referenceValue": "projects/firestore-db-and-auth/databases/(default)/documents/test"
650            },
651            "exampleGeoPointValue": {
652                "geoPointValue": {
653                    "latitude": 48.830108,
654                    "longitude": 2.367104
655                }
656            }
657        },
658        "createTime": "2020-04-28T14:52:51.250511Z",
659        "updateTime": "2020-04-28T14:52:51.250511Z"
660        }"#;
661        let document: Result<crate::dto::Document, serde_json::Error> = serde_json::from_str(&doc);
662        assert!(document.is_ok());
663    }
664}