diff --git a/index.js b/index.js index 6ce1c53..0d898a3 100644 --- a/index.js +++ b/index.js @@ -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) { @@ -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); } @@ -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. @@ -2906,6 +3005,11 @@ ({}) ([Array_ (Type), Type, Any, Boolean_]) (test), + validate: + def ('validate') + ({}) + ([Type, Any, Either_ (Array_ (Object_)) (Any)]) + (validate), NullaryType: def ('NullaryType') ({}) diff --git a/test/index.js b/test/index.js index 71ed57e..fb9e0f5 100644 --- a/test/index.js +++ b/test/index.js @@ -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})); + + }); + +});