diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 0000000..3c0b300 --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,29 @@ +name: PR testing + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + +jobs: + test-pr: + name: Test PR - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v3 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: '12.x' + + - name: Install Dependencies + run: yarn install + + - name: Run tests + run: yarn test + + - name: Build Problem Library + run: yarn build \ No newline at end of file diff --git a/__tests__/_helper.ts b/__tests__/_helper.ts new file mode 100644 index 0000000..eb2ceb8 --- /dev/null +++ b/__tests__/_helper.ts @@ -0,0 +1,21 @@ +import { Problem } from "../src" +import { ProblemInterface } from "../src/types" + +export default class ProblemContextHelper { + + _problemInstance: Problem; + + _instanceOptions: ProblemInterface = { + type: "null-or-falsey-document", + title: "The AsyncAPI document is null or a JS falsey value.", + detail: "The AsyncAPI document is null or a JS falsey value.", + leaveThisWhenCopy:"This is used to test copy function: LEAVE PROPS. This will not be undefined in new copy. ", + skipThisWhenCopy:"This is used to test copy functionL SKIP PROPS. This should be undefined in new copy.", + } + + + constructor() { + this._problemInstance = new Problem(this._instanceOptions); + } + +} \ No newline at end of file diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts new file mode 100644 index 0000000..eb50fb6 --- /dev/null +++ b/__tests__/index.test.ts @@ -0,0 +1,79 @@ +import { Problem } from "../src"; +import { COPY_MODE } from "../src/constants"; +import ProblemContextHelper from "./_helper"; + +const _testContext = new ProblemContextHelper(); + +describe("Class Methods Test Suite", () => { + test("Create Class with Custom Keys", () => { + const customKey = "RCA"; + _testContext._problemInstance[customKey]="Root Cause Analysis" + const testProblem = new Problem(_testContext._problemInstance); + expect(testProblem).toHaveProperty(customKey); + }); + + test("Method: Copy, mode: LEAVE_PROPS", () => { + const _problemCopy = _testContext._problemInstance.copy( + COPY_MODE.LEAVE_PROPS, + ["leaveThisWhenCopy"] + ); + expect(_problemCopy).toBeInstanceOf(Problem); + expect(_problemCopy.type).toBe(_testContext._problemInstance.type); + expect(_problemCopy.title).toBe(_testContext._problemInstance.title); + // check if skipthiswhencopy is omitted. + expect(_problemCopy.skipThisWhenCopy).toBe(undefined); + // check if leavethiswhencopy is not omitted + expect(_problemCopy.leaveThisWhenCopy).toBe(_testContext._problemInstance.leaveThisWhenCopy); + }); + + test("Method: Copy, mode: SKIP_PROPS", () => { + const _copiedProblem = _testContext._problemInstance.copy( + COPY_MODE.SKIP_PROPS, + ["skipThisWhenCopy"] + ); + expect(_copiedProblem).toBeInstanceOf(Problem); + // check if leavethiswhencopy is not emitted. + expect(_copiedProblem.leaveThisWhenCopy).toBe(_testContext._problemInstance.leaveThisWhenCopy); + // check if skipthis is omitted + expect(_copiedProblem.skipThisWhenCopy).toBeUndefined(); + }); + + test("Method: isOfType", () => { + const _TYPE_TRUTH_CHECK = "null-or-falsey-document"; + const _TYPE_FALSE_CHECK = "undefined-document"; + expect( + _testContext._problemInstance.isOfType(_TYPE_TRUTH_CHECK) + ).toBeTruthy(); + expect( + _testContext._problemInstance.isOfType(_TYPE_FALSE_CHECK) + ).toBeFalsy(); + }); + + test("Method: Update", async () => { + const LINK: string = "test-link/"; + const updates = { + link: LINK, + }; + await _testContext._problemInstance.update({ updates }); + expect(_testContext._problemInstance).toHaveProperty("link", LINK); + }); + + test("Method: toJSON", () => { + const _problem = _testContext._problemInstance; + + // Run updates first to add stack property, required for testing isJSON + const updates = { + stack: "./filepath", + }; + _problem.update({ updates }); + + // non-stringified stack + expect(_problem.toJSON({ includeStack: true })).toHaveProperty( + "stack", + updates.stack + ); + expect(_problem.toJSON({ includeStack: false })).not.toHaveProperty( + "stack" + ); + }); +}); diff --git a/jest.config.ts b/jest.config.ts index 72a24eb..9d7218d 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -26,6 +26,9 @@ const config: Config.InitialOptions = { collectCoverageFrom: [ 'src/**' ], + testPathIgnorePatterns: [ + "__tests__/_helper.ts" + ] }; export default config; \ No newline at end of file diff --git a/package.json b/package.json index 21c0eb4..ebf904d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint:fix": "eslint --no-error-on-unmatched-pattern --max-warnings 0 --config \".eslintrc\" \".\" --fix", "generate:readme:toc": "markdown-toc -i \"README.md\"" }, + "bugs": { "url": "https://github.com/imabp/asyncapi_problem/issues" }, diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..4269f6e --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,9 @@ +export const DEFAULT_KEYS = [ + "type", + "title", +]; + +export enum COPY_MODE { + SKIP_PROPS = "skipProps", + LEAVE_PROPS = "leaveProps", +} \ No newline at end of file diff --git a/src/problem.ts b/src/problem.ts index 089a692..7eb6e6c 100644 --- a/src/problem.ts +++ b/src/problem.ts @@ -1,62 +1,87 @@ -import { httpObject, ProblemInterface } from "types"; - -enum COPY_MODE { - SKIP_PROPS = 'skipProps', - LEAVE_PROPS = 'leaveProps' -} +import { DEFAULT_KEYS } from "./constants"; +import { + HttpObject, + ProblemInterface, + ToJsonParamType, + UpdateProblemParamType, +} from "./types"; +import { COPY_MODE } from "./constants"; +import { objectToProblemMap } from "./util"; export class Problem extends Error implements ProblemInterface { - public type: string; - public title: string; - public instance?: string; - public detail?: string; - public http?: httpObject - [key: string]: any; - - - constructor(problem: ProblemInterface, customKeys?: string[]) { - super(problem.detail || problem.title); - this.http = problem.http - this.type = problem.type - this.title = problem.title; - this.detail = problem.detail; - this.instance = problem.instance; - this.stack = problem.stack; - customKeys?.map((customKey) => { - this[customKey] = problem[customKey]; - }) - } + public type: string; + public title: string; + public instance?: string; + public detail?: string; + public http?: HttpObject; + [key: string]: any; - copy(problem: ProblemInterface, mode: COPY_MODE, props: string[]): Problem { - switch (mode) { - - case COPY_MODE.LEAVE_PROPS: - return new Problem(problem, props); - - case COPY_MODE.SKIP_PROPS: - default: - let keysToBeCopied: string[] = []; - for (let key in problem) { - if (props.includes(key)) - continue; - keysToBeCopied.push(key) - } - return new Problem(problem, keysToBeCopied) - } - }; - - toJSON(problem: Problem, includeStack = false): ProblemInterface { + constructor(protected readonly problem: ProblemInterface) { + super(problem.detail || problem.title); + this.http = problem.http; + this.type = problem.type; + this.title = problem.title; + this.detail = problem.detail; + this.instance = problem.instance; + this.stack = problem.stack; - const { name, message, stack, ...rest } = problem; + // add extra keys + Object.keys(problem) + .filter((el) => !DEFAULT_KEYS.includes(el)) + .forEach((k) => (this[k] = problem[k])); + } - const jsonObject = { - ...rest + copy(mode: COPY_MODE = COPY_MODE.LEAVE_PROPS, props: string[] = []): Problem { + switch (mode) { + // returns a new problem object with preserved keys passed as props + case COPY_MODE.LEAVE_PROPS:{ + let newProblemKeyValuePairs:Record = { + type: this.problem.type, + title:this.problem.title, } + props.forEach((key)=>{ + newProblemKeyValuePairs={...newProblemKeyValuePairs, [key]:this.problem[key]} + }) + const newProblem = new Problem(objectToProblemMap(newProblemKeyValuePairs)); + return newProblem; + } + // skip the copy of keys + case COPY_MODE.SKIP_PROPS: + default: { + let newProblemKeyValuePairs:Record={}; - if (includeStack) - jsonObject.stack = stack; + // loop to copy only the required keys + for (let key in this.problem) { + // Skip only those keys, which are given in props and NOT a default key. + if (props.includes(key) && !DEFAULT_KEYS.includes(key)) continue; + newProblemKeyValuePairs[key] = this.problem[key]; + } + const newProblem = new Problem(objectToProblemMap(newProblemKeyValuePairs)) + return newProblem; + } + } + } - return jsonObject; + toJSON({ includeStack = false }: ToJsonParamType) { + const { stack, ...rest } = this; + if (includeStack) { + return { + ...this, + stack: this.stack, + }; } -}; \ No newline at end of file + + return { ...rest }; + } + + isOfType(type: string) { + return this.type === type; + } + + update({ updates }: UpdateProblemParamType) { + Object.keys(updates).forEach((i) => { + this[i] = updates[i]; + }); + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 7246f7c..49c1603 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,14 +1,24 @@ +import { Problem } from "../problem"; + export type ProblemInterface = { - http?: httpObject, - type: string, - title: string, // Title should be description of http status, if type is not present. - detail?: string, - instance?: string, // Details to reproduce the error. - stack?: string; - [key: string]: any, // Custom Field of Problem -} - -export type httpObject= { - status: number, // Status Code - [key: string]: any, + type: string; + title: string; + http?: HttpObject; + detail?: string; + instance?: string; + stack?: string; + [key: string]: any; +}; + +export type HttpObject = { + status: number; // Status Code + [key: string]: any; +}; + +export type UpdateProblemParamType = { + updates: { [key: string]: any }; +}; + +export type ToJsonParamType = { + includeStack?:boolean } \ No newline at end of file diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..0804e86 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,10 @@ +import { ProblemInterface } from "types"; + +export const objectToProblemMap = (obj:Record) =>{ + const type: string = obj.type; + const title: string = obj.title; + const problemObject:ProblemInterface = { + type, title, ...obj + } + return problemObject +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f7bc416..15aa78b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "lib": [ "esnext" ], + "composite": true, "declaration": true, "allowJs": true, "skipLibCheck": true, @@ -19,7 +20,12 @@ "resolveJsonModule": true, "isolatedModules": true, }, + "include": [ "src" + ], + "exclude": [ + "__tests__", + "./lib" ] } \ No newline at end of file