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
111 changes: 111 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,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 +1506,105 @@
};
}

//# validate :: Type -> a -> Array (Either Object a)
//.
//. Takes a type, and any value. Returns an `Array` of
//. `Right a` if the value is a member of the type;
//. `Left Object` for each property that is invalid.
//. The first index in an array of `Left`s is always named `$$`,
//. which refers to the entire value.
function validate(t) {
return function(x) {
// 1. build list of prop, validate function and value object for all keys
// 2. when value = null, use MissingValue object
// 3. run all remainder validate functions for nested type validation
// 4. remove all Rights from list
// 5. concatenate $$ (in returnValue) with all Lefts

var returnValue = [];
var $$Result = t.validate ([]) (x);

// 1 and 2
// props :: Array (Either Object Object)
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]
});
});

var validateRights = Z.compose (function(p) {
if (p.result.isLeft) {
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
});
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidchambers this part I'm really not sure about. The idea is to get the name of the Type that is invalid for nested types. I haven't seen any propPath that didn't exactly contain ['$1'] but I'm sure that there are cases.

} else {
return Left ({
error: 'MissingValue',
type: p.type.name,
name: p.name,
value: p.value
});
}
} else {
return Right (p);
}
}, function(p) {
return {
name: p.name,
result: p.type.validate ([]) (p.value),
type: p.type,
value: p.value
};
});

returnValue.push (
$$Result.isLeft
? Left ({
error: 'WrongValue',
type: t.name || t.type,
name: '$$',
value: x
})
: $$Result
);

// 3
var tmp0 = Z.map (function(prop) {
return Z.chain (validateRights, prop);
}, props);

// 4
var tmp1 = Z.filter (function(either) {
return either.isLeft;
}, tmp0);

// 5
return Z.concat (returnValue, tmp1);
// 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 +3012,11 @@
({})
([Array_ (Type), Type, Any, Boolean_])
(test),
validate:
def ('validate')
({})
([Type, Any, Array_ (Either_ (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}),
Left ({'error': 'MissingValue', 'name': 'bar', 'type': 'FooBar', 'value': null}),
Left ({'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}),
Left ({'error': 'MissingValue', 'name': 'bar', 'type': 'FooBar', 'value': undefined}),
Left ({'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': {}}),
Left ({'error': 'MissingValue', 'name': 'bar', 'type': 'Number', 'value': undefined}),
Left ({'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}}),
Left ({'error': 'MissingValue', 'name': 'bar', 'type': 'Number', 'value': undefined}),
Left ({'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}}),
Left ({'error': 'WrongValue', 'name': 'bar', 'type': 'Number', 'value': null}),
Left ({'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}}),
Left ({'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'}}),
Left ({'error': 'WrongValue', 'name': 'date', 'type': 'DateIso', 'value': '2020-04-100'}),
]);

eq ($.validate (model2) (undefined))
([
Left ({'error': 'WrongValue', 'name': '$$', 'type': 'RECORD', 'value': undefined}),
Left ({'error': 'MissingValue', 'name': 'date', 'type': 'RECORD', 'value': undefined}),
Left ({'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'}}),
Left ({'error': 'WrongValue', 'name': 'date', 'type': 'DateIso', 'value': '2020-04-100'}),
Left ({'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'}}),
Left ({'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'}}),
Left ({'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': []}}),
Left ({'error': 'WrongValue', 'name': 'date', 'type': 'NonEmpty', 'value': []}),
]);

eq ($.validate (model2) ({bool: false, date: '2020-04-10'}))
([Right ({date: '2020-04-10', bool: false})]);
dotnetCarpenter marked this conversation as resolved.
Show resolved Hide resolved

});

});