Skip to content
Draft
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
104 changes: 104 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@
// Right :: b -> Either a b
var Right = Either.Right;

// lefts :: (Filterable f, Functor f) => f (Either a b) -> f a
var lefts = Z.compose (map (prop ('value')), filter (prop ('isLeft')));

// B :: (b -> c) -> (a -> b) -> a -> c
function B(f) {
return function(g) {
Expand Down Expand Up @@ -251,6 +254,13 @@
};
}

// map :: Functor f => (a -⁠> b) -⁠> f a -⁠> f b
function map(f) {
return function(xs) {
return Z.map (f, xs);
};
}

// init :: Array a -> Array a
function init(xs) { return xs.slice (0, -1); }

Expand Down Expand Up @@ -1499,6 +1509,95 @@
};
}

//# validate :: Type -> a -> Either (Array ValidationError) a
//.
//. Takes a type, and any value. Returns `Right a` if
//. the value is a member of the type;
//. `Left (Array ValidationError)` for each property
//. that is invalid. The first index in a `Left` array
//. is always named `$$`, which refers to the entire value.
function validate(t) {
return function(x) {
// $$Result :: {value, propPath} e => Either e a
var $$Result = t.validate ([]) (x);

// props :: Array (Either ValidationError TestObject)
var props = t.keys.map (function(p) {
return x == null
? Left ({
error: 'MissingValue',
type: t.name || t.type,
name: p,
value: x
})
: Right ({
name: p,
type: t.types[p],
value: x[p]
});
});

// validateTestObject :: TestObject -> Either ValidationError TestObject
var validateTestObject = Z.compose (function(p) {
if (p.result.isRight) {
return Right (p);
} else if (p.name in x) {
return Left ({
error: 'WrongValue',
// TODO: figure out what propPath really is
type: p.result.value.propPath.length > 0
? p.type.types[p.result.value.propPath[0]].name
: p.type.name,
name: p.name,
value: p.value
});
} else {
return Left ({
error: 'MissingValue',
type: p.type.name,
name: p.name,
value: p.value
});
}
}, function(p) {
return {
name: p.name,
result: p.type.validate ([]) (p.value),
type: p.type,
value: p.value
};
});

if ($$Result.isLeft) {
// tmp0 :: Array (ValidationError)
var tmp0 = lefts (Z.map (function(prop) {
return Z.chain (validateTestObject, prop);
}, props));

// tmp1 :: Array (ValidationError)
var tmp1 = Z.prepend ({
error: 'WrongValue',
type: t.name || t.type,
name: '$$',
value: x
}, tmp0);

// return :: Left (Array ValidationError)
return Left (tmp1);
} else {
// return :: Right a
return $$Result;
}

// return Z.concat (
// returnValue,
// Z.filter (
// either => either.isLeft,
// Z.map (prop => Z.map (validateRights, prop), props))
// );
};
}

//. ### Type constructors
//.
//. sanctuary-def provides several functions for defining types.
Expand Down Expand Up @@ -2906,6 +3005,11 @@
({})
([Array_ (Type), Type, Any, Boolean_])
(test),
validate:
def ('validate')
({})
([Type, Any, Either_ (Array_ (Object_)) (Any)])
(validate),
NullaryType:
def ('NullaryType')
({})
Expand Down
137 changes: 137 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3877,3 +3877,140 @@ suite ('interoperability', () => {
});

});

suite ('validate', () => {

test ('Undefined', () => {

eq ($.validate ($.Undefined) (undefined))
(Right (undefined));

});

test ('NamedRecordType', () => {
// FooBar :: Type
const FooBar = $.NamedRecordType
('FooBar')
('')
([])
({foo: $.String,
bar: $.Number});

// null is not a member of ‘FooBar’
eq ($.validate (FooBar) (null))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'FooBar', 'value': null},
{'error': 'MissingValue', 'name': 'bar', 'type': 'FooBar', 'value': null},
{'error': 'MissingValue', 'name': 'foo', 'type': 'FooBar', 'value': null},
]));

// undefined is not a member of ‘FooBar’
eq ($.validate (FooBar) (undefined))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'FooBar', 'value': undefined},
{'error': 'MissingValue', 'name': 'bar', 'type': 'FooBar', 'value': undefined},
{'error': 'MissingValue', 'name': 'foo', 'type': 'FooBar', 'value': undefined},
]));

// ''bar' field is missing', ''foo' field is missing'
eq ($.validate (FooBar) ({}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'FooBar', 'value': {}},
{'error': 'MissingValue', 'name': 'bar', 'type': 'Number', 'value': undefined},
{'error': 'MissingValue', 'name': 'foo', 'type': 'String', 'value': undefined},
]));

// 'bar' field is missing
eq ($.validate (FooBar) ({foo: null}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'FooBar', 'value': {'foo': null}},
{'error': 'MissingValue', 'name': 'bar', 'type': 'Number', 'value': undefined},
{'error': 'WrongValue', 'name': 'foo', 'type': 'String', 'value': null},
]));

// Value of 'bar' field, null, is not a member of ‘Number’
eq ($.validate (FooBar) ({foo: null, bar: null}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'FooBar', 'value': {'bar': null, 'foo': null}},
{'error': 'WrongValue', 'name': 'bar', 'type': 'Number', 'value': null},
{'error': 'WrongValue', 'name': 'foo', 'type': 'String', 'value': null},
]));

// Value of 'foo' field, null, is not a member of ‘String’
eq ($.validate (FooBar) ({foo: null, bar: 42}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'FooBar', 'value': {'bar': 42, 'foo': null}},
{'error': 'WrongValue', 'name': 'foo', 'type': 'String', 'value': null},
]));

eq ($.validate (FooBar) ({foo: 'blue', bar: 42}))
(Right ({foo: 'blue', bar: 42}));

});

test ('Custom Type', () => {

// $DateIso :: NullaryType
const $DateIso = (
$.NullaryType ('DateIso')
('https://www.w3.org/QA/Tips/iso-date')
([$.String])
(x => /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2]\d|3[0-1])$/.test (x))
);

const model1 = $.RecordType ({
date: $DateIso,
});

const model2 = $.RecordType ({
date: $.NonEmpty ($DateIso),
bool: $.Boolean,
});

eq ($.validate (model1) ({date: '2020-04-10'}))
(Right ({date: '2020-04-10'}));

eq ($.validate (model1) ({date: '2020-04-100'}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'RECORD', 'value': {'date': '2020-04-100'}},
{'error': 'WrongValue', 'name': 'date', 'type': 'DateIso', 'value': '2020-04-100'},
]));

eq ($.validate (model2) (undefined))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'RECORD', 'value': undefined},
{'error': 'MissingValue', 'name': 'date', 'type': 'RECORD', 'value': undefined},
{'error': 'MissingValue', 'name': 'bool', 'type': 'RECORD', 'value': undefined},
]));

eq ($.validate (model2) ({bool: 'foobar', date: '2020-04-100'}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'RECORD', 'value': {'bool': 'foobar', 'date': '2020-04-100'}},
{'error': 'WrongValue', 'name': 'date', 'type': 'DateIso', 'value': '2020-04-100'},
{'error': 'WrongValue', 'name': 'bool', 'type': 'Boolean', 'value': 'foobar'},
]));

eq ($.validate (model2) ({date: '2020-04-10', bool: 'foobar'}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'RECORD', 'value': {'bool': 'foobar', 'date': '2020-04-10'}},
{'error': 'WrongValue', 'name': 'bool', 'type': 'Boolean', 'value': 'foobar'},
]));

eq ($.validate (model2) ({date: '2020-04-100', bool: true}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'RECORD', 'value': {'bool': true, 'date': '2020-04-100'}},
{'error': 'WrongValue', 'name': 'date', 'type': 'DateIso', 'value': '2020-04-100'},
]));

eq ($.validate (model2) ({date: [], bool: false}))
(Left ([
{'error': 'WrongValue', 'name': '$$', 'type': 'RECORD', 'value': {'bool': false, 'date': []}},
{'error': 'WrongValue', 'name': 'date', 'type': 'NonEmpty', 'value': []},
]));

eq ($.validate (model2) ({bool: false, date: '2020-04-10'}))
(Right ({date: '2020-04-10', bool: false}));

});

});