Skip to content

Commit

Permalink
Add MinLength helper.
Browse files Browse the repository at this point in the history
  • Loading branch information
Nemikolh authored Apr 7, 2021
1 parent 316c105 commit ae0ef6e
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 12 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
branches: [ $default-branch ]
pull_request:
branches: [ $default-branch ]

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: yarn
- run: yarn run build
- run: yarn run lint
- run: yarn run dtslint
- run: yarn test
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Version 0.4.9

* Add `MinLength` helper to require a field to have a minimum length.
Supported type are `STRING`, `Arr(..)` and `Dict(..)`.

# Version 0.4.8

* Augment type limit for Enum and EnumObj to accept up to 8 values
Expand Down
27 changes: 26 additions & 1 deletion dtslint/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { newValidator, TypeOf, Any, SchemaValidator, TRUE, FALSE, Dict } from '../src';
import { Nullable, IGNORE, NUMBER, BOOL, STRING, Enum } from '../src';
import { EnumObj, Optional, Obj, Arr, Str } from '../src';
import { EnumObj, Optional, Obj, Arr, Str, MinLength } from '../src';

// Basic types

Expand Down Expand Up @@ -198,3 +198,28 @@ function t18() {
const a = val.value;
}
}

function t21() {
// $ExpectError
newValidator(MinLength(NUMBER, 12));
// $ExpectError
newValidator(MinLength(Nullable(STRING), 12));
// $ExpectError
newValidator(MinLength(Obj({}), 12));
// $ExpectError
newValidator(MinLength(IGNORE, 12));
// $ExpectError
newValidator(MinLength(NUMBER, 12));
newValidator(MinLength(STRING, 12));
newValidator(MinLength(Arr(NUMBER), 12));
newValidator(MinLength(Dict(Obj({})), 12));
}

function t22() {
const schema = newValidator(Nullable(MinLength(STRING, 12)));
const val = schema.validate(null);
if (val.type === 'success') {
// $ExpectType string | null
const a = val.value;
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "typesafe-schema",
"version": "0.4.8",
"version": "0.4.9",
"description": "Validate your data and get a well typed value out of it!",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -19,6 +19,6 @@
"jest": "26.6.3",
"ts-jest": "26.5.0",
"tslint": "5.20.1",
"typescript": "4.1.3"
"typescript": "4.2.3"
}
}
56 changes: 52 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,21 +126,27 @@ class SchemaType<O> {
readonly _O!: O;
}

// Traits to differentiate types
interface HasLengthType {
_sl: unknown;
}

// Guards (used by the runtime validator)
class NumberType extends SchemaType<number> {}
class BooleanType extends SchemaType<boolean> {}
class IgnoreType extends SchemaType<unknown> {}
class StringType extends SchemaType<string> {}
class StringType extends SchemaType<string> implements HasLengthType { _sl: unknown; }
class MatchRegexType extends SchemaType<string> { constructor(public regex: RegExp) { super(); } }
class ValType<V> extends SchemaType<V> { constructor(public val: V) { super(); } }
class StringValType<S extends string> extends ValType<S> { constructor(val: S) { super(val); } }
class NumberValType<N extends number> extends ValType<N> { constructor(val: N) { super(val); } }
class InterfaceType<P, O> extends SchemaType<O> { constructor(public props: P) { super(); } }
class NullableType<E, O> extends SchemaType<O> { constructor(public schema: E) { super(); } }
class MaybeUndefinedType<E, O> extends SchemaType<O> { constructor(public schema: E) { super(); } }
class ArrayType<E, O> extends SchemaType<O> { constructor(public elementSchema: E) { super(); } }
class DictType<E, O> extends SchemaType<O> { constructor(public elementSchema: E) { super(); } }
class ArrayType<E, O> extends SchemaType<O> implements HasLengthType { _sl: unknown; constructor(public elementSchema: E) { super(); } }
class DictType<E, O> extends SchemaType<O> implements HasLengthType { _sl: unknown; constructor(public elementSchema: E) { super(); } }
class EnumType<E, O> extends SchemaType<O> { constructor(public possibleValues: E) { super(); } }
class MinLengthType<O> extends SchemaType<O> { constructor(public schema: O, public length: number) { super(); } }

// Type utils
export interface Any extends SchemaType<any> {} // Not needed but make the code more readable
Expand Down Expand Up @@ -289,6 +295,21 @@ export function Optional<T extends Any>(schema: T): MaybeUndefinedC<T> {
return new MaybeUndefinedType(schema);
}

/**
* This schema can be applied to STRING, Arr(..) or Dict(...) to only
* allowed values that have a minimum length.
*
* - For string values it correspond to the string length property
* - For arrays values it correspond to the array length property
* - For dictionaries it correspond to `Object.keys(val).length`
*
* @param schema type of the value that needs to have a minimum length
* @param length minimum length required.
*/
export function MinLength<T extends Any & HasLengthType>(schema: T, length: number): MinLengthType<TypeOf<T>> {
return new MinLengthType(schema, length);
}

/**
* This schema offer more control over which string values should
* be accepted.
Expand Down Expand Up @@ -399,13 +420,40 @@ function validateObject<T extends Any>(value: any, schema: T, path: string, stri
`None of the variant matched ${JSON.stringify(value)}, errors:\n ${trace}`,
);
}
if (schema instanceof MinLengthType) {
const res = validateObject(value, schema.schema, path, strict);
const minLength = schema.length;
if (res.type === 'success') {
if (typeofVal === 'string') {
if (value.length < minLength) {
return error(
path,
`'${value}' does not satisfy the minimum length requirement (${minLength})`,
);
}
} else if (Array.isArray(value)) {
if (value.length < minLength) {
return error(
path,
`'${JSON.stringify(value)}' does not satisfy the minimum length requirement (${minLength})`,
);
}
} else if (Object.keys(value).length < minLength) {
return error(
path,
`'${JSON.stringify(value)}' does not satisfy the minimum length requirement (${minLength})`,
);
}
}
return res;
}
if (schema instanceof NullableType) {
if (value === null) {
return success();
}
return validateObject(value, schema.schema, path + '?', strict);
}
if (schema === STRING) {
if (schema === STRING as any) {
return iferror(typeofVal === 'string', path, `Got ${value === null ? 'null' : typeofVal}, expected string`);
}
if (schema === BOOL) {
Expand Down
73 changes: 72 additions & 1 deletion tests/validate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { newValidator, Enum, EnumObj, Obj, Arr, MatchRegex, TRUE, FALSE, Dict, BOOL } from '../src';
import { newValidator, Enum, EnumObj, Obj, Arr, MatchRegex, TRUE, FALSE, Dict, BOOL, MinLength } from '../src';
import { STRING, NUMBER } from '../src';

describe('Enum', () => {
Expand Down Expand Up @@ -295,4 +295,75 @@ describe('null', () => {
].join('\n'),
});
});
});

describe('MinLength', () => {


it('should show null error if the value is null', () => {
const schema = newValidator(MinLength(STRING, 12));
const value = null;
expect(schema.validate(value)).toEqual({
type: 'error',
path: '',
reason: 'Got null, expected string',
});
});

it('should reject is string is not long enough', () => {
const schema = newValidator(MinLength(STRING, 12));
const value = 'foobar';
expect(schema.validate(value)).toEqual({
type: 'error',
path: '',
reason: `'foobar' does not satisfy the minimum length requirement (12)`,
});
});

it('should reject if array is not long enough', () => {
const schema = newValidator(MinLength(Arr(STRING), 12));
const value = ['foobar'];
expect(schema.validate(value)).toEqual({
type: 'error',
path: '',
reason: `'["foobar"]' does not satisfy the minimum length requirement (12)`,
});
});

it('should reject if dict is not long enough', () => {
const schema = newValidator(MinLength(Dict(STRING), 12));
const value = { a: 'foobar' };
expect(schema.validate(value)).toEqual({
type: 'error',
path: '',
reason: `'{"a":"foobar"}' does not satisfy the minimum length requirement (12)`,
});
});

it('should accept valid values for string', () => {
const schema = newValidator(MinLength(STRING, 6));
const value = 'foobar';
expect(schema.validate(value)).toEqual({
type: 'success',
value,
});
});

it('should accept valid values for dict', () => {
const schema = newValidator(MinLength(Arr(STRING), 1));
const value = ['foobar'];
expect(schema.validate(value)).toEqual({
type: 'success',
value,
});
});

it('should accept valid values for array', () => {
const schema = newValidator(MinLength(Dict(STRING), 2));
const value = { a: 'foobar', b: '', c: '' };
expect(schema.validate(value)).toEqual({
type: 'success',
value,
});
});
});
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"arrow-parens": false,
"no-consecutive-blank-lines": false,
"max-line-length": false,
"variable-name": false,
"semicolon": [true, "always", "ignore-interfaces"],
"array-type": [true, "array"],
"quotemark": [true, "single"]
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4245,10 +4245,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=

typescript@4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
typescript@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==

union-value@^1.0.0:
version "1.0.1"
Expand Down

0 comments on commit ae0ef6e

Please sign in to comment.