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..cd5033f 100644 --- a/src/main/kotlin/com/google/actions/api/impl/AogRequest.kt +++ b/src/main/kotlin/com/google/actions/api/impl/AogRequest.kt @@ -16,16 +16,165 @@ package com.google.actions.api.impl -import com.google.actions.api.* -import com.google.actions.api.impl.io.* -import com.google.api.services.actions_fulfillment.v2.model.* +import com.google.actions.api.ARG_CONFIRMATION +import com.google.actions.api.ARG_DATETIME +import com.google.actions.api.ARG_IS_FINAL_REPROMPT +import com.google.actions.api.ARG_MEDIA_STATUS +import com.google.actions.api.ARG_OPTION +import com.google.actions.api.ARG_PERMISSION +import com.google.actions.api.ARG_PLACE +import com.google.actions.api.ARG_REGISTER_UPDATE +import com.google.actions.api.ARG_REPROMPT_COUNT +import com.google.actions.api.ARG_SIGN_IN +import com.google.actions.api.ActionContext +import com.google.actions.api.ActionRequest +import com.google.actions.api.impl.io.ActionActionMetadataDeserializer +import com.google.actions.api.impl.io.ActionDeserializer +import com.google.actions.api.impl.io.AndroidAppDeserializer +import com.google.actions.api.impl.io.AndroidAppVersionFilterDeserializer +import com.google.actions.api.impl.io.AppRequestDeserializer +import com.google.actions.api.impl.io.ArgumentDeserializer +import com.google.actions.api.impl.io.CartDeserializer +import com.google.actions.api.impl.io.CheckInInfoDeserializer +import com.google.actions.api.impl.io.CustomerInfoDeserializer +import com.google.actions.api.impl.io.DateTimeValueDeserializer +import com.google.actions.api.impl.io.DeviceDeserializer +import com.google.actions.api.impl.io.DisclosureDeserializer +import com.google.actions.api.impl.io.DisclosurePresentationOptionsDeserializer +import com.google.actions.api.impl.io.DisclosureTextDeserializer +import com.google.actions.api.impl.io.DisclosureTextTextLinkDeserializer +import com.google.actions.api.impl.io.EntitlementDeserializer +import com.google.actions.api.impl.io.EventCharacterDeserializer +import com.google.actions.api.impl.io.ExtensionDeserializer +import com.google.actions.api.impl.io.ImageDeserializer +import com.google.actions.api.impl.io.InputDeserializer +import com.google.actions.api.impl.io.LineItemDeserializer +import com.google.actions.api.impl.io.LineItemSubLineDeserializer +import com.google.actions.api.impl.io.LineItemV3Deserializer +import com.google.actions.api.impl.io.LocationDeserializer +import com.google.actions.api.impl.io.MerchantDeserializer +import com.google.actions.api.impl.io.MerchantUnitMeasureDeserializer +import com.google.actions.api.impl.io.MerchantV3Deserializer +import com.google.actions.api.impl.io.MoneyDeserializer +import com.google.actions.api.impl.io.MoneyV3Deserializer +import com.google.actions.api.impl.io.OpenUrlActionDeserializer +import com.google.actions.api.impl.io.OrderContentsDeserializer +import com.google.actions.api.impl.io.OrderDeserializer +import com.google.actions.api.impl.io.OrderV3Deserializer +import com.google.actions.api.impl.io.PackageEntitlementDeserializer +import com.google.actions.api.impl.io.PaymentDataDeserializer +import com.google.actions.api.impl.io.PaymentInfoDeserializer +import com.google.actions.api.impl.io.PaymentInfoGoogleProvidedPaymentInstrumentDeserializer +import com.google.actions.api.impl.io.PaymentInfoV3Deserializer +import com.google.actions.api.impl.io.PaymentMethodDisplayInfoDeserializer +import com.google.actions.api.impl.io.PaymentResultDeserializer +import com.google.actions.api.impl.io.PhoneNumberDeserializer +import com.google.actions.api.impl.io.PickupInfoCurbsideInfoDeserializer +import com.google.actions.api.impl.io.PickupInfoDeserializer +import com.google.actions.api.impl.io.PriceAttributeDeserializer +import com.google.actions.api.impl.io.PriceDeserializer +import com.google.actions.api.impl.io.ProductDetailsDeserializer +import com.google.actions.api.impl.io.PromotionDeserializer +import com.google.actions.api.impl.io.PromotionV3Deserializer +import com.google.actions.api.impl.io.ProposedOrderDeserializer +import com.google.actions.api.impl.io.PurchaseErrorDeserializer +import com.google.actions.api.impl.io.PurchaseFulfillmentInfoDeserializer +import com.google.actions.api.impl.io.PurchaseItemExtensionDeserializer +import com.google.actions.api.impl.io.PurchaseItemExtensionItemOptionDeserializer +import com.google.actions.api.impl.io.PurchaseOrderExtensionDeserializer +import com.google.actions.api.impl.io.PurchaseReturnsInfoDeserializer +import com.google.actions.api.impl.io.RawInputDeserializer +import com.google.actions.api.impl.io.ReservationItemExtensionDeserializer +import com.google.actions.api.impl.io.SignedDataDeserializer +import com.google.actions.api.impl.io.StaffFacilitatorDeserializer +import com.google.actions.api.impl.io.StatusDeserializer +import com.google.actions.api.impl.io.SurfaceDeserializer +import com.google.actions.api.impl.io.TicketEventDeserializer +import com.google.actions.api.impl.io.TicketOrderExtensionDeserializer +import com.google.actions.api.impl.io.TimeV3Deserializer +import com.google.actions.api.impl.io.TransactionRequirementsCheckResultDeserializer +import com.google.actions.api.impl.io.UserDeserializer +import com.google.actions.api.impl.io.UserInfoDeserializer +import com.google.actions.api.impl.io.VehicleDeserializer +import com.google.actions.api.impl.io.genericType +import com.google.api.services.actions_fulfillment.v2.model.Action +import com.google.api.services.actions_fulfillment.v2.model.ActionActionMetadata +import com.google.api.services.actions_fulfillment.v2.model.AndroidApp +import com.google.api.services.actions_fulfillment.v2.model.AndroidAppVersionFilter +import com.google.api.services.actions_fulfillment.v2.model.AppRequest +import com.google.api.services.actions_fulfillment.v2.model.Argument +import com.google.api.services.actions_fulfillment.v2.model.Cart +import com.google.api.services.actions_fulfillment.v2.model.CheckInInfo +import com.google.api.services.actions_fulfillment.v2.model.CustomerInfo +import com.google.api.services.actions_fulfillment.v2.model.DateTime +import com.google.api.services.actions_fulfillment.v2.model.Device +import com.google.api.services.actions_fulfillment.v2.model.Disclosure +import com.google.api.services.actions_fulfillment.v2.model.DisclosurePresentationOptions +import com.google.api.services.actions_fulfillment.v2.model.DisclosureText +import com.google.api.services.actions_fulfillment.v2.model.DisclosureTextTextLink +import com.google.api.services.actions_fulfillment.v2.model.Entitlement +import com.google.api.services.actions_fulfillment.v2.model.EventCharacter +import com.google.api.services.actions_fulfillment.v2.model.Image +import com.google.api.services.actions_fulfillment.v2.model.Input +import com.google.api.services.actions_fulfillment.v2.model.LineItem +import com.google.api.services.actions_fulfillment.v2.model.LineItemSubLine +import com.google.api.services.actions_fulfillment.v2.model.LineItemV3 +import com.google.api.services.actions_fulfillment.v2.model.Location +import com.google.api.services.actions_fulfillment.v2.model.Merchant +import com.google.api.services.actions_fulfillment.v2.model.MerchantUnitMeasure +import com.google.api.services.actions_fulfillment.v2.model.MerchantV3 +import com.google.api.services.actions_fulfillment.v2.model.Money +import com.google.api.services.actions_fulfillment.v2.model.MoneyV3 +import com.google.api.services.actions_fulfillment.v2.model.OpenUrlAction +import com.google.api.services.actions_fulfillment.v2.model.Order +import com.google.api.services.actions_fulfillment.v2.model.OrderContents +import com.google.api.services.actions_fulfillment.v2.model.OrderV3 +import com.google.api.services.actions_fulfillment.v2.model.PackageEntitlement +import com.google.api.services.actions_fulfillment.v2.model.PaymentData +import com.google.api.services.actions_fulfillment.v2.model.PaymentInfo +import com.google.api.services.actions_fulfillment.v2.model.PaymentInfoGoogleProvidedPaymentInstrument +import com.google.api.services.actions_fulfillment.v2.model.PaymentInfoV3 +import com.google.api.services.actions_fulfillment.v2.model.PaymentMethodDisplayInfo +import com.google.api.services.actions_fulfillment.v2.model.PaymentResult +import com.google.api.services.actions_fulfillment.v2.model.PhoneNumber +import com.google.api.services.actions_fulfillment.v2.model.PickupInfo +import com.google.api.services.actions_fulfillment.v2.model.PickupInfoCurbsideInfo +import com.google.api.services.actions_fulfillment.v2.model.Price +import com.google.api.services.actions_fulfillment.v2.model.PriceAttribute +import com.google.api.services.actions_fulfillment.v2.model.ProductDetails +import com.google.api.services.actions_fulfillment.v2.model.Promotion +import com.google.api.services.actions_fulfillment.v2.model.PromotionV3 +import com.google.api.services.actions_fulfillment.v2.model.ProposedOrder +import com.google.api.services.actions_fulfillment.v2.model.PurchaseError +import com.google.api.services.actions_fulfillment.v2.model.PurchaseFulfillmentInfo +import com.google.api.services.actions_fulfillment.v2.model.PurchaseItemExtension +import com.google.api.services.actions_fulfillment.v2.model.PurchaseItemExtensionItemOption +import com.google.api.services.actions_fulfillment.v2.model.PurchaseOrderExtension +import com.google.api.services.actions_fulfillment.v2.model.PurchaseReturnsInfo +import com.google.api.services.actions_fulfillment.v2.model.RawInput +import com.google.api.services.actions_fulfillment.v2.model.ReservationItemExtension +import com.google.api.services.actions_fulfillment.v2.model.SignedData +import com.google.api.services.actions_fulfillment.v2.model.StaffFacilitator +import com.google.api.services.actions_fulfillment.v2.model.Status +import com.google.api.services.actions_fulfillment.v2.model.Surface +import com.google.api.services.actions_fulfillment.v2.model.TicketEvent +import com.google.api.services.actions_fulfillment.v2.model.TicketOrderExtension +import com.google.api.services.actions_fulfillment.v2.model.TimeV3 +import com.google.api.services.actions_fulfillment.v2.model.TransactionRequirementsCheckResult +import com.google.api.services.actions_fulfillment.v2.model.User +import com.google.api.services.actions_fulfillment.v2.model.UserInfo +import com.google.api.services.actions_fulfillment.v2.model.Vehicle import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookRequest import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonObject import com.google.gson.reflect.TypeToken import org.slf4j.LoggerFactory -import java.util.* +import java.io.InputStream +import java.io.InputStreamReader +import java.util.ArrayList +import java.util.HashMap +import java.util.Locale internal class AogRequest internal constructor( override val appRequest: AppRequest) : ActionRequest { @@ -201,6 +350,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 +498,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 +507,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), + 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) { @@ -379,7 +547,7 @@ internal class AogRequest internal constructor( // conversationData is empty here. DialogflowRequest should contain the // values as it is read from outputContext. aogRequest.conversationData = fromJson( - conversation.conversationToken) + conversation.conversationToken) } } return aogRequest 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..97a3566 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( @@ -132,6 +133,10 @@ 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) } 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..d8ae3ed 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,38 +170,43 @@ 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), WebhookRequest::class.java), + headers + ) + + private fun create( + webhookRequest: WebhookRequest, + headers: Map<*, *>? + ): DialogflowRequest { val originalDetectIntentRequest = - webhookRequest.originalDetectIntentRequest + webhookRequest.originalDetectIntentRequest val payload = originalDetectIntentRequest?.payload - if (payload != null) { - aogRequest = AogRequest.create(gson.toJson(payload), headers, - partOfDialogflowRequest = true) + val aogRequest = if (payload != null) { + AogRequest.create(gson.toJson(payload), headers, + partOfDialogflowRequest = true) } else { - aogRequest = AogRequest.create(JsonObject(), headers, - partOfDialogflowRequest = true) + AogRequest.create(JsonObject(), headers, + partOfDialogflowRequest = true) } return DialogflowRequest(webhookRequest, aogRequest) 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..89f9828 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 @@ -53,18 +57,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 { + dialogflowResponse: DialogflowResponse, + writer: Writer) { val gson = GsonBuilder().create() val googlePayload = dialogflowResponse.googlePayload val webhookResponse = dialogflowResponse.webhookResponse @@ -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( @@ -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..d2fcbd8 100644 --- a/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeRequest.kt +++ b/src/main/kotlin/com/google/actions/api/smarthome/SmartHomeRequest.kt @@ -16,7 +16,11 @@ package com.google.actions.api.smarthome +import com.google.gson.JsonObject import org.json.JSONObject +import org.json.JSONTokener +import java.io.IOException +import java.io.InputStream /** * 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(inputStream))) + + 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..e7d9a8b 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,12 @@ import com.google.protobuf.Struct import com.google.protobuf.util.JsonFormat import org.json.JSONException import org.json.JSONObject +import org.json.JSONStringer +import org.json.JSONTokener +import org.json.JSONWriter +import java.io.OutputStream +import java.io.OutputStreamWriter +import java.io.StringWriter /** * A representation of the JSON payload that should be sent during a smart home request. @@ -29,6 +35,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/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