diff --git a/src/main/kotlin/com/google/actions/api/ActionResponse.kt b/src/main/kotlin/com/google/actions/api/ActionResponse.kt index 76a81d7..51b2c15 100644 --- a/src/main/kotlin/com/google/actions/api/ActionResponse.kt +++ b/src/main/kotlin/com/google/actions/api/ActionResponse.kt @@ -20,6 +20,8 @@ import com.google.api.services.actions_fulfillment.v2.model.AppResponse import com.google.api.services.actions_fulfillment.v2.model.ExpectedIntent import com.google.api.services.actions_fulfillment.v2.model.RichResponse import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse +import java.io.IOException +import java.io.OutputStream /** * Defines requirements of an object that represents a response from the Actions @@ -58,6 +60,16 @@ interface ActionResponse { */ val helperIntent: ExpectedIntent? + /** + * Writes the JSON representation of the response to the given output stream. + * + * This is more efficient than calling [toJson] first and then writing the string. + * + * @param outputStream The output stream to write to. Must be closed by the caller. + */ + @Throws(IOException::class) + fun writeTo(outputStream: OutputStream) + /** * Returns the JSON representation of the response. */ diff --git a/src/main/kotlin/com/google/actions/api/ActionsSdkApp.kt b/src/main/kotlin/com/google/actions/api/ActionsSdkApp.kt index 5baf370..b1b960b 100644 --- a/src/main/kotlin/com/google/actions/api/ActionsSdkApp.kt +++ b/src/main/kotlin/com/google/actions/api/ActionsSdkApp.kt @@ -19,6 +19,7 @@ package com.google.actions.api import com.google.actions.api.impl.AogRequest import com.google.actions.api.response.ResponseBuilder import org.slf4j.LoggerFactory +import java.io.InputStream /** * Implementation of App for ActionsSDK based webhook. Developers must extend @@ -50,6 +51,11 @@ open class ActionsSdkApp : DefaultApp() { return AogRequest.create(inputJson, headers) } + override fun createRequest(inputStream: InputStream, headers: Map<*, *>?): ActionRequest { + LOG.info("ActionsSdkApp.createRequest..") + return AogRequest.create(inputStream, headers) + } + override fun getResponseBuilder(request: ActionRequest): ResponseBuilder { val responseBuilder = ResponseBuilder( usesDialogflow = false, diff --git a/src/main/kotlin/com/google/actions/api/DefaultApp.kt b/src/main/kotlin/com/google/actions/api/DefaultApp.kt index 2686679..c3a0ff6 100644 --- a/src/main/kotlin/com/google/actions/api/DefaultApp.kt +++ b/src/main/kotlin/com/google/actions/api/DefaultApp.kt @@ -18,6 +18,7 @@ package com.google.actions.api import com.google.actions.api.response.ResponseBuilder import org.slf4j.LoggerFactory +import java.io.InputStream import java.util.concurrent.CompletableFuture /** @@ -42,6 +43,20 @@ abstract class DefaultApp : App { abstract fun createRequest(inputJson: String, headers: Map<*, *>?): ActionRequest + /** + * Creates an ActionRequest from the specified input stream and metadata. + * + * This is semantically equivalent to reading the stream as a String using + * UTF-8 encoding and then calling `createRequest` with the resulting + * string. + * + * @param inputStream The input stream. Must be closed by the caller + * @param headers Map containing metadata, usually from the HTTP request + * headers. + */ + abstract fun createRequest(inputStream: InputStream, headers: Map<*, *>?): + ActionRequest + /** * @return A ResponseBuilder for this App. */ diff --git a/src/main/kotlin/com/google/actions/api/DialogflowApp.kt b/src/main/kotlin/com/google/actions/api/DialogflowApp.kt index 0b4be74..32c238f 100644 --- a/src/main/kotlin/com/google/actions/api/DialogflowApp.kt +++ b/src/main/kotlin/com/google/actions/api/DialogflowApp.kt @@ -18,6 +18,7 @@ package com.google.actions.api import com.google.actions.api.impl.DialogflowRequest import com.google.actions.api.response.ResponseBuilder +import java.io.InputStream /** * Implementation of App for Dialogflow based webhook. Developers must extend @@ -48,6 +49,10 @@ open class DialogflowApp : DefaultApp() { return DialogflowRequest.create(inputJson, headers) } + override fun createRequest(inputStream: InputStream, headers: Map<*, *>?): ActionRequest { + return DialogflowRequest.create(inputStream, headers) + } + override fun getResponseBuilder(request: ActionRequest): ResponseBuilder { val responseBuilder = ResponseBuilder( usesDialogflow = true, diff --git a/src/main/kotlin/com/google/actions/api/impl/AogRequest.kt b/src/main/kotlin/com/google/actions/api/impl/AogRequest.kt index e03bc22..2254445 100644 --- a/src/main/kotlin/com/google/actions/api/impl/AogRequest.kt +++ b/src/main/kotlin/com/google/actions/api/impl/AogRequest.kt @@ -25,6 +25,8 @@ import com.google.gson.GsonBuilder import com.google.gson.JsonObject import com.google.gson.reflect.TypeToken import org.slf4j.LoggerFactory +import java.io.InputStream +import java.io.InputStreamReader import java.util.* internal class AogRequest internal constructor( @@ -201,6 +203,145 @@ internal class AogRequest internal constructor( companion object { private val LOG = LoggerFactory.getLogger(AogRequest::class.java.name) + private val gson = GsonBuilder() + .registerTypeAdapter(AppRequest::class.java, + AppRequestDeserializer()) + .registerTypeAdapter(User::class.java, + UserDeserializer()) + .registerTypeAdapter(Input::class.java, + InputDeserializer()) + .registerTypeAdapter(Status::class.java, + StatusDeserializer()) + .registerTypeAdapter(Surface::class.java, + SurfaceDeserializer()) + .registerTypeAdapter(Device::class.java, + DeviceDeserializer()) + .registerTypeAdapter(Location::class.java, + LocationDeserializer()) + .registerTypeAdapter(Argument::class.java, + ArgumentDeserializer()) + .registerTypeAdapter(RawInput::class.java, + RawInputDeserializer()) + .registerTypeAdapter(PackageEntitlement::class.java, + PackageEntitlementDeserializer()) + .registerTypeAdapter(Entitlement::class.java, + EntitlementDeserializer()) + .registerTypeAdapter(SignedData::class.java, + SignedDataDeserializer()) + .registerTypeAdapter(DateTime::class.java, + DateTimeValueDeserializer()) + .registerTypeAdapter(Order::class.java, + OrderDeserializer()) + .registerTypeAdapter(CustomerInfo::class.java, + CustomerInfoDeserializer()) + .registerTypeAdapter(ProposedOrder::class.java, + ProposedOrderDeserializer()) + .registerTypeAdapter(Cart::class.java, + CartDeserializer()) + .registerTypeAdapter(LineItem::class.java, + LineItemDeserializer()) + .registerTypeAdapter(LineItemSubLine::class.java, + LineItemSubLineDeserializer()) + .registerTypeAdapter(Promotion::class.java, + PromotionDeserializer()) + .registerTypeAdapter(Merchant::class.java, + MerchantDeserializer()) + .registerTypeAdapter(Image::class.java, + ImageDeserializer()) + .registerTypeAdapter(Price::class.java, + PriceDeserializer()) + .registerTypeAdapter(Money::class.java, + MoneyDeserializer()) + .registerTypeAdapter(PaymentInfo::class.java, + PaymentInfoDeserializer()) + .registerTypeAdapter(PaymentInfoGoogleProvidedPaymentInstrument::class.java, + PaymentInfoGoogleProvidedPaymentInstrumentDeserializer()) + .registerTypeAdapter(TransactionRequirementsCheckResult::class.java, + TransactionRequirementsCheckResultDeserializer()) + .registerTypeAdapter(OrderV3::class.java, + OrderV3Deserializer()) + .registerTypeAdapter(UserInfo::class.java, + UserInfoDeserializer()) + .registerTypeAdapter(OrderContents::class.java, + OrderContentsDeserializer()) + .registerTypeAdapter(PaymentData::class.java, + PaymentDataDeserializer()) + .registerTypeAdapter(PurchaseOrderExtension::class.java, + PurchaseOrderExtensionDeserializer()) + .registerTypeAdapter(TicketOrderExtension::class.java, + TicketOrderExtensionDeserializer()) + .registerTypeAdapter(MerchantV3::class.java, + MerchantV3Deserializer()) + .registerTypeAdapter(Disclosure::class.java, + DisclosureDeserializer()) + .registerTypeAdapter(Action::class.java, + ActionDeserializer()) + .registerTypeAdapter(PriceAttribute::class.java, + PriceAttributeDeserializer()) + .registerTypeAdapter(PromotionV3::class.java, + PromotionV3Deserializer()) + .registerTypeAdapter(PhoneNumber::class.java, + PhoneNumberDeserializer()) + .registerTypeAdapter(LineItemV3::class.java, + LineItemV3Deserializer()) + .registerTypeAdapter(PaymentInfoV3::class.java, + PaymentInfoV3Deserializer()) + .registerTypeAdapter(PaymentResult::class.java, + PaymentResultDeserializer()) + .registerTypeAdapter(PurchaseFulfillmentInfo::class.java, + PurchaseFulfillmentInfoDeserializer()) + .registerTypeAdapter(PurchaseReturnsInfo::class.java, + PurchaseReturnsInfoDeserializer()) + .registerTypeAdapter(PurchaseError::class.java, + PurchaseErrorDeserializer()) + .registerTypeAdapter(TicketEvent::class.java, + TicketEventDeserializer()) + .registerTypeAdapter(DisclosureText::class.java, + DisclosureTextDeserializer()) + .registerTypeAdapter(DisclosurePresentationOptions::class.java, + DisclosurePresentationOptionsDeserializer()) + .registerTypeAdapter(ActionActionMetadata::class.java, + ActionActionMetadataDeserializer()) + .registerTypeAdapter(OpenUrlAction::class.java, + OpenUrlActionDeserializer()) + .registerTypeAdapter(MoneyV3::class.java, + MoneyV3Deserializer()) + .registerTypeAdapter(PurchaseItemExtension::class.java, + PurchaseItemExtensionDeserializer()) + .registerTypeAdapter(ReservationItemExtension::class.java, + ReservationItemExtensionDeserializer()) + .registerTypeAdapter(PaymentMethodDisplayInfo::class.java, + PaymentMethodDisplayInfoDeserializer()) + .registerTypeAdapter(TimeV3::class.java, + TimeV3Deserializer()) + .registerTypeAdapter(PickupInfo::class.java, + PickupInfoDeserializer()) + .registerTypeAdapter(CheckInInfo::class.java, + CheckInInfoDeserializer()) + .registerTypeAdapter(EventCharacter::class.java, + EventCharacterDeserializer()) + .registerTypeAdapter(DisclosureTextTextLink::class.java, + DisclosureTextTextLinkDeserializer()) + .registerTypeAdapter(AndroidApp::class.java, + AndroidAppDeserializer()) + .registerTypeAdapter(ProductDetails::class.java, + ProductDetailsDeserializer()) + .registerTypeAdapter(MerchantUnitMeasure::class.java, + MerchantUnitMeasureDeserializer()) + .registerTypeAdapter(PurchaseItemExtensionItemOption::class.java, + PurchaseItemExtensionItemOptionDeserializer()) + .registerTypeAdapter(StaffFacilitator::class.java, + StaffFacilitatorDeserializer()) + .registerTypeAdapter(PickupInfoCurbsideInfo::class.java, + PickupInfoCurbsideInfoDeserializer()) + .registerTypeAdapter(AndroidAppVersionFilter::class.java, + AndroidAppVersionFilterDeserializer()) + .registerTypeAdapter(Vehicle::class.java, + VehicleDeserializer()) + .registerTypeAdapter(genericType>(), + ExtensionDeserializer()) + .create() + fun create(appRequest: AppRequest): AogRequest { return AogRequest(appRequest) } @@ -210,8 +351,7 @@ internal class AogRequest internal constructor( headers: Map<*, *>? = HashMap(), partOfDialogflowRequest: Boolean = false): AogRequest { - val gson = Gson() - return create(gson.fromJson(body, JsonObject::class.java), headers, + return create(gson.fromJson(body, AppRequest::class.java), headers, partOfDialogflowRequest) } @@ -220,148 +360,29 @@ internal class AogRequest internal constructor( headers: Map<*, *>? = HashMap(), partOfDialogflowRequest: Boolean = false): AogRequest { - val gsonBuilder = GsonBuilder() - gsonBuilder - .registerTypeAdapter(AppRequest::class.java, - AppRequestDeserializer()) - .registerTypeAdapter(User::class.java, - UserDeserializer()) - .registerTypeAdapter(Input::class.java, - InputDeserializer()) - .registerTypeAdapter(Status::class.java, - StatusDeserializer()) - .registerTypeAdapter(Surface::class.java, - SurfaceDeserializer()) - .registerTypeAdapter(Device::class.java, - DeviceDeserializer()) - .registerTypeAdapter(Location::class.java, - LocationDeserializer()) - .registerTypeAdapter(Argument::class.java, - ArgumentDeserializer()) - .registerTypeAdapter(RawInput::class.java, - RawInputDeserializer()) - .registerTypeAdapter(PackageEntitlement::class.java, - PackageEntitlementDeserializer()) - .registerTypeAdapter(Entitlement::class.java, - EntitlementDeserializer()) - .registerTypeAdapter(SignedData::class.java, - SignedDataDeserializer()) - .registerTypeAdapter(DateTime::class.java, - DateTimeValueDeserializer()) - .registerTypeAdapter(Order::class.java, - OrderDeserializer()) - .registerTypeAdapter(CustomerInfo::class.java, - CustomerInfoDeserializer()) - .registerTypeAdapter(ProposedOrder::class.java, - ProposedOrderDeserializer()) - .registerTypeAdapter(Cart::class.java, - CartDeserializer()) - .registerTypeAdapter(LineItem::class.java, - LineItemDeserializer()) - .registerTypeAdapter(LineItemSubLine::class.java, - LineItemSubLineDeserializer()) - .registerTypeAdapter(Promotion::class.java, - PromotionDeserializer()) - .registerTypeAdapter(Merchant::class.java, - MerchantDeserializer()) - .registerTypeAdapter(Image::class.java, - ImageDeserializer()) - .registerTypeAdapter(Price::class.java, - PriceDeserializer()) - .registerTypeAdapter(Money::class.java, - MoneyDeserializer()) - .registerTypeAdapter(PaymentInfo::class.java, - PaymentInfoDeserializer()) - .registerTypeAdapter(PaymentInfoGoogleProvidedPaymentInstrument::class.java, - PaymentInfoGoogleProvidedPaymentInstrumentDeserializer()) - .registerTypeAdapter(TransactionRequirementsCheckResult::class.java, - TransactionRequirementsCheckResultDeserializer()) - .registerTypeAdapter(OrderV3::class.java, - OrderV3Deserializer()) - .registerTypeAdapter(UserInfo::class.java, - UserInfoDeserializer()) - .registerTypeAdapter(OrderContents::class.java, - OrderContentsDeserializer()) - .registerTypeAdapter(PaymentData::class.java, - PaymentDataDeserializer()) - .registerTypeAdapter(PurchaseOrderExtension::class.java, - PurchaseOrderExtensionDeserializer()) - .registerTypeAdapter(TicketOrderExtension::class.java, - TicketOrderExtensionDeserializer()) - .registerTypeAdapter(MerchantV3::class.java, - MerchantV3Deserializer()) - .registerTypeAdapter(Disclosure::class.java, - DisclosureDeserializer()) - .registerTypeAdapter(Action::class.java, - ActionDeserializer()) - .registerTypeAdapter(PriceAttribute::class.java, - PriceAttributeDeserializer()) - .registerTypeAdapter(PromotionV3::class.java, - PromotionV3Deserializer()) - .registerTypeAdapter(PhoneNumber::class.java, - PhoneNumberDeserializer()) - .registerTypeAdapter(LineItemV3::class.java, - LineItemV3Deserializer()) - .registerTypeAdapter(PaymentInfoV3::class.java, - PaymentInfoV3Deserializer()) - .registerTypeAdapter(PaymentResult::class.java, - PaymentResultDeserializer()) - .registerTypeAdapter(PurchaseFulfillmentInfo::class.java, - PurchaseFulfillmentInfoDeserializer()) - .registerTypeAdapter(PurchaseReturnsInfo::class.java, - PurchaseReturnsInfoDeserializer()) - .registerTypeAdapter(PurchaseError::class.java, - PurchaseErrorDeserializer()) - .registerTypeAdapter(TicketEvent::class.java, - TicketEventDeserializer()) - .registerTypeAdapter(DisclosureText::class.java, - DisclosureTextDeserializer()) - .registerTypeAdapter(DisclosurePresentationOptions::class.java, - DisclosurePresentationOptionsDeserializer()) - .registerTypeAdapter(ActionActionMetadata::class.java, - ActionActionMetadataDeserializer()) - .registerTypeAdapter(OpenUrlAction::class.java, - OpenUrlActionDeserializer()) - .registerTypeAdapter(MoneyV3::class.java, - MoneyV3Deserializer()) - .registerTypeAdapter(PurchaseItemExtension::class.java, - PurchaseItemExtensionDeserializer()) - .registerTypeAdapter(ReservationItemExtension::class.java, - ReservationItemExtensionDeserializer()) - .registerTypeAdapter(PaymentMethodDisplayInfo::class.java, - PaymentMethodDisplayInfoDeserializer()) - .registerTypeAdapter(TimeV3::class.java, - TimeV3Deserializer()) - .registerTypeAdapter(PickupInfo::class.java, - PickupInfoDeserializer()) - .registerTypeAdapter(CheckInInfo::class.java, - CheckInInfoDeserializer()) - .registerTypeAdapter(EventCharacter::class.java, - EventCharacterDeserializer()) - .registerTypeAdapter(DisclosureTextTextLink::class.java, - DisclosureTextTextLinkDeserializer()) - .registerTypeAdapter(AndroidApp::class.java, - AndroidAppDeserializer()) - .registerTypeAdapter(ProductDetails::class.java, - ProductDetailsDeserializer()) - .registerTypeAdapter(MerchantUnitMeasure::class.java, - MerchantUnitMeasureDeserializer()) - .registerTypeAdapter(PurchaseItemExtensionItemOption::class.java, - PurchaseItemExtensionItemOptionDeserializer()) - .registerTypeAdapter(StaffFacilitator::class.java, - StaffFacilitatorDeserializer()) - .registerTypeAdapter(PickupInfoCurbsideInfo::class.java, - PickupInfoCurbsideInfoDeserializer()) - .registerTypeAdapter(AndroidAppVersionFilter::class.java, - AndroidAppVersionFilterDeserializer()) - .registerTypeAdapter(Vehicle::class.java, - VehicleDeserializer()) - .registerTypeAdapter(genericType>(), - ExtensionDeserializer()) - - val gson = gsonBuilder.create() + val appRequest = gson.fromJson(json, AppRequest::class.java) + return create(appRequest, headers, partOfDialogflowRequest) + } + fun create( + inputStream: InputStream, + headers: Map<*, *>? = HashMap(), + partOfDialogflowRequest: Boolean = false + ): AogRequest { + + val appRequest = gson.fromJson( + InputStreamReader(inputStream, Charsets.UTF_8), + AppRequest::class.java + ) + return create(appRequest, headers, partOfDialogflowRequest) + } + + fun create( + appRequest: AppRequest, + headers: Map<*, *>? = HashMap(), + partOfDialogflowRequest: Boolean + ): AogRequest { val aogRequest = create(appRequest) val user = aogRequest.appRequest.user if (user != null) { @@ -387,7 +408,6 @@ internal class AogRequest internal constructor( private fun fromJson(serializedValue: String?): MutableMap { if (serializedValue != null && !serializedValue.isEmpty()) { - val gson = Gson() try { val map: Map = gson.fromJson(serializedValue, object : TypeToken>() {}.type) diff --git a/src/main/kotlin/com/google/actions/api/impl/AogResponse.kt b/src/main/kotlin/com/google/actions/api/impl/AogResponse.kt index 1928a5f..b2587a6 100644 --- a/src/main/kotlin/com/google/actions/api/impl/AogResponse.kt +++ b/src/main/kotlin/com/google/actions/api/impl/AogResponse.kt @@ -22,6 +22,7 @@ import com.google.actions.api.response.ResponseBuilder import com.google.api.services.actions_fulfillment.v2.model.* import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse import com.google.gson.Gson +import java.io.OutputStream import java.util.* internal class AogResponse internal constructor( @@ -85,12 +86,12 @@ internal class AogResponse internal constructor( if (conversationData != null) { val dataMap = HashMap() dataMap["data"] = conversationData - appResponse?.conversationToken = Gson().toJson(dataMap) + appResponse?.conversationToken = gson.toJson(dataMap) } if (userStorage != null) { val dataMap = HashMap() dataMap["data"] = userStorage - appResponse?.userStorage = Gson().toJson(dataMap) + appResponse?.userStorage = gson.toJson(dataMap) } } } @@ -132,7 +133,15 @@ internal class AogResponse internal constructor( appResponse?.expectedInputs = expectedInputs } + override fun writeTo(outputStream: OutputStream) { + ResponseSerializer(sessionId).writeJsonV2To(this, outputStream) + } + override fun toJson(): String { return ResponseSerializer(sessionId).toJsonV2(this) } + + companion object { + private val gson = Gson() + } } diff --git a/src/main/kotlin/com/google/actions/api/impl/DialogflowRequest.kt b/src/main/kotlin/com/google/actions/api/impl/DialogflowRequest.kt index 1a65c97..c4ec049 100644 --- a/src/main/kotlin/com/google/actions/api/impl/DialogflowRequest.kt +++ b/src/main/kotlin/com/google/actions/api/impl/DialogflowRequest.kt @@ -23,6 +23,8 @@ import com.google.api.services.actions_fulfillment.v2.model.* import com.google.api.services.dialogflow_fulfillment.v2.model.* import com.google.gson.* import com.google.gson.reflect.TypeToken +import java.io.InputStream +import java.io.InputStreamReader import java.lang.reflect.Type import java.util.* @@ -168,37 +170,42 @@ internal class DialogflowRequest internal constructor( } companion object { - - fun create(body: String, headers: Map<*, *>?): DialogflowRequest { - val gson = Gson() - return create(gson.fromJson(body, JsonObject::class.java), headers) - } - - fun create(json: JsonObject, headers: Map<*, *>?): DialogflowRequest { - val gsonBuilder = GsonBuilder() - gsonBuilder - .registerTypeAdapter(WebhookRequest::class.java, - WebhookRequestDeserializer()) - .registerTypeAdapter(QueryResult::class.java, - QueryResultDeserializer()) - .registerTypeAdapter(Context::class.java, - ContextDeserializer()) - .registerTypeAdapter(OriginalDetectIntentRequest::class.java, - OriginalDetectIntentRequestDeserializer()) - - val gson = gsonBuilder.create() - val webhookRequest = gson.fromJson(json, - WebhookRequest::class.java) - val aogRequest: AogRequest + private val gson = GsonBuilder() + .registerTypeAdapter(WebhookRequest::class.java, + WebhookRequestDeserializer()) + .registerTypeAdapter(QueryResult::class.java, + QueryResultDeserializer()) + .registerTypeAdapter(Context::class.java, + ContextDeserializer()) + .registerTypeAdapter(OriginalDetectIntentRequest::class.java, + OriginalDetectIntentRequestDeserializer()) + .create() + + fun create(body: String, headers: Map<*, *>?): DialogflowRequest = + create(gson.fromJson(body, WebhookRequest::class.java), headers) + + fun create(json: JsonObject, headers: Map<*, *>?): DialogflowRequest = + create(gson.fromJson(json, WebhookRequest::class.java), headers) + + fun create(inputStream: InputStream, headers: Map<*, *>?): DialogflowRequest = + create( + gson.fromJson(InputStreamReader(inputStream, Charsets.UTF_8), WebhookRequest::class.java), + headers + ) + + private fun create( + webhookRequest: WebhookRequest, + headers: Map<*, *>? + ): DialogflowRequest { val originalDetectIntentRequest = webhookRequest.originalDetectIntentRequest val payload = originalDetectIntentRequest?.payload - if (payload != null) { - aogRequest = AogRequest.create(gson.toJson(payload), headers, + val aogRequest = if (payload != null) { + AogRequest.create(gson.toJson(payload), headers, partOfDialogflowRequest = true) } else { - aogRequest = AogRequest.create(JsonObject(), headers, + AogRequest.create(JsonObject(), headers, partOfDialogflowRequest = true) } diff --git a/src/main/kotlin/com/google/actions/api/impl/DialogflowResponse.kt b/src/main/kotlin/com/google/actions/api/impl/DialogflowResponse.kt index 6526836..d7ef8cc 100644 --- a/src/main/kotlin/com/google/actions/api/impl/DialogflowResponse.kt +++ b/src/main/kotlin/com/google/actions/api/impl/DialogflowResponse.kt @@ -25,6 +25,7 @@ import com.google.api.services.actions_fulfillment.v2.model.AppResponse import com.google.api.services.actions_fulfillment.v2.model.ExpectedIntent import com.google.api.services.actions_fulfillment.v2.model.RichResponse import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse +import java.io.OutputStream internal class DialogflowResponse internal constructor( responseBuilder: ResponseBuilder) : ActionResponse { @@ -59,6 +60,10 @@ internal class DialogflowResponse internal constructor( override val helperIntent: ExpectedIntent? get() = googlePayload?.helperIntent + override fun writeTo(outputStream: OutputStream) { + ResponseSerializer(sessionId).writeJsonV2To(this, outputStream) + } + override fun toJson(): String { return ResponseSerializer(sessionId).toJsonV2(this) } diff --git a/src/main/kotlin/com/google/actions/api/impl/io/ResponseSerializer.kt b/src/main/kotlin/com/google/actions/api/impl/io/ResponseSerializer.kt index 99ce707..3240650 100644 --- a/src/main/kotlin/com/google/actions/api/impl/io/ResponseSerializer.kt +++ b/src/main/kotlin/com/google/actions/api/impl/io/ResponseSerializer.kt @@ -26,6 +26,10 @@ import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse import com.google.gson.Gson import com.google.gson.GsonBuilder import org.slf4j.LoggerFactory +import java.io.OutputStream +import java.io.OutputStreamWriter +import java.io.StringWriter +import java.io.Writer import java.util.* import kotlin.collections.ArrayList import kotlin.collections.set @@ -36,6 +40,7 @@ internal class ResponseSerializer( private companion object { val includeVersionMetadata = false val LOG = LoggerFactory.getLogger(ResponseSerializer::class.java.name) + val gson = GsonBuilder().create() fun getLibraryMetadata(): Map { val metadataProperties = ResourceBundle.getBundle("metadata") @@ -53,19 +58,27 @@ internal class ResponseSerializer( ) } - fun toJsonV2(response: ActionResponse): String { + fun toJsonV2(response: ActionResponse): String = + StringWriter().use { writeJsonV2To(response, it) }.toString() + + fun writeJsonV2To(response: ActionResponse, outputStream: OutputStream) { + val writer = OutputStreamWriter(outputStream, Charsets.UTF_8) + writeJsonV2To(response, writer) + writer.flush() + } + + private fun writeJsonV2To(response: ActionResponse, writer: Writer) { when (response) { - is DialogflowResponse -> return serializeDialogflowResponseV2( - response) - is AogResponse -> return serializeAogResponse(response) + is DialogflowResponse -> serializeDialogflowResponseV2(response, writer) + is AogResponse -> serializeAogResponse(response, writer) } LOG.warn("Unable to serialize the response.") throw Exception("Unable to serialize the response") } private fun serializeDialogflowResponseV2( - dialogflowResponse: DialogflowResponse): String { - val gson = GsonBuilder().create() + dialogflowResponse: DialogflowResponse, + writer: Writer) { val googlePayload = dialogflowResponse.googlePayload val webhookResponse = dialogflowResponse.webhookResponse val conversationData = dialogflowResponse.conversationData @@ -97,7 +110,7 @@ internal class ResponseSerializer( metadata["google_library"] = getLibraryMetadata() webhookResponseMap["metadata"] = metadata } - return gson.toJson(webhookResponseMap) + gson.toJson(webhookResponseMap, writer) } private fun setContext( @@ -194,7 +207,7 @@ internal class ResponseSerializer( if (userStorage != null) { val dataMap = HashMap() dataMap["data"] = userStorage - this.userStorage = Gson().toJson(dataMap) + this.userStorage = gson.toJson(dataMap) } this.isSsml = false } @@ -228,7 +241,7 @@ internal class ResponseSerializer( } @Throws(Exception::class) - private fun serializeAogResponse(aogResponse: AogResponse): String { + private fun serializeAogResponse(aogResponse: AogResponse, writer: Writer) { aogResponse.prepareAppResponse() checkSimpleResponseIsPresent(aogResponse) val appResponseMap = aogResponse.appResponse!!.toMutableMap() @@ -239,7 +252,7 @@ internal class ResponseSerializer( appResponseMap["ResponseMetadata"] = map } - return Gson().toJson(appResponseMap) + gson.toJson(appResponseMap, writer) } @Throws(Exception::class) diff --git a/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeApp.kt b/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeApp.kt index 2957b56..a2b0abe 100644 --- a/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeApp.kt +++ b/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeApp.kt @@ -23,6 +23,7 @@ import com.google.home.graph.v1.HomeGraphApiServiceProto import io.grpc.ManagedChannelBuilder import io.grpc.auth.MoreCallCredentials import java.io.FileInputStream +import java.io.InputStream import java.util.concurrent.CompletableFuture abstract class SmartHomeApp : App { @@ -49,6 +50,18 @@ abstract class SmartHomeApp : App { return SmartHomeRequest.create(inputJson) } + /** + * Builds a [SmartHomeRequest] object from an [InputStream]. + * + * This is semantically equivalent as reading the input stream as an UTF-8 string and then calling createRequest + * with the resulting string. + * + * @param inputStream The input stream to read from. The stream must be closed by the caller. + * @return A parsed request object + */ + fun createRequest(inputStream: InputStream): SmartHomeRequest = + SmartHomeRequest.create(inputStream) + /** * The intent handler for action.devices.SYNC that is implemented in your smart home Action * @@ -140,17 +153,24 @@ abstract class SmartHomeApp : App { return try { val request = createRequest(inputJson) - val response = routeRequest(request, headers) - - val future: CompletableFuture = CompletableFuture() - future.complete(response) - future.thenApply { this.getAsJson(it) } - .exceptionally { throwable -> throwable.message } + handleRequest(request, headers) + .thenApply { getAsJson(it) } + .exceptionally { throwable -> throwable.message } } catch (e: Exception) { handleError(e) } } + fun handleRequest(request: SmartHomeRequest, headers: Map<*, *>?): CompletableFuture = + try { + val response = routeRequest(request, headers) + CompletableFuture.completedFuture(response) + } catch (e: Exception) { + CompletableFuture() + .apply { completeExceptionally(e) } + } + + @Throws(Exception::class) private fun routeRequest(request: SmartHomeRequest, headers: Map<*, *>?): SmartHomeResponse { when (request.javaClass) { diff --git a/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeRequest.kt b/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeRequest.kt index 928f667..8187026 100644 --- a/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeRequest.kt +++ b/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeRequest.kt @@ -17,6 +17,10 @@ package com.google.actions.api.smarthome import org.json.JSONObject +import org.json.JSONTokener +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader /** * A representation of the JSON payload received during a smart home request. @@ -32,8 +36,13 @@ open class SmartHomeRequest { } companion object { - fun create(inputJson: String): SmartHomeRequest { - val json = JSONObject(inputJson) + fun create(inputStream: InputStream): SmartHomeRequest = + create(JSONObject(JSONTokener(InputStreamReader(inputStream, Charsets.UTF_8)))) + + fun create(inputJson: String): SmartHomeRequest = + create(JSONObject(inputJson)) + + private fun create(json: JSONObject): SmartHomeRequest { val requestId = json.getString("requestId") val inputs = json.getJSONArray("inputs") val request = inputs.getJSONObject(0) diff --git a/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeResponse.kt b/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeResponse.kt index a21ec61..48ee457 100644 --- a/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeResponse.kt +++ b/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeResponse.kt @@ -22,6 +22,8 @@ import com.google.protobuf.Struct import com.google.protobuf.util.JsonFormat import org.json.JSONException import org.json.JSONObject +import java.io.OutputStream +import java.io.OutputStreamWriter /** * A representation of the JSON payload that should be sent during a smart home request. @@ -29,6 +31,12 @@ import org.json.JSONObject * @see Public documentation */ open class SmartHomeResponse { + open fun writeTo(outputStream: OutputStream) { + val writer = OutputStreamWriter(outputStream, Charsets.UTF_8) + build().write(writer) + writer.flush() + } + open fun build(): JSONObject { return JSONObject() // Return empty object } diff --git a/src/main/kotlin/com/google/actions/api/test/MockRequestBuilder.kt b/src/main/kotlin/com/google/actions/api/test/MockRequestBuilder.kt index a4d1089..519bef4 100644 --- a/src/main/kotlin/com/google/actions/api/test/MockRequestBuilder.kt +++ b/src/main/kotlin/com/google/actions/api/test/MockRequestBuilder.kt @@ -134,8 +134,6 @@ class MockRequestBuilder() { } fun build(): ActionRequest { - val gson = Gson() - if (userProfile != null) { user.profile = userProfile } @@ -209,6 +207,8 @@ class MockRequestBuilder() { } companion object PreBuilt { + private val gson = Gson() + fun welcome( intent: String, usesDialogflow: Boolean = true): MockRequestBuilder { diff --git a/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeRequestTest.kt b/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeRequestTest.kt index 9542ad0..255a5b5 100644 --- a/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeRequestTest.kt +++ b/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeRequestTest.kt @@ -37,6 +37,12 @@ class SmartHomeRequestTest { return SmartHomeRequest.create(json.toString()) } + @Throws(IOException::class) + private fun fromStream(file: String): SmartHomeRequest { + val absolutePath = Paths.get("src", "test", "resources", file) + return Files.newInputStream(absolutePath).use { SmartHomeRequest.create(it) } + } + @Test @Throws(Exception::class) fun basicSyncJsonIsParsed() { @@ -47,6 +53,16 @@ class SmartHomeRequestTest { Assert.assertEquals(request.inputs[0].intent, "action.devices.SYNC") } + @Test + @Throws(Exception::class) + fun basicSyncStreamIsParsed() { + val request = fromStream("smarthome_sync_request.json") as SyncRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.SYNC") + } + @Test @Throws(Exception::class) fun basicQueryJsonIsParsed() { @@ -63,6 +79,22 @@ class SmartHomeRequestTest { Assert.assertEquals(payload.devices[1].id, "456") } + @Test + @Throws(Exception::class) + fun basicQueryStreamIsParsed() { + val request = fromStream("smarthome_query_request.json") as QueryRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.QUERY") + + val payload = (request.inputs[0] as QueryRequest.Inputs).payload + Assert.assertEquals(payload.devices.size, 2) + Assert.assertEquals(payload.devices[0].id, "123") + + Assert.assertEquals(payload.devices[1].id, "456") + } + @Test @Throws(Exception::class) fun customDataQueryJsonIsParsed() { @@ -81,6 +113,24 @@ class SmartHomeRequestTest { Assert.assertEquals(payload.devices[1].customData!!["fooValue"], 12) } + @Test + @Throws(Exception::class) + fun customDataQueryStreamIsParsed() { + val request = fromStream("smarthome_query_customdata_request.json") as QueryRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.QUERY") + + val payload = (request.inputs[0] as QueryRequest.Inputs).payload + Assert.assertEquals(payload.devices.size, 2) + Assert.assertEquals(payload.devices[0].id, "123") + Assert.assertEquals(payload.devices[0].customData!!["fooValue"], 74) + + Assert.assertEquals(payload.devices[1].id, "456") + Assert.assertEquals(payload.devices[1].customData!!["fooValue"], 12) + } + @Test @Throws(Exception::class) fun basicExecuteJsonIsParsed() { @@ -100,6 +150,25 @@ class SmartHomeRequestTest { Assert.assertEquals(payload.commands[0].execution[0].params!!["on"], true) } + @Test + @Throws(Exception::class) + fun basicExecuteStreamIsParsed() { + val request = fromStream("smarthome_execute_request.json") as ExecuteRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.EXECUTE") + + val payload = (request.inputs[0] as ExecuteRequest.Inputs).payload + Assert.assertEquals(payload.commands.size, 1) + Assert.assertEquals(payload.commands[0].devices.size, 2) + Assert.assertEquals(payload.commands[0].devices[0].id, "123") + Assert.assertEquals(payload.commands[0].devices[1].id, "456") + Assert.assertEquals(payload.commands[0].execution.size, 1) + Assert.assertEquals(payload.commands[0].execution[0].command, "action.devices.commands.OnOff") + Assert.assertEquals(payload.commands[0].execution[0].params!!["on"], true) + } + @Test @Throws(Exception::class) fun twoFactorExecuteJsonIsParsed() { @@ -115,6 +184,21 @@ class SmartHomeRequestTest { Assert.assertEquals(payload.commands[0].execution[1].challenge!!["ack"], true) } + @Test + @Throws(Exception::class) + fun twoFactorExecuteStreamIsParsed() { + val request = fromStream("smarthome_execute_2fa_request.json") as ExecuteRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.EXECUTE") + + val payload = (request.inputs[0] as ExecuteRequest.Inputs).payload + Assert.assertEquals(payload.commands[0].execution.size, 2) + Assert.assertEquals(payload.commands[0].execution[0].challenge!!["pin"], "333222") + Assert.assertEquals(payload.commands[0].execution[1].challenge!!["ack"], true) + } + @Test @Throws(Exception::class) fun customDataExecuteJsonIsParsed() { @@ -136,6 +220,27 @@ class SmartHomeRequestTest { Assert.assertEquals(payload.commands[0].execution[0].params!!["on"], true) } + @Test + @Throws(Exception::class) + fun customDataExecuteStreamIsParsed() { + val request = fromStream("smarthome_execute_customdata_request.json") as ExecuteRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.EXECUTE") + + val payload = (request.inputs[0] as ExecuteRequest.Inputs).payload + Assert.assertEquals(payload.commands.size, 1) + Assert.assertEquals(payload.commands[0].devices.size, 2) + Assert.assertEquals(payload.commands[0].devices[0].id, "123") + Assert.assertEquals(payload.commands[0].devices[0].customData!!["fooValue"], 74) + Assert.assertEquals(payload.commands[0].devices[1].id, "456") + Assert.assertEquals(payload.commands[0].devices[1].customData!!["fooValue"], 36) + Assert.assertEquals(payload.commands[0].execution.size, 1) + Assert.assertEquals(payload.commands[0].execution[0].command, "action.devices.commands.OnOff") + Assert.assertEquals(payload.commands[0].execution[0].params!!["on"], true) + } + @Test @Throws(Exception::class) fun dockExecuteJsonIsParsed() { @@ -153,6 +258,23 @@ class SmartHomeRequestTest { Assert.assertEquals(payload.commands[0].execution[0].command, "action.devices.commands.Dock") } + @Test + @Throws(Exception::class) + fun dockExecuteStreamIsParsed() { + val request = fromStream("smarthome_execute_dock_request.json") as ExecuteRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.EXECUTE") + + val payload = (request.inputs[0] as ExecuteRequest.Inputs).payload + Assert.assertEquals(payload.commands.size, 1) + Assert.assertEquals(payload.commands[0].devices.size, 1) + Assert.assertEquals(payload.commands[0].devices[0].id, "vacuumJawn") + Assert.assertEquals(payload.commands[0].execution.size, 1) + Assert.assertEquals(payload.commands[0].execution[0].command, "action.devices.commands.Dock") + } + @Test @Throws(Exception::class) fun basicDisconnectJsonIsParsed() { @@ -163,4 +285,14 @@ class SmartHomeRequestTest { Assert.assertEquals(request.inputs[0].intent, "action.devices.DISCONNECT") } + @Test + @Throws(Exception::class) + fun basicDisconnectStreamIsParsed() { + val request = fromStream("smarthome_disconnect_request.json") as DisconnectRequest + Assert.assertNotNull(request) + Assert.assertNotNull(request.requestId) + Assert.assertEquals(request.inputs.size, 1) + Assert.assertEquals(request.inputs[0].intent, "action.devices.DISCONNECT") + } + } \ No newline at end of file diff --git a/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeResponseTest.kt b/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeResponseTest.kt index 3aec110..106674c 100644 --- a/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeResponseTest.kt +++ b/src/test/kotlin/com/google/actions/api/smarthome/SmartHomeResponseTest.kt @@ -20,6 +20,7 @@ import com.google.home.graph.v1.DeviceProto import org.json.JSONObject import org.junit.Assert import org.junit.Test +import java.io.ByteArrayOutputStream import java.io.IOException import java.nio.file.Files import java.nio.file.Paths @@ -35,8 +36,8 @@ class SmartHomeResponseTest { @Test fun testResponseRoute() { - val request = fromFile("smarthome_sync_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_sync_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -56,11 +57,20 @@ class SmartHomeResponseTest { } } - app.handleRequest(request, null) // This should call onSync, or it will fail + app.handleRequest(requestJson, null).get() // This should call onSync, or it will fail + app.handleRequest(app.createRequest(requestJson), null).get() // This should call onSync, or it will fail + + val erroneousRequestJson = fromFile("smarthome_query_request.json") + try { + app.handleRequest(erroneousRequestJson, null) + // This should fail + Assert.fail("The expected request is not implemented") + } catch (e: kotlin.NotImplementedError) { + // Caught the exception + } try { - val erroneousRequest = fromFile("smarthome_query_request.json") - app.handleRequest(erroneousRequest, null) + app.handleRequest(app.createRequest(erroneousRequestJson), null) // This should fail Assert.fail("The expected request is not implemented") } catch (e: kotlin.NotImplementedError) { @@ -70,8 +80,8 @@ class SmartHomeResponseTest { @Test fun testSyncResponse() { - val request = fromFile("smarthome_sync_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_sync_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -173,17 +183,23 @@ class SmartHomeResponseTest { } } - val jsonString = app.handleRequest(request, null).get() // This should call onSync + val jsonString = app.handleRequest(requestJson, null).get() // This should call onSync val expectedJson = fromFile("smarthome_sync_response.json") .replace(Regex("\n\\s*"), "") // Remove newlines .replace(Regex(":\\s"), ":") // Remove space after colon Assert.assertEquals(expectedJson, jsonString) + + val response = app.handleRequest(app.createRequest(requestJson), null).get() + Assert.assertEquals(expectedJson, ByteArrayOutputStream().use { + response.writeTo(it) + it.toString("UTF-8") + }) } @Test fun testLocalSyncResponse() { - val request = fromFile("smarthome_sync_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_sync_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -234,17 +250,24 @@ class SmartHomeResponseTest { } } - val jsonString = app.handleRequest(request, null).get() // This should call onSync + val jsonString = app.handleRequest(requestJson, null).get() // This should call onSync val expectedJson = fromFile("smarthome_sync_response_local.json") .replace(Regex("\n\\s*"), "") // Remove newlines .replace(Regex(":\\s"), ":") // Remove space after colon Assert.assertEquals(expectedJson, jsonString) + + val request = app.createRequest(requestJson) + val response = app.handleRequest(request, null).get() // This should call onSync + Assert.assertEquals(expectedJson, ByteArrayOutputStream().use { + response.writeTo(it) + it.toString("UTF-8") + }) } @Test fun testSyncResponseWithTraitList() { - val request = fromFile("smarthome_sync_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_sync_request.json") + Assert.assertNotNull(requestJson) val traitListOutlet = mutableListOf("action.devices.traits.OnOff") val traitListLight = mutableListOf( "action.devices.traits.OnOff", @@ -350,11 +373,18 @@ class SmartHomeResponseTest { } } - val jsonString = app.handleRequest(request, null).get() // This should call onSync + val jsonString = app.handleRequest(requestJson, null).get() // This should call onSync val expectedJson = fromFile("smarthome_sync_response.json") .replace(Regex("\n\\s*"), "") // Remove newlines .replace(Regex(":\\s"), ":") // Remove space after colon Assert.assertEquals(expectedJson, jsonString) + + val request = app.createRequest(requestJson) + val response = app.handleRequest(request, null).get() // This should call onSync + Assert.assertEquals(expectedJson, ByteArrayOutputStream().use { + response.writeTo(it) + it.toString("UTF-8") + }) } @Test fun testSyncResponseCustomData() { @@ -407,8 +437,8 @@ class SmartHomeResponseTest { @Test fun testQueryRoute() { - val request = fromFile("smarthome_query_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_query_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -428,13 +458,15 @@ class SmartHomeResponseTest { } } - app.handleRequest(request, null) // This should call onQuery, or it will fail + app.handleRequest(requestJson, null).get() // This should call onQuery, or it will fail + val request = app.createRequest(requestJson) + app.handleRequest(request, null).get() // This should call onQuery, or it will fail } @Test fun testQueryResponse() { - val request = fromFile("smarthome_query_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_query_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -473,17 +505,25 @@ class SmartHomeResponseTest { } } - val jsonString = app.handleRequest(request, null).get() // This should call onSync + val jsonString = app.handleRequest(requestJson, null).get() // This should call onQuery val expectedJson = fromFile("smarthome_query_response.json") .replace(Regex("\n\\s*"), "") // Remove newlines .replace(Regex(":\\s"), ":") // Remove space after colon Assert.assertEquals(expectedJson, jsonString) + + val request = app.createRequest(requestJson) + val response = app.handleRequest(request, null).get() // This should call onQuery + Assert.assertEquals(expectedJson, ByteArrayOutputStream().use { + response.writeTo(it) + it.toString("UTF-8") + }) + } @Test fun testExecuteRoute() { - val request = fromFile("smarthome_execute_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_execute_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -503,13 +543,15 @@ class SmartHomeResponseTest { } } - app.handleRequest(request, null) // This should call onExecute, or it will fail + app.handleRequest(requestJson, null).get() // This should call onExecute, or it will fail + val request = app.createRequest(requestJson) + app.handleRequest(request, null).get() } @Test fun testExecuteResponse() { - val request = fromFile("smarthome_execute_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_execute_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -551,17 +593,24 @@ class SmartHomeResponseTest { } } - val jsonString = app.handleRequest(request, null).get() // This should call onSync + val jsonString = app.handleRequest(requestJson, null).get() // This should call onExecute val expectedJson = fromFile("smarthome_execute_response.json") .replace(Regex("\n\\s*"), "") // Remove newlines .replace(Regex(":\\s"), ":") // Remove space after colon Assert.assertEquals(expectedJson, jsonString) + + val request = app.createRequest(requestJson) + val response = app.handleRequest(request, null).get() // This should call onExecute + Assert.assertEquals(expectedJson, ByteArrayOutputStream().use { + response.writeTo(it) + it.toString("UTF-8") + }) } @Test fun testExecute2FAResponse() { - val request = fromFile("smarthome_execute_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_execute_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -606,17 +655,24 @@ class SmartHomeResponseTest { } } - val jsonString = app.handleRequest(request, null).get() // This should call onSync + val jsonString = app.handleRequest(requestJson, null).get() // This should call onExecute val expectedJson = fromFile("smarthome_execute_2fa_response.json") .replace(Regex("\n\\s*"), "") // Remove newlines .replace(Regex(":\\s"), ":") // Remove space after colon Assert.assertEquals(expectedJson, jsonString) + + val request = app.createRequest(requestJson) + val response = app.handleRequest(request, null).get() // This should call onExecute + Assert.assertEquals(expectedJson, ByteArrayOutputStream().use { + response.writeTo(it) + it.toString("UTF-8") + }) } @Test fun testDisconnectRoute() { - val request = fromFile("smarthome_disconnect_request.json") - Assert.assertNotNull(request) + val requestJson = fromFile("smarthome_disconnect_request.json") + Assert.assertNotNull(requestJson) val app = object : SmartHomeApp() { override fun onSync(request: SyncRequest, headers: Map<*, *>?): SyncResponse { @@ -634,6 +690,8 @@ class SmartHomeResponseTest { override fun onDisconnect(request: DisconnectRequest, headers: Map<*, *>?): Unit {} } - app.handleRequest(request, null) // This should call onDisconnect, or it will fail + app.handleRequest(requestJson, null).get() // This should call onDisconnect, or it will fail + val request = app.createRequest(requestJson) + app.handleRequest(request, null).get() // This should call onDisconnect, or it will fail } } \ No newline at end of file