Skip to content

Commit

Permalink
feat: support for Markdown output (#90)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukasz Gornicki <lpgornicki@gmail.com>
Co-authored-by: Aayush Kumar Sahu <aayushmau5@gmail.com>
  • Loading branch information
3 people authored May 20, 2022
1 parent 4e16ca1 commit 218aa5e
Show file tree
Hide file tree
Showing 11 changed files with 461 additions and 26 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
13 changes: 8 additions & 5 deletions src/asyncapidiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

/**
Expand All @@ -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});
}

/**
Expand All @@ -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});
}

/**
Expand All @@ -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});
}
}
76 changes: 76 additions & 0 deletions src/helpers/MarkdownHelpers.ts
Original file line number Diff line number Diff line change
@@ -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 `<details>
<summary> ${config.label} </summary>
\`\`\`${config.markdownSubtype}
${displayData}
\`\`\`
</details>
`;
}
36 changes: 36 additions & 0 deletions src/helpers/output/convertToMarkdown.ts
Original file line number Diff line number Diff line change
@@ -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);
}
21 changes: 12 additions & 9 deletions src/helpers/output/toProperFormat.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
data: T,
outputType: OutputType
): T | string {
if (outputType === 'yaml' || outputType === 'yml') {
return convertToYAML(data);
export default function toProperFormat<T>(config: FormatterConfig<T>): 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;
}
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});
}
29 changes: 25 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<T> {
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;
}
55 changes: 54 additions & 1 deletion test/asycnapidiff.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});
});
Loading

0 comments on commit 218aa5e

Please sign in to comment.