Skip to content

Commit

Permalink
feat(validation-presenter-table): package release
Browse files Browse the repository at this point in the history
  • Loading branch information
Matii96 committed Dec 22, 2022
1 parent 689e26b commit ce81cb3
Show file tree
Hide file tree
Showing 16 changed files with 380 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Adapted configuration data is transformed into templates and validated via [clas
- [Core](https://github.com/Matii96/unifig/tree/main/packages/core) - project implementation for vanilla typescript
- [Nest](https://github.com/Matii96/unifig/tree/main/packages/nest) - nestjs integration
- [Env Adapter](https://github.com/Matii96/unifig/tree/main/packages/env-adapter) - adapter for environment variables and .env files
- [Validation presenter: table](https://github.com/Matii96/unifig/tree/main/packages/validation-presenter-table) - transforms configuration validation errors into

## Local development

Expand Down
71 changes: 71 additions & 0 deletions packages/validation-presenter-table/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Table validation presenter for [Unifig](https://github.com/Matii96/unifig)

Transforms configuration validation errors into clear table.

## Table of contents

- [Installation](#installation)
- [Quick Start](#quick_start)
- [Example output](#example_output)
- [License](#license)

## Installation

<a name="installation"></a>

```bash
npm i @unifig/validation-presenter-table
# or
yarn add @unifig/validation-presenter-table
```

## Quick Start

<a name="quick_start"></a>

```ts
// main.ts
import { Config, PlainConfigAdapter } from '@unifig/core';
import { Config, PlainConfigAdapter } from '@unifig/validation-presenter-table@unifig/validation-presenter-table';

async function bootstrap() {
const validationResult = await Config.register({
template: Settings,
adapter: new PlainConfigAdapter({}),
});
if (validationResult) {
console.error(toTable(validationResult));
process.exit(1);
}
}

bootstrap();
```

## Example output

<a name="example_output"></a>

```
┌──────────────────┬─────────────┬─────────────┬───────────────┬────────────────────┐
│ Template │ Property │ Source │ Current Value │ Failed constraints │
├──────────────────┼─────────────┼─────────────┼───────────────┼────────────────────┤
│ │ port │ PORT │ not-a-port │ isInt │
│ ├─────────────┼─────────────┼───────────────┼────────────────────┤
│ StorageOptions │ db.url │ DB_URL │ undefined │ isString │
│ ├─────────────┼─────────────┼───────────────┼────────────────────┤
│ │ db.password │ DB_PASSWORD │ undefined │ isString │
├──────────────────┼─────────────┼─────────────┼───────────────┼────────────────────┤
│ NetworkOptions │ ipRange │ IP_RANGE │ undefined │ isDefined │
└──────────────────┴─────────────┴─────────────┴───────────────┴────────────────────┘
```

`Source` column presents where property value was taken from.

Example: from env variables in case of [Env Adapter](https://github.com/Matii96/unifig/tree/main/packages/adapter-env).

## License

<a name="license"></a>

This project is licensed under the MIT License - see the [LICENSE file](https://github.com/Matii96/unifig/tree/main/LICENSE) for details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`toTable should format table from 2d errors 1`] = `
"┌──────────────────┬─────────────┬─────────────┬───────────────┬────────────────────┐
│ Template │ Property │ Source │ Current Value │ Failed constraints │
├──────────────────┼─────────────┼─────────────┼───────────────┼────────────────────┤
│ │ port │ PORT │ not-a-port │ isInt │
│ ├─────────────┼─────────────┼───────────────┼────────────────────┤
│ StorageOptions │ db.url │ DB_URL │ undefined │ isString │
│ ├─────────────┼─────────────┼───────────────┼────────────────────┤
│ │ db.password │ DB_PASSWORD │ undefined │ isString │
├──────────────────┼─────────────┼─────────────┼───────────────┼────────────────────┤
│ NetworkOptions │ ipRange │ │ undefined │ isDefined │
└──────────────────┴─────────────┴─────────────┴───────────────┴────────────────────┘
"
`;
1 change: 1 addition & 0 deletions packages/validation-presenter-table/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './to-table';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`TemplateRow should format row from 1d error 1`] = `
[
"",
"port",
"PORT",
"not-a-port",
"isInt",
]
`;

exports[`TemplateRow should format table from 2d errors 1`] = `
[
"",
"db",
"",
"undefined",
"",
]
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { mockFailed1dValidation, mockFailed2dValidation } from '../validator.mocks';
import { TemplateRow } from './template-row';

describe('TemplateRow', () => {
it('should format row from 1d error', () => {
expect(TemplateRow.fromValidationError(mockFailed1dValidation().errors[0]).toArray()).toMatchSnapshot();
});

it('should format table from 2d errors', () => {
expect(TemplateRow.fromValidationError(mockFailed2dValidation()[0].errors[1]).toArray()).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ConfigPropertyValidationError, ConfigSubtemplateValidationError, FailedConstraint } from '@unifig/core';

export class TemplateRow {
propertyParentPrefix?: string;
property: string;
source?: string;
currentValue?: any;
failedConstraints?: FailedConstraint[];

static fromValidationError(
error: ConfigPropertyValidationError | ConfigSubtemplateValidationError,
propertyParentPrefix?: string
) {
const row = new TemplateRow();
row.propertyParentPrefix = propertyParentPrefix;
row.property = error.property;
row.failedConstraints = error.failedConstraints;

if (error instanceof ConfigPropertyValidationError) {
row.source = error.source;
row.currentValue = error.currentValue;
}

return row;
}

toArray(): [string, string, string, string, string] {
return [
'',
(this.propertyParentPrefix ?? '') + this.property,
this.source ?? '',
this.currentValue ?? 'undefined',
this.failedConstraintsToNames(),
];
}

private failedConstraintsToNames() {
return this.failedConstraints
? this.failedConstraints.map((failedConstraint) => failedConstraint.name).join(', ')
: '';
}
}
6 changes: 6 additions & 0 deletions packages/validation-presenter-table/lib/to-table.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ToTableOptions {
/**
* @default norc
*/
border?: 'honeywell' | 'norc' | 'ramac' | 'void';
}
9 changes: 9 additions & 0 deletions packages/validation-presenter-table/lib/to-table.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ConfigValidationException } from '@unifig/core';
import { mockFailed2dValidation } from './validator.mocks';
import { toTable } from './to-table';

describe('toTable', () => {
it('should format table from 2d errors', () => {
expect(toTable(new ConfigValidationException(mockFailed2dValidation()))).toMatchSnapshot();
});
});
56 changes: 56 additions & 0 deletions packages/validation-presenter-table/lib/to-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
ConfigTemplateValidationError,
ConfigValidationException,
ConfigPropertyValidationError,
ConfigSubtemplateValidationError,
} from '@unifig/core';
import { getBorderCharacters, SpanningCellConfig, table } from 'table';
import { TemplateRow } from './template-row/template-row';
import { ToTableOptions } from './to-table.options';

const HEADER = ['Template', 'Property', 'Source', 'Current Value', 'Failed constraints'];

export const toTable = (validationException: ConfigValidationException, options: ToTableOptions = {}): string => {
const tableRows = formatTableRowsGroup(validationException.errors);
const tableData = [HEADER, ...tableRows.flatMap(({ templateTableData }) => templateTableData)];

return table(tableData, {
columns: [{ alignment: 'left', width: 16 }],
spanningCells: tableRows.map(({ spanningCells }) => spanningCells),
border: getBorderCharacters(options.border ?? 'norc'),
});
};

const formatTableRowsGroup = (failedValidations: ConfigTemplateValidationError[]) => {
let spanningCellsRowIdx = 1;
return failedValidations.map((failedValidation) => {
const templateTableData = failedValidation.errors
.flatMap((error) => formatTemplateRows(error))
.map((row) => row.toArray());
templateTableData[0][0] = failedValidation.template.name;

const spanningCells: SpanningCellConfig = {
col: 0,
row: spanningCellsRowIdx,
rowSpan: templateTableData.length,
verticalAlignment: 'middle',
};
spanningCellsRowIdx += templateTableData.length;
return { templateTableData, spanningCells };
});
};

const formatTemplateRows = (
error: ConfigPropertyValidationError | ConfigSubtemplateValidationError,
parentPrefix = ''
): TemplateRow[] => {
if (error instanceof ConfigPropertyValidationError) {
return [TemplateRow.fromValidationError(error, parentPrefix)];
}
if (error instanceof ConfigSubtemplateValidationError) {
const propertyParentPrefix = parentPrefix + error.property + '.';
const subtemplateRows = error.children.flatMap((child) => formatTemplateRows(child, propertyParentPrefix));
return error.failedConstraints ? [TemplateRow.fromValidationError(error), ...subtemplateRows] : subtemplateRows;
}
throw new Error('Passed plain validation object: ' + JSON.stringify(error));
};
58 changes: 58 additions & 0 deletions packages/validation-presenter-table/lib/validator.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
ConfigPropertyValidationError,
ConfigSubtemplateValidationError,
ConfigTemplateValidationError,
} from '@unifig/core';

export class StorageOptions {}
export class NetworkOptions {}

export const mockFailed1dValidation = () =>
new ConfigTemplateValidationError({
template: StorageOptions,
errors: [
new ConfigPropertyValidationError({
currentValue: 'not-a-port',
failedConstraints: [{ details: 'port must be an integer number', name: 'isInt' }],
property: 'port',
source: 'PORT',
}),
],
}) satisfies ConfigTemplateValidationError;

export const mockFailed2dValidation = () =>
[
{
errors: [
...mockFailed1dValidation().errors,
new ConfigSubtemplateValidationError({
property: 'db',
children: [
new ConfigPropertyValidationError({
currentValue: undefined,
failedConstraints: [{ details: 'url must be a string', name: 'isString' }],
property: 'url',
source: 'DB_URL',
}),
new ConfigPropertyValidationError({
currentValue: undefined,
failedConstraints: [{ details: 'password must be a string', name: 'isString' }],
property: 'password',
source: 'DB_PASSWORD',
}),
],
}),
],
template: StorageOptions,
},
{
errors: [
new ConfigSubtemplateValidationError({
property: 'ipRange',
failedConstraints: [{ details: 'ipRange must be defined', name: 'isDefined' }],
children: [],
}),
],
template: NetworkOptions,
},
] satisfies ConfigTemplateValidationError[];
38 changes: 38 additions & 0 deletions packages/validation-presenter-table/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@unifig/validation-presenter-table",
"version": "0.0.1",
"description": "Table validation presenter for unifig",
"keywords": [
"unifig",
"env"
],
"author": "Matii96",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Matii96/unifig.git"
},
"scripts": {
"build": "tsc --build tsconfig.build.json",
"test": "jest --config ../../jest-units.config.json packages/validation-presenter-table",
"test:e2e": "jest --config ../../jest-e2e.config.json packages/validation-presenter-table"
},
"bugs": {
"url": "https://github.com/Matii96/unifig/issues"
},
"homepage": "https://github.com/Matii96/unifig",
"dependencies": {
"table": "^6.8.1"
},
"devDependencies": {
"@unifig/core": "workspace:^"
},
"peerDependencies": {
"@unifig/core": ">=0.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`@unifig/validation-presenter-table (e2e) should format table from 1d error 1`] = `
"┌──────────────────┬──────────┬────────┬───────────────┬────────────────────┐
│ Template │ Property │ Source │ Current Value │ Failed constraints │
├──────────────────┼──────────┼────────┼───────────────┼────────────────────┤
│ StorageOptions │ port │ PORT │ not-a-port │ isInt │
└──────────────────┴──────────┴────────┴───────────────┴────────────────────┘
"
`;

exports[`@unifig/validation-presenter-table (e2e) should format table from 2d errors 1`] = `
"┌──────────────────┬─────────────┬─────────────┬───────────────┬────────────────────┐
│ Template │ Property │ Source │ Current Value │ Failed constraints │
├──────────────────┼─────────────┼─────────────┼───────────────┼────────────────────┤
│ │ port │ PORT │ not-a-port │ isInt │
│ ├─────────────┼─────────────┼───────────────┼────────────────────┤
│ StorageOptions │ db.url │ DB_URL │ undefined │ isString │
│ ├─────────────┼─────────────┼───────────────┼────────────────────┤
│ │ db.password │ DB_PASSWORD │ undefined │ isString │
├──────────────────┼─────────────┼─────────────┼───────────────┼────────────────────┤
│ NetworkOptions │ ipRange │ │ undefined │ isDefined │
└──────────────────┴─────────────┴─────────────┴───────────────┴────────────────────┘
"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ConfigValidationException } from '@unifig/core';
import { mockFailed1dValidation, mockFailed2dValidation } from '../lib/validator.mocks';
import { toTable } from '../lib';

describe('@unifig/validation-presenter-table (e2e)', () => {
it('should format table from 1d error', () => {
expect(toTable(new ConfigValidationException([mockFailed1dValidation()]))).toMatchSnapshot();
});

it('should format table from 2d errors', () => {
expect(toTable(new ConfigValidationException(mockFailed2dValidation()))).toMatchSnapshot();
});
});
5 changes: 5 additions & 0 deletions packages/validation-presenter-table/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["./lib"],
"exclude": ["**/*.mock.ts", "**/*.mocks.ts", "**/*.spec.ts"]
}
Loading

0 comments on commit ce81cb3

Please sign in to comment.