-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
244 lines (218 loc) · 8.39 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
var chalk = require('chalk');
var lodash = require('lodash-checkit');
var util = require('util');
var parser = require('./_parser');
var helpers = require('./lib/helpers');
var normalize = require('./lib/normalize');
var mixins = require('./lib/mixins');
var fillSrcWithVoids = helpers.fillSrcWithVoids;
var fillTargWithVoids = helpers.fillTargWithVoids;
var checkSupersetMatch = helpers.checkSupersetMatch;
var checkSubsetMatch = helpers.checkSubsetMatch;
var debug = false;
var _ = lodash.mixin(mixins);
var lodashModule = _; // lodash-checkit by default, but can be overriden by customization
// This curries the '|' separated arguments of a matcher or filter function
// e.g. _.isBetween|10|15 --> function (s) {return _.isBetween(s, 10, 15)}
var curryFunctionSpec = function (fnSpec) {
var fnSplit = ( /^([^\|]+)(.*)/.exec(fnSpec) || ['dummy']).slice(1);
var fnName = fnSplit.shift();
var fnRest = fnSplit.shift();
var fn = lodashModule[fnName];
if (! lodashModule[fnName]) {
throw new Error('The function _.' + fnName + " doesn't exist");
}
while (fnRest) {
fnSplit = (
/^\|\"([^\"]*)\"(.*)/.exec(fnRest) ||
/^\|\'([^\']*)\'(.*)/.exec(fnRest) || ['dummy']
).slice(1);
var fnArg = fnSplit.shift();
if (!fnArg) {
fnSplit = (/^\|([^\|]*)(.*)/.exec(fnRest) || ['dummy']).slice(1);
fnArg = fnSplit.shift();
if (! isNaN(fnArg)) {
fnArg = Number(fnArg);
}
}
fnRest = fnSplit.shift();
if (debug) {
// eslint-disable-next-line no-console
console.log('fnName', fnName, 'fnArg', fnArg, 'fnRest', fnRest);
}
fn = _.bind(fn, lodashModule, _, fnArg);
}
return fn;
};
var echo = function (val) { return val; };
var matchMembers = function (targVal, srcVal, matcher) {
// eslint-disable-next-line no-console
if (debug) console.log('matchMembers', targVal, srcVal);
var newSrcObj = fillSrcWithVoids(targVal, srcVal);
var newTargObj = fillTargWithVoids(targVal, srcVal);
// eslint-disable-next-line no-console
if (debug) console.log('matchMembers newSrcObj', newSrcObj, 'newTargObj', newTargObj);
return _.isMatchWith(newTargObj, newSrcObj, matcher);
};
var matcher = function (makeMsg, targVal, srcVal, key) {
// eslint-disable-next-line no-console
if (debug) console.log('matcher', targVal, srcVal);
var isMatch;
if (_.isArray(targVal)) {
if (!_.isArrayLike(srcVal)) {
makeMsg(srcVal, targVal);
return false;
}
if (_.includes(targVal, '__MP_superset')) return checkSupersetMatch(makeMsg, targVal, srcVal);
if (_.includes(targVal, '__MP_subset')) return checkSubsetMatch(makeMsg, targVal, srcVal);
if (_.includes(targVal, '__MP_equalset')) {
isMatch = checkSubsetMatch(echo, targVal, srcVal) && checkSupersetMatch(echo, targVal, srcVal);
if (!isMatch) {
makeMsg(srcVal, targVal, '', "Array ${src} isn't an equalset match of Array ${tgt}");
}
return isMatch;
}
isMatch = (targVal.length === srcVal.length) &&
_.isMatchWith(targVal, srcVal, matcher.bind(null, makeMsg));
if (!isMatch && !makeMsg().length) {
makeMsg(srcVal, targVal);
}
return isMatch;
}
if (_.isPlainObject(targVal)) {
var hasNonMapApplyKeys = false;
var mapApplyResults = [];
_.forEach(targVal, function (tv, k) {
var mapApplyMatch = k.match(/^__MP_(map|apply)\d+\s*(.*)/) || [];
// eslint-disable-next-line no-console
if (debug) console.log('mapApplyMatch', k, mapApplyMatch);
var mapApply = mapApplyMatch[1];
var mapApplyFname = mapApplyMatch[2];
var fn = mapApplyFname ? curryFunctionSpec(mapApplyFname) : echo;
if (mapApply === 'map') {
if (_.isObject(srcVal)) {
if (mapApplyFname) {
if (_.isArray(srcVal)) {
mapApplyResults.push(matcher(makeMsg, tv, _.map(srcVal, fn), key));
} else {
mapApplyResults.push(matcher(makeMsg, tv, _.mapValues(srcVal, fn), key));
}
} else {
_.forEach(srcVal, function (so) {
mapApplyResults.push(matcher(makeMsg, tv, so, key));
});
}
} else {
// mapApplyResults.push(matchMembers(tv, fn(srcVal), matcher.bind(null, matchFailMsg)));
mapApplyResults.push(matcher(makeMsg, tv, fn(srcVal)));
}
} else if (mapApply === 'apply') {
// eslint-disable-next-line no-console
if (debug) console.log('apply result', fn(srcVal));
mapApplyResults.push(matcher(makeMsg, tv, fn(srcVal)));
} else {
hasNonMapApplyKeys = true;
}
});
if (mapApplyResults.length) {
if (hasNonMapApplyKeys) {
var targStr = util.inspect(targVal)
.replace(/__MP_apply\d+\s*/, '"<-."')
.replace(/__MP_map\d+\s*/, '"<-."');
throw new Error('target ' + targStr +
' has both ordinary keys as well as keys for map(<=) and/or apply(<-)');
}
return _.every(mapApplyResults);
}
if (!_.isArrayLike(srcVal) && !_.isNumber(srcVal) && !_.isString(srcVal) && !_.isNil(srcVal)) {
return matchMembers(targVal, srcVal, matcher.bind(null, makeMsg));
}
}
var matchFn = null;
if ( /^__MP_match/.test(targVal)) {
matchFn = curryFunctionSpec(targVal.replace(/^__MP_match\s*/, ''));
} else if (_.isFunction(targVal)) {
matchFn = targVal;
} else if (/^__MP_regex/.test(targVal)) {
var targMatch = targVal.match(/^__MP_regex\(([^)]*)\)\s(.*)/);
var re = new RegExp(targMatch[2], targMatch[1]);
matchFn = RegExp.prototype.test.bind(re);
} else if (_.isRegExp(targVal)) {
matchFn = RegExp.prototype.test.bind(targVal);
}
// Here's where the leaf node item comparison happens.
var currentIsMatch = matchFn ? matchFn(srcVal) : targVal === srcVal;
if (!currentIsMatch) {
if (key === '__testObj') {
makeMsg(srcVal, targVal);
} else if (/^\d+$/.test(key)) {
makeMsg(srcVal, targVal, key, "Array[${key}] = ${src} didn't match target Array[${key}] = ${tgt}");
} else {
makeMsg(srcVal, targVal, key, "{${key}: ${src}} didn't match target {${key}: ${tgt}}");
}
}
return currentIsMatch;
};
var makeMsg = function (matchFailMsg, srcVal, targVal, key, template) {
if (arguments.length === 1) {
return matchFailMsg;
}
if (!template) {
template = "${src} didn't match target ${tgt}";
}
if (_.isArray(targVal)) {
targVal = _.without(targVal, '__MP_superset', '__MP_subset', '__MP_equalset');
}
if (_.isFunction(targVal)) {
targVal = targVal.toString().split(/\n/)[0].replace(/\{.*/, '{...}');
}
var param = {
key: key,
src: util.inspect(srcVal),
tgt: util.inspect(targVal).replace(/__MP_match\s*/g, '_.')
};
matchFailMsg.push(_.template(template)(param));
};
var matchPattern = function (sourceData, targetPattern) {
var targetObject = targetPattern;
if (_.isObject(targetPattern)) {
targetObject = normalize(targetPattern);
}
if (_.isString(targetPattern)) {
try {
targetObject = parser.parse(targetPattern);
// eslint-disable-next-line no-console
if (debug) console.log('parsed', targetObject);
}
catch (error) {
var msg = 'matchPattern: Error parsing pattern: ' + error.message;
var errStart = error.location.start.offset;
var errEnd = error.location.end.offset;
var startLine = error.location.start.line;
var startColumn = error.location.start.column;
msg += '\nline:' + startLine + ' column:' + startColumn;
var chalkPattern = chalk.green(targetPattern.slice(0, errStart)) +
chalk.red(targetPattern.slice(errStart, errEnd)) +
chalk.blue(targetPattern.slice(errEnd));
var chalkLines = chalkPattern.split(/\n/);
var outLines = chalkLines.slice(0,startLine);
outLines.push(_.repeat('-', startColumn - 1) + '^' + '---');
Array.prototype.push.apply(outLines, chalkLines.slice(startLine));
msg += '\n' + outLines.join('\n');
throw new Error(msg);
}
}
// eslint-disable-next-line no-console
if (debug) console.log('parse/normalize targetObject', targetObject);
var matchFailMsg = [];
_.isMatchWith(
{__testObj: targetObject},
{__testObj: sourceData},
matcher.bind(null, makeMsg.bind(null, matchFailMsg)));
return matchFailMsg.length ? matchFailMsg.join('\n') : null;
};
matchPattern.use = function (newLodashModule) {
lodashModule = newLodashModule;
};
matchPattern.getLodashModule = function () { return lodashModule; };
module.exports = matchPattern;