Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate 🎁🎅 #308

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
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}));

});

});