Skip to content

Commit

Permalink
Next stuff pack (#37)
Browse files Browse the repository at this point in the history
* feat(dev): small changes to .editorconfig, package.json
* feat: add toMatchSuccessResult jest macher
* feat: extra functions,types, classes
  • Loading branch information
Mararok authored Dec 26, 2023
1 parent 51c39b3 commit eeeac37
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 24 deletions.
7 changes: 4 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ end_of_line = lf
trim_trailing_whitespace = true

[Makefile]
charset = utf-8
indent_style = tab
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true

[*.{md,txt}]
indent_style = space
trim_trailing_whitespace = false
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Jest matcher: toMatchSuccessResult
- function stripAnsiColors()
- function wrapToIterable()
- interface GetQueryOptions
- class RetryHelper
- type DropFirstItem
- type TupleTail
- type ParamsTail

## [0.12.3] - 2023-12-03

### Added
Expand All @@ -19,6 +30,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- many changes

[unreleased] https://github.com/hexancore/common/compare/0.12.3...HEAD
[0.12.3] https://github.com/hexancore/common/compare/0.10.4...0.12.3
[0.10.4] https://github.com/test_owner/test_repository/releases/tag/0.10.4
[unreleased] https://github.com/hexancore/common/compare/0.12.3...HEAD
[0.12.3] https://github.com/hexancore/common/compare/0.10.4...0.12.3
[0.10.4] https://github.com/test_owner/test_repository/releases/tag/0.10.4
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@
"build": "rm -fr lib && tsc -p tsconfig.mjs.json && tsc -p tsconfig.cjs.json && bash ./bin/fixbuild.sh",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"test": "jest --maxWorkers=50%",
"test": "jest --runInBand",
"test:clearCache": "jest --clearCache",
"test:unit": "jest --maxWorkers=50% --group=unit",
"test:watch": "jest --maxWorkers=50% --watchAll",
"test:unit": "jest --runInBand --group=unit",
"test:watch": "jest --runInBand --watchAll",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"prepublish": "yarn test && yarn run build"
Expand Down
2 changes: 1 addition & 1 deletion src/Log/TestLogger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LogLevel, LogRecord } from '../Util';
import { LogLevel, LogRecord } from './';
import { AbstractLogger } from './AbstractLogger';

/**
Expand Down
41 changes: 41 additions & 0 deletions src/Test/Matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { AppError, AppErrorProps, Result } from '../Util';

interface HexancoreCommonMatchers<R = unknown> {
toMatchSuccessResult(expected: any): R;
toMatchAppError(expected: AppErrorProps): R;
}

Expand All @@ -16,6 +17,46 @@ declare global {
}

expect.extend({
toMatchSuccessResult(received, expected) {
if (!(received instanceof Result)) {
return {
message: () => `Expected: instance of success result\n Received: ${this.utils.printReceived(received)}`,
pass: false,
};
}

const printExpectedAndReceived = (current): string => {
return `${this.utils.printExpected(expected)}\nReceived:\n ${this.utils.printReceived(current)}`;
};

const messageHeader = `Expected result value to be${this.isNot ? ' not' : ''}:\n`;

if (received.isError()) {
return {
message: () => `${messageHeader} ${this.utils.printExpected(expected)}\nReceived error:\n ${JSON.stringify(received.e, undefined, 2)}`,
pass: false,
};
}

const current = received.v;
const pass = this.equals(current, expected);
const finalPass = this.isNot ? !pass : pass;

return {
pass: pass,
message: () => {
if (this.isNot) {
return `${messageHeader} ${printExpectedAndReceived(current)}`;
} else {
return finalPass
? `${messageHeader} ${printExpectedAndReceived(current)}`
: `${messageHeader} ${printExpectedAndReceived(current)}\n\n${this.utils.diff(expected, current, )}`;
}

}
};
},

toMatchAppError(received: Result<any> | AppError, expected: AppError) {
if (received instanceof Result && received.isSuccess()) {
return {
Expand Down
3 changes: 2 additions & 1 deletion src/Util/AppError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JsonSerialize, LogLevel, LogRecord, LogTags } from '.';
import { LogLevel, LogRecord } from '../Log';
import { JsonSerialize, } from '.';
import { ErrorHelper, ErrorPlain } from './Error/ErrorHelper';

export const IGNORE_ERROR_TYPE = 'core.ignore_error';
Expand Down
7 changes: 7 additions & 0 deletions src/Util/QueryHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { OrderDirection } from "./types";

export interface GetQueryOptions<T> {
orderBy?: Record<keyof T, OrderDirection>;
limit?: number;
offset?: number;
}
5 changes: 3 additions & 2 deletions src/Util/Result.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppError, AppErrorProps, INTERNAL_ERROR } from './AppError';
import { AR, ERRA } from './AsyncResult';
import { LogicError } from './Error';

export type R<T> = Result<T>;

Expand All @@ -17,7 +18,7 @@ export class Result<T> {

public get v(): T {
if (this.isError()) {
throw new ReferenceError("Can't use on ErrorResult: " + this.value.type);
throw new LogicError("Can't use v() on ErrorResult: " + this.value.type);
}

return this.value;
Expand Down Expand Up @@ -57,7 +58,7 @@ export class Result<T> {
return this.value;
}

throw new ReferenceError("Can't use on SuccessResult");
throw new LogicError("Can't use e() on SuccessResult");
}

public mapErr(fn: (e: AppError) => AppError | AppErrorProps): R<T> {
Expand Down
35 changes: 35 additions & 0 deletions src/Util/RetryHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AppError, AppErrorCode } from './AppError';
import { AR, ARP, OKA, ERRA, PS } from './AsyncResult';

export interface RetryOptions {
id: string;
maxAttempts?: number;
retryDelay?: number | ((attempt: number, maxAttempts: number) => Promise<void>);
}

const defaultRetryDelayFn = (delay: number, attempt: number, maxAttempts: number) => {
return new Promise((resolve) => setTimeout(resolve, delay));
};

export class RetryHelper {
public static async retryAsync<T>(fn: () => AR<T>, options: RetryOptions): ARP<T> {
const retryDelayFn = typeof options.retryDelay === 'function' ? options.retryDelay : defaultRetryDelayFn.bind(options.retryDelay);
let lastError: AppError;
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
const result = await fn();
if (result.isSuccess()) {
return OKA(result.v);
}

lastError = result.e;
await retryDelayFn(attempt, options.maxAttempts);
}

return ERRA({
type: 'core.util.retry_helper.retry_max_attempts',
code: AppErrorCode.INTERNAL_ERROR,
cause: lastError,
data: {id: options.id}
});
}
}
10 changes: 10 additions & 0 deletions src/Util/functions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AR, ARP, AsyncResult, ERRA, OKA, PS } from './AsyncResult';
import { MissingError } from './Error';

export function isIterable(obj: any): boolean {
Expand Down Expand Up @@ -41,3 +42,12 @@ export function getEnvOrError(name: string): string {
export function wrapToArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}

export function wrapToIterable<T>(value: T | T[] | Iterable<T>): Iterable<T> {
return (isIterable(value) ? value : Array.isArray(value) ? value : [value]) as any;
}

export function stripAnsiColors(s: string): string {
// eslint-disable-next-line no-control-regex
return s.replace(/\x1B[[(?);]{0,2}(;?\d)*./g, '');
}
16 changes: 9 additions & 7 deletions src/Util/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
export * from './AppMeta';
export * from './AppError';
export * from './Error';

export * from './Result';
export * from './AsyncResult';

export * from './CurrentTime';
export * from './Validator';
export * from './SanitizeHelper';
export * from './functions';
export * from './types';
export * from './CurrentTime';
export * from './Dto';
export * from '../Log';
export * from './AppMeta';
export * from './Error/index';
export * from './RetryHelper';
export * from './QueryHelper';

export * from './Dto';
export * from './JsonSerialize';
export * from './StringTemplate';

export * from './types';
export * from './functions';
22 changes: 20 additions & 2 deletions src/Util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,34 @@
export type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> }>;

/**
* Removes last tuple element
* Removes last tuple element.
*/
export type Head<T extends any[]> = T extends [...infer Head, any] ? Head : any[];

export type DropLastParam<F extends (...args: any) => any> = Head<Parameters<F>>;

/**
* Returs Tuple without first item.
*/
export type DropFirstItem<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;

/**
* Returns tuple without N first items.
*/
export type TupleTail<T extends any[], N extends number, R extends any[] = []> = R['length'] extends N
? T
: TupleTail<DropFirstItem<T>, N, [any, ...R]>;

export type ParamsTail<T extends (...args: any) => any, N extends number> = TupleTail<Parameters<T>, N>;

export type FilterNotStartsWith<Set, Needle extends string> = Set extends `${Needle}${infer _X}` ? never : Set;
export type FilterStartsWith<Set, Needle extends string> = Set extends `${Needle}${infer _X}` ? Set : never;
export type StripPrefix<T extends string, prefix extends string> = T extends `${prefix}${infer Prefix}` ? Prefix : never;

export type ExtractIterableType<T extends Iterable<any>> = T extends Iterable<infer U> ? U : T;

export type HasAnyRequiredProperty<T> = {[K in keyof T]-?: {} extends Pick<T, K> ? never : K;}[keyof T] extends never? false: true;
export type HasAnyRequiredProperty<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T] extends never ? false : true;

export type OneOrIterable<T> = T | Iterable<T>;

export type OrderDirection = 'ASC' | 'DESC';
69 changes: 69 additions & 0 deletions test/unit/Test/Matchers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @group unit
*/

import { ERR, OK, stripAnsiColors } from '@';

describe('Matchers', () => {
describe('toMatchSuccessResult', () => {
test('When success result and same value', () => {
const current = OK(10);

expect(current).toMatchSuccessResult(10);
});

test('When success result and other value, the throw', () => {
expect.assertions(2);

try {
const current = OK(10);
expect(current).toMatchSuccessResult('test');
} catch (e) {
const expected = [
'Expected result value to be:',
' "test"',
'Received:',
' 10',
'',
' Comparing two different types of values. Expected string but received number.'
].join('\n');
expect(stripAnsiColors(e.message)).toEqual(expected);
}
});

test('When success result and negation, then throw', () => {
expect.assertions(2);

try {
const current = OK(10);
expect(current).not.toMatchSuccessResult(10);
} catch (e) {
const expected = ['Expected result value to be not:', ' 10', 'Received:', ' 10'].join('\n');
expect(stripAnsiColors(e.message)).toEqual(expected);
}
});

test('When error result, then throw', () => {
expect.assertions(2);
const current = ERR('test.error');
try {

expect(current).toMatchSuccessResult(10);
} catch (e) {
const expected = [
'Expected result value to be:',
' 10',
'Received error:',
` ${JSON.stringify(current.e, undefined, 2)}`,
].join('\n');
expect(stripAnsiColors(e.message)).toEqual(expected);
}
});

test('When error result and negation', () => {
const current = ERR('test.error');

expect(current).not.toMatchSuccessResult(10);
});
});
});
4 changes: 2 additions & 2 deletions test/unit/Util/Result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe(path.basename(__filename, '.test.ts'), () => {

expect(current.isSuccess()).toBeTruthy();
expect(current.v).toBe('test_data');
expect(() => current.e).toThrow("Can't use on SuccessResult");
expect(() => current.e).toThrow("Can't use e() on SuccessResult");
});

test('ERR()', () => {
Expand All @@ -22,7 +22,7 @@ describe(path.basename(__filename, '.test.ts'), () => {
expect(current.e.type).toBe('test_type');
expect(current.e.code).toBe(400);
expect(current.e.data).toBe('test_data');
expect(() => current.v).toThrow("Can't use on ErrorResult: test_type");
expect(() => current.v).toThrow("Can't use v() on ErrorResult: test_type");
});

test('map() when SuccessResult', () => {
Expand Down

0 comments on commit eeeac37

Please sign in to comment.