Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetching asyncapi file from server in cli and library #346

Merged
merged 3 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
```

**generate from URL:**
derberg marked this conversation as resolved.
Show resolved Hide resolved
```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 {
derberg marked this conversation as resolved.
Show resolved Hide resolved
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