Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
{
"extends": "standard",
"rules": {
"semi": ["error", "always"],
"space-before-function-paren": ["error", "never"],
"eqeqeq": ["error", "smart"],
"semi": [
"error",
"always"
],
"space-before-function-paren": [
"error",
"never"
],
"eqeqeq": [
"error",
"smart"
],
"no-return-assign": 0
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
package-lock.json
yarn.lock
coverage
.vscode
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "validate",
"description": "Validate object properties in javascript.",
"version": "5.1.0",
"version": "5.2.0",
"author": "Eivind Fjeldstad",
"repository": "eivindfjeldstad/validate",
"keywords": [
Expand All @@ -22,7 +22,7 @@
"node": ">=7.6"
},
"dependencies": {
"@eivifj/dot": "^1.0.1",
"@eivifj/dot": "^1.0.3",
"component-type": "1.2.1",
"typecast": "0.0.1"
},
Expand Down
7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -703,10 +703,17 @@ Typecast given `value`

##### Examples

###### **String**
```javascript
prop.type(String)
prop.typecast(123) // => '123'
```
###### **Objcet**
```javascript
prop.type(Object)
prop.typecast('{"x":123}') // => {x:123}
prop.typecast('{x:123}') // => {value:'{x:123}'}
```

Returns **Mixed**

Expand Down
13 changes: 13 additions & 0 deletions src/property.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ export default class Property {
return this.type(Array);
}

/**
* Convenience method for setting type to `Object`
*
* @example
* prop.object()
*
* @return {Property}
*/

object() {
return this.type(Object);
}

/**
* Convenience method for setting type to `Date`
*
Expand Down
26 changes: 23 additions & 3 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import Validators from './validators';
import ValidationError from './error';
import { walk, enumerate, join, assign } from './utils';

const typecastObjectPoly = function(val) {
if (val == null) return {};
if (val instanceof Object) return val;
if (typeof val != 'string') return { value: val };
let obj = {};
try {
obj = JSON.parse(val);
} catch (error) {
obj = { value: val };
}
return obj;
};

/**
* A Schema defines the structure that objects should be validated against.
*
Expand Down Expand Up @@ -54,7 +67,7 @@ export default class Schema {
this.props = {};
this.messages = Object.assign({}, Messages);
this.validators = Object.assign({}, Validators);
this.typecasters = Object.assign({}, typecast);
this.typecasters = Object.assign({}, { object: typecastObjectPoly, ...typecast });
Object.keys(obj).forEach(k => this.path(k, obj[k]));
}

Expand Down Expand Up @@ -86,6 +99,11 @@ export default class Schema {
this.path(prefix).type(Array);
}

// Catchall Object placeholder
if (suffix === '*') {
this.path(prefix).type(Object);
}

// Nested schema
if (rules instanceof Schema) {
rules.hook((k, v) => this.path(join(k, path), v));
Expand Down Expand Up @@ -186,8 +204,9 @@ export default class Schema {
*/

strip(obj) {
walk(obj, (path, prop) => {
walk(obj, (path, prop, isCatchall = false) => {
if (this.props[prop]) return true;
if (isCatchall) return false;
dot.delete(obj, path);
return false;
});
Expand All @@ -206,8 +225,9 @@ export default class Schema {
enforce(obj) {
const errors = [];

walk(obj, (path, prop) => {
walk(obj, (path, prop, isCatchall = false) => {
if (this.props[prop]) return true;
if (isCatchall) return false;
const error = new ValidationError(Messages.illegal(path), path);
errors.push(error);
return false;
Expand Down
15 changes: 13 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function assign(key, val, obj) {
*/

export function enumerate(path, obj, callback) {
const parts = path.split(/\.\$(?=\.|$)/);
const parts = path.split(/\.[$*](?=\.|$|\*)/);
const first = parts.shift();
const arr = dot.get(obj, first);

Expand All @@ -32,6 +32,14 @@ export function enumerate(path, obj, callback) {
}

if (!Array.isArray(arr)) {
if (typeOf(arr) === 'object') {
const keys = Object.keys(arr);
for (let i = 0; i < keys.length; i++) {
const current = join(keys[i], first);
const next = current + parts.join('.*');
enumerate(next, obj, callback);
}
}
return;
}

Expand Down Expand Up @@ -65,7 +73,10 @@ export function walk(obj, callback, path, prop) {
for (const [key, val] of Object.entries(obj)) {
const newPath = join(key, path);
const newProp = join(key, prop);
if (callback(newPath, newProp)) {
const newCatchProp = join('*', prop);
if (callback(newPath, newCatchProp, true)) {
walk(val, callback, newPath, newCatchProp);
} else if (callback(newPath, newProp)) {
walk(val, callback, newPath, newProp);
}
}
Expand Down
30 changes: 21 additions & 9 deletions test/property.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ describe('Property', () => {
test('should register each object property as a validator', () => {
const prop = new Property('test', new Schema());
prop.use({
one: (v) => v !== 1,
two: (v) => v !== 2
one: v => v !== 1,
two: v => v !== 2,
});
expect(prop.validate(1)).toBeInstanceOf(Error);
expect(prop.validate(2)).toBeInstanceOf(Error);
Expand All @@ -59,12 +59,12 @@ describe('Property', () => {

schema.message({
one: () => 'error 1',
two: () => 'error 2'
two: () => 'error 2',
});

prop.use({
one: (v) => v !== 1,
two: (v) => v !== 2
one: v => v !== 1,
two: v => v !== 2,
});

expect(prop.validate(1).message).toBe('error 1');
Expand All @@ -76,7 +76,7 @@ describe('Property', () => {
let first, second;
prop.use({
one: [(v, c, arg) => first = arg, 1],
two: [(v, c, arg) => second = arg, 2]
two: [(v, c, arg) => second = arg, 2],
});
prop.validate({ test: 1 });
expect(first).toBe(1);
Expand Down Expand Up @@ -170,6 +170,14 @@ describe('Property', () => {
});
});

describe('.object()', () => {
test('should set type to object', () => {
const prop = new Property('test', new Schema());
prop.object();
expect(prop._type).toBe(Object);
});
});

describe('.match()', () => {
test('should register a validator', () => {
const prop = new Property('test', new Schema());
Expand Down Expand Up @@ -324,6 +332,10 @@ describe('Property', () => {
const prop = new Property('test', new Schema());
prop.type(String);
expect(prop.typecast(123)).toBe('123');
prop.type(Object);
expect(prop.typecast(123)).toStrictEqual({ value: 123 });
expect(prop.typecast('{"a":123}')).toStrictEqual({ a: 123 });
expect(prop.typecast('{a:123}')).toStrictEqual({ value: '{a:123}' });
});

test('should throw if no typecaster exists', () => {
Expand Down Expand Up @@ -370,7 +382,7 @@ describe('Property', () => {
prop.use({
context: (v, c) => {
ctx = c;
}
},
});

prop.validate('abc', obj);
Expand All @@ -384,7 +396,7 @@ describe('Property', () => {
prop.use({
context: (v, c, p) => {
path = p;
}
},
});

prop.validate('abc', { test: 1 });
Expand All @@ -407,7 +419,7 @@ describe('Property', () => {
const prop = new Property('test', schema);
prop.properties({
hello: String,
world: String
world: String,
});
expect(schema.props).toHaveProperty(['test.hello']);
expect(schema.props).toHaveProperty(['test.world']);
Expand Down
63 changes: 61 additions & 2 deletions test/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ describe('Schema', () => {
expect(schema.props['hello.$']._type).toBe(Number);
});
});

describe('when given a path ending with *', () => {
test('should set `property.type` to object', () => {
const schema = new Schema();
schema.path('hello.*');
expect(schema.props.hello._type).toBe(Object);
});

test('should apply rules to each property of the object', () => {
const schema = new Schema();
schema.path('hello.*').type(Number);
expect(schema.props.hello._type).toBe(Object);
expect(schema.props['hello.*']._type).toBe(Number);
});
});
});

describe('.strip()', () => {
Expand Down Expand Up @@ -152,6 +167,29 @@ describe('Schema', () => {
expect(res).toHaveLength(3);
});

test('should work with * a placeholder for object keys', () => {
const schema = new Schema();
schema.path('a.*.c').required();
schema.path('a.*.b.*').type(String);
schema.path('a.*.b.*.*').type(String);
const res = schema.validate({
a: {
x: {
b: { z: { m: 'hello' } },
c: {
k: {
l: ['hello', 2]
}
}
},
y: {
b: { z: ['hello', 1] }
}
}
});
expect(res).toHaveLength(3);
});

test('should strip by default', () => {
const schema = new Schema({ a: { type: Number } });
const obj = { a: 1, b: 1 };
Expand Down Expand Up @@ -203,6 +241,27 @@ describe('Schema', () => {
});
});

test('should typecast objects and properties within objects', () => {
const schema = new Schema();
schema.path('a.*.b').required();
schema.path('a.*.b.*').type(String);
schema.path('a.*.c.*.*').type(String);
schema.path('b.*').type(Number);

const obj = {
a: { y: { b: { z: 1 } }, x: { b: { j: 'hello' }, c: { m: { l: { o: 2056 } } } } },
b: { n: '1', t: '2' }
};

const res = schema.validate(obj, { typecast: true });
expect(res).toHaveLength(0);

expect(obj).toEqual({
a: { y: { b: { z: '1' } }, x: { b: { j: 'hello' }, c: { m: { l: '[object Object]' } } } },
b: { n: 1, t: 2 }
});
});

test('should not typecast undefined', () => {
const schema = new Schema({ name: { type: String } });
const wrap = () => schema.validate({}, { typecast: true });
Expand Down Expand Up @@ -293,15 +352,15 @@ describe('Schema', () => {
test('should set default typecasters', () => {
const obj = { name: 123 };
const schema = new Schema({ name: { type: 'hello' } });
schema.typecaster('hello', (val) => val.toString());
schema.typecaster('hello', val => val.toString());
schema.typecast(obj);
expect(obj.name).toBe('123');
});

test('should set default typecasters', () => {
const obj = { name: 123 };
const schema = new Schema({ name: { type: 'hello' } });
schema.typecaster({ hello: (val) => val.toString() });
schema.typecaster({ hello: val => val.toString() });
schema.typecast(obj);
expect(obj.name).toBe('123');
});
Expand Down