Repository: rusoto/rusoto (language: rust)

Issue:
Lex Runtime PostTextRespone Type is incorrect
I was trying to use the lex runtime today and kept receiving serde deserialization errors when using the postText fn.

Turns out AWS will return null values in the JSON for the ```slots``` field. The current type for ```slots``` is: ```Option<HashMap<String,String>>``` should be ```Option<HashMap<String,Option<String>>>```

Happy to PR if you can point me in the right direction.


Discussion / hints:
Hi @jjcomer!

Can you give a little more details about the problem you're running into?

Using `Option<HashMap<String,Option<String>>>`, while technically possible, seems like it would be a bit less convenient to use, so I'd like to make sure we find the right solution for this problem!
No problem.
When you create a Lex bot you can define variables that you are waiting for the bot to deduce throughout the users' conversation. Until the value has been found, lex returns a null.

Here is a JSON response from postText:

```json
{
    "message": "What type of flowers would you like to order?",
    "intent-name": "OrderFlowers",
    "dialog-state": "ElicitSlot",
    "slot-to-elicit": "FlowerType",
    "message-format": "PlainText",
    "slots": {
        "PickupTime": null,
        "FlowerType": null,
        "PickupDate": null
    }
}
```

If you wanted to keep the current type, you could filter out the keys with ```null```, but then you wouldn't know from the response which slots are waiting to be filled.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80fe98c351a..6f0e9ade8c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@
 
 (Please put changes here)
 
+- Deserialize PostTextResponse correctly by allowing null values in the slots field
+
 ## [0.40.0] - 2019-06-28
 
 - Only emit types used in service during crate generation
diff --git a/rusoto/services/lex-runtime/src/custom/mod.rs b/rusoto/services/lex-runtime/src/custom/mod.rs
index e69de29bb2d..e4234693714 100644
--- a/rusoto/services/lex-runtime/src/custom/mod.rs
+++ b/rusoto/services/lex-runtime/src/custom/mod.rs
@@ -0,0 +1,2 @@
+#[cfg(test)]
+mod custom_tests;
diff --git a/rusoto/services/lex-runtime/src/generated.rs b/rusoto/services/lex-runtime/src/generated.rs
index 893237915fd..321f875683c 100644
--- a/rusoto/services/lex-runtime/src/generated.rs
+++ b/rusoto/services/lex-runtime/src/generated.rs
@@ -180,7 +180,7 @@ pub struct PostTextResponse {
     /// <p> The intent slots that Amazon Lex detected from the user input in the conversation. </p> <p>Amazon Lex creates a resolution list containing likely values for a slot. The value that it returns is determined by the <code>valueSelectionStrategy</code> selected when the slot type was created or updated. If <code>valueSelectionStrategy</code> is set to <code>ORIGINAL_VALUE</code>, the value provided by the user is returned, if the user value is similar to the slot values. If <code>valueSelectionStrategy</code> is set to <code>TOP_RESOLUTION</code> Amazon Lex returns the first value in the resolution list or, if there is no resolution list, null. If you don't specify a <code>valueSelectionStrategy</code>, the default is <code>ORIGINAL_VALUE</code>.</p>
     #[serde(rename = "slots")]
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub slots: Option<::std::collections::HashMap<String, String>>,
+    pub slots: Option<::std::collections::HashMap<String, Option<String>>>,
 }
 
 /// <p>If you configure a response card when creating your bots, Amazon Lex substitutes the session attributes and slot values that are available, and then returns it. The response card can also come from a Lambda function ( <code>dialogCodeHook</code> and <code>fulfillmentActivity</code> on an intent).</p>
diff --git a/service_crategen/src/commands/generate/codegen/mod.rs b/service_crategen/src/commands/generate/codegen/mod.rs
index c0a328e2cf1..5e353bb353c 100644
--- a/service_crategen/src/commands/generate/codegen/mod.rs
+++ b/service_crategen/src/commands/generate/codegen/mod.rs
@@ -34,7 +34,11 @@ pub trait GenerateProtocol {
     /// Generate the various `use` statements required by the module generatedfor this service
     fn generate_prelude(&self, writer: &mut FileWriter, service: &Service<'_>) -> IoResult;
 
-    fn generate_method_signatures(&self, writer: &mut FileWriter, service: &Service<'_>) -> IoResult;
+    fn generate_method_signatures(
+        &self,
+        writer: &mut FileWriter,
+        service: &Service<'_>,
+    ) -> IoResult;
 
     /// Generate a method for each `Operation` in the `Service` to execute that method remotely
     ///
@@ -318,9 +322,11 @@ pub fn mutate_type_name_for_streaming(type_name: &str) -> String {
 
 fn find_shapes_to_generate(service: &Service<'_>) -> BTreeSet<String> {
     let mut shapes_to_generate = BTreeSet::<String>::new();
+
     let mut visitor = |shape_name: &str, _shape: &Shape| {
         shapes_to_generate.insert(shape_name.to_owned())
     };
+
     for operation in service.operations().values() {
         if let Some(ref input) = operation.input {
             service.visit_shapes(&input.shape, &mut visitor);
@@ -335,9 +341,13 @@ fn find_shapes_to_generate(service: &Service<'_>) -> BTreeSet<String> {
         }
     }
     return shapes_to_generate;
-} 
+}
 
-fn generate_types<P>(writer: &mut FileWriter, service: &Service<'_>, protocol_generator: &P) -> IoResult
+fn generate_types<P>(
+    writer: &mut FileWriter,
+    service: &Service<'_>,
+    protocol_generator: &P,
+) -> IoResult
 where
     P: GenerateProtocol,
 {
@@ -556,6 +566,10 @@ fn generate_struct_fields<P: GenerateProtocol>(
             // See https://github.com/rusoto/rusoto/issues/1419 for more information
             if service.name() == "CodePipeline" && shape_name == "ActionRevision" && name == "revision_change_id" || name == "created" {
                 lines.push(format!("pub {}: Option<{}>,", name, rs_type))
+            // In pratice, Lex can return null values for slots that are not filled. The documentation
+            // does not mention that the slot values themselves can be null.
+            } else if service.name() == "Amazon Lex Runtime Service"  && shape_name == "PostTextResponse" && name == "slots"{
+                lines.push(format!("pub {}: Option<::std::collections::HashMap<String, Option<String>>>,", name))
             } else if shape.required(member_name) {
                 lines.push(format!("pub {}: {},", name, rs_type))
             } else if name == "type" {


Test patch:
diff --git a/rusoto/services/lex-runtime/src/custom/custom_tests.rs b/rusoto/services/lex-runtime/src/custom/custom_tests.rs
new file mode 100644
index 00000000000..94b944ffba1
--- /dev/null
+++ b/rusoto/services/lex-runtime/src/custom/custom_tests.rs
@@ -0,0 +1,52 @@
+extern crate rusoto_mock;
+
+use crate::generated::{LexRuntime, LexRuntimeClient, PostTextRequest, PostTextResponse};
+use rusoto_core::Region;
+use std::collections::HashMap;
+
+use self::rusoto_mock::*;
+
+#[test]
+fn test_post_text_resposnse_serialization() {
+    let mock_resp_body = r#"{
+      "dialogState": "ElicitSlot",
+      "intentName": "BookCar",
+      "message": "In what city do you need to rent a car?",
+      "messageFormat": "PlainText",
+      "responseCard": null,
+      "sessionAttributes": {},
+      "slotToElicit": "PickUpCity",
+      "slots": {
+        "CarType": null,
+        "PickUpCity": "Boston"
+      }
+    }"#;
+    let mock_request = MockRequestDispatcher::with_status(200).with_body(mock_resp_body);
+
+    let lex_client =
+        LexRuntimeClient::new_with(mock_request, MockCredentialsProvider, Region::UsEast1);
+
+    let post_text_req = PostTextRequest {
+        input_text: "Book a car".to_owned(),
+        user_id: "rs".to_owned(),
+        ..Default::default()
+    };
+
+    let mut slots = HashMap::new();
+    slots.insert("CarType".to_owned(), None);
+    slots.insert("PickUpCity".to_owned(), Some("Boston".to_owned()));
+
+    let expected = PostTextResponse {
+        dialog_state: Some("ElicitSlot".to_owned()),
+        intent_name: Some("BookCar".to_owned()),
+        message: Some("In what city do you need to rent a car?".to_owned()),
+        message_format: Some("PlainText".to_owned()),
+        slot_to_elicit: Some("PickUpCity".to_owned()),
+        slots: Some(slots),
+        response_card: None,
+        session_attributes: Some(HashMap::new()),
+    };
+
+    let result: PostTextResponse = lex_client.post_text(post_text_req).sync().unwrap();
+    assert_eq!(result, expected);
+}