diff --git a/package.json b/package.json
index 7905f9f..1bf97ed 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,8 @@
"homepage": "https://github.com/asyncapi/diff#readme",
"dependencies": {
"fast-json-patch": "^3.0.0-1",
- "js-yaml": "^4.1.0"
+ "js-yaml": "^4.1.0",
+ "json2md": "^1.12.0"
},
"devDependencies": {
"@asyncapi/parser": "^1.15.0",
@@ -52,6 +53,7 @@
"@semantic-release/release-notes-generator": "^9.0.1",
"@types/jest": "^26.0.23",
"@types/js-yaml": "^4.0.5",
+ "@types/json2md": "^1.5.1",
"@types/node": "^15.12.1",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
diff --git a/src/asyncapidiff.ts b/src/asyncapidiff.ts
index 89833d8..fe4d8e6 100644
--- a/src/asyncapidiff.ts
+++ b/src/asyncapidiff.ts
@@ -7,21 +7,24 @@ import {
} from './types';
import { breaking, nonBreaking, unclassified } from './constants';
import toProperFormat from './helpers/output/toProperFormat';
+import {MarkdownSubtype} from './types';
/**
* Implements methods to deal with diff output.
* @class
*
- * @returns {AsyncAPIDiff} AsynAPIDiff
+ * @returns {AsyncAPIDiff} AsyncAPIDiff
*/
export default class AsyncAPIDiff {
private output: JSONOutput;
private outputType: OutputType;
+ private markdownSubtype: MarkdownSubtype;
constructor(output: string, options: AsyncAPIDiffOptions) {
// output is a stringified JSON
this.output = JSON.parse(output);
this.outputType = options.outputType;
+ this.markdownSubtype = options.markdownSubtype || 'json';
}
/**
@@ -32,7 +35,7 @@ export default class AsyncAPIDiff {
(diff) => diff.type === breaking
);
- return toProperFormat(breakingChanges, this.outputType);
+ return toProperFormat({data: breakingChanges, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
}
/**
@@ -43,7 +46,7 @@ export default class AsyncAPIDiff {
(diff) => diff.type === nonBreaking
);
- return toProperFormat(nonBreakingChanges, this.outputType);
+ return toProperFormat({data: nonBreakingChanges, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
}
/**
@@ -54,13 +57,13 @@ export default class AsyncAPIDiff {
(diff) => diff.type === unclassified
);
- return toProperFormat(unclassifiedChanges, this.outputType);
+ return toProperFormat({data: unclassifiedChanges, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
}
/**
* @returns {Output} The full output
*/
getOutput(): Output {
- return toProperFormat(this.output, this.outputType);
+ return toProperFormat({data: this.output, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
}
}
diff --git a/src/helpers/MarkdownHelpers.ts b/src/helpers/MarkdownHelpers.ts
new file mode 100644
index 0000000..00aaa7b
--- /dev/null
+++ b/src/helpers/MarkdownHelpers.ts
@@ -0,0 +1,76 @@
+import {ChangeMarkdownGenerationConfig, MarkdownDropdownGenerationConfig} from '../types';
+import convertToYAML from './output/convertToYAML';
+
+/**
+ * Groups an array of changes by their 'type' property
+ * @param object The input object
+ * @returns The grouped object
+ */
+export function groupChangesByType(object: any): { string: [{ path: string, any: any }] } {
+ return object.reduce((objectsByKeyValue: { [x: string]: any; }, obj: { [x: string]: any; }) => {
+ const value = obj['type'];
+ // eslint-disable-next-line security/detect-object-injection
+ objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
+ return objectsByKeyValue;
+ }, {});
+}
+
+/**
+ * Sets the first letter of a string to uppercase
+ * @param s The input string
+ * @returns The string with the first letter capitalised
+ */
+export function capitaliseFirstLetter(s: string): string {
+ return s[0].toUpperCase() + s.slice(1);
+}
+
+/**
+ * Generates the Markdown list items for a single change
+ * @param: config Configuration options for the generated markdown
+ * @param config.change The object describing the change
+ * @param config.markdownSubtype the format to display the dropdown data in
+ * @returns The Markdown list describing the change
+ */
+export function generateMarkdownForChange(config: ChangeMarkdownGenerationConfig): any {
+ const toAppend: any[] = [`**Path**: \`${config.change.path}\``];
+ const listItem = {ul: [] as any[]};
+
+ for (const [label, value] of Object.entries(config.change)) {
+ if (label !== 'path' && label !== 'type') {
+ // if the value is an object, display within a dropdown
+ if (typeof value === 'object') {
+ listItem.ul.push(convertDataToDropdown({
+ label: capitaliseFirstLetter(label),
+ data: value,
+ markdownSubtype: config.markdownSubtype
+ }));
+ } else {
+ listItem.ul.push(`**${capitaliseFirstLetter(label)}**: ${value}`);
+ }
+ }
+ }
+
+ toAppend.push(listItem);
+ return toAppend;
+}
+
+/**
+ * Converts the label and data to a markdown dropdown
+ * @param config: Configuration options for the generated dropdown
+ * @param config.label The summary / title
+ * @param config.data The data to hide in dropdown
+ * @param config.markdownSubtype the format to display the dropdown data in
+ * @returns Markdown string with the label as a summary and the data formatted as JSON code
+ */
+export function convertDataToDropdown(config: MarkdownDropdownGenerationConfig): string {
+ const displayData = config.markdownSubtype === 'json' ? JSON.stringify(config.data, null, 2) : convertToYAML(config.data);
+
+ return `
+ ${config.label}
+
+\`\`\`${config.markdownSubtype}
+${displayData}
+\`\`\`
+
+`;
+}
diff --git a/src/helpers/output/convertToMarkdown.ts b/src/helpers/output/convertToMarkdown.ts
new file mode 100644
index 0000000..8cb43f4
--- /dev/null
+++ b/src/helpers/output/convertToMarkdown.ts
@@ -0,0 +1,36 @@
+import json2md from 'json2md';
+import {
+ capitaliseFirstLetter,
+ generateMarkdownForChange,
+ groupChangesByType
+} from '../MarkdownHelpers';
+import {MarkdownSubtype} from '../../types';
+
+/**
+ * Converts the diff to Markdown
+ * @param object The input object
+ * @param markdownSubtype the format to display the dropdown data in
+ * @returns Markdown output
+ */
+export default function convertToMarkdown(object: any, markdownSubtype: MarkdownSubtype): string {
+ if (Object.prototype.hasOwnProperty.call(object, 'changes')) {
+ object = object.changes;
+ }
+
+ const changeTypeGroups = groupChangesByType(object);
+
+ const markdownStructure = [];
+
+ for (const [changeType, changes] of Object.entries(changeTypeGroups)) {
+ markdownStructure.push({h2: capitaliseFirstLetter(changeType)});
+ const outerList = {ul: [] as any[]};
+
+ for (const change of changes) {
+ outerList.ul.push(...generateMarkdownForChange({change, markdownSubtype}));
+ }
+
+ markdownStructure.push(outerList);
+ }
+
+ return json2md(markdownStructure);
+}
diff --git a/src/helpers/output/toProperFormat.ts b/src/helpers/output/toProperFormat.ts
index dff4bbf..017ff29 100644
--- a/src/helpers/output/toProperFormat.ts
+++ b/src/helpers/output/toProperFormat.ts
@@ -1,18 +1,21 @@
-import { OutputType } from '../../types';
+import {FormatterConfig} from '../../types';
import convertToYAML from './convertToYAML';
+import convertToMarkdown from './convertToMarkdown';
/**
* Converts diff data to the specified format
- * @param data The diff data
+ * @param config: Configuration options for the target format
+ * @param config.data The diff data
+ * @param config.outputType The intended type of the output
+ * @param config.markdownSubtype the format to display the dropdown data in
* @returns formatted diff output
*/
-export default function toProperFormat(
- data: T,
- outputType: OutputType
-): T | string {
- if (outputType === 'yaml' || outputType === 'yml') {
- return convertToYAML(data);
+export default function toProperFormat(config: FormatterConfig): T | string {
+ if (config.outputType === 'yaml' || config.outputType === 'yml') {
+ return convertToYAML(config.data);
+ } else if (config.outputType === 'markdown' || config.outputType === 'md') {
+ return convertToMarkdown(config.data, config.markdownSubtype);
}
- return data;
+ return config.data;
}
diff --git a/src/main.ts b/src/main.ts
index 89edfc5..09082b0 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -44,5 +44,6 @@ export function diff(
const output = categorizeChanges(standard as OverrideStandard, diffOutput);
return new AsyncAPIDiff(JSON.stringify(output), {
outputType: config.outputType || 'json',
+ markdownSubtype: config.markdownSubtype || 'json'
});
}
diff --git a/src/types.ts b/src/types.ts
index 873754e..9bc778b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,7 +1,7 @@
-import { ReplaceOperation, AddOperation } from 'fast-json-patch';
+import {ReplaceOperation, AddOperation} from 'fast-json-patch';
-import { standard } from './standard';
-import { breaking, nonBreaking, unclassified } from './constants';
+import {standard} from './standard';
+import {breaking, nonBreaking, unclassified} from './constants';
export type ActionType = 'add' | 'remove' | 'edit';
@@ -46,13 +46,34 @@ export interface OverrideObject {
export type OverrideStandard = StandardType & OverrideObject;
-export type OutputType = 'json' | 'yaml' | 'yml';
+export type OutputType = 'json' | 'yaml' | 'yml' | 'markdown' | 'md';
+
+export type MarkdownSubtype = 'json' | 'yaml' | 'yml';
+
+export interface FormatterConfig {
+ data: T,
+ outputType: OutputType,
+ markdownSubtype: MarkdownSubtype
+}
+
+export interface ChangeMarkdownGenerationConfig {
+ change: { path: string, any: any },
+ markdownSubtype: MarkdownSubtype
+}
+
+export interface MarkdownDropdownGenerationConfig {
+ label: string,
+ data: { string: any },
+ markdownSubtype: MarkdownSubtype
+}
export interface AsyncAPIDiffOptions {
outputType: OutputType;
+ markdownSubtype?: MarkdownSubtype;
}
export interface Config {
override?: OverrideObject;
outputType?: OutputType;
+ markdownSubtype?: MarkdownSubtype;
}
diff --git a/test/asycnapidiff.spec.ts b/test/asycnapidiff.spec.ts
index a3b9027..9fc4c1f 100644
--- a/test/asycnapidiff.spec.ts
+++ b/test/asycnapidiff.spec.ts
@@ -9,12 +9,22 @@ import {
YAMLNonbreakingChanges,
YAMLOutputDiff,
YAMLUnclassifiedChanges,
+ MarkdownBreakingChanges,
+ MarkdownNonbreakingChanges,
+ MarkdownOutputDiff,
+ MarkdownUnclassifiedChanges,
+ MarkdownJSONSubtypeChanges,
+ MarkdownYAMLSubtypeChanges,
} from './fixtures/asyncapidiff.fixtures';
+import {
+ diffOutput
+} from './fixtures/main.fixtures';
+
describe('AsyncAPIDiff wrapper', () => {
test('checks the instance', () => {
expect(
- new AsyncAPIDiff(JSON.stringify(inputDiff), { outputType: 'json' })
+ new AsyncAPIDiff(JSON.stringify(inputDiff), {outputType: 'json'})
).toBeInstanceOf(AsyncAPIDiff);
});
@@ -73,4 +83,47 @@ describe('AsyncAPIDiff wrapper', () => {
});
expect(diff.unclassified()).toEqual(YAMLUnclassifiedChanges);
});
+
+ test('Markdown: returns the original full output', () => {
+ const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
+ outputType: 'markdown',
+ });
+ expect(diff.getOutput()).toEqual(MarkdownOutputDiff);
+ });
+
+ test('Markdown: returns breaking changes', () => {
+ const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
+ outputType: 'markdown',
+ });
+ expect(diff.breaking()).toEqual(MarkdownBreakingChanges);
+ });
+
+ test('Markdown: returns non-breaking changes', () => {
+ const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
+ outputType: 'markdown',
+ });
+ expect(diff.nonBreaking()).toEqual(MarkdownNonbreakingChanges);
+ });
+
+ test('Markdown: returns unclassified changes', () => {
+ const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
+ outputType: 'markdown',
+ });
+ expect(diff.unclassified()).toEqual(MarkdownUnclassifiedChanges);
+ });
+
+ test('Markdown: returns changes using subtype JSON as the default', () => {
+ const diff = new AsyncAPIDiff(JSON.stringify(diffOutput), {
+ outputType: 'markdown',
+ });
+ expect(diff.getOutput()).toEqual(MarkdownJSONSubtypeChanges);
+ });
+
+ test('Markdown: returns changes using subtype YAML', () => {
+ const diff = new AsyncAPIDiff(JSON.stringify(diffOutput), {
+ outputType: 'markdown',
+ markdownSubtype: 'yaml',
+ });
+ expect(diff.getOutput()).toEqual(MarkdownYAMLSubtypeChanges);
+ });
});
diff --git a/test/fixtures/asyncapidiff.fixtures.ts b/test/fixtures/asyncapidiff.fixtures.ts
index a95ed7a..56fe8aa 100644
--- a/test/fixtures/asyncapidiff.fixtures.ts
+++ b/test/fixtures/asyncapidiff.fixtures.ts
@@ -56,3 +56,227 @@ export const YAMLNonbreakingChanges = `- type: non-breaking
export const YAMLUnclassifiedChanges = `- type: unclassified
path: /info
`;
+
+export const MarkdownOutputDiff = `## Breaking
+
+
+ - **Path**: \`/servers\`
+
+
+## Non-breaking
+
+
+ - **Path**: \`/channels\`
+
+
+## Unclassified
+
+
+ - **Path**: \`/info\`
+
+`;
+
+export const MarkdownBreakingChanges = `## Breaking
+
+
+ - **Path**: \`/servers\`
+
+`;
+
+export const MarkdownNonbreakingChanges = `## Non-breaking
+
+
+ - **Path**: \`/channels\`
+
+`;
+
+export const MarkdownUnclassifiedChanges = `## Unclassified
+
+
+ - **Path**: \`/info\`
+
+`;
+
+export const MarkdownJSONSubtypeChanges = `## Breaking
+
+
+ - **Path**: \`/channels/mychannel\`
+ - **Action**: remove
+ -
+ Before
+
+ \`\`\`json
+ {
+ "publish": {
+ "message": {
+ "headers": {
+ "properties": {
+ "some-common-header": {
+ "type": "string",
+ "x-parser-schema-id": ""
+ }
+ },
+ "type": "object",
+ "x-parser-schema-id": ""
+ },
+ "schemaFormat": "application/vnd.aai.asyncapi;version=2.0.0",
+ "x-parser-message-name": "channelMessage",
+ "x-parser-message-parsed": true,
+ "x-parser-original-traits": [
+ {
+ "headers": {
+ "properties": {
+ "some-common-header": {
+ "type": "string",
+ "x-parser-schema-id": ""
+ }
+ },
+ "type": "object",
+ "x-parser-schema-id": ""
+ },
+ "x-some-extension": "some extension"
+ }
+ ],
+ "x-some-extension": "some extension"
+ }
+ }
+ }
+ \`\`\`
+
+
+
+ - **Path**: \`/info/version\`
+ - **Action**: edit
+ - **After**: 1.1.0
+ - **Before**: 1.0.0
+
+
+## Non-breaking
+
+
+ - **Path**: \`/channels/anotherChannel\`
+ - **Action**: add
+ -
+ After
+
+ \`\`\`json
+ {
+ "publish": {
+ "message": {
+ "headers": {
+ "properties": {
+ "some-common-header": {
+ "type": "string",
+ "x-parser-schema-id": ""
+ }
+ },
+ "type": "object",
+ "x-parser-schema-id": ""
+ },
+ "schemaFormat": "application/vnd.aai.asyncapi;version=2.0.0",
+ "x-parser-message-name": "channelMessage",
+ "x-parser-message-parsed": true,
+ "x-parser-original-traits": [
+ {
+ "headers": {
+ "properties": {
+ "some-common-header": {
+ "type": "string",
+ "x-parser-schema-id": ""
+ }
+ },
+ "type": "object",
+ "x-parser-schema-id": ""
+ },
+ "x-some-extension": "some extension"
+ }
+ ],
+ "x-some-extension": "some extension"
+ }
+ }
+ }
+ \`\`\`
+
+
+
+`;
+
+export const MarkdownYAMLSubtypeChanges = `## Breaking
+
+
+ - **Path**: \`/channels/mychannel\`
+ - **Action**: remove
+ -
+ Before
+
+ \`\`\`yaml
+ publish:
+ message:
+ headers:
+ properties:
+ some-common-header:
+ type: string
+ x-parser-schema-id:
+ type: object
+ x-parser-schema-id:
+ schemaFormat: application/vnd.aai.asyncapi;version=2.0.0
+ x-parser-message-name: channelMessage
+ x-parser-message-parsed: true
+ x-parser-original-traits:
+ - headers:
+ properties:
+ some-common-header:
+ type: string
+ x-parser-schema-id:
+ type: object
+ x-parser-schema-id:
+ x-some-extension: some extension
+ x-some-extension: some extension
+
+ \`\`\`
+
+
+
+ - **Path**: \`/info/version\`
+ - **Action**: edit
+ - **After**: 1.1.0
+ - **Before**: 1.0.0
+
+
+## Non-breaking
+
+
+ - **Path**: \`/channels/anotherChannel\`
+ - **Action**: add
+ -
+ After
+
+ \`\`\`yaml
+ publish:
+ message:
+ headers:
+ properties:
+ some-common-header:
+ type: string
+ x-parser-schema-id:
+ type: object
+ x-parser-schema-id:
+ schemaFormat: application/vnd.aai.asyncapi;version=2.0.0
+ x-parser-message-name: channelMessage
+ x-parser-message-parsed: true
+ x-parser-original-traits:
+ - headers:
+ properties:
+ some-common-header:
+ type: string
+ x-parser-schema-id:
+ type: object
+ x-parser-schema-id:
+ x-some-extension: some extension
+ x-some-extension: some extension
+
+ \`\`\`
+
+
+
+`;
diff --git a/test/fixtures/main.fixtures.ts b/test/fixtures/main.fixtures.ts
index 355c1ee..289daf5 100644
--- a/test/fixtures/main.fixtures.ts
+++ b/test/fixtures/main.fixtures.ts
@@ -332,3 +332,13 @@ export const YAMLArrayChanges = `changes:
before: 2
type: breaking
`;
+
+export const MarkdownArrayChanges = `## Breaking
+
+
+ - **Path**: \`/servers/google/variables/port/enum/1\`
+ - **Action**: remove
+ - **IsArrayIndex**: true
+ - **Before**: 2
+
+`;
diff --git a/test/main.spec.ts b/test/main.spec.ts
index 419db0f..b481550 100644
--- a/test/main.spec.ts
+++ b/test/main.spec.ts
@@ -1,10 +1,10 @@
-import { parse } from '@asyncapi/parser';
-import { readFileSync } from 'fs';
-import { resolve } from 'path';
+import {parse} from '@asyncapi/parser';
+import {readFileSync} from 'fs';
+import {resolve} from 'path';
import AsyncAPIDiff from '../src/asyncapidiff';
-import { diff } from '../src/main';
-import { OverrideObject } from '../src/types';
+import {diff} from '../src';
+import {OverrideObject} from '../src';
import {
diffOutput,
@@ -16,6 +16,7 @@ import {
specDocument2,
arrayChanges,
YAMLArrayChanges,
+ MarkdownArrayChanges
} from './fixtures/main.fixtures';
describe('main function', () => {
@@ -68,7 +69,12 @@ describe('main function', () => {
});
test('YAML: checks output with array changes', () => {
- const output = diff(specDocument1, specDocument2, { outputType: 'yaml' });
+ const output = diff(specDocument1, specDocument2, {outputType: 'yaml'});
expect(output.getOutput()).toEqual(YAMLArrayChanges);
});
+
+ test('Markdown: checks output with array changes', () => {
+ const output = diff(specDocument1, specDocument2, {outputType: 'markdown'});
+ expect(output.getOutput()).toEqual(MarkdownArrayChanges);
+ });
});