Skip to content

Commit

Permalink
feat: created specificationUrl class (#110)
Browse files Browse the repository at this point in the history
* feat: created specificationUrl class

* refactor: moving file read logic to seperate function

* feat: supports loading spec file from URL

* fix: lint fixes

* chore: resolving some conflits

* feat: added tests for validating specification files from url

* chore: lint fixes

* feat: created error class for when url is invalid

* feat: lint and code smell fixes

* chore: updating validate argument description

* feat: made changes requested by @fmvilas

* fix: fixing failing tests because of wrong url conditions

* chore: updated the variable names accordingly
  • Loading branch information
Souvikns authored Jan 4, 2022
1 parent 8dd13cf commit 0dd0cb8
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 15 deletions.
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@oclif/plugin-help": "^3.2.3",
"@types/inquirer": "^8.1.3",
"@types/ws": "^8.2.0",
"node-fetch": "^2.0.0",
"chalk": "^4.1.0",
"chokidar": "^3.5.2",
"indent-string": "^4.0.0",
Expand All @@ -42,6 +43,7 @@
"@types/lodash.template": "^4.4.4",
"@types/mocha": "^5.2.7",
"@types/node": "^10.17.60",
"@types/node-fetch": "^2.0.2",
"@types/serve-handler": "^6.1.1",
"@types/wrap-ansi": "^8.0.1",
"@typescript-eslint/eslint-plugin": "^4.28.4",
Expand Down
4 changes: 2 additions & 2 deletions src/commands/start/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export default class StartStudio extends Command {

async run() {
const { flags } = this.parse(StartStudio);
const filePath = flags.file || (await load()).getPath();
const filePath = flags.file || (await load()).getFilePath();
const port = flags.port;

startStudio(filePath, port);
startStudio(filePath as string, port);
}
}
15 changes: 10 additions & 5 deletions src/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {flags } from '@oclif/command';
import { flags } from '@oclif/command';
import * as parser from '@asyncapi/parser';
import Command from '../base';
import { ValidationError } from '../errors/validation-error';
Expand All @@ -13,14 +13,14 @@ export default class Validate extends Command {
}

static args = [
{ name: 'spec-file', description: 'spec path or context-name', required: false },
{ name: 'spec-file', description: 'spec path, url, or context-name', required: false },
]

async run() {
const { args } = this.parse(Validate);
const filePath = args['spec-file'];
let specFile;

try {
specFile = await load(filePath);
} catch (err) {
Expand All @@ -34,8 +34,13 @@ export default class Validate extends Command {
}
}
try {
await parser.parse(await specFile.read());
this.log(`File ${specFile.getPath()} successfully validated!`);
if (specFile.getFilePath()) {
await parser.parse(specFile.text());
this.log(`File ${specFile.getFilePath()} successfully validated!`);
} else if (specFile.getFileURL()) {
await parser.parse(specFile.text());
this.log(`URL ${specFile.getFileURL()} successfully validated`);
}
} catch (error) {
throw new ValidationError({
type: 'parser-error',
Expand Down
7 changes: 7 additions & 0 deletions src/errors/specification-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@ export class SpecificationFileNotFound extends SpecificationFileError {
}
}
}

export class SpecificationURLNotFound extends SpecificationFileError {
constructor(URL: string) {
super();
this.message = `Unable to fetch specification file from url: ${URL}`;
}
}
69 changes: 61 additions & 8 deletions src/models/SpecificationFile.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import fetch from 'node-fetch';

import { loadContext } from './Context';
import { SpecificationFileNotFound } from '../errors/specification-file';
import { SpecificationFileNotFound, SpecificationURLNotFound } from '../errors/specification-file';

const { readFile, lstat } = fs;
const allowedFileNames: string[] = [
Expand All @@ -12,14 +13,52 @@ const allowedFileNames: string[] = [
];
const TYPE_CONTEXT_NAME = 'context-name';
const TYPE_FILE_PATH = 'file-path';
const TYPE_URL = 'url-path';

export class Specification {
private readonly spec: string;
private readonly filePath?: string;
private readonly fileURL?: string;
constructor(spec: string, options?: { filepath?: string, fileURL?: string }) {
this.spec = spec;
this.filePath = options?.filepath;
this.fileURL = options?.fileURL;
}

text() {
return this.spec;
}

getFilePath() {
return this.filePath;
}

getFileURL() {
return this.fileURL;
}

static async fromFile(filepath: string) {
return new Specification(await readFile(filepath, { encoding: 'utf8' }), { filepath });
}

static async fromURL(URLpath: string) {
let response;
try {
response = await fetch(URLpath, { method: 'GET' });
} catch (error) {
throw new SpecificationURLNotFound(URLpath);
}
return new Specification(await response?.text() as string, { fileURL: URLpath });
}
}

export default class SpecificationFile {
private readonly pathToFile: string;

constructor(filePath: string) {
this.pathToFile = filePath;
}

getPath(): string {
return this.pathToFile;
}
Expand All @@ -29,22 +68,26 @@ export default class SpecificationFile {
}
}

export async function load(filePathOrContextName?: string): Promise<SpecificationFile> {
export async function load(filePathOrContextName?: string): Promise<Specification> {
if (filePathOrContextName) {
const type = await nameType(filePathOrContextName);
if (type === TYPE_CONTEXT_NAME) {
return loadFromContext(filePathOrContextName);
}
}

if (type === TYPE_URL) {
return Specification.fromURL(filePathOrContextName);
}
await fileExists(filePathOrContextName);
return new SpecificationFile(filePathOrContextName);
return Specification.fromFile(filePathOrContextName);
}

try {
return await loadFromContext();
} catch (e) {
const autoDetectedSpecFile = await detectSpecFile();
if (autoDetectedSpecFile) {
return new SpecificationFile(autoDetectedSpecFile);
return Specification.fromFile(autoDetectedSpecFile);
}
if (!filePathOrContextName || !autoDetectedSpecFile) {
throw e;
Expand All @@ -65,10 +108,20 @@ export async function nameType(name: string): Promise<string> {
}
return TYPE_CONTEXT_NAME;
} catch (e) {
if (await isURL(name)) {return TYPE_URL;}
return TYPE_CONTEXT_NAME;
}
}

export async function isURL(urlpath: string): Promise<boolean> {
try {
const url = new URL(urlpath);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (error) {
return false;
}
}

export async function fileExists(name: string): Promise<boolean> {
try {
if ((await lstat(name)).isFile()) {
Expand All @@ -80,9 +133,9 @@ export async function fileExists(name: string): Promise<boolean> {
}
}

async function loadFromContext(contextName?: string): Promise<SpecificationFile> {
async function loadFromContext(contextName?: string): Promise<Specification> {
const context = await loadContext(contextName);
return new SpecificationFile(context);
return Specification.fromFile(context);
}

async function detectSpecFile(): Promise<string | undefined> {
Expand Down
10 changes: 10 additions & 0 deletions test/commands/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ describe('validate', () => {
expect(ctx.stderr).to.equals('ValidationError: There is no file or context with name "./test/not-found.yml".\n');
done();
});

test
.stderr()
.stdout()
.command(['validate', 'https://bit.ly/asyncapi'])
.it('works when url is passed', (ctx, done) => {
expect(ctx.stdout).to.equals('URL https://bit.ly/asyncapi successfully validated\n');
expect(ctx.stderr).to.equals('');
done();
});
});

describe('with context names', () => {
Expand Down

0 comments on commit 0dd0cb8

Please sign in to comment.