Skip to content

Commit

Permalink
docs, errors
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Jan 4, 2022
1 parent 8f6e45f commit 6b1caea
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 19 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,29 @@ array()
.cast(['', 1, 0, 4, false, null]); // => ['', 1, 0, 4, false]
```

### tuple

Tuples, are fixed length arrays where each item has a distinct type.

Inherits from [`Schema`](#Schema).

```js
import { tuple, string, number, InferType } from 'yup';

let schema = tuple([
string().label('name'),
number().label('age').positive().integer(),
]);

await schema.validate(['James', 3]); // ['James', 3]

await schema.validate(['James', -24]); // => ValidationError: age must be a positive number

InferType<typeof schema> // [string, number] | undefined
```

tuples have no default casting behavior.

### object

Define an object schema. Options passed into `isValid` are also passed to child schemas.
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
"typescript": "^4.5.4"
},
"dependencies": {
"nanoclone": "^1.0.0",
"property-expr": "^2.0.4",
"tiny-case": "^1.0.2",
"toposort": "^2.0.2"
Expand Down
24 changes: 24 additions & 0 deletions src/locale.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import printValue from './util/printValue';
import { Message } from './types';
import ValidationError from './ValidationError';

export interface MixedLocale {
default?: Message;
Expand Down Expand Up @@ -49,6 +50,10 @@ export interface ArrayLocale {
max?: Message<{ max: number }>;
}

export interface TupleLocale {
notType?: Message;
}

export interface BooleanLocale {
isValue?: Message;
}
Expand Down Expand Up @@ -128,6 +133,25 @@ export let array: Required<ArrayLocale> = {
length: '${path} must have ${length} items',
};

export let tuple: Required<TupleLocale> = {
notType: (params) => {
const { path, value, spec } = params;
const typeLen = spec.types.length;
if (Array.isArray(value)) {
if (value.length < typeLen)
return `${path} tuple value has too few items, expected a length of ${typeLen} but got ${
value.length
} for value: \`${printValue(value, true)}\``;
if (value.length > typeLen)
return `${path} tuple value has too many items, expected a length of ${typeLen} but got ${
value.length
} for value: \`${printValue(value, true)}\``;
}

return ValidationError.formatError(mixed.notType, params);
},
};

export default Object.assign(Object.create(null), {
mixed,
string,
Expand Down
11 changes: 5 additions & 6 deletions src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// @ts-ignore
import cloneDeep from 'nanoclone';

import { mixed as locale } from './locale';
import Condition, {
ConditionBuilder,
Expand Down Expand Up @@ -35,6 +32,7 @@ import Reference from './Reference';
import isAbsent from './util/isAbsent';
import type { Flags, Maybe, ResolveFlags, Thunk, _ } from './util/types';
import toArray from './util/toArray';
import cloneDeep from './util/cloneDeep';

export type SchemaSpec<TDefault> = {
coarce: boolean;
Expand Down Expand Up @@ -347,7 +345,7 @@ export default abstract class Schema<
`The value of ${
options.path || 'field'
} could not be cast to a value ` +
`that satisfies the schema type: "${resolvedSchema._type}". \n\n` +
`that satisfies the schema type: "${resolvedSchema.type}". \n\n` +
`attempted value: ${formattedValue} \n` +
(formattedResult !== formattedValue
? `result of cast: ${formattedResult}`
Expand Down Expand Up @@ -401,6 +399,7 @@ export default abstract class Schema<
originalValue,
schema: this,
label: this.spec.label,
spec: this.spec,
sync,
from,
};
Expand Down Expand Up @@ -826,7 +825,7 @@ export default abstract class Schema<
if (!isAbsent(value) && !this.schema._typeCheck(value))
return this.createError({
params: {
type: this.schema._type,
type: this.schema.type,
},
});
return true;
Expand Down Expand Up @@ -967,7 +966,7 @@ for (const method of ['validate', 'validateSync'])
value,
options.context,
);
return schema[method](parent && parent[parentPath], {
return (schema as any)[method](parent && parent[parentPath], {
...options,
parent,
path,
Expand Down
14 changes: 7 additions & 7 deletions src/tuple.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
// @ts-ignore

import type {
AnyObject,
InternalOptions,
ISchema,
Maybe,
Message,
} from './types';
import type { AnyObject, InternalOptions, ISchema, Message } from './types';
import type {
Defined,
Flags,
Expand All @@ -15,9 +9,11 @@ import type {
Thunk,
ToggleDefault,
UnsetFlag,
Maybe,
} from './util/types';
import Schema, { RunTest, SchemaSpec } from './schema';
import ValidationError from './ValidationError';
import { tuple as tupleLocale } from './locale';

type AnyTuple = [unknown, ...unknown[]];

Expand Down Expand Up @@ -87,6 +83,10 @@ export default class TupleSchema<
return Array.isArray(v) && v.length === types.length;
},
});

this.withMutation(() => {
this.typeError(tupleLocale.notType);
});
}

protected _cast(inputValue: any, options: InternalOptions<TContext>) {
Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ResolveOptions } from './Condition';
import type { AnySchema, CastOptions, SchemaFieldDescription } from './schema';
import type {
AnySchema,
CastOptions,
SchemaFieldDescription,
SchemaSpec,
} from './schema';
import type { Test } from './util/createValidation';
import type { AnyObject } from './util/objectTypes';
import type { Flags } from './util/types';
Expand Down Expand Up @@ -75,6 +80,7 @@ export interface MessageParams {
originalValue: any;
label: string;
type: string;
spec: SchemaSpec<any> & Record<string, unknown>;
}

export type Message<Extra extends Record<string, unknown> = any> =
Expand Down
45 changes: 45 additions & 0 deletions src/util/cloneDeep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// tweaked from https://github.com/Kelin2025/nanoclone/blob/0abeb7635bda9b68ef2277093f76dbe3bf3948e1/src/index.js
// MIT licensed

import isSchema from './isSchema';

function clone(src: unknown, seen: Map<any, any> = new Map()) {
if (isSchema(src) || !src || typeof src !== 'object') return src;
if (seen.has(src)) return seen.get(src);

let copy: any;
if (src instanceof Date) {
// Date
copy = new Date(src.getTime());
seen.set(src, copy);
} else if (src instanceof RegExp) {
// RegExp
copy = new RegExp(src);
seen.set(src, copy);
} else if (Array.isArray(src)) {
// Array
copy = new Array(src.length);
seen.set(src, copy);
for (let i = 0; i < src.length; i++) copy[i] = clone(src[i], seen);
} else if (src instanceof Map) {
// Map
copy = new Map();
seen.set(src, copy);
for (const [k, v] of src.entries()) copy.set(k, clone(v, seen));
} else if (src instanceof Set) {
// Set
copy = new Set();
seen.set(src, copy);
for (const v of src) copy.add(clone(v, seen));
} else if (src instanceof Object) {
// Object
copy = {};
seen.set(src, copy);
for (const [k, v] of Object.entries(src)) copy[k] = clone(v, seen);
} else {
throw Error(`Unable to clone ${src}`);
}
return copy;
}

export default clone;
6 changes: 5 additions & 1 deletion src/util/createValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
InternalOptions,
ExtraParams,
ISchema,
MessageParams,
} from '../types';
import Reference from '../Reference';
import type { AnySchema } from '../schema';
Expand All @@ -29,7 +30,7 @@ export type TestContext<TContext = {}> = {
originalValue: any;
parent: any;
from?: Array<{ schema: ISchema<any, TContext>; value: any }>;
schema: any; // TODO: Schema<any>;
schema: any;
resolve: <T>(value: T | Reference<T>) => T;
createError: (params?: CreateErrorOptions) => ValidationError;
};
Expand All @@ -48,6 +49,7 @@ export type TestOptions<TSchema extends AnySchema = AnySchema> = {
originalValue: any;
schema: TSchema;
sync?: boolean;
spec: MessageParams['spec'];
};

export type TestConfig<TValue = unknown, TContext = {}> = {
Expand Down Expand Up @@ -79,6 +81,7 @@ export default function createValidation(config: {
label,
options,
originalValue,
spec,
sync,
...rest
}: TestOptions<TSchema>,
Expand All @@ -98,6 +101,7 @@ export default function createValidation(config: {
originalValue,
label,
path: overrides.path || path,
spec,
...params,
...overrides.params,
};
Expand Down
17 changes: 15 additions & 2 deletions src/util/reach.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { forEach } from 'property-expr';
import { AnySchema } from '..';
import type ArraySchema from '../array';
import type { ISchema } from '../types';

let trim = (part: string) => part.substr(0, part.length - 1).substr(1);

export function getIn(schema: any, path: string, value?: any, context = value) {
let isInnerTypeChema = (
schema: AnySchema<any, any>,
): schema is ArraySchema<any, any, any, any> =>
'innerType' in schema && schema.type === 'array';

export function getIn<C = any>(
schema: ISchema<any, C>,
path: string,
value?: any,
context: C = value,
) {
let parent: any, lastPart: string, lastPartDebug: string;

// root path: ''
Expand Down Expand Up @@ -35,7 +48,7 @@ export function getIn(schema: any, path: string, value?: any, context = value) {
if (!schema.fields || !schema.fields[part])
throw new Error(
`The schema does not contain the path: ${path}. ` +
`(failed at: ${lastPartDebug} which is a type: "${schema._type}")`,
`(failed at: ${lastPartDebug} which is a type: "${schema.type}")`,
);

parent = value;
Expand Down
2 changes: 1 addition & 1 deletion test/mixed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ describe('Mixed Types ', () => {
let inst = mixed().default('hi');

expect(function () {
expect(inst.concat(string())._type).toBe('string');
expect(inst.concat(string()).type).toBe('string');
}).not.toThrowError(TypeError);
});

Expand Down
29 changes: 29 additions & 0 deletions test/tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,34 @@ describe('Array types', () => {

expect(await inst.validate(['4', 3])).toEqual([4, '3']);
});

it('should use labels', async () => {
let schema = tuple([
string().label('name'),
number().positive().integer().label('age'),
]);

await expect(schema.validate(['James', -24.55])).rejects.toThrow(
'age must be a positive number',
);
});

it('should throw useful type error for lenght', async () => {
let schema = tuple([string().label('name'), number().label('age')]);

// expect(() => schema.cast(['James'])).toThrowError(
// 'this tuple value has too few items, expected a length of 2 but got 1 for value',
// );
await expect(schema.validate(['James'])).rejects.toThrowError(
'this tuple value has too few items, expected a length of 2 but got 1 for value',
);

await expect(schema.validate(['James', 2, 4])).rejects.toThrowError(
'this tuple value has too many items, expected a length of 2 but got 3 for value',
);
// expect(() => schema.validate(['James', 2, 4])).rejects.toThrowError(
// 'this tuple value has too many items, expected a length of 2 but got 3 for value',
// );
});
});
});
Loading

0 comments on commit 6b1caea

Please sign in to comment.