git-off

git off handles large files in git repos
git clone https://noulin.net/git/git-off.git
Log | Files | Refs | README

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;