Skip to content

Commit

Permalink
feat: fetching asyncapi file from server using cli or lib (#346)
Browse files Browse the repository at this point in the history
Co-authored-by: Fran Méndez <[email protected]>
  • Loading branch information
derberg and fmvilas authored May 21, 2020
1 parent 98f36e9 commit 7408c69
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 13 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Options:
ag asyncapi.yaml @asyncapi/html-template
```
**Generating from a URL:**
```bash
ag https://raw.githubusercontent.com/asyncapi/asyncapi/master/examples/2.0.0/streetlights.yml @asyncapi/html-template
```
**Specify where to put the result:**
```bash
ag asyncapi.yaml @asyncapi/html-template -o ./docs
Expand Down
33 changes: 22 additions & 11 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ const xfs = require('fs.extra');
const packageInfo = require('./package.json');
const Generator = require('./lib/generator');
const Watcher = require('./lib/watcher');
const { isLocalTemplate } = require('./lib/utils');
const { isLocalTemplate, isFilePath } = require('./lib/utils');

const red = text => `\x1b[31m${text}\x1b[0m`;
const magenta = text => `\x1b[35m${text}\x1b[0m`;
const yellow = text => `\x1b[33m${text}\x1b[0m`;
const green = text => `\x1b[32m${text}\x1b[0m`;

let asyncapiFile;
let asyncapiDocPath;
let template;
const params = {};
const noOverwriteGlobs = [];
Expand Down Expand Up @@ -49,8 +49,8 @@ const showErrorAndExit = err => {
program
.version(packageInfo.version)
.arguments('<asyncapi> <template>')
.action((asyncAPIPath, tmpl) => {
asyncapiFile = path.resolve(asyncAPIPath);
.action((path, tmpl) => {
asyncapiDocPath = path;
template = tmpl;
})
.option('-d, --disable-hook <hookType>', 'disable a specific hook type', disableHooksParser)
Expand All @@ -63,10 +63,11 @@ program
.option('--watch-template', 'watches the template directory and the AsyncAPI document, and re-generate the files when changes occur. Ignores the output directory. This flag should be used only for template development.')
.parse(process.argv);

if (!asyncapiFile) {
console.error(red('> Path to AsyncAPI file not provided.'));
if (!asyncapiDocPath) {
console.error(red('> Path or URL to AsyncAPI file not provided.'));
program.help(); // This exits the process
}
const isAsyncapiDocLocal = isFilePath(asyncapiDocPath);

xfs.mkdirp(program.output, async err => {
if (err) return showErrorAndExit(err);
Expand All @@ -78,19 +79,25 @@ xfs.mkdirp(program.output, async err => {

// If we want to watch for changes do that
if (program.watchTemplate) {
let watcher;
const watchDir = path.resolve(template);
const outputPath = path.resolve(watchDir, program.output);
// Template name is needed as it is not always a part of the cli commad
// There is a use case that you run generator from a root of the template with `./` path
const templateName = require(path.resolve(watchDir,'package.json')).name;

console.log(`[WATCHER] Watching for changes in the template directory ${magenta(watchDir)} and in the AsyncAPI file ${magenta(asyncapiFile)}`);

if (isAsyncapiDocLocal) {
console.log(`[WATCHER] Watching for changes in the template directory ${magenta(watchDir)} and in the AsyncAPI file ${magenta(asyncapiDocPath)}`);
watcher = new Watcher([asyncapiDocPath, watchDir], outputPath);
} else {
console.log(`[WATCHER] Watching for changes in the template directory ${magenta(watchDir)}`);
watcher = new Watcher(watchDir, outputPath);
}
// Must check template in its installation path in generator to use isLocalTemplate function
if (!await isLocalTemplate(path.resolve(Generator.DEFAULT_TEMPLATES_DIR, templateName))) {
console.warn(`WARNING: ${template} is a remote template. Changes may be lost on subsequent installations.`);
}
const outputPath = path.resolve(watchDir, program.output);
const watcher = new Watcher([asyncapiFile, watchDir], outputPath);

watcher.watch(async (changedFiles) => {
console.clear();
console.log('[WATCHER] Change detected');
Expand Down Expand Up @@ -139,7 +146,11 @@ function generate(targetDir) {
debug: program.debug
});

await generator.generateFromFile(asyncapiFile);
if (isAsyncapiDocLocal) {
await generator.generateFromFile(path.resolve(asyncapiDocPath));
} else {
await generator.generateFromURL(asyncapiDocPath);
}
console.log(green('\n\nDone! ✨'));
console.log(`${yellow('Check out your shiny new generated files at ') + magenta(program.output) + yellow('.')}\n`);
resolve();
Expand Down
37 changes: 37 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
* [.debug](#Generator+debug) : <code>Boolean</code>
* [.install](#Generator+install) : <code>Boolean</code>
* [.templateConfig](#Generator+templateConfig) : <code>Object</code>
* [.hooks](#Generator+hooks) : <code>Object</code>
* [.templateParams](#Generator+templateParams) : <code>Object</code>
* [.generate(asyncapiDocument)](#Generator+generate) ⇒ <code>Promise</code>
* [.generateFromString(asyncapiString, [parserOptions])](#Generator+generateFromString) ⇒ <code>Promise</code>
* [.generateFromURL(asyncapiURL)](#Generator+generateFromURL) ⇒ <code>Promise</code>
* [.generateFromFile(asyncapiFile)](#Generator+generateFromFile) ⇒ <code>Promise</code>
* [.installTemplate([force])](#Generator+installTemplate)
* _static_
Expand Down Expand Up @@ -117,6 +119,12 @@ Install the template and its dependencies, even when the template has already be
### generator.templateConfig : <code>Object</code>
The template configuration.

**Kind**: instance property of [<code>Generator</code>](#Generator)
<a name="Generator+hooks"></a>

### generator.hooks : <code>Object</code>
Hooks object with hooks functionst grouped by the hook type.

**Kind**: instance property of [<code>Generator</code>](#Generator)
<a name="Generator+templateParams"></a>

Expand Down Expand Up @@ -198,6 +206,35 @@ try {
console.error(e);
}
```
<a name="Generator+generateFromURL"></a>

### generator.generateFromURL(asyncapiURL) ⇒ <code>Promise</code>
Generates files from a given template and AsyncAPI file stored on external server.

**Kind**: instance method of [<code>Generator</code>](#Generator)

| Param | Type | Description |
| --- | --- | --- |
| asyncapiURL | <code>String</code> | Link to server and AsyncAPI file. |

**Example**
```js
generator
.generateFromURL('https://example.com/asyncapi.yaml')
.then(() => {
console.log('Done!');
})
.catch(console.error);
```
**Example** *(Using async/await)*
```js
try {
await generator.generateFromURL('https://example.com/asyncapi.yaml');
console.log('Done!');
} catch (e) {
console.error(e);
}
```
<a name="Generator+generateFromFile"></a>

### generator.generateFromFile(asyncapiFile) ⇒ <code>Promise</code>
Expand Down
5 changes: 5 additions & 0 deletions lib/__mocks__/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ utils.readFile = jest.fn(async (filePath) => {
return utils.__files[filePath];
});

utils.__contentOfFetchedFile = '';
utils.fetchSpec = jest.fn(async (fileUrl) => {
return utils.__contentOfFetchedFile;
});

utils.__isFileSystemPathValue = false;
utils.isFileSystemPath = jest.fn(() => utils.__isFileSystemPathValue);

Expand Down
32 changes: 31 additions & 1 deletion lib/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const {
readDir,
writeFile,
copyFile,
exists
exists,
fetchSpec
} = require('./utils');
const { registerFilters } = require('./filtersRegistry');
const { registerHooks } = require('./hooksRegistry');
Expand Down Expand Up @@ -109,6 +110,7 @@ class Generator {
this.install = install;
/** @type {Object} The template configuration. */
this.templateConfig = {};
/** @type {Object} Hooks object with hooks functionst grouped by the hook type. */
this.hooks = {};

// Load template configuration
Expand Down Expand Up @@ -233,6 +235,34 @@ class Generator {
return this.generate(this.asyncapi);
}

/**
* Generates files from a given template and AsyncAPI file stored on external server.
*
* @example
* generator
* .generateFromURL('https://example.com/asyncapi.yaml')
* .then(() => {
* console.log('Done!');
* })
* .catch(console.error);
*
* @example <caption>Using async/await</caption>
* try {
* await generator.generateFromURL('https://example.com/asyncapi.yaml');
* console.log('Done!');
* } catch (e) {
* console.error(e);
* }
*
* @param {String} asyncapiURL Link to AsyncAPI file
* @return {Promise}
*/
async generateFromURL(asyncapiURL) {
const doc = await fetchSpec(asyncapiURL);

return this.generateFromString(doc);
}

/**
* Generates files from a given template and AsyncAPI file.
*
Expand Down
26 changes: 26 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const fs = require('fs');
const util = require('util');
const path = require('path');
const fetch = require('node-fetch');
const url = require('url');
const utils = module.exports;

utils.lstat = util.promisify(fs.lstat);
Expand Down Expand Up @@ -80,4 +82,28 @@ utils.getLocalTemplateDetails = async (templatePath) => {
link: linkTarget,
resolvedLink: path.resolve(path.dirname(templatePath), linkTarget),
};
};

/**
* Fetches an AsyncAPI document from the given URL and return its content as string
*
* @param {String} url URL where the AsyncAPI document is located.
* @returns Promise<String>} Content of fetched file.
*/
utils.fetchSpec = (url) => {
return new Promise((resolve, reject) => {
fetch(url)
.then(res => resolve(res.text()))
.catch(reject);
});
};

/**
* Checks if given string is URL and if not, we assume it is file path
*
* @param {String} str Information representing file path or url
* @returns {Boolean}
*/
utils.isFilePath = (str) => {
return !url.parse(str).hostname;
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"js-yaml": "^3.13.1",
"markdown-it": "^8.4.1",
"minimatch": "^3.0.4",
"node-fetch": "^2.6.0",
"npmi": "^4.0.0",
"nunjucks": "^3.2.0",
"semver": "^7.3.2",
Expand Down
18 changes: 17 additions & 1 deletion test/generator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ describe('Generator', () => {
const generateFromStringMock = jest.fn().mockResolvedValue();
const gen = new Generator('testTemplate', __dirname);
gen.generateFromString = generateFromStringMock;
await gen.generateFromFile('fake-asyncapi.yml');
await gen.generateFromFile(filePath);
expect(utils.readFile).toHaveBeenCalled();
expect(utils.readFile.mock.calls[0][0]).toBe(filePath);
expect(utils.readFile.mock.calls[0][1]).toStrictEqual({ encoding: 'utf8' });
Expand All @@ -280,6 +280,22 @@ describe('Generator', () => {
});
});

describe('#generateFromURL', () => {
it('calls fetch and generateFromString with the right params', async () => {
const utils = require('../lib/utils');
const asyncapiURL = 'http://example.com/fake-asyncapi.yml';
utils.__contentOfFetchedFile = 'fake text';

const generateFromStringMock = jest.fn().mockResolvedValue();
const gen = new Generator('testTemplate', __dirname);
gen.generateFromString = generateFromStringMock;
await gen.generateFromURL(asyncapiURL);
expect(utils.fetchSpec).toHaveBeenCalled();
expect(utils.fetchSpec.mock.calls[0][0]).toBe(asyncapiURL);
expect(generateFromStringMock.mock.calls[0][0]).toBe('fake text');
});
});

describe('#installTemplate', () => {
let npmiMock;
let utils;
Expand Down

0 comments on commit 7408c69

Please sign in to comment.