Skip to content

Commit 218aa5e

Browse files
shivensinha4derbergaayushmau5
authored
feat: support for Markdown output (#90)
Co-authored-by: Lukasz Gornicki <[email protected]> Co-authored-by: Aayush Kumar Sahu <[email protected]>
1 parent 4e16ca1 commit 218aa5e

11 files changed

+461
-26
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"homepage": "https://github.com/asyncapi/diff#readme",
4343
"dependencies": {
4444
"fast-json-patch": "^3.0.0-1",
45-
"js-yaml": "^4.1.0"
45+
"js-yaml": "^4.1.0",
46+
"json2md": "^1.12.0"
4647
},
4748
"devDependencies": {
4849
"@asyncapi/parser": "^1.15.0",
@@ -52,6 +53,7 @@
5253
"@semantic-release/release-notes-generator": "^9.0.1",
5354
"@types/jest": "^26.0.23",
5455
"@types/js-yaml": "^4.0.5",
56+
"@types/json2md": "^1.5.1",
5557
"@types/node": "^15.12.1",
5658
"@typescript-eslint/eslint-plugin": "^4.26.0",
5759
"@typescript-eslint/parser": "^4.26.0",

src/asyncapidiff.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,24 @@ import {
77
} from './types';
88
import { breaking, nonBreaking, unclassified } from './constants';
99
import toProperFormat from './helpers/output/toProperFormat';
10+
import {MarkdownSubtype} from './types';
1011

1112
/**
1213
* Implements methods to deal with diff output.
1314
* @class
1415
*
15-
* @returns {AsyncAPIDiff} AsynAPIDiff
16+
* @returns {AsyncAPIDiff} AsyncAPIDiff
1617
*/
1718
export default class AsyncAPIDiff {
1819
private output: JSONOutput;
1920
private outputType: OutputType;
21+
private markdownSubtype: MarkdownSubtype;
2022

2123
constructor(output: string, options: AsyncAPIDiffOptions) {
2224
// output is a stringified JSON
2325
this.output = JSON.parse(output);
2426
this.outputType = options.outputType;
27+
this.markdownSubtype = options.markdownSubtype || 'json';
2528
}
2629

2730
/**
@@ -32,7 +35,7 @@ export default class AsyncAPIDiff {
3235
(diff) => diff.type === breaking
3336
);
3437

35-
return toProperFormat(breakingChanges, this.outputType);
38+
return toProperFormat({data: breakingChanges, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
3639
}
3740

3841
/**
@@ -43,7 +46,7 @@ export default class AsyncAPIDiff {
4346
(diff) => diff.type === nonBreaking
4447
);
4548

46-
return toProperFormat(nonBreakingChanges, this.outputType);
49+
return toProperFormat({data: nonBreakingChanges, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
4750
}
4851

4952
/**
@@ -54,13 +57,13 @@ export default class AsyncAPIDiff {
5457
(diff) => diff.type === unclassified
5558
);
5659

57-
return toProperFormat(unclassifiedChanges, this.outputType);
60+
return toProperFormat({data: unclassifiedChanges, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
5861
}
5962

6063
/**
6164
* @returns {Output} The full output
6265
*/
6366
getOutput(): Output {
64-
return toProperFormat(this.output, this.outputType);
67+
return toProperFormat({data: this.output, outputType: this.outputType, markdownSubtype: this.markdownSubtype});
6568
}
6669
}

src/helpers/MarkdownHelpers.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {ChangeMarkdownGenerationConfig, MarkdownDropdownGenerationConfig} from '../types';
2+
import convertToYAML from './output/convertToYAML';
3+
4+
/**
5+
* Groups an array of changes by their 'type' property
6+
* @param object The input object
7+
* @returns The grouped object
8+
*/
9+
export function groupChangesByType(object: any): { string: [{ path: string, any: any }] } {
10+
return object.reduce((objectsByKeyValue: { [x: string]: any; }, obj: { [x: string]: any; }) => {
11+
const value = obj['type'];
12+
// eslint-disable-next-line security/detect-object-injection
13+
objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
14+
return objectsByKeyValue;
15+
}, {});
16+
}
17+
18+
/**
19+
* Sets the first letter of a string to uppercase
20+
* @param s The input string
21+
* @returns The string with the first letter capitalised
22+
*/
23+
export function capitaliseFirstLetter(s: string): string {
24+
return s[0].toUpperCase() + s.slice(1);
25+
}
26+
27+
/**
28+
* Generates the Markdown list items for a single change
29+
* @param: config Configuration options for the generated markdown
30+
* @param config.change The object describing the change
31+
* @param config.markdownSubtype the format to display the dropdown data in
32+
* @returns The Markdown list describing the change
33+
*/
34+
export function generateMarkdownForChange(config: ChangeMarkdownGenerationConfig): any {
35+
const toAppend: any[] = [`**Path**: \`${config.change.path}\``];
36+
const listItem = {ul: [] as any[]};
37+
38+
for (const [label, value] of Object.entries(config.change)) {
39+
if (label !== 'path' && label !== 'type') {
40+
// if the value is an object, display within a dropdown
41+
if (typeof value === 'object') {
42+
listItem.ul.push(convertDataToDropdown({
43+
label: capitaliseFirstLetter(label),
44+
data: value,
45+
markdownSubtype: config.markdownSubtype
46+
}));
47+
} else {
48+
listItem.ul.push(`**${capitaliseFirstLetter(label)}**: ${value}`);
49+
}
50+
}
51+
}
52+
53+
toAppend.push(listItem);
54+
return toAppend;
55+
}
56+
57+
/**
58+
* Converts the label and data to a markdown dropdown
59+
* @param config: Configuration options for the generated dropdown
60+
* @param config.label The summary / title
61+
* @param config.data The data to hide in dropdown
62+
* @param config.markdownSubtype the format to display the dropdown data in
63+
* @returns Markdown string with the label as a summary and the data formatted as JSON code
64+
*/
65+
export function convertDataToDropdown(config: MarkdownDropdownGenerationConfig): string {
66+
const displayData = config.markdownSubtype === 'json' ? JSON.stringify(config.data, null, 2) : convertToYAML(config.data);
67+
68+
return `<details>
69+
<summary> ${config.label} </summary>
70+
71+
\`\`\`${config.markdownSubtype}
72+
${displayData}
73+
\`\`\`
74+
</details>
75+
`;
76+
}
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import json2md from 'json2md';
2+
import {
3+
capitaliseFirstLetter,
4+
generateMarkdownForChange,
5+
groupChangesByType
6+
} from '../MarkdownHelpers';
7+
import {MarkdownSubtype} from '../../types';
8+
9+
/**
10+
* Converts the diff to Markdown
11+
* @param object The input object
12+
* @param markdownSubtype the format to display the dropdown data in
13+
* @returns Markdown output
14+
*/
15+
export default function convertToMarkdown(object: any, markdownSubtype: MarkdownSubtype): string {
16+
if (Object.prototype.hasOwnProperty.call(object, 'changes')) {
17+
object = object.changes;
18+
}
19+
20+
const changeTypeGroups = groupChangesByType(object);
21+
22+
const markdownStructure = [];
23+
24+
for (const [changeType, changes] of Object.entries(changeTypeGroups)) {
25+
markdownStructure.push({h2: capitaliseFirstLetter(changeType)});
26+
const outerList = {ul: [] as any[]};
27+
28+
for (const change of changes) {
29+
outerList.ul.push(...generateMarkdownForChange({change, markdownSubtype}));
30+
}
31+
32+
markdownStructure.push(outerList);
33+
}
34+
35+
return json2md(markdownStructure);
36+
}

src/helpers/output/toProperFormat.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import { OutputType } from '../../types';
1+
import {FormatterConfig} from '../../types';
22
import convertToYAML from './convertToYAML';
3+
import convertToMarkdown from './convertToMarkdown';
34

45
/**
56
* Converts diff data to the specified format
6-
* @param data The diff data
7+
* @param config: Configuration options for the target format
8+
* @param config.data The diff data
9+
* @param config.outputType The intended type of the output
10+
* @param config.markdownSubtype the format to display the dropdown data in
711
* @returns formatted diff output
812
*/
9-
export default function toProperFormat<T>(
10-
data: T,
11-
outputType: OutputType
12-
): T | string {
13-
if (outputType === 'yaml' || outputType === 'yml') {
14-
return convertToYAML(data);
13+
export default function toProperFormat<T>(config: FormatterConfig<T>): T | string {
14+
if (config.outputType === 'yaml' || config.outputType === 'yml') {
15+
return convertToYAML(config.data);
16+
} else if (config.outputType === 'markdown' || config.outputType === 'md') {
17+
return convertToMarkdown(config.data, config.markdownSubtype);
1518
}
1619

17-
return data;
20+
return config.data;
1821
}

src/main.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ export function diff(
4444
const output = categorizeChanges(standard as OverrideStandard, diffOutput);
4545
return new AsyncAPIDiff(JSON.stringify(output), {
4646
outputType: config.outputType || 'json',
47+
markdownSubtype: config.markdownSubtype || 'json'
4748
});
4849
}

src/types.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { ReplaceOperation, AddOperation } from 'fast-json-patch';
1+
import {ReplaceOperation, AddOperation} from 'fast-json-patch';
22

3-
import { standard } from './standard';
4-
import { breaking, nonBreaking, unclassified } from './constants';
3+
import {standard} from './standard';
4+
import {breaking, nonBreaking, unclassified} from './constants';
55

66
export type ActionType = 'add' | 'remove' | 'edit';
77

@@ -46,13 +46,34 @@ export interface OverrideObject {
4646

4747
export type OverrideStandard = StandardType & OverrideObject;
4848

49-
export type OutputType = 'json' | 'yaml' | 'yml';
49+
export type OutputType = 'json' | 'yaml' | 'yml' | 'markdown' | 'md';
50+
51+
export type MarkdownSubtype = 'json' | 'yaml' | 'yml';
52+
53+
export interface FormatterConfig<T> {
54+
data: T,
55+
outputType: OutputType,
56+
markdownSubtype: MarkdownSubtype
57+
}
58+
59+
export interface ChangeMarkdownGenerationConfig {
60+
change: { path: string, any: any },
61+
markdownSubtype: MarkdownSubtype
62+
}
63+
64+
export interface MarkdownDropdownGenerationConfig {
65+
label: string,
66+
data: { string: any },
67+
markdownSubtype: MarkdownSubtype
68+
}
5069

5170
export interface AsyncAPIDiffOptions {
5271
outputType: OutputType;
72+
markdownSubtype?: MarkdownSubtype;
5373
}
5474

5575
export interface Config {
5676
override?: OverrideObject;
5777
outputType?: OutputType;
78+
markdownSubtype?: MarkdownSubtype;
5879
}

test/asycnapidiff.spec.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,22 @@ import {
99
YAMLNonbreakingChanges,
1010
YAMLOutputDiff,
1111
YAMLUnclassifiedChanges,
12+
MarkdownBreakingChanges,
13+
MarkdownNonbreakingChanges,
14+
MarkdownOutputDiff,
15+
MarkdownUnclassifiedChanges,
16+
MarkdownJSONSubtypeChanges,
17+
MarkdownYAMLSubtypeChanges,
1218
} from './fixtures/asyncapidiff.fixtures';
1319

20+
import {
21+
diffOutput
22+
} from './fixtures/main.fixtures';
23+
1424
describe('AsyncAPIDiff wrapper', () => {
1525
test('checks the instance', () => {
1626
expect(
17-
new AsyncAPIDiff(JSON.stringify(inputDiff), { outputType: 'json' })
27+
new AsyncAPIDiff(JSON.stringify(inputDiff), {outputType: 'json'})
1828
).toBeInstanceOf(AsyncAPIDiff);
1929
});
2030

@@ -73,4 +83,47 @@ describe('AsyncAPIDiff wrapper', () => {
7383
});
7484
expect(diff.unclassified()).toEqual(YAMLUnclassifiedChanges);
7585
});
86+
87+
test('Markdown: returns the original full output', () => {
88+
const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
89+
outputType: 'markdown',
90+
});
91+
expect(diff.getOutput()).toEqual(MarkdownOutputDiff);
92+
});
93+
94+
test('Markdown: returns breaking changes', () => {
95+
const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
96+
outputType: 'markdown',
97+
});
98+
expect(diff.breaking()).toEqual(MarkdownBreakingChanges);
99+
});
100+
101+
test('Markdown: returns non-breaking changes', () => {
102+
const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
103+
outputType: 'markdown',
104+
});
105+
expect(diff.nonBreaking()).toEqual(MarkdownNonbreakingChanges);
106+
});
107+
108+
test('Markdown: returns unclassified changes', () => {
109+
const diff = new AsyncAPIDiff(JSON.stringify(inputDiff), {
110+
outputType: 'markdown',
111+
});
112+
expect(diff.unclassified()).toEqual(MarkdownUnclassifiedChanges);
113+
});
114+
115+
test('Markdown: returns changes using subtype JSON as the default', () => {
116+
const diff = new AsyncAPIDiff(JSON.stringify(diffOutput), {
117+
outputType: 'markdown',
118+
});
119+
expect(diff.getOutput()).toEqual(MarkdownJSONSubtypeChanges);
120+
});
121+
122+
test('Markdown: returns changes using subtype YAML', () => {
123+
const diff = new AsyncAPIDiff(JSON.stringify(diffOutput), {
124+
outputType: 'markdown',
125+
markdownSubtype: 'yaml',
126+
});
127+
expect(diff.getOutput()).toEqual(MarkdownYAMLSubtypeChanges);
128+
});
76129
});

0 commit comments

Comments
 (0)