var fs = require('fs'); var path = require('path'); var pruneShapes = require('./prune-shapes').pruneShapes; var CUSTOM_CONFIG_ENUMS = { DUALSTACK: { FILE_NAME: 'config_use_dualstack', INTERFACE: 'UseDualstackConfigOptions' } }; function TSGenerator(options) { this._sdkRootDir = options.SdkRootDirectory || process.cwd(); this._apiRootDir = path.join(this._sdkRootDir, 'apis'); this._metadataPath = path.join(this._apiRootDir, 'metadata.json'); this._clientsDir = path.join(this._sdkRootDir, 'clients'); this.metadata = null; this.typings = {}; this.fillApiModelFileNames(this._apiRootDir); this.streamTypes = {}; } /** * Loads the AWS SDK metadata.json file. */ TSGenerator.prototype.loadMetadata = function loadMetadata() { var metadataFile = fs.readFileSync(this._metadataPath); this.metadata = JSON.parse(metadataFile); return this.metadata; }; /** * Modifies metadata to include api model filenames. */ TSGenerator.prototype.fillApiModelFileNames = function fillApiModelFileNames(apisPath) { var modelPaths = fs.readdirSync(apisPath); if (!this.metadata) { this.loadMetadata(); } var metadata = this.metadata; // sort paths so latest versions appear first modelPaths = modelPaths.sort(function sort(a, b) { if (a < b) { return 1; } else if (a > b) { return -1; } else { return 0; } }); // Only get latest version of models var foundModels = Object.create(null); modelPaths.forEach(function(modelFileName) { var match = modelFileName.match(/^(.+)(-[\d]{4}-[\d]{2}-[\d]{2})\.normal\.json$/i); if (match) { var model = match[1]; // add version var version = match[2].substring(1); if (!foundModels[model]) { foundModels[model] = { latestFileName: modelFileName, versions: [version] }; } else { foundModels[model].versions.push(version); } } }); // now update the metadata var keys = Object.keys(metadata); keys.forEach(function(key) { var modelName = metadata[key].prefix || key; var modelInfo = foundModels[modelName]; metadata[key].api_path = modelInfo.latestFileName; // find waiters file var baseName = modelInfo.latestFileName.split('.')[0]; if (modelPaths.indexOf(baseName + '.waiters2.json') >= 0) { metadata[key].waiters_path = baseName + '.waiters2.json'; } // add versions if (!metadata[key].versions) { metadata[key].versions = []; } metadata[key].versions = [].concat(metadata[key].versions, modelInfo.versions); }); }; TSGenerator.prototype.updateDynamoDBDocumentClient = function updateDynamoDBDocumentClient() { // read in document client customization var docClientCustomCode = fs.readFileSync(path.join(this._sdkRootDir, 'lib', 'dynamodb', 'document_client.d.ts')).toString(); var lines = docClientCustomCode.split('\n'); var namespaceIndexStart = -1; var namespaceIndexEnd = -1; for (var i = 0, iLen = lines.length; i < iLen; i++) { var line = lines[i]; // find exported namespace if (line.indexOf('//') >= 0) { namespaceIndexStart = i; } if (line.indexOf('//') >= 0) { namespaceIndexEnd = i; break; } } if (namespaceIndexStart >= 0 && namespaceIndexEnd >= 0) { // insert doc client interfaces lines.splice(namespaceIndexStart + 1, (namespaceIndexEnd - namespaceIndexStart - 1), this.generateDocumentClientInterfaces(1)); var code = lines.join('\n'); this.writeTypingsFile('document_client', path.join(this._sdkRootDir, 'lib', 'dynamodb'), code); } }; /** * Generates the file containing DocumentClient interfaces. */ TSGenerator.prototype.generateDocumentClientInterfaces = function generateDocumentClientInterfaces(tabCount) { tabCount = tabCount || 0; var self = this; // get the dynamodb model var dynamodbModel = this.loadServiceApi('dynamodb'); var code = ''; // stub Blob interface code += this.tabs(tabCount) + 'interface Blob {}\n'; // generate shapes var modelShapes = dynamodbModel.shapes; // iterate over each shape var shapeKeys = Object.keys(modelShapes); shapeKeys.forEach(function (shapeKey) { var modelShape = modelShapes[shapeKey]; // ignore exceptions if (modelShape.exception) { return; } // overwrite AttributeValue if (shapeKey === 'AttributeValue') { code += self.generateDocString('A JavaScript object or native type.', tabCount); code += self.tabs(tabCount) + 'export type ' + shapeKey + ' = any;\n'; return; } code += self.generateTypingsFromShape(dynamodbModel, shapeKey, modelShape, tabCount, []); }); return code; }; /** * Returns a service model based on the serviceIdentifier. */ TSGenerator.prototype.loadServiceApi = function loadServiceApi(serviceIdentifier) { // first, find the correct identifier var metadata = this.metadata; var serviceFilePath = path.join(this._apiRootDir, metadata[serviceIdentifier].api_path); var serviceModelFile = fs.readFileSync(serviceFilePath); var serviceModel = JSON.parse(serviceModelFile); // load waiters file if it exists var waiterFilePath; if (metadata[serviceIdentifier].waiters_path) { waiterFilePath = path.join(this._apiRootDir, metadata[serviceIdentifier].waiters_path); var waiterModelFile = fs.readFileSync(waiterFilePath); var waiterModel = JSON.parse(waiterModelFile); serviceModel.waiters = waiterModel.waiters; } return serviceModel; }; /** * Determines if a member is required by checking for it in a list. */ TSGenerator.prototype.checkRequired = function checkRequired(list, member) { if (list.indexOf(member) >= 0) { return true; } return false; }; /** * Generates whitespace based on the count. */ TSGenerator.prototype.tabs = function tabs(count) { var code = ''; for (var i = 0; i < count; i++) { code += ' '; } return code; }; /** * Transforms documentation string to a more readable format. */ TSGenerator.prototype.transformDocumentation = function transformDocumentation(documentation) { if (!documentation) { return ''; } documentation = documentation.replace(/<(?:.|\n)*?>/gm, ''); documentation = documentation.replace(/\*\//g, '*'); return documentation; }; /** * Returns a doc string based on the supplied documentation. * Also tabs the doc string if a count is provided. */ TSGenerator.prototype.generateDocString = function generateDocString(documentation, tabCount) { tabCount = tabCount || 0; var code = ''; code += this.tabs(tabCount) + '/**\n'; code += this.tabs(tabCount) + ' * ' + this.transformDocumentation(documentation) + '\n'; code += this.tabs(tabCount) + ' */\n'; return code; }; /** * Returns an array of custom configuration options based on a service identiffier. * Custom configuration options are determined by checking the metadata.json file. */ TSGenerator.prototype.generateCustomConfigFromMetadata = function generateCustomConfigFromMetadata(serviceIdentifier) { // some services have additional configuration options that are defined in the metadata.json file // i.e. dualstackAvailable = useDualstack // create reference to custom options var customConfigurations = []; var serviceMetadata = this.metadata[serviceIdentifier]; // loop through metadata members for (var memberName in serviceMetadata) { if (!serviceMetadata.hasOwnProperty(memberName)) { continue; } // check configs switch (memberName) { case 'dualstackAvailable': customConfigurations.push(CUSTOM_CONFIG_ENUMS.DUALSTACK); break; } } return customConfigurations; }; TSGenerator.prototype.generateSafeShapeName = function generateSafeShapeName(name, blacklist) { blacklist = blacklist || []; if (blacklist.indexOf(name) >= 0) { return '_' + name; } return name; }; TSGenerator.prototype.extractTypesDependOnStream = function extractTypesDependOnStream(shapeKey, modelShape) { var streamTypeList = []; if (typeof modelShape !== "object" || Object.keys(modelShape).length === 0) { return []; } if (modelShape.streaming) { streamTypeList.push(shapeKey); return streamTypeList; } for (var subModelKey in modelShape) { var subModel = modelShape[subModelKey]; var subStreamTypeList = this.extractTypesDependOnStream(subModelKey, subModel); if (Object.keys(subStreamTypeList).length !== 0) { for (var streamType of subStreamTypeList) { streamTypeList.push(streamType); } } } return streamTypeList; } TSGenerator.prototype.addReadableType = function addReadableType(shapeKey) { var code = ''; if (this.streamTypes[shapeKey]) { code += '|Readable'; } else if (shapeKey[0] === '_' && this.streamTypes[shapeKey.slice(1)]) { code += '|Readable'; } return code; } /** * Generates a type or interface based on the shape. */ TSGenerator.prototype.generateTypingsFromShape = function generateTypingsFromShape(model, shapeKey, shape, tabCount, customClassNames) { // some shapes shouldn't be generated if they are javascript primitives var jsPrimitives = ['string', 'boolean', 'number']; if (jsPrimitives.indexOf(shapeKey) >= 0) { return ''; } if (['Date', 'Blob'].indexOf(shapeKey) >= 0) { shapeKey = '_' + shapeKey; } // In at least one (cloudfront.Signer) case, a class on a service namespace clashes with a shape shapeKey = this.generateSafeShapeName(shapeKey, customClassNames); var self = this; var code = ''; tabCount = tabCount || 0; var tabs = this.tabs; var type = shape.type; if (shape.eventstream) { // eventstreams MUST be structures var members = Object.keys(shape.members); var events = members.map(function(member) { // each member is an individual event type, so each must be optional return member + '?:' + shape.members[member].shape; }); return code += tabs(tabCount) + 'export type ' + shapeKey + ' = EventStream<{' + events.join(',') + '}>;\n'; } if (type === 'structure') { code += tabs(tabCount) + 'export interface ' + shapeKey + ' {\n'; var members = shape.members; // cycle through members var memberKeys = Object.keys(members); memberKeys.forEach(function(memberKey) { // docs var member = members[memberKey]; if (member.documentation) { code += self.generateDocString(member.documentation, tabCount + 1); } var required = self.checkRequired(shape.required || [], memberKey) ? '' : '?'; var memberType = member.shape; if (member.eventpayload) { // eventpayloads are always either structures, or buffers if (['blob', 'binary'].indexOf(model.shapes[memberType].type) >= 0) { memberType = 'Buffer'; } } memberType = self.generateSafeShapeName(memberType, [].concat(customClassNames, ['Date', 'Blob'])); code += tabs(tabCount + 1) + memberKey + required + ': ' + memberType + ';\n'; }); code += tabs(tabCount) + '}\n'; } else if (type === 'list') { code += tabs(tabCount) + 'export type ' + shapeKey + ' = ' + this.generateSafeShapeName(shape.member.shape, customClassNames) + '[];\n'; } else if (type === 'map') { code += tabs(tabCount) + 'export type ' + shapeKey + ' = {[key: string]: ' + this.generateSafeShapeName(shape.value.shape, customClassNames) + '};\n'; } else if (type === 'string' || type === 'character') { var stringType = 'string'; if (Array.isArray(shape.enum)) { stringType = shape.enum.map(function(s) { return '"' + s + '"'; }).join('|') + '|' + stringType; } code += tabs(tabCount) + 'export type ' + shapeKey + ' = ' + stringType + ';\n'; } else if (['double', 'long', 'short', 'biginteger', 'bigdecimal', 'integer', 'float'].indexOf(type) >= 0) { code += tabs(tabCount) + 'export type ' + shapeKey + ' = number;\n'; } else if (type === 'timestamp') { code += tabs(tabCount) + 'export type ' + shapeKey + ' = Date;\n'; } else if (type === 'boolean') { code += tabs(tabCount) + 'export type ' + shapeKey + ' = boolean;\n'; } else if (type === 'blob' || type === 'binary') { code += tabs(tabCount) + 'export type ' + shapeKey + ' = Buffer|Uint8Array|Blob|string' + self.addReadableType(shapeKey) +';\n'; } return code; }; /** * Generates a class method type for an operation. */ TSGenerator.prototype.generateTypingsFromOperations = function generateTypingsFromOperations(className, operation, operationName, tabCount) { var code = ''; tabCount = tabCount || 0; var tabs = this.tabs; var input = operation.input; var output = operation.output; operationName = operationName.charAt(0).toLowerCase() + operationName.substring(1); var inputShape = input ? className + '.Types.' + input.shape : '{}'; var outputShape = output ? className + '.Types.' + output.shape : '{}'; if (input) { code += this.generateDocString(operation.documentation, tabCount); code += tabs(tabCount) + operationName + '(params: ' + inputShape + ', callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; } code += this.generateDocString(operation.documentation, tabCount); code += tabs(tabCount) + operationName + '(callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; return code; }; TSGenerator.prototype.generateConfigurationServicePlaceholders = function generateConfigurationServicePlaceholders() { /** * Should create a config service placeholder */ var self = this; var metadata = this.metadata; // Iterate over every service var serviceIdentifiers = Object.keys(metadata); var code = ''; var configCode = ''; var versionsCode = ''; code += 'import * as AWS from \'../clients/all\';\n'; configCode += 'export abstract class ConfigurationServicePlaceholders {\n'; versionsCode += 'export interface ConfigurationServiceApiVersions {\n'; serviceIdentifiers.forEach(function(serviceIdentifier) { var className = self.metadata[serviceIdentifier].name; configCode += self.tabs(1) + serviceIdentifier + '?: AWS.' + className + '.Types.ClientConfiguration;\n'; versionsCode += self.tabs(1) + serviceIdentifier + '?: AWS.' + className + '.Types.apiVersion;\n'; }); configCode += '}\n'; versionsCode += '}\n'; code += configCode + versionsCode; this.writeTypingsFile('config_service_placeholders', path.join(this._sdkRootDir, 'lib'), code); }; TSGenerator.prototype.getServiceApiVersions = function generateServiceApiVersions(serviceIdentifier) { var metadata = this.metadata; var versions = metadata[serviceIdentifier].versions || []; // transform results (to get rid of '*' and sort versions = versions.map(function(version) { return version.replace('*', ''); }).sort(); return versions; }; /** * Generates class method types for a waiter. */ TSGenerator.prototype.generateTypingsFromWaiters = function generateTypingsFromWaiters(className, waiterState, waiter, underlyingOperation, tabCount) { var code = ''; tabCount = tabCount || 0; var operationName = waiter.operation.charAt(0).toLowerCase() + waiter.operation.substring(1); waiterState = waiterState.charAt(0).toLowerCase() + waiterState.substring(1); var docString = 'Waits for the ' + waiterState + ' state by periodically calling the underlying ' + className + '.' + operationName + 'operation every ' + waiter.delay + ' seconds (at most ' + waiter.maxAttempts + ' times).'; if (waiter.description) { docString += ' ' + waiter.description; } // get input and output var inputShape = '{}'; var outputShape = '{}'; if (underlyingOperation.input) { inputShape = className + '.Types.' + underlyingOperation.input.shape; } if (underlyingOperation.output) { outputShape = className + '.Types.' + underlyingOperation.output.shape; } code += this.generateDocString(docString, tabCount); code += this.tabs(tabCount) + 'waitFor(state: "' + waiterState + '", params: ' + inputShape + ' & {$waiter?: WaiterConfiguration}, callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; code += this.generateDocString(docString, tabCount); code += this.tabs(tabCount) + 'waitFor(state: "' + waiterState + '", callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; return code; }; /** * Returns whether a service has customizations to include. */ TSGenerator.prototype.includeCustomService = function includeCustomService(serviceIdentifier) { // check services directory var servicesDir = path.join(this._sdkRootDir, 'lib', 'services'); var fileNames = fs.readdirSync(servicesDir); fileNames = fileNames.filter(function(fileName) { return fileName === serviceIdentifier + '.d.ts'; }); return !!fileNames.length; }; /** * Generates typings for classes that live on a service client namespace. */ TSGenerator.prototype.generateCustomNamespaceTypes = function generateCustomNamespaceTypes(serviceIdentifier, className) { var self = this; var tsCustomizationsJson = require('./ts-customizations'); var customClasses = []; var code = ''; var serviceInfo = tsCustomizationsJson[serviceIdentifier] || null; // exit early if no customizations found if (!serviceInfo) { return null; } code += 'declare namespace ' + className + ' {\n'; //generate import code var importCode = ''; serviceInfo.forEach(function(data) { var aliases = []; var imports = data.imports || []; imports.forEach(function(pair) { aliases.push(pair.name + ' as ' + pair.alias); code += self.tabs(1) + 'export import ' + pair.name + ' = ' + pair.alias + ';\n'; customClasses.push(pair.name); }); if (aliases.length) { importCode += 'import {' + aliases.join(', ') + '} from \'../' + data.path + '\';\n'; } }); code += '}\n'; return { importCode: importCode, namespaceCode: code, customClassNames: customClasses }; }; TSGenerator.prototype.containsEventStreams = function containsEventStreams(model) { var shapeNames = Object.keys(model.shapes); for (var name of shapeNames) { if (model.shapes[name].eventstream) { return true; } } return false; }; /** * Generates the typings for a service based on the serviceIdentifier. */ TSGenerator.prototype.processServiceModel = function processServiceModel(serviceIdentifier) { var model = this.loadServiceApi(serviceIdentifier); pruneShapes(model); var self = this; var code = ''; var className = this.metadata[serviceIdentifier].name; var customNamespaces = this.generateCustomNamespaceTypes(serviceIdentifier, className); var customClassNames = customNamespaces ? customNamespaces.customClassNames : []; var waiters = model.waiters || Object.create(null); var waiterKeys = Object.keys(waiters); // generate imports code += 'import {Request} from \'../lib/request\';\n'; code += 'import {Response} from \'../lib/response\';\n'; code += 'import {AWSError} from \'../lib/error\';\n'; var hasCustomizations = this.includeCustomService(serviceIdentifier); var parentClass = hasCustomizations ? className + 'Customizations' : 'Service'; if (hasCustomizations) { code += 'import {' + parentClass + '} from \'../lib/services/' + serviceIdentifier + '\';\n'; } else { code += 'import {' + parentClass + '} from \'../lib/service\';\n'; } if (waiterKeys.length) { code += 'import {WaiterConfiguration} from \'../lib/service\';\n'; } code += 'import {ServiceConfigurationOptions} from \'../lib/service\';\n'; // get any custom config options var customConfig = this.generateCustomConfigFromMetadata(serviceIdentifier); var hasCustomConfig = !!customConfig.length; var customConfigTypes = ['ServiceConfigurationOptions']; code += 'import {ConfigBase as Config} from \'../lib/config\';\n'; if (hasCustomConfig) { // generate import statements and custom config type customConfig.forEach(function(config) { code += 'import {' + config.INTERFACE + '} from \'../lib/' + config.FILE_NAME + '\';\n'; customConfigTypes.push(config.INTERFACE); }); } if (this.containsEventStreams(model)) { code += 'import {EventStream} from \'../lib/event-stream/event-stream\';\n'; } // import custom namespaces if (customNamespaces) { code += customNamespaces.importCode; } code += 'interface Blob {}\n'; // generate methods var modelOperations = model.operations; var operationKeys = Object.keys(modelOperations); code += 'declare class ' + className + ' extends ' + parentClass + ' {\n'; // create constructor code += this.generateDocString('Constructs a service object. This object has one method for each API operation.', 1); code += this.tabs(1) + 'constructor(options?: ' + className + '.Types.ClientConfiguration' + ')\n'; code += this.tabs(1) + 'config: Config & ' + className + '.Types.ClientConfiguration' + ';\n'; operationKeys.forEach(function (operationKey) { code += self.generateTypingsFromOperations(className, modelOperations[operationKey], operationKey, 1); }); // generate waitFor methods waiterKeys.forEach(function (waitersKey) { var waiter = waiters[waitersKey]; var operation = modelOperations[waiter.operation]; code += self.generateTypingsFromWaiters(className, waitersKey, waiter, operation, 1); }); code += '}\n'; // check for static classes on namespace if (customNamespaces) { code += customNamespaces.namespaceCode; } // shapes should map to interfaces var modelShapes = model.shapes; // iterate over each shape var shapeKeys = Object.keys(modelShapes); code += 'declare namespace ' + className + ' {\n'; // preprocess shapes to fetch out needed dependency. e.g. "streaming": true shapeKeys.forEach(function (shapeKey) { var modelShape = modelShapes[shapeKey]; var streamTypeList = self.extractTypesDependOnStream(shapeKey, modelShape); for (var streamType of streamTypeList) { self.streamTypes[streamType] = true; } }); shapeKeys.forEach(function (shapeKey) { var modelShape = modelShapes[shapeKey]; // ignore exceptions if (modelShape.exception) { return; } code += self.generateTypingsFromShape(model, shapeKey, modelShape, 1, customClassNames); }); //add extra dependencies like 'streaming' if (Object.keys(self.streamTypes).length !== 0) { var insertPos = code.indexOf('interface Blob {}'); code = code.slice(0, insertPos) + 'import {Readable} from \'stream\';\n' + code.slice(insertPos); } this.streamTypes = {}; code += this.generateDocString('A string in YYYY-MM-DD format that represents the latest possible API version that can be used in this service. Specify \'latest\' to use the latest possible version.', 1); code += this.tabs(1) + 'export type apiVersion = "' + this.getServiceApiVersions(serviceIdentifier).join('"|"') + '"|"latest"|string;\n'; code += this.tabs(1) + 'export interface ClientApiVersions {\n'; code += this.generateDocString('A string in YYYY-MM-DD format that represents the latest possible API version that can be used in this service. Specify \'latest\' to use the latest possible version.', 2); code += this.tabs(2) + 'apiVersion?: apiVersion;\n'; code += this.tabs(1) + '}\n'; code += this.tabs(1) + 'export type ClientConfiguration = ' + customConfigTypes.join(' & ') + ' & ClientApiVersions;\n'; // export interfaces under Types namespace for backwards-compatibility code += this.generateDocString('Contains interfaces for use with the ' + className + ' client.', 1); code += this.tabs(1) + 'export import Types = ' + className + ';\n'; code += '}\n'; code += 'export = ' + className + ';\n'; return code; }; /** * Write Typings file to the specified directory. */ TSGenerator.prototype.writeTypingsFile = function writeTypingsFile(name, directory, code) { fs.writeFileSync(path.join(directory, name + '.d.ts'), code); }; /** * Create the typescript definition files for every service. */ TSGenerator.prototype.generateAllClientTypings = function generateAllClientTypings() { var self = this; var metadata = this.metadata; // Iterate over every service var serviceIdentifiers = Object.keys(metadata); serviceIdentifiers.forEach(function(serviceIdentifier) { var code = self.processServiceModel(serviceIdentifier); self.writeTypingsFile(serviceIdentifier, self._clientsDir, code); }); }; /** * Create the typescript definition files for the all and browser_default exports. */ TSGenerator.prototype.generateGroupedClients = function generateGroupedClients() { var metadata = this.metadata; var allCode = ''; var browserCode = ''; // Iterate over every service var serviceIdentifiers = Object.keys(metadata); serviceIdentifiers.forEach(function(serviceIdentifier) { var className = metadata[serviceIdentifier].name; var code = 'export import ' + className + ' = require(\'./' + serviceIdentifier + '\');\n'; allCode += code; if (metadata[serviceIdentifier].cors) { browserCode += code; } }); this.writeTypingsFile('all', this._clientsDir, allCode); this.writeTypingsFile('browser_default', this._clientsDir, browserCode); }; module.exports = TSGenerator;