UNPKG

data-matching

Version:

Matches a data object against a reference value

770 lines (692 loc) 20.8 kB
const _ = require("lodash"); const sm = require("string-matching"); const util = require("util"); const MatchingError = sm.MatchingError; const { XMLParser } = require('fast-xml-parser'); const re_string_matching_indication = /(^|[^!])!{/; var non_zero = (x, dict, throw_matching_error, path) => { if (typeof x != "number") { if (throw_matching_error) throw new MatchingError(path, "expected to be a number"); return false; } if (x == 0) { if (throw_matching_error) throw new MatchingError(path, "expected to be non_zero"); return false; } return "is non_zero"; }; non_zero.__name__ = "non_zero"; var non_blank_str = (x, dict, throw_matching_error, path) => { if (typeof x != "string") { if (throw_matching_error) throw new MatchingError(path, "expected to be a string"); return false; } if (x == "") { if (throw_matching_error) throw new MatchingError(path, "expected to be non_blank_str"); return false; } return "non_blank_str"; }; non_blank_str.__name__ = "non_blank_str"; var str_equal = (s) => { var f = (x, dict, throw_matching_error, path) => { if (x.toString() !== s.toString()) { if (throw_matching_error) throw new MatchingError( path, `not str_equal expected='${s}' received='${x}'`, ); return false; } return "str_equal"; }; f.__name__ = `str_equal['${s}']`; return f; }; var _typeof = (v) => { var t = typeof v; if (t === "object") { if (v instanceof Array) return "array"; else if (v === undefined) return "undefined"; else if (v === null) return "null"; else return "dict"; } else { return t; } }; var _match_arrays = ( expected, received, dict, full_match, throw_matching_error, path, ) => { var reason; print_debug(`${path}: checking`); if (expected.length != received.length) { reason = `arrays lengths do not match: expected_len=${expected.length} received_len=${received.length}`; if (throw_matching_error) throw new MatchingError(path, reason); print_debug(`${path}: ${reason}`); return false; } for (var i = 0; i < expected.length; i++) { if ( !_match( expected[i], received[i], dict, full_match, throw_matching_error, path + "[" + i + "]", ) ) { return false; } } return "array matched"; }; var print_debug = (s) => { //console.error(s) // this actually is not an error. It is just to avoid messing with STDOUT from client code. }; var _match_dicts = ( expected, received, dict, full_match, throw_matching_error, path, ) => { var reason; print_debug(`${path}: checking`); var keys_e = new Set(Object.keys(expected)); var keys_r = new Set(Object.keys(received)); for (var key of keys_e) { print_debug(`${path}.${key}: checking`); var val_e = expected[key]; if (val_e == absent) { if (keys_r.has(key)) { reason = "should be absent"; if (throw_matching_error) throw new MatchingError(`${path}.${key}`, reason); print_debug(`${path}.${key}: ${reason}`); return false; } else { print_debug(`${path}.${key}: absent as expected`); } } else { var rec = received[key]; if (rec === undefined) { reason = "should be present"; if (throw_matching_error) throw new MatchingError(`${path}.${key}`, reason); print_debug(`${path}.${key}: ${reason}`); return false; } if ( !_match( expected[key], rec, dict, full_match, throw_matching_error, path + "." + key, ) ) { return false; } } print_debug(`${path}.${key}: matched`); keys_r.delete(key); } if (full_match) { if (keys_r.size > 0) { reason = `full match failed due extra keys [${Array.from(keys_r)}]`; if (throw_matching_error) throw new MatchingError(path, reason); print_debug(`${path}: ${reason}`); return false; } } return "object matched"; }; var collect = (var_name, matcher) => { var f = (val, dict, throw_matching_error, path) => { if (matcher) { if ( !_match(matcher, val, dict, false, throw_matching_error, path) ) { return; } } if (typeof dict[var_name] == "undefined") { dict[var_name] = val; print_debug(`${path}: collect OK`); return true; } else { if (dict[var_name] != val) { var reason = `cannot set '${var_name}' to '${util.inspect( val, )}' because it is already set to '${util.inspect( dict[var_name], )}'`; if (throw_maching_error) throw new MatchingError(path, reason); print_debug(`${path}: ${reason}`); return false; } print_debug( `${path}: collect OK (found already set to same value)`, ); return true; } }; f.__name__ = `collect['${var_name}']`; return f; }; var _match = ( expected, received, dict, full_match, throw_matching_error, path, ) => { var reason; var type_e = _typeof(expected); var type_r = _typeof(received); if (type_e == "undefined") { // this means to ignore received value print_debug(`${path}: required to be ignored`); return true; } if (type_e == type_r) { if (type_e == "array") { return _match_arrays( expected, received, dict, full_match, throw_matching_error, path, ); } else if (type_e == "dict") { return _match_dicts( expected, received, dict, full_match, throw_matching_error, path, ); } if(type_r == "function" && expected.__name__.startsWith('collect[')) { return expected(received, dict, throw_matching_error, path) } if (expected != received) { //console.error("throw_matching_error:", throw_matching_error) reason = `expected='${expected}' received='${received}'`; if (throw_matching_error) throw new MatchingError(path, reason); print_debug(`${path}: ${reason}`); return false; } //print_debug(`${path}: matched`) return "matched"; } if (type_e == "function") { var res = expected(received, dict, throw_matching_error, path); if (res) print_debug(`${path}: ${res} (matched)`); return res; } if (expected === received) { return true; } else { var reason = `expected (${JSON.stringify( expected, )}) and received (${JSON.stringify(received)}) differ`; if (throw_matching_error) throw new MatchingError(path, reason); } }; var push = (var_name, matcher) => { var f = (val, dict, throw_matching_error, path) => { if (matcher) { if ( !_match(matcher, val, dict, false, throw_matching_error, path) ) { return; } } if (typeof dict[var_name] == "undefined") { dict[var_name] = [val]; return true; } else if (!Array.isArray(dict[var_name])) { var reason = `'${var_name}' is not an Array`; if (throw_maching_error) throw new MatchingError(path, reason); print_debug(`${path}: ${reason}`); return false; } else { dict[var_name].push(val); return true; } }; f.__name__ = `push['${var_name}']`; return f; }; var pop = (var_name, matcher) => { var f = (val, dict, throw_matching_error, path) => { if (matcher) { if ( !_match(matcher, val, dict, false, throw_matching_error, path) ) { return; } } if (typeof dict[var_name] == "undefined") { var reason = `'${var_name}' is undefined`; if (throw_matching_error) throw new MatchingError(path, reason) print_debug(`${path}: ${reason}`) return false } else if (!Array.isArray(dict[var_name])) { var reason = `'${var_name}' is not an Array`; if (throw_matching_error) throw new MatchingError(path, reason) print_debug(`${path}: ${reason}`); return false } else { const idx = dict[var_name].indexOf(val) if (idx !== -1) { dict[var_name].splice(idx, 1) return true } else { var reason = `${val} not found in the array ${var_name}` if (throw_matching_error) throw new MatchingError(path, reason) print_debug(`${path}: ${reason}`) return false } } }; f.__name__ = `pop['${var_name}']`; return f; }; const absent = () => { return "I am the absent function"; }; absent.__name__ = "absent"; const anything = () => { return true; }; anything.__name__ = "anything"; var _deepMap = (obj, iterator, context) => { return _.transform(obj, function (result, val, key) { var type_val = _typeof(val); result[key] = type_val == "array" || type_val == "dict" ? _deepMap(val, iterator, context) : iterator.call(context, val, key, obj); }); }; var matchify_strings = (evt) => { if (typeof evt == "function") { return evt; } return _deepMap(evt, (x) => { if (typeof x == "string" && x.match(re_string_matching_indication)) { return sm.gen_matcher(x); } else { return x; } }); }; var partial_match = (expected) => { var expected2 = matchify_strings(expected); var f = (received, dict, throw_matching_error, path) => { return _match( expected2, received, dict, false, throw_matching_error, path, ); }; f.__original_data__ = expected; f.__name__ = "partial_match"; return f; }; var full_match = (expected) => { var expected2 = matchify_strings(expected); var f = (received, dict, throw_matching_error, path) => { return _match( expected2, received, dict, true, throw_matching_error, path, ); }; f.__original_data__ = expected; f.__name__ = "full_match"; return f; }; var json = (expected, full_match) => { var expected2 = matchify_strings(expected); var f = (s, dict, throw_matching_error, path) => { var received = JSON.parse(s); return _match( expected2, received, dict, full_match, throw_matching_error, path, ); }; f.__original_data__ = expected; f.__name__ = "json" + (full_match ? "_full_match" : "_partial_match"); return f; }; var xml = (expected, full_match) => { var expected2 = matchify_strings(expected); var f = (s, dict, throw_matching_error, path) => { var parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', preserveOrder: true, //attributesGroupName: 'attrs', // cannot be use yet. See https://github.com/NaturalIntelligence/fast-xml-parser/issues/705 //trimValues: true, }) var received = parser.parse(s); return _match( expected2, received, dict, full_match, throw_matching_error, path, ); }; f.__original_data__ = expected; f.__name__ = "xml" + (full_match ? "_full_match" : "_partial_match"); return f; }; var kv_str = ( expected, param_sep, kv_sep, preparse_decoder, postparse_decoder, full_match, ) => { var expected2 = matchify_strings(expected); var f = (s, dict, throw_matching_error, path) => { var received = s; if (preparse_decoder) { received = preparse_decoder(s); } received = _.chain(received) .split(param_sep) .map((s) => { var parts = s.split(kv_sep); var key = parts[0]; var val = parts.slice(1).join(kv_sep); if (postparse_decoder) { val = postparse_decoder(val); } return [key, val]; }) .fromPairs() .value(); return _match( expected2, received, dict, full_match, throw_matching_error, path, ); }; f.__original_data__ = expected; f.__name__ = "kv_str" + (full_match ? "_full_match" : "_partial_match"); return f; }; var matcher = (name, expected) => { if (!typeof expected == "function") throw new Error("Must be a function"); var f = (received, dict, throw_matching_error, path) => { var res = expected(received); if (res == true) return true; if (throw_matching_error) { throw new MatchingError(path, res); } return false; }; f.__name__ = name; return f; }; var any_of = (matchers, name) => { var f = (received, dict, throw_matching_error, path) => { var res; //we cannot use matchers.forEach() as a return doesn't interrupt iteration. for (var i = 0; i < matchers.length; i++) { var matcher = matchers[i]; var dict_clone = _.cloneDeep(dict); res = _match(matcher, received, dict_clone, false, false, path); if (res) { _.assign(dict, dict_clone); if (name) { dict[name] = received; } return res; } } if (throw_matching_error) { throw new MatchingError(path, "any_of no match"); } return res; }; f.__original_data__ = matchers; f.__name__ = "any_of"; return f; }; var unordered_list = (expected) => { return (received_list, dict, throw_matching_error, path) => { if (_typeof(received_list) != "array") { if (throw_matching_error) { throw new MatchingError(path, "unordered_list got non list"); } return false; } if (expected.length != received_list.length) { reason = `arrays lengths do not match: expected_len=${expected.length} received_len=${received_list.length}`; if (throw_matching_error) throw new MatchingError(path, reason); print_debug(`${path}: ${reason}`); return false; } var exp = _.cloneDeep(expected); for (var i = 0; i < received_list.length; i++) { var received = received_list[i]; for (var j = 0; j < exp.length; j++) { var matcher = exp[j]; var res = _match(matcher, received, dict, false, false, path); if (res) { //remove element exp.splice(j, 1); break; } } } if (exp.length == 0) { return true; } if (throw_matching_error) { throw new MatchingError(path, "unordered_list no match"); } return false; }; }; var gen_gen_matcher = (parser, extractor, name) => { return (expected) => { var expected2 = matchify_strings(expected); var f = (s, dict, throw_matching_error, path) => { console.error("s:", s); var received = parser(s); console.error("received:", received); return _.every(expected2, (val, key) => { console.error("key:", key); var item = extractor(received, key); if (val == absent && item) { if (throw_matching_error) { throw Error(`key ${path}.${key} expected to be absent`); } return false; } var full_match = false; return _match( val, item, dict, full_match, throw_matching_error, `${path}.${key}`, ); }); }; f.__original_data__ = expected; f.__name__ = name; return f; }; }; const pop_match = (expected, array, dict) => { const throw_matching_error = false; const path = ""; const full_match = false; var expected2 = matchify_strings(expected); var index = undefined; for (var i = 0; i < array.length; i++) { var item = array[i]; if (typeof expected2 == "function") { if (expected2(item, dict, throw_matching_error, path)) { index = i; break; } } else { if ( _match( expected2, item, dict, full_match, throw_matching_error, "", ) ) { index = i; break; } } } if (index >= 0) { return array.splice(index, 1)[0]; } return undefined; }; const reverse_pop_match = (item, array, dict) => { const throw_matching_error = false; const path = ""; const full_match = false; var array2 = matchify_strings(array); var index = undefined; for (var i = 0; i < array2.length; i++) { var expected = array2[i]; if (typeof expected == "function") { if (expected(item, dict, throw_matching_error, path)) { index = i; break; } } else { if ( _match( expected, item, dict, full_match, throw_matching_error, "", ) ) { index = i; break; } } } if (index >= 0) { return array.splice(index, 1)[0]; } return undefined; }; module.exports = { absent, non_zero, non_blank_str, str_equal, collect, push, pop, partial_match, full_match, json_partial_match: (expected) => { return json(expected, false); }, json_full_match: (expected) => { return json(expected, true); }, kv_str_partial_match: ( expected, param_sep, kv_sep, preparse_decoder, postparse_decoder, ) => { return kv_str( expected, param_sep, kv_sep, preparse_decoder, postparse_decoder, false, ); }, kv_str_full_match: ( expected, param_sep, kv_sep, preparse_decoder, postparse_decoder, ) => { return kv_str( expected, param_sep, kv_sep, preparse_decoder, postparse_decoder, true, ); }, any_of, unordered_list, matcher, pm: partial_match, fm: full_match, json, xml, kv_str, m: matcher, matchify_strings, match: _match, MatchingError, _: anything, gen_gen_matcher, pop_match, reverse_pop_match, };