vim_rs 0.4.4

Rust Bindings for the VMware by Broadcom vCenter VI JSON API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/// XML serialization and deserialization for miniserde types.
///
/// Follows serde-xml-rs conventions:
///   - Map keys starting with `@` are XML attributes
///   - Map key `#text` is text content of an element
///   - Namespace prefixes are part of the name (e.g., `soapenv:Body`)
///   - Sequences produce repeated child elements
///
/// This enables miniserde types to round-trip through XML without any
/// special derive macros — the same `Serialize`/`Deserialize` impls
/// that work with JSON work here, just with field-name conventions.
pub(crate) mod client;
pub mod soap;
pub mod ser;
pub mod de;


#[cfg(all(test, feature = "xml"))]
mod tests {
    use super::{de::from_xml, soap};
    use crate::core::client::{extract_property, PropertyValue};
    use crate::types::boxed_types::ValueElements;
    use crate::types::enums::{ManagedEntityStatusEnum, MoTypesEnum};
    use crate::types::struct_enum::StructType;
    use crate::types::structs::{Event, ManagedObjectReference, MethodFault, RetrieveResult, UpdateSet};
    use crate::types::vim_any::VimAny;
    use miniserde::json::{Number, Value};

    /// Minimal XML for empty ArrayOfX - the pattern that causes Property Explorer to fail.
    /// From vtui.log: changeSet with val xsi:type="ArrayOfCustomFieldDef" and no children.
    const EMPTY_ARRAY_VAL: &str = r#"<val xsi:type="ArrayOfCustomFieldDef"></val>"#;

    /// Minimal XML for empty ArrayOfX - the pattern that causes Property Explorer to fail.
    /// From vtui.log: changeSet with val xsi:type="ArrayOfCustomFieldDef" and no children.
    const EMPTY_ARRAY_VAL_WITH_NS: &str = r#"<val xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ArrayOfCustomFieldDef"></val>"#;

    /// Same as typical `xsi:type`, but **alternate namespace prefix** (govc vcsim style).
    const TYPED_STRING_VAL_VCSIM_PREFIX: &str = r#"<val xmlns:_XMLSchema-instance="http://www.w3.org/2001/XMLSchema-instance" _XMLSchema-instance:type="xsd:string">probe</val>"#;

    /// Successful WaitForUpdatesEx response from vtui.log (VM list view).
    const WAIT_FOR_UPDATES_EX_SUCCESS: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<WaitForUpdatesExResponse xmlns="urn:vim25"><returnval><version>1</version><filterSet><filter type="PropertyFilter">session[521da002-3565-aed7-bfc9-a37573136c0e]52b39f40-3106-c12b-715c-bc09e1862cbf</filter><objectSet><kind>enter</kind><obj type="VirtualMachine">1</obj><changeSet><name>name</name><op>assign</op><val xsi:type="xsd:string">NFS server</val></changeSet><changeSet><name>overallStatus</name><op>assign</op><val xsi:type="ManagedEntityStatus">green</val></changeSet><changeSet><name>runtime.powerState</name><op>assign</op><val xsi:type="VirtualMachinePowerState">poweredOff</val></changeSet></objectSet></filterSet></returnval></WaitForUpdatesExResponse>
</soapenv:Body>
</soapenv:Envelope>"#;

    /// `RetrievePropertiesEx` sample: `disabledMethod` as `ArrayOfString` with typed `<string>`
    /// elements (typical vCenter SOAP).
    const RETRIEVE_PROPERTIES_EX_DISABLED_METHOD: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<RetrievePropertiesExResponse xmlns="urn:vim25"><returnval><objects><obj type="VirtualMachine">1</obj><propSet><name>disabledMethod</name><val xsi:type="ArrayOfString"><string xsi:type="xsd:string">setCustomValue</string><string xsi:type="xsd:string">PowerOffVM_Task</string></val></propSet></objects></returnval></RetrievePropertiesExResponse>
</soapenv:Body>
</soapenv:Envelope>"#;

    #[test]
    fn test_mor_deserialize() {
        let moref: ManagedObjectReference =
            from_xml(r#"<val type="VirtualMachine">5-envmgr</val>"#)
                .expect("ManagedObjectReference deserialize failed.");
        assert_eq!(moref.r#type, MoTypesEnum::VirtualMachine);
        assert_eq!(moref.value, "5-envmgr");
    }

    #[test]
    fn test_vim_any_from_moref() {
        let vim_any: VimAny =
            // Easy mode - no buffering, just set the mor flags
            //from_xml(r#"<val xsi:type="ManagedObjectReference" type="Folder">ha-folder-vm</val>"#)
            // This is the real test that is hard as it requires buffering
            from_xml(r#"<val type="Folder" xsi:type="ManagedObjectReference">ha-folder-vm</val>"#)
                .expect("ManagedObjectReference deserialize failed.");
        match &vim_any {
            VimAny::Object(v) => {
                let moref: Option<&ManagedObjectReference> = v.as_any_ref().downcast_ref();
                assert!(moref.is_some());
                assert_eq!(moref.unwrap().r#type, MoTypesEnum::Folder);
                assert_eq!(moref.unwrap().value, "ha-folder-vm");
            }
            other => panic!("expected ManagedObjectReference, got {:?}", other),
        }
    }

    /// Minimal test: empty ArrayOfCustomFieldDef must deserialize to VimAny::Value(ArrayOfCustomFieldDef(vec![])).
    #[test]
    fn test_empty_array_val_deserialize() {
        let vim_any: VimAny =
            from_xml(EMPTY_ARRAY_VAL).expect("empty ArrayOfCustomFieldDef should deserialize");
        match &vim_any {
            VimAny::Value(ValueElements::ArrayOfCustomFieldDef(v)) => assert!(v.is_empty()),
            other => panic!("expected ArrayOfCustomFieldDef, got {:?}", other),
        }
    }

    #[test]
    fn test_empty_array_val_with_ns_deserialize() {
        let vim_any: VimAny = from_xml(EMPTY_ARRAY_VAL_WITH_NS)
            .expect("empty ArrayOfCustomFieldDef should deserialize");
        match &vim_any {
            VimAny::Value(ValueElements::ArrayOfCustomFieldDef(v)) => assert!(v.is_empty()),
            other => panic!("expected ArrayOfCustomFieldDef, got {:?}", other),
        }
    }

    /// Schema-instance `type` must be honored for any namespace **prefix** (`xsi:type` and
    /// `_XMLSchema-instance:type` are the same attribute in different bindings).
    #[test]
    fn test_schema_instance_type_alternate_prefix_same_as_xsi() {
        let via_alt: VimAny = from_xml(TYPED_STRING_VAL_VCSIM_PREFIX)
            .expect("vcsim-style schema-instance prefix should deserialize");
        let via_xsi: VimAny = from_xml(
            r#"<val xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">probe</val>"#,
        )
        .expect("xsi:type control");

        match (&via_alt, &via_xsi) {
            (
                VimAny::Value(ValueElements::PrimitiveString(a)),
                VimAny::Value(ValueElements::PrimitiveString(b)),
            ) => assert_eq!(a, b),
            pair => panic!("expected both PrimitiveString, got {:?}", pair),
        }

        let enum_alt: VimAny = from_xml(
            r#"<val xmlns:_XMLSchema-instance="http://www.w3.org/2001/XMLSchema-instance" _XMLSchema-instance:type="ManagedEntityStatus">green</val>"#,
        )
        .expect("enum with alternate prefix");
        match enum_alt {
            VimAny::Value(ValueElements::ManagedEntityStatus(e)) => {
                assert_eq!(e, ManagedEntityStatusEnum::Green);
            }
            other => panic!("expected ManagedEntityStatus, got {:?}", other),
        }
    }

    /// Local name `type` with a prefix bound to a URI other than XML Schema instance must not be
    /// treated as `xsi:type` (namespace resolution, not `*:type` heuristics).
    #[test]
    fn test_type_attribute_wrong_namespace_not_schema_instance() {
        let r = from_xml::<VimAny>(
            r#"<val xmlns:p="http://example.com/wrong" p:type="xsd:string">x</val>"#,
        );
        assert!(
            r.is_err(),
            "p:type bound to wrong URI must not coerce to PrimitiveString: {:?}",
            r.ok()
        );
    }

    /// SOAP property GET: `extract_property` + `PropertyValue::Parsed` (zero re-serialization).
    #[test]
    fn extract_property_soap_val_array_of_string() {
        let xml = r#"<val xsi:type="ArrayOfString"><string xsi:type="xsd:string">a</string><string>b</string></val>"#;
        let vim_any: VimAny = from_xml(xml).expect("VimAny from property val");
        let v: Vec<String> =
            extract_property(PropertyValue::Parsed(vim_any)).expect("Vec<String> from VimAny");
        assert_eq!(v, vec!["a".to_string(), "b".to_string()]);
    }

    #[test]
    fn test_retrieve_result_disabled_method_array_of_string() {
        let mut result: RetrieveResult = soap::vim_response(RETRIEVE_PROPERTIES_EX_DISABLED_METHOD)
            .expect("RetrieveResult with disabledMethod ArrayOfString should deserialize");
        assert_eq!(result.objects.len(), 1);
        let obj = result.objects.swap_remove(0);
        let ps = obj.prop_set.expect("prop_set");
        assert_eq!(ps.len(), 1);
        assert_eq!(ps[0].name, "disabledMethod");
        let methods = ps.into_iter().next().expect("one prop").val;
        let v: Vec<String> =
            extract_property(PropertyValue::Parsed(methods)).expect("extract_property");
        assert_eq!(v.as_slice(), ["setCustomValue", "PowerOffVM_Task"]);
    }

    #[test]
    fn test_wait_for_updates_ex_success_deserialize() {
        // VM list view response - parses successfully.
        let update_set: UpdateSet = soap::vim_response(WAIT_FOR_UPDATES_EX_SUCCESS).unwrap();
        assert_eq!(update_set.version, "1");
        let filter_set = update_set.filter_set.as_ref().unwrap();
        assert_eq!(filter_set.len(), 1);
        assert_eq!(
            filter_set[0].filter.value,
            "session[521da002-3565-aed7-bfc9-a37573136c0e]52b39f40-3106-c12b-715c-bc09e1862cbf"
        );
    }

    /// Minimal UpdateSet with only the empty-array changeSet.
    const WAIT_FOR_UPDATES_EX_EMPTY_ARRAY_ONLY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<WaitForUpdatesExResponse xmlns="urn:vim25"><returnval><version>2</version><filterSet><filter type="PropertyFilter">x</filter><objectSet><kind>enter</kind><obj type="VirtualMachine">7</obj><changeSet><name>availableField</name><op>assign</op><val xsi:type="ArrayOfCustomFieldDef"></val></changeSet></objectSet></filterSet></returnval></WaitForUpdatesExResponse>
</soapenv:Body>
</soapenv:Envelope>"#;

    #[test]
    fn test_wait_for_updates_ex_empty_array_only() {
        // Just the empty ArrayOfCustomFieldDef changeSet in full UpdateSet structure.
        let result: Result<UpdateSet, _> = soap::vim_response(WAIT_FOR_UPDATES_EX_EMPTY_ARRAY_ONLY);
        let update_set =
            result.expect("UpdateSet with only empty-array changeSet should deserialize");
        assert_eq!(update_set.version, "2");
    }

    // --- Property Explorer case breakdown: run with `cargo test --features xml property_explorer -- --ignored` ---

    /// Case 1: changeSet with no val element (alarmActionsEnabled).
    const PROP_EXPLORER_ALARM_ONLY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<WaitForUpdatesExResponse xmlns="urn:vim25"><returnval><version>2</version><filterSet><filter type="PropertyFilter">x</filter><objectSet><kind>enter</kind><obj type="VirtualMachine">7</obj><changeSet><name>alarmActionsEnabled</name><op>assign</op></changeSet></objectSet></filterSet></returnval></WaitForUpdatesExResponse>
</soapenv:Body>
</soapenv:Envelope>"#;

    /// Case 2: empty ArrayOfCustomFieldDef (availableField).
    const PROP_EXPLORER_EMPTY_ARRAY_ONLY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<WaitForUpdatesExResponse xmlns="urn:vim25"><returnval><version>2</version><filterSet><filter type="PropertyFilter">x</filter><objectSet><kind>enter</kind><obj type="VirtualMachine">7</obj><changeSet><name>availableField</name><op>assign</op><val xsi:type="ArrayOfCustomFieldDef"></val></changeSet></objectSet></filterSet></returnval></WaitForUpdatesExResponse>
</soapenv:Body>
</soapenv:Envelope>"#;

    /// Case 3: alarmActionsEnabled + availableField (no val + empty array).
    const PROP_EXPLORER_ALARM_AND_EMPTY_ARRAY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
 xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<WaitForUpdatesExResponse xmlns="urn:vim25"><returnval><version>2</version><filterSet><filter type="PropertyFilter">x</filter><objectSet><kind>enter</kind><obj type="VirtualMachine">7</obj><changeSet><name>alarmActionsEnabled</name><op>assign</op></changeSet><changeSet><name>availableField</name><op>assign</op><val xsi:type="ArrayOfCustomFieldDef"></val></changeSet></objectSet></filterSet></returnval></WaitForUpdatesExResponse>
</soapenv:Body>
</soapenv:Envelope>"#;

    #[test]
    fn test_property_explorer_case1_alarm_only() {
        let result: Result<UpdateSet, _> = soap::vim_response(PROP_EXPLORER_ALARM_ONLY);
        result.expect("changeSet with no val should deserialize");
    }

    #[test]
    fn test_property_explorer_case2_empty_array_only() {
        let result: Result<UpdateSet, _> = soap::vim_response(PROP_EXPLORER_EMPTY_ARRAY_ONLY);
        result.expect("empty ArrayOfCustomFieldDef changeSet should deserialize");
    }

    #[test]
    fn test_property_explorer_case4_alarm_and_empty_array() {
        let result: Result<UpdateSet, _> = soap::vim_response(PROP_EXPLORER_ALARM_AND_EMPTY_ARRAY);
        result.expect("alarm + empty array changeSets should deserialize");
    }

    /// Pruned `MethodFault` / `Event` XML: typed extra fields (registry + `ApiTypedValueVisitor`).
    #[test]
    fn test_xml_vapp_property_fault_simple() {
        let xml = r#"<fault xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="VAppPropertyFault">
        <faultMessage>
            <key>config.product.version</key>
            <message>Product Version: 1.0.0</message>
        </faultMessage>
        <id>config.product.version</id>
        <category>string</category>
        <label>Product Version</label>
        <type>string</type>
        <value>1.0.0</value>
    </fault>"#;

        let fault: MethodFault = from_xml(xml).unwrap();
        assert_eq!(fault.type_, Some(StructType::VAppPropertyFault));
        assert!(fault.fault_message.is_some());
        match fault.extra_fields_.get("id") {
            Some(Value::String(s)) => assert_eq!(s, "config.product.version"),
            o => panic!("id: {:?}", o),
        }
        match fault.extra_fields_.get("label") {
            Some(Value::String(s)) => assert_eq!(s, "Product Version"),
            o => panic!("label: {:?}", o),
        }
        match fault.extra_fields_.get("value") {
            Some(Value::String(s)) => assert_eq!(s, "1.0.0"),
            o => panic!("value: {:?}", o),
        }
    }

    #[test]
    fn test_xml_vapp_property_fault_with_args() {
        let xml = r#"<fault xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="VAppPropertyFault">
        <faultMessage>
            <key>config.product.version</key>
            <arg xsi:type="KeyAnyValue">
                <key>config.product.version</key>
                <value xsi:type="xsd:string">1.0.0</value>
            </arg>
            <message>Product Version: 1.0.0</message>
        </faultMessage>
        <id>config.product.version</id>
        <category>string</category>
        <label>Product Version</label>
        <type>string</type>
        <value>1.0.0</value>
    </fault>"#;

        let fault: MethodFault = from_xml(xml).unwrap();
        assert_eq!(fault.type_, Some(StructType::VAppPropertyFault));
        assert!(fault.fault_message.is_some());
        assert!(fault.extra_fields_.get("label").is_some());
    }

    #[test]
    fn test_xml_event_ex_extra_fields() {
        let xml = r#"<event xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="EventEx">
        <key>12345</key>
        <chainId>100</chainId>
        <createdTime>2024-06-15T10:30:00Z</createdTime>
        <userName>admin</userName>
        <eventTypeId>com.vmware.example.event</eventTypeId>
        <severity>info</severity>
        <message>Something happened</message>
    </event>"#;

        let event: Event = from_xml(xml).unwrap();
        assert_eq!(event.type_, Some(StructType::EventEx));
        assert_eq!(event.key, 12345);
        assert_eq!(event.user_name, "admin");
        match event.extra_fields_.get("eventTypeId") {
            Some(Value::String(s)) => assert_eq!(s, "com.vmware.example.event"),
            o => panic!("eventTypeId: {:?}", o),
        }
        match event.extra_fields_.get("severity") {
            Some(Value::String(s)) => assert_eq!(s, "info"),
            o => panic!("severity: {:?}", o),
        }
    }

    #[test]
    fn test_xml_fault_numeric_extra_field() {
        let xml = r#"<fault xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ReadOnlyDisksWithLegacyDestination">
        <roDiskCount>2</roDiskCount>
        <timeoutDanger>false</timeoutDanger>
    </fault>"#;

        let fault: MethodFault = from_xml(xml).unwrap();
        assert_eq!(
            fault.type_,
            Some(StructType::ReadOnlyDisksWithLegacyDestination)
        );
        match fault.extra_fields_.get("roDiskCount") {
            Some(Value::Number(Number::I64(n))) => assert_eq!(*n, 2),
            o => panic!("roDiskCount: {:?}", o),
        }
        match fault.extra_fields_.get("timeoutDanger") {
            Some(Value::Bool(b)) => assert!(!b),
            o => panic!("timeoutDanger: {:?}", o),
        }
    }

    #[test]
    fn test_xml_event_arguments_array() {
        let xml = r#"<event xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="EventEx">
        <key>1</key>
        <chainId>1</chainId>
        <createdTime>2024-01-01T00:00:00Z</createdTime>
        <userName>system</userName>
        <eventTypeId>test.event</eventTypeId>
        <message>m</message>
        <arguments xsi:type="KeyAnyValue">
            <key>arg1</key>
            <value xsi:type="xsd:string">val1</value>
        </arguments>
        <arguments xsi:type="KeyAnyValue">
            <key>arg2</key>
            <value xsi:type="xsd:string">val2</value>
        </arguments>
    </event>"#;

        let event: Event = from_xml(xml).unwrap();
        let args = event.extra_fields_.get("arguments");
        assert!(args.is_some());
        if let Some(Value::Array(arr)) = args {
            assert_eq!(arr.len(), 2);
        } else {
            panic!("arguments should be Value::Array, got {:?}", args);
        }
    }

    #[test]
    fn test_xml_single_arguments_still_array() {
        let xml = r#"<event xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="EventEx">
        <key>1</key>
        <chainId>1</chainId>
        <createdTime>2024-01-01T00:00:00Z</createdTime>
        <userName>system</userName>
        <eventTypeId>test.event</eventTypeId>
        <message>m</message>
        <arguments xsi:type="KeyAnyValue">
            <key>only_arg</key>
            <value xsi:type="xsd:string">only_val</value>
        </arguments>
    </event>"#;

        let event: Event = from_xml(xml).unwrap();
        let args = event.extra_fields_.get("arguments");
        if let Some(Value::Array(arr)) = args {
            assert_eq!(arr.len(), 1);
        } else {
            panic!("single arguments element should still be Array, got {:?}", args);
        }
    }

}