UNPKG

aws-iot-device-sdk-v2

Version:
425 lines (331 loc) 13.7 kB
/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ import {CrtError, mqtt_request_response} from "aws-crt"; import {ServiceError} from "./mqtt_request_response"; export interface StreamingOperationConfig { operationName: string, serviceModel: RequestResponseServiceModel, client: mqtt_request_response.RequestResponseClient, modelConfig: any, } export interface RequestResponseOperationConfig { operationName: string, serviceModel: RequestResponseServiceModel, client: mqtt_request_response.RequestResponseClient, request: any, } export type MessageDeserializer = (payload: ArrayBuffer) => any; export interface RequestResponsePath { topic: string, correlationTokenJsonPath?: string, deserializer: MessageDeserializer; } export interface RequestResponseOperationModel { inputShapeName: string; payloadTransformer: (request: any) => ArrayBuffer; subscriptionGenerator: (request: any) => Array<string>; responsePathGenerator: (request: any) => Array<RequestResponsePath>; publishTopicGenerator: (request: any) => string; correlationTokenApplicator: (request: any) => [any, string | undefined]; } export interface StreamingOperationModel { inputShapeName: string; subscriptionGenerator: (config: any) => string; deserializer: MessageDeserializer; } export interface RequestResponseServiceModel { // operation name -> operation model requestResponseOperations: Map<string, RequestResponseOperationModel>, // operation name -> operation model streamingOperations: Map<string, StreamingOperationModel>, // shape name -> validator function shapeValidators: Map<string, (value: any) => void>; } function buildResponseDeserializerMap(paths: Array<RequestResponsePath>) : Map<string, MessageDeserializer> { return new Map<string, MessageDeserializer>( paths.map((path) => { return [path.topic, path.deserializer]; }) ); } function buildResponsePaths(paths: Array<RequestResponsePath>) : Array<mqtt_request_response.ResponsePath> { return paths.map((path) => { let responsePath : mqtt_request_response.ResponsePath = { topic: path.topic }; if (path.correlationTokenJsonPath) { responsePath.correlationTokenJsonPath = path.correlationTokenJsonPath; } return responsePath; }); } export async function doRequestResponse<ResponseType>(options: RequestResponseOperationConfig) : Promise<ResponseType> { return new Promise<ResponseType>(async (resolve, reject) => { try { let operationModel = options.serviceModel.requestResponseOperations.get(options.operationName); if (!operationModel) { reject(createServiceError(`Operation "${options.operationName}" not in client's service model`)); return; } let validator = options.serviceModel.shapeValidators.get(operationModel.inputShapeName); if (!validator) { reject(createServiceError(`Operation "${options.operationName}" does not have an input validator`)); return; } validator(options.request); let publishTopic = operationModel.publishTopicGenerator(options.request); let subscriptionsNeeded = operationModel.subscriptionGenerator(options.request); let modelPaths = operationModel.responsePathGenerator(options.request); let deserializerMap = buildResponseDeserializerMap(modelPaths); let responsePaths = buildResponsePaths(modelPaths); let [request, correlationToken] = operationModel.correlationTokenApplicator(options.request); let payload = operationModel.payloadTransformer(request); let requestOptions: mqtt_request_response.RequestResponseOperationOptions = { subscriptionTopicFilters: subscriptionsNeeded, responsePaths: responsePaths, publishTopic: publishTopic, payload: payload }; if (correlationToken) { requestOptions.correlationToken = correlationToken; } let response = await options.client.submitRequest(requestOptions); let responseTopic = response.topic; let wasSuccess = responseTopic.endsWith("accepted"); // May need to eventually model let responsePayload = response.payload; let deserializer = deserializerMap.get(responseTopic); if (!deserializer) { reject(createServiceError(`Operation "${options.operationName}" does not have a deserializer for topic "${responseTopic}"`)); return; } let deserializedResponse = deserializer(responsePayload) as ResponseType; if (wasSuccess) { resolve(deserializedResponse); } else { reject(createServiceError("Request failed", undefined, deserializedResponse)); } } catch (err) { if (err instanceof ServiceError) { reject(err); } else if (err instanceof CrtError) { reject(createServiceError("CrtError", err as CrtError)); } else { reject(createServiceError((err as Error).toString())); } } }); } export function createServiceError(description: string, internalError?: CrtError, modeledError?: any) { return new ServiceError({ description: description, internalError: internalError, modeledError: modeledError, }); } function throwMissingPropertyError(propertyName?: string) : void { if (propertyName) { throw createServiceError(`validation failure - missing required property '${propertyName}'`); } else { throw createServiceError(`validation failure - missing required property`); } } function throwInvalidPropertyValueError(valueDescription: string, propertyName?: string) : void { if (propertyName) { throw createServiceError(`validation failure - property '${propertyName}' must be ${valueDescription}`); } else { throw createServiceError(`validation failure - property must be ${valueDescription}`); } } export function validateValueAsTopicSegment(value : any, propertyName?: string) : void { if (value === undefined) { throwMissingPropertyError(propertyName); } if (typeof value !== 'string') { throwInvalidPropertyValueError("a string", propertyName); } if (value.includes("/") || value.includes("#") || value.includes("+")) { throwInvalidPropertyValueError("a valid MQTT topic", propertyName); } } export function validateOptionalValueAsNumber(value: any, propertyName?: string) { if (value === undefined) { return; } validateValueAsNumber(value, propertyName); } export function validateValueAsNumber(value: any, propertyName?: string) { if (value == undefined) { throwMissingPropertyError(propertyName); } if (typeof value !== 'number') { throwInvalidPropertyValueError("a number", propertyName); } } ////////////// export function validateValueAsString(value : any, propertyName?: string) : void { if (value === undefined) { throwMissingPropertyError(propertyName); } if (typeof value !== 'string') { throwInvalidPropertyValueError('a string value', propertyName); } } export function validateValueAsOptionalString(value : any, propertyName?: string) : void { if (value === undefined) { return; } validateValueAsString(value, propertyName); } export function validateValueAsInteger(value : any, propertyName?: string) { if (value === undefined) { throwMissingPropertyError(propertyName); } if (typeof value !== 'number' || !Number.isSafeInteger(value as number)) { throwInvalidPropertyValueError('an integer value', propertyName); } } export function validateValueAsOptionalInteger(value : any, propertyName?: string) { if (value === undefined) { return; } validateValueAsInteger(value, propertyName); } export function validateValueAsBoolean(value : any, propertyName?: string) { if (value === undefined) { throwMissingPropertyError(propertyName); } if (typeof value !== 'boolean') { throwInvalidPropertyValueError('a boolean value', propertyName); } } export function validateValueAsOptionalBoolean(value : any, propertyName?: string) { if (value === undefined) { return; } validateValueAsBoolean(value, propertyName); } export function validateValueAsDate(value : any, propertyName?: string) { if (value === undefined) { throwMissingPropertyError(propertyName); } if (!(value instanceof Date) || isNaN((value as Date).getTime())) { throwInvalidPropertyValueError('a Date value', propertyName); } } export function validateValueAsOptionalDate(value : any, propertyName?: string) { if (value === undefined) { return; } validateValueAsDate(value, propertyName); } export function validateValueAsBlob(value : any, propertyName?: string) { if (value === undefined) { throwMissingPropertyError(propertyName); } /* there doesn't seem to be a good way of checking if something is an ArrayBuffer */ if ((typeof value !== 'string') && !ArrayBuffer.isView(value) && (!value.byteLength || !value.maxByteLength)) { throwInvalidPropertyValueError('a value convertible to a binary payload', propertyName); } } export function validateValueAsOptionalBlob(value : any, propertyName?: string) { if (value === undefined) { return; } validateValueAsBlob(value, propertyName); } export function validateValueAsAny(value : any, propertyName?: string) { if (value === undefined) { throwMissingPropertyError(propertyName); } } export function validateValueAsOptionalAny(value : any, propertyName?: string) { if (value === undefined) { return; } validateValueAsAny(value, propertyName); } export type ElementValidator = (value : any) => void; export function validateValueAsArray(value : any, elementValidator : ElementValidator, propertyName?: string) { if (value === undefined) { throwMissingPropertyError(propertyName); } if (!Array.isArray(value)) { throwInvalidPropertyValueError('an array value', propertyName); } for (const element of value) { try { elementValidator(element); } catch (err) { let serviceError : ServiceError = err as ServiceError; if (propertyName) { throw createServiceError(`Array property '${propertyName}' contains an invalid value: ${serviceError.toString()}`); } else { throw createServiceError(`Array contains an invalid value: ${serviceError.toString()}`); } } } } export function validateValueAsOptionalArray(value : any, elementValidator : ElementValidator, propertyName?: string) { if (value === undefined) { return; } validateValueAsArray(value, elementValidator, propertyName); } export function validateValueAsMap(value : any, keyValidator : ElementValidator, valueValidator : ElementValidator, propertyName?: string) { if (value === undefined) { return; } /* if (!(value instanceof Map)) { throwInvalidPropertyValueError('a map value', propertyName); }*/ //let valueAsMap = value as Map<any, any>; for (const key in value) { try { keyValidator(key); } catch (err) { let serviceError : ServiceError = err as ServiceError; if (propertyName) { throw createServiceError(`Map property '${propertyName}' contains an invalid key: ${serviceError.toString()}`); } else { throw createServiceError(`Map contains an invalid key: ${serviceError.toString()}`); } } let val = value[key]; try { valueValidator(val); } catch (err) { let serviceError : ServiceError = err as ServiceError; if (propertyName) { throw createServiceError(`Map property '${propertyName}' contains an invalid value: ${serviceError.toString()}`); } else { throw createServiceError(`Map contains an invalid value: ${serviceError.toString()}`); } } } } export function validateValueAsOptionalMap(value : any, keyValidator : ElementValidator, valueValidator : ElementValidator, propertyName?: string) { if (value === undefined) { return; } validateValueAsMap(value, keyValidator, valueValidator, propertyName); } export function validateValueAsObject(value : any, elementValidator : ElementValidator, propertyName: string) { if (value === undefined) { throwMissingPropertyError(propertyName); } try { elementValidator(value); } catch (err) { let serviceError : ServiceError = err as ServiceError; throw createServiceError(`Property '${propertyName}' contains an invalid value: ${serviceError.toString()}`); } } export function validateValueAsOptionalObject(value : any, elementValidator : ElementValidator, propertyName: string,) { if (value === undefined) { return; } validateValueAsObject(value, elementValidator, propertyName); }