ts-generator.js (20265B)
1 var fs = require('fs'); 2 var path = require('path'); 3 4 var CUSTOM_CONFIG_ENUMS = { 5 DUALSTACK: { 6 FILE_NAME: 'config_use_dualstack', 7 INTERFACE: 'UseDualstackConfigOptions' 8 } 9 }; 10 11 function TSGenerator(options) { 12 this._sdkRootDir = options.SdkRootDirectory || process.cwd(); 13 this._apiRootDir = path.join(this._sdkRootDir, 'apis'); 14 this._metadataPath = path.join(this._apiRootDir, 'metadata.json'); 15 this._clientsDir = path.join(this._sdkRootDir, 'clients'); 16 this.metadata = null; 17 this.typings = {}; 18 this.fillApiModelFileNames(this._apiRootDir); 19 } 20 21 /** 22 * Loads the AWS SDK metadata.json file. 23 */ 24 TSGenerator.prototype.loadMetadata = function loadMetadata() { 25 var metadataFile = fs.readFileSync(this._metadataPath); 26 this.metadata = JSON.parse(metadataFile); 27 return this.metadata; 28 }; 29 30 /** 31 * Modifies metadata to include api model filenames. 32 */ 33 TSGenerator.prototype.fillApiModelFileNames = function fillApiModelFileNames(apisPath) { 34 var modelPaths = fs.readdirSync(apisPath); 35 if (!this.metadata) { 36 this.loadMetadata(); 37 } 38 var metadata = this.metadata; 39 40 // sort paths so latest versions appear first 41 modelPaths = modelPaths.sort(function sort(a, b) { 42 if (a < b) { 43 return 1; 44 } else if (a > b) { 45 return -1; 46 } else { 47 return 0; 48 } 49 }); 50 51 // Only get latest version of models 52 var foundModels = Object.create(null); 53 modelPaths.forEach(function(modelFileName) { 54 var match = modelFileName.match(/^(.+)(-[\d]{4}-[\d]{2}-[\d]{2})\.normal\.json$/i); 55 if (match) { 56 var model = match[1]; 57 // add version 58 var version = match[2].substring(1); 59 if (!foundModels[model]) { 60 foundModels[model] = { 61 latestFileName: modelFileName, 62 versions: [version] 63 }; 64 } else { 65 foundModels[model].versions.push(version); 66 } 67 } 68 }); 69 70 // now update the metadata 71 var keys = Object.keys(metadata); 72 keys.forEach(function(key) { 73 var modelName = metadata[key].prefix || key; 74 var modelInfo = foundModels[modelName]; 75 metadata[key].api_path = modelInfo.latestFileName; 76 // find waiters file 77 var baseName = modelInfo.latestFileName.split('.')[0]; 78 if (modelPaths.indexOf(baseName + '.waiters2.json') >= 0) { 79 metadata[key].waiters_path = baseName + '.waiters2.json'; 80 } 81 // add versions 82 if (!metadata[key].versions) { 83 metadata[key].versions = []; 84 } 85 metadata[key].versions = [].concat(metadata[key].versions, modelInfo.versions); 86 }); 87 }; 88 89 /** 90 * Generates the file containing DocumentClient interfaces. 91 */ 92 TSGenerator.prototype.generateDocumentClientInterfaces = function generateDocumentClientInterfaces() { 93 var self = this; 94 // get the dynamodb model 95 var dynamodbModel = this.loadServiceApi('dynamodb'); 96 var code = ''; 97 // stub Blob interface 98 code += 'interface Blob {}\n'; 99 // generate shapes 100 var modelShapes = dynamodbModel.shapes; 101 // iterate over each shape 102 var shapeKeys = Object.keys(modelShapes); 103 shapeKeys.forEach(function (shapeKey) { 104 var modelShape = modelShapes[shapeKey]; 105 // ignore exceptions 106 if (modelShape.exception) { 107 return; 108 } 109 // overwrite AttributeValue 110 if (shapeKey === 'AttributeValue') { 111 code += self.generateDocString('A JavaScript object or native type.'); 112 code += 'export type ' + shapeKey + ' = any;\n'; 113 return; 114 } 115 code += self.generateTypingsFromShape(shapeKey, modelShape); 116 }); 117 118 // write file: 119 this.writeTypingsFile('document_client_interfaces', path.join(this._sdkRootDir, 'lib', 'dynamodb'), code); 120 }; 121 122 /** 123 * Returns a service model based on the serviceIdentifier. 124 */ 125 TSGenerator.prototype.loadServiceApi = function loadServiceApi(serviceIdentifier) { 126 // first, find the correct identifier 127 var metadata = this.metadata; 128 var serviceFilePath = path.join(this._apiRootDir, metadata[serviceIdentifier].api_path); 129 var serviceModelFile = fs.readFileSync(serviceFilePath); 130 var serviceModel = JSON.parse(serviceModelFile); 131 // load waiters file if it exists 132 var waiterFilePath; 133 if (metadata[serviceIdentifier].waiters_path) { 134 waiterFilePath = path.join(this._apiRootDir, metadata[serviceIdentifier].waiters_path); 135 var waiterModelFile = fs.readFileSync(waiterFilePath); 136 var waiterModel = JSON.parse(waiterModelFile); 137 serviceModel.waiters = waiterModel.waiters; 138 } 139 140 return serviceModel; 141 }; 142 143 /** 144 * Determines if a member is required by checking for it in a list. 145 */ 146 TSGenerator.prototype.checkRequired = function checkRequired(list, member) { 147 if (list.indexOf(member) >= 0) { 148 return true; 149 } 150 return false; 151 }; 152 153 /** 154 * Generates whitespace based on the count. 155 */ 156 TSGenerator.prototype.tabs = function tabs(count) { 157 var code = ''; 158 for (var i = 0; i < count; i++) { 159 code += ' '; 160 } 161 return code; 162 }; 163 164 /** 165 * Transforms documentation string to a more readable format. 166 */ 167 TSGenerator.prototype.transformDocumentation = function transformDocumentation(documentation) { 168 if (!documentation) { 169 return ''; 170 } 171 documentation = documentation.replace(/<(?:.|\n)*?>/gm, ''); 172 documentation = documentation.replace(/\*\//g, '*'); 173 return documentation; 174 }; 175 176 /** 177 * Returns a doc string based on the supplied documentation. 178 * Also tabs the doc string if a count is provided. 179 */ 180 TSGenerator.prototype.generateDocString = function generateDocString(documentation, tabCount) { 181 tabCount = tabCount || 0; 182 var code = ''; 183 code += this.tabs(tabCount) + '/**\n'; 184 code += this.tabs(tabCount) + ' * ' + this.transformDocumentation(documentation) + '\n'; 185 code += this.tabs(tabCount) + ' */\n'; 186 return code; 187 }; 188 189 /** 190 * Returns an array of custom configuration options based on a service identiffier. 191 * Custom configuration options are determined by checking the metadata.json file. 192 */ 193 TSGenerator.prototype.generateCustomConfigFromMetadata = function generateCustomConfigFromMetadata(serviceIdentifier) { 194 // some services have additional configuration options that are defined in the metadata.json file 195 // i.e. dualstackAvailable = useDualstack 196 // create reference to custom options 197 var customConfigurations = []; 198 var serviceMetadata = this.metadata[serviceIdentifier]; 199 // loop through metadata members 200 for (var memberName in serviceMetadata) { 201 if (!serviceMetadata.hasOwnProperty(memberName)) { 202 continue; 203 } 204 // check configs 205 switch (memberName) { 206 case 'dualstackAvailable': 207 customConfigurations.push(CUSTOM_CONFIG_ENUMS.DUALSTACK); 208 break; 209 } 210 } 211 212 return customConfigurations; 213 }; 214 215 /** 216 * Generates a type or interface based on the shape. 217 */ 218 TSGenerator.prototype.generateTypingsFromShape = function generateTypingsFromShape(shapeKey, shape, tabCount) { 219 // some shapes shouldn't be generated if they are javascript primitives 220 var jsPrimitives = ['string', 'boolean', 'number']; 221 if (jsPrimitives.indexOf(shapeKey) >= 0) { 222 return ''; 223 } 224 225 if (['Date', 'Blob'].indexOf(shapeKey) >= 0) { 226 shapeKey = '_' + shapeKey; 227 } 228 var self = this; 229 var code = ''; 230 tabCount = tabCount || 0; 231 var tabs = this.tabs; 232 var type = shape.type; 233 if (type === 'structure') { 234 code += tabs(tabCount) + 'export interface ' + shapeKey + ' {\n'; 235 var members = shape.members; 236 // cycle through members 237 var memberKeys = Object.keys(members); 238 memberKeys.forEach(function(memberKey) { 239 // docs 240 var member = members[memberKey]; 241 if (member.documentation) { 242 code += self.generateDocString(member.documentation, tabCount + 1); 243 } 244 var required = self.checkRequired(shape.required || [], memberKey) ? '' : '?'; 245 var memberType = member.shape; 246 if (['Date', 'Blob'].indexOf(memberType) >= 0) { 247 memberType = '_' + memberType; 248 } 249 code += tabs(tabCount + 1) + memberKey + required + ': ' + memberType + ';\n'; 250 }); 251 code += tabs(tabCount) + '}\n'; 252 } else if (type === 'list') { 253 code += tabs(tabCount) + 'export type ' + shapeKey + ' = ' + shape.member.shape + '[];\n'; 254 } else if (type === 'map') { 255 code += tabs(tabCount) + 'export type ' + shapeKey + ' = {[key: string]: ' + shape.value.shape + '};\n'; 256 } else if (type === 'string' || type === 'character') { 257 var stringType = 'string'; 258 if (Array.isArray(shape.enum)) { 259 stringType = shape.enum.map(function(s) { 260 return '"' + s + '"'; 261 }).join('|') + '|' + stringType; 262 } 263 code += tabs(tabCount) + 'export type ' + shapeKey + ' = ' + stringType + ';\n'; 264 } else if (['double', 'long', 'short', 'biginteger', 'bigdecimal', 'integer', 'float'].indexOf(type) >= 0) { 265 code += tabs(tabCount) + 'export type ' + shapeKey + ' = number;\n'; 266 } else if (type === 'timestamp') { 267 code += tabs(tabCount) + 'export type ' + shapeKey + ' = Date;\n'; 268 } else if (type === 'boolean') { 269 code += tabs(tabCount) + 'export type ' + shapeKey + ' = boolean;\n'; 270 } else if (type === 'blob' || type === 'binary') { 271 code += tabs(tabCount) + 'export type ' + shapeKey + ' = Buffer|Uint8Array|Blob|string;\n'; 272 } 273 return code; 274 }; 275 276 /** 277 * Generates a class method type for an operation. 278 */ 279 TSGenerator.prototype.generateTypingsFromOperations = function generateTypingsFromOperations(className, operation, operationName, tabCount) { 280 var code = ''; 281 tabCount = tabCount || 0; 282 var tabs = this.tabs; 283 284 var input = operation.input; 285 var output = operation.output; 286 operationName = operationName.charAt(0).toLowerCase() + operationName.substring(1); 287 288 var inputShape = input ? className + '.Types.' + input.shape : '{}'; 289 var outputShape = output ? className + '.Types.' + output.shape : '{}'; 290 291 if (input) { 292 code += this.generateDocString(operation.documentation, tabCount); 293 code += tabs(tabCount) + operationName + '(params: ' + inputShape + ', callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; 294 } 295 code += this.generateDocString(operation.documentation, tabCount); 296 code += tabs(tabCount) + operationName + '(callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; 297 298 return code; 299 }; 300 301 TSGenerator.prototype.generateConfigurationServicePlaceholders = function generateConfigurationServicePlaceholders() { 302 /** 303 * Should create a config service placeholder 304 */ 305 var self = this; 306 var metadata = this.metadata; 307 // Iterate over every service 308 var serviceIdentifiers = Object.keys(metadata); 309 var code = ''; 310 var configCode = ''; 311 var versionsCode = ''; 312 code += 'import * as AWS from \'../clients/all\';\n'; 313 configCode += 'export abstract class ConfigurationServicePlaceholders {\n'; 314 versionsCode += 'export interface ConfigurationServiceApiVersions {\n'; 315 serviceIdentifiers.forEach(function(serviceIdentifier) { 316 var className = self.metadata[serviceIdentifier].name; 317 configCode += self.tabs(1) + serviceIdentifier + '?: AWS.' + className + '.Types.ClientConfiguration;\n'; 318 versionsCode += self.tabs(1) + serviceIdentifier + '?: AWS.' + className + '.Types.apiVersion;\n'; 319 }); 320 configCode += '}\n'; 321 versionsCode += '}\n'; 322 323 code += configCode + versionsCode; 324 this.writeTypingsFile('config_service_placeholders', path.join(this._sdkRootDir, 'lib'), code); 325 }; 326 327 TSGenerator.prototype.getServiceApiVersions = function generateServiceApiVersions(serviceIdentifier) { 328 var metadata = this.metadata; 329 var versions = metadata[serviceIdentifier].versions || []; 330 // transform results (to get rid of '*' and sort 331 versions = versions.map(function(version) { 332 return version.replace('*', ''); 333 }).sort(); 334 return versions; 335 }; 336 337 /** 338 * Generates class method types for a waiter. 339 */ 340 TSGenerator.prototype.generateTypingsFromWaiters = function generateTypingsFromWaiters(className, waiterState, waiter, underlyingOperation, tabCount) { 341 var code = ''; 342 tabCount = tabCount || 0; 343 var operationName = waiter.operation.charAt(0).toLowerCase() + waiter.operation.substring(1); 344 waiterState = waiterState.charAt(0).toLowerCase() + waiterState.substring(1); 345 var docString = 'Waits for the ' + waiterState + ' state by periodically calling the underlying ' + className + '.' + operationName + 'operation every ' + waiter.delay + ' seconds (at most ' + waiter.maxAttempts + ' times).'; 346 if (waiter.description) { 347 docString += ' ' + waiter.description; 348 } 349 350 // get input and output 351 var inputShape = '{}'; 352 var outputShape = '{}'; 353 if (underlyingOperation.input) { 354 inputShape = className + '.Types.' + underlyingOperation.input.shape; 355 } 356 if (underlyingOperation.output) { 357 outputShape = className + '.Types.' + underlyingOperation.output.shape; 358 } 359 360 code += this.generateDocString(docString, tabCount); 361 code += this.tabs(tabCount) + 'waitFor(state: "' + waiterState + '", params: ' + inputShape + ', callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; 362 code += this.generateDocString(docString, tabCount); 363 code += this.tabs(tabCount) + 'waitFor(state: "' + waiterState + '", callback?: (err: AWSError, data: ' + outputShape + ') => void): Request<' + outputShape + ', AWSError>;\n'; 364 365 return code; 366 }; 367 368 /** 369 * Returns whether a service has customizations to include. 370 */ 371 TSGenerator.prototype.includeCustomService = function includeCustomService(serviceIdentifier) { 372 // check services directory 373 var servicesDir = path.join(this._sdkRootDir, 'lib', 'services'); 374 var fileNames = fs.readdirSync(servicesDir); 375 fileNames = fileNames.filter(function(fileName) { 376 return fileName === serviceIdentifier + '.d.ts'; 377 }); 378 return !!fileNames.length; 379 }; 380 381 /** 382 * Generates the typings for a service based on the serviceIdentifier. 383 */ 384 TSGenerator.prototype.processServiceModel = function processServiceModel(serviceIdentifier) { 385 var model = this.loadServiceApi(serviceIdentifier); 386 var self = this; 387 var code = ''; 388 var className = this.metadata[serviceIdentifier].name; 389 // generate imports 390 code += 'import {Request} from \'../lib/request\';\n'; 391 code += 'import {Response} from \'../lib/response\';\n'; 392 code += 'import {AWSError} from \'../lib/error\';\n'; 393 var hasCustomizations = this.includeCustomService(serviceIdentifier); 394 var parentClass = hasCustomizations ? className + 'Customizations' : 'Service'; 395 if (hasCustomizations) { 396 code += 'import {' + parentClass + '} from \'../lib/services/' + serviceIdentifier + '\';\n'; 397 } else { 398 code += 'import {' + parentClass + '} from \'../lib/service\';\n'; 399 } 400 code += 'import {ServiceConfigurationOptions} from \'../lib/service\';\n'; 401 // get any custom config options 402 var customConfig = this.generateCustomConfigFromMetadata(serviceIdentifier); 403 var hasCustomConfig = !!customConfig.length; 404 var customConfigTypes = ['ServiceConfigurationOptions']; 405 code += 'import {ConfigBase as Config} from \'../lib/config\';\n'; 406 if (hasCustomConfig) { 407 // generate import statements and custom config type 408 customConfig.forEach(function(config) { 409 code += 'import {' + config.INTERFACE + '} from \'../lib/' + config.FILE_NAME + '\';\n'; 410 customConfigTypes.push(config.INTERFACE); 411 }); 412 } 413 code += 'interface Blob {}\n'; 414 // generate methods 415 var modelOperations = model.operations; 416 var operationKeys = Object.keys(modelOperations); 417 code += 'declare class ' + className + ' extends ' + parentClass + ' {\n'; 418 // create constructor 419 code += this.generateDocString('Constructs a service object. This object has one method for each API operation.', 1); 420 code += this.tabs(1) + 'constructor(options?: ' + className + '.Types.ClientConfiguration' + ')\n'; 421 code += this.tabs(1) + 'config: Config & ' + className + '.Types.ClientConfiguration' + ';\n'; 422 423 operationKeys.forEach(function (operationKey) { 424 code += self.generateTypingsFromOperations(className, modelOperations[operationKey], operationKey, 1); 425 }); 426 427 // generate waitFor methods 428 var waiters = model.waiters || Object.create(null); 429 var waiterKeys = Object.keys(waiters); 430 waiterKeys.forEach(function (waitersKey) { 431 var waiter = waiters[waitersKey]; 432 var operation = modelOperations[waiter.operation]; 433 code += self.generateTypingsFromWaiters(className, waitersKey, waiter, operation, 1); 434 }); 435 436 code += '}\n'; 437 438 // shapes should map to interfaces 439 var modelShapes = model.shapes; 440 // iterate over each shape 441 var shapeKeys = Object.keys(modelShapes); 442 code += 'declare namespace ' + className + '.Types {\n'; 443 shapeKeys.forEach(function (shapeKey) { 444 var modelShape = modelShapes[shapeKey]; 445 // ignore exceptions 446 if (modelShape.exception) { 447 return; 448 } 449 code += self.generateTypingsFromShape(shapeKey, modelShape, 1); 450 }); 451 452 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); 453 code += this.tabs(1) + 'export type apiVersion = "' + this.getServiceApiVersions(serviceIdentifier).join('"|"') + '"|"latest"|string;\n'; 454 code += this.tabs(1) + 'export interface ClientApiVersions {\n'; 455 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); 456 code += this.tabs(2) + 'apiVersion?: apiVersion;\n'; 457 code += this.tabs(1) + '}\n'; 458 code += this.tabs(1) + 'export type ClientConfiguration = ' + customConfigTypes.join(' & ') + ' & ClientApiVersions;\n'; 459 code += '}\n'; 460 461 code += 'export = ' + className + ';\n'; 462 return code; 463 }; 464 465 /** 466 * Write Typings file to the specified directory. 467 */ 468 TSGenerator.prototype.writeTypingsFile = function writeTypingsFile(name, directory, code) { 469 fs.writeFileSync(path.join(directory, name + '.d.ts'), code); 470 }; 471 472 /** 473 * Create the typescript definition files for every service. 474 */ 475 TSGenerator.prototype.generateAllClientTypings = function generateAllClientTypings() { 476 var self = this; 477 var metadata = this.metadata; 478 // Iterate over every service 479 var serviceIdentifiers = Object.keys(metadata); 480 serviceIdentifiers.forEach(function(serviceIdentifier) { 481 var code = self.processServiceModel(serviceIdentifier); 482 self.writeTypingsFile(serviceIdentifier, self._clientsDir, code); 483 }); 484 }; 485 486 /** 487 * Create the typescript definition files for the all and browser_default exports. 488 */ 489 TSGenerator.prototype.generateGroupedClients = function generateGroupedClients() { 490 var metadata = this.metadata; 491 var allCode = ''; 492 var browserCode = ''; 493 // Iterate over every service 494 var serviceIdentifiers = Object.keys(metadata); 495 serviceIdentifiers.forEach(function(serviceIdentifier) { 496 var className = metadata[serviceIdentifier].name; 497 var code = 'export import ' + className + ' = require(\'./' + serviceIdentifier + '\');\n'; 498 allCode += code; 499 if (metadata[serviceIdentifier].cors) { 500 browserCode += code; 501 } 502 }); 503 this.writeTypingsFile('all', this._clientsDir, allCode); 504 this.writeTypingsFile('browser_default', this._clientsDir, browserCode); 505 }; 506 507 module.exports = TSGenerator;