googleapis
Version:
Google APIs Client Library for Node.js
385 lines • 15.8 kB
JavaScript
"use strict";
// Copyright 2014-2016, Google, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const google_auth_library_1 = require("google-auth-library");
const mkdirp = require("mkdirp");
const nunjucks = require("nunjucks");
const p_queue_1 = require("p-queue");
const path = require("path");
const url = require("url");
const util = require("util");
const writeFile = util.promisify(fs.writeFile);
const readDir = util.promisify(fs.readdir);
const FRAGMENT_URL = 'https://storage.googleapis.com/apisnippets-staging/public/';
const srcPath = path.join(__dirname, '../../../src');
const TEMPLATES_DIR = path.join(srcPath, 'generator/templates');
const API_TEMPLATE = path.join(TEMPLATES_DIR, 'api-endpoint.njk');
const RESERVED_PARAMS = ['resource', 'media', 'auth'];
function getObjectType(item) {
if (item.additionalProperties) {
const valueType = getType(item.additionalProperties);
return `{ [key: string]: ${valueType}; }`;
}
else if (item.properties) {
const fields = item.properties;
const objectType = Object.keys(fields)
.map(field => `${cleanPropertyName(field)}?: ${getType(fields[field])};`)
.join(' ');
return `{ ${objectType} }`;
}
else {
return 'any';
}
}
function isSimpleType(type) {
if (type.indexOf('{') > -1) {
return false;
}
return true;
}
function cleanPropertyName(prop) {
const match = prop.match(/[-@.]/g);
return match ? `'${prop}'` : prop;
}
function camelify(name) {
// If the name has a `-`, remove it and camelize.
// Ex: `well-known` => `wellKnown`
if (name.includes('-')) {
const parts = name.split('-').filter(x => !!x);
name = parts
.map((part, i) => {
if (i === 0) {
return part;
}
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join('');
}
return name;
}
function getType(item) {
if (item.$ref) {
return `Schema$${item.$ref}`;
}
switch (item.type) {
case 'integer':
return 'number';
case 'object':
return getObjectType(item);
case 'array':
const innerType = getType(item.items);
if (isSimpleType(innerType)) {
return `${innerType}[]`;
}
else {
return `Array<${innerType}>`;
}
default:
return item.type;
}
}
class Generator {
/**
* Generator for generating API endpoints
* @param options Options for generation
*/
constructor(options = {}) {
this.transporter = new google_auth_library_1.DefaultTransporter();
this.requestQueue = new p_queue_1.default({ concurrency: 50 });
this.state = new Map();
this.options = options;
this.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(TEMPLATES_DIR), { trimBlocks: true });
this.env.addFilter('buildurl', buildurl);
this.env.addFilter('oneLine', this.oneLine);
this.env.addFilter('getType', getType);
this.env.addFilter('cleanPropertyName', cleanPropertyName);
this.env.addFilter('cleanComments', this.cleanComments);
this.env.addFilter('camelify', camelify);
this.env.addFilter('getPathParams', this.getPathParams);
this.env.addFilter('getSafeParamName', this.getSafeParamName);
this.env.addFilter('hasResourceParam', this.hasResourceParam);
this.env.addFilter('cleanPaths', str => {
return str
? str
.replace(/\/\*\//gi, '/x/')
.replace(/\/\*`/gi, '/x')
.replace(/\*\//gi, 'x/')
.replace(/\\n/gi, 'x/')
: '';
});
}
/**
* A multi-line string is turned into one line.
* @param str String to process
* @return Single line string processed
*/
oneLine(str) {
return str ? str.replace(/\n/g, ' ') : '';
}
/**
* Clean a string of comment tags.
* @param str String to process
* @return Single line string processed
*/
cleanComments(str) {
// Convert /* into /x and */ into x/
return str ? str.replace(/\*\//g, 'x/').replace(/\/\*/g, '/x') : '';
}
getPathParams(params) {
const pathParams = new Array();
if (typeof params !== 'object') {
params = {};
}
Object.keys(params).forEach(key => {
if (params[key].location === 'path') {
pathParams.push(key);
}
});
return pathParams;
}
getSafeParamName(param) {
if (RESERVED_PARAMS.indexOf(param) !== -1) {
return param + '_';
}
return param;
}
hasResourceParam(method) {
return method.parameters && method.parameters['resource'];
}
/**
* Add a requests to the rate limited queue.
* @param opts Options to pass to the default transporter
*/
request(opts) {
return this.requestQueue.add(() => {
return this.transporter.request(opts);
});
}
/**
* Log output of generator. Works just like console.log.
*/
log(...args) {
if (this.options && this.options.debug) {
console.log(...args);
}
}
/**
* Write to the state log, which is used for debugging.
* @param id DiscoveryRestUrl of the endpoint to log
* @param message
*/
logResult(id, message) {
if (!this.state.has(id)) {
this.state.set(id, new Array());
}
this.state.get(id).push(message);
}
/**
* Generate all APIs and write to files.
*/
async generateAllAPIs(discoveryUrl) {
const headers = this.options.includePrivate
? {}
: { 'X-User-Ip': '0.0.0.0' };
const res = await this.request({ url: discoveryUrl, headers });
const apis = res.data.items;
const queue = new p_queue_1.default({ concurrency: 10 });
console.log(`Generating ${apis.length} APIs...`);
queue.addAll(apis.map(api => {
return async () => {
this.log('Generating API for %s...', api.id);
this.logResult(api.discoveryRestUrl, 'Attempting first generateAPI call...');
try {
const results = await this.generateAPI(api.discoveryRestUrl);
this.logResult(api.discoveryRestUrl, `GenerateAPI call success!`);
}
catch (e) {
this.logResult(api.discoveryRestUrl, `GenerateAPI call failed with error: ${e}, moving on.`);
console.error(`Failed to generate API: ${api.id}`);
console.log(api.id +
'\n-----------\n' +
util.inspect(this.state.get(api.discoveryRestUrl), {
maxArrayLength: null,
}) +
'\n');
}
};
}));
try {
await queue.onIdle();
await this.generateIndex(apis);
}
catch (e) {
console.log(util.inspect(this.state, { maxArrayLength: null }));
}
}
async generateIndex(metadata) {
const apis = {};
const apisPath = path.join(srcPath, 'apis');
const indexPath = path.join(apisPath, 'index.ts');
const rootIndexPath = path.join(apisPath, '../', 'index.ts');
// Dynamically discover available APIs
const files = await readDir(apisPath);
for (const file of files) {
const filePath = path.join(apisPath, file);
if (!(await util.promisify(fs.stat)(filePath)).isDirectory()) {
continue;
}
apis[file] = {};
const files = await readDir(path.join(apisPath, file));
for (const version of files) {
const parts = path.parse(version);
if (!version.endsWith('.d.ts') && parts.ext === '.ts') {
apis[file][version] = parts.name;
const desc = metadata.filter(x => x.name === file)[0].description;
// generate the index.ts
const apiIdxPath = path.join(apisPath, file, 'index.ts');
const result = this.env.render('api-index.njk', {
name: file,
api: apis[file],
});
await writeFile(apiIdxPath, result);
// generate the package.json
const pkgPath = path.join(apisPath, file, 'package.json');
const pkgResult = this.env.render('package.json.njk', {
name: file,
desc,
});
await writeFile(pkgPath, pkgResult);
// generate the README.md
const rdPath = path.join(apisPath, file, 'README.md');
const rdResult = this.env.render('README.md.njk', { name: file, desc });
await writeFile(rdPath, rdResult);
// generate the tsconfig.json
const tsPath = path.join(apisPath, file, 'tsconfig.json');
const tsResult = this.env.render('tsconfig.json.njk');
await writeFile(tsPath, tsResult);
// generate the webpack.config.js
const wpPath = path.join(apisPath, file, 'webpack.config.js');
const wpResult = this.env.render('webpack.config.js.njk', {
name: file,
});
await writeFile(wpPath, wpResult);
}
}
}
const result = this.env.render('index.njk', { apis });
await writeFile(indexPath, result, { encoding: 'utf8' });
const res2 = this.env.render('root-index.njk', { apis });
await writeFile(rootIndexPath, res2, { encoding: 'utf8' });
}
/**
* Given a discovery doc, parse it and recursively iterate over the various
* embedded links.
*/
getFragmentsForSchema(apiDiscoveryUrl, schema, apiPath, tasks) {
if (schema.methods) {
for (const methodName in schema.methods) {
if (schema.methods.hasOwnProperty(methodName)) {
const methodSchema = schema.methods[methodName];
methodSchema.sampleUrl = apiPath + '.' + methodName + '.frag.json';
tasks.push(async () => {
this.logResult(apiDiscoveryUrl, `Making fragment request...`);
this.logResult(apiDiscoveryUrl, methodSchema.sampleUrl);
try {
const res = await this.request({
url: methodSchema.sampleUrl,
});
this.logResult(apiDiscoveryUrl, `Fragment request complete.`);
if (res.data &&
res.data.codeFragment &&
res.data.codeFragment['Node.js']) {
let fragment = res.data.codeFragment['Node.js'].fragment;
fragment = fragment.replace(/\/\*/gi, '/<');
fragment = fragment.replace(/\*\//gi, '>/');
fragment = fragment.replace(/`\*/gi, '`<');
fragment = fragment.replace(/\*`/gi, '>`');
const lines = fragment.split('\n');
lines.forEach((line, i) => {
lines[i] = '*' + (line ? ' ' + lines[i] : '');
});
fragment = lines.join('\n');
methodSchema.fragment = fragment;
}
}
catch (err) {
this.logResult(apiDiscoveryUrl, `Fragment request err: ${err}`);
if (!err.message || err.message.indexOf('AccessDenied') === -1) {
throw err;
}
this.logResult(apiDiscoveryUrl, 'Ignoring error.');
}
});
}
}
}
if (schema.resources) {
for (const resourceName in schema.resources) {
if (schema.resources.hasOwnProperty(resourceName)) {
this.getFragmentsForSchema(apiDiscoveryUrl, schema.resources[resourceName], apiPath + '.' + resourceName, tasks);
}
}
}
}
/**
* Generate API file given discovery URL
* @param apiDiscoveryUri URL or filename of discovery doc for API
*/
async generateAPI(apiDiscoveryUrl) {
const parts = url.parse(apiDiscoveryUrl);
if (apiDiscoveryUrl && !parts.protocol) {
this.log('Reading from file ' + apiDiscoveryUrl);
const file = await util.promisify(fs.readFile)(apiDiscoveryUrl, {
encoding: 'utf-8',
});
await this.generate(apiDiscoveryUrl, JSON.parse(file));
}
else {
this.logResult(apiDiscoveryUrl, `Starting discovery doc request...`);
this.logResult(apiDiscoveryUrl, apiDiscoveryUrl);
const res = await this.request({ url: apiDiscoveryUrl });
await this.generate(apiDiscoveryUrl, res.data);
}
}
async generate(apiDiscoveryUrl, schema) {
this.logResult(apiDiscoveryUrl, `Discovery doc request complete.`);
const tasks = new Array();
this.getFragmentsForSchema(apiDiscoveryUrl, schema, `${FRAGMENT_URL}${schema.name}/${schema.version}/0/${schema.name}`, tasks);
// e.g. apis/drive/v2.js
const exportFilename = path.join(srcPath, 'apis', schema.name, schema.version + '.ts');
this.logResult(apiDiscoveryUrl, `Generating templates...`);
this.logResult(apiDiscoveryUrl, `Step 1...`);
await Promise.all(tasks.map(t => t()));
this.logResult(apiDiscoveryUrl, `Step 2...`);
const contents = this.env.render(API_TEMPLATE, { api: schema });
await util.promisify(mkdirp)(path.dirname(exportFilename));
this.logResult(apiDiscoveryUrl, `Step 3...`);
await writeFile(exportFilename, contents, { encoding: 'utf8' });
this.logResult(apiDiscoveryUrl, `Template generation complete.`);
return exportFilename;
}
}
exports.Generator = Generator;
/**
* Build a string used to create a URL from the discovery doc provided URL.
* replace double slashes with single slash (except in https://)
* @private
* @param input URL to build from
* @return Resulting built URL
*/
function buildurl(input) {
return input ? `'${input}'`.replace(/([^:]\/)\/+/g, '$1') : '';
}
//# sourceMappingURL=generator.js.map