Friendly Functions
2024
A friendly function is one that tells you how to call it, and if there is an argument which part of the argument is wrong.
- If you call it with nothing, it returns a pattern describing how to call it.
- If you call it with an object that matches the pattern, then, great!
- If you call it with an object that only partially matches the pattern, it will return an object containing a combination of matched values and unmatched patterns.
How can you tell the difference between a successful function invocation and an unsuccessful one? Return something other than an object! The most general (and useful) convention is to return an array.
Related:
- Zod "TypeScript-first schema validation with static type inference "
- JSONSchema and the schema store
js
import {checkValue, validate} from '/friendly.js';
const {str, num, bool, fun, obj, undef, arr, same,
all, contains, equals} = as;
const testPattern = {
a: ['num', 'between', 0, 10],
b: ['str'],
c: ['optional', 'str', 'between', 0, 10],
d: {
e: ['num', 'between', 0, 10],
f: ['optional', 'str', 'between', 3, 8],
},
g: ['str', 'pick', 1, 'apple', 'orange', 'bannana'],
};
// Here's a value that matches
const testValid = {
a: 5,
b: 'hi',
d: {
e: 3,
f: 'abcd',
},
g: 'apple'
};
// Here's one that doesn't
const testNotValid = {
a: 5,
b: 'hi',
d: {
e: 11, //this one fails.
f: 'abcd',
},
g: 'pear', // so does this one
};
// checkValue() tests - The normal happy cases
// undef is GOOD, it means the function was satisfied and didn't have to spit something back.
undef(checkValue('foo', 'foo'));
undef(checkValue(1, 1));
undef(checkValue(['str'], 'a'));
undef(checkValue(['str'], ''));
undef(checkValue(['num'], .1));
undef(checkValue(['int'], 1));
undef(checkValue(['bool'], false));
undef(checkValue(['arr'], [1,2,3]));
undef(checkValue(['obj'], {a:1}));
undef(checkValue(['undef']));
undef(checkValue(['optional']));
undef(checkValue(['num','between', 0, 3], 2));
undef(checkValue(['str', 'between', 1, 3], "a")); // checking length
undef(checkValue(['str', 'between', 1, 3], "abc"));
undef(checkValue(['str', 'size', 3], "abc"));
undef(checkValue(['optional','num','between', 0, 3], 2));
undef(checkValue(['optional','num','between', 0, 3] ));
// checkValue() tests - Bad scalar matches
equals(checkValue('foo', 'bar'), ['equals', 'foo']);
equals(checkValue(1, 2), ['equals', 1]);
equals(checkValue(['num'], 'a'), ['num']);
equals(checkValue(['int'], .1), ['int']);
equals(checkValue(['str'], 1), ['str']);
equals(checkValue(['between', 0, 3], 4), ['between', 0, 3]);
equals(checkValue(['str', 'between', 0, 3], 'abcd'), ['between', 0, 3]);
// checkValue() tests - optional doesn't mean you can supply a bad value!
equals(checkValue(['optional', 'num'], 'a'), ['num']);
equals(checkValue(['optional', 'between', 0, 3], 4), ['between', 0, 3]);
equals(checkValue(['optional', 'num', 'between', 0, 3], 'a'), ['num']);
undef(checkValue(['optional', 'num', 'between', 0, 10], 5));
// validate() tests - single key
undef(validate({a: ['num']}, {a: 1}));
equals(validate({a: ['num']}, {a: 'a'}), {a: ['num']}, 'failed key');
equals(validate({a: ['num']}, {}), {a: ['num']}, 'missing non-optional');
// validate() tests multi key
equals(validate({a: ['num'], b: ['num']}, {a: 1, b: 1}), undefined, 'multi-key success');
equals(validate({a: ['num'], b: ['num']}, {a: 1, b: ''}), {b: ['num']}, 'multi-key fail');
// TEST: If we returned matching values this would be
// assertEquals(validate({a: ['num'], b: ['num']}, {a: 1, b: ''}), {a: 1, b: ['num']}, 'multi-key fail');
// validate() tests special cases
undef(validate({}, {everything: 'passes'}));
equals(validate(null, {everything: 'fails'}), {});
equals(validate({a: ['str']}, 'non-obj value fails'), {a: ['str']});
// validate() satisfied when optional is ignored
undef(validate({
a: 'dec',
b: ['optional', 'num', 'between', 0, 10]
}, {
a: 'dec'
}));
undef(validate({
handler: 'dec',
b: ['optional', 'num', 'between', 0, 10]
}, {
handler: 'dec'
})
);
undef(validate({
msg: 'dec',
b: ['optional', 'num', 'between', 0, 10]
}, {
msg: 'dec',
})
);
// validate() returns failures even if some succeeded.
equals(validate({
e: ['num', 'between', 0, 10],
f: ['optional', 'str', 'between', 3, 8],
}, {
e: 11,
f: "abcd",
}), {
e: ['between', 0, 10],
});
// nested object
undef(validate({
a: ['num'],
b: {c: ['num']},
}, {
a: 1,
b: {c: 1},
})
);
equals(validate({
a: ['num'],
b: {c: ['num']}
}, {
a: 1,
b: {c: ''}
}), {
b: {c: ['num']}
});
// the full monty
undef(validate(testPattern, testValid));
equals(validate(testPattern, testNotValid), {d: {e: ['between', 0, 10]}, g:["pick", 1, ["apple","orange","bannana"] ]}, 'full monty should NOT be valid');
/** docs
A simple friendly function.
Friendly functions can only take one object arg.
If the the argument is valid, we execute the function.
If the argument is invalid, we return the failed pattern.
*/
const friendly = input => {
const pattern = {a: ['bool']};
if (!validate(pattern, input)) { // concern: It's not intuitive to check !validate. rename to 'invalid'?
return 2;
} else {
return validate(pattern, input);
}
}
// Lots of wrong ways to call this function; only one right way
equals(friendly(), {a: ['bool']})
equals(friendly(1), {a: ['bool']})
equals(friendly({a: 1}), {a: ['bool']})
equals(friendly({b: true}), {a: ['bool']})
equals(friendly({a: true}), 2)
Copyright SimpatiCorp 2024