@depup/joi 18.1.1-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/trace.js ADDED
@@ -0,0 +1,346 @@
1
+ 'use strict';
2
+
3
+ const { deepEqual } = require('@hapi/hoek');
4
+ const Pinpoint = require('@hapi/pinpoint');
5
+
6
+ const Errors = require('./errors');
7
+
8
+
9
+ const internals = {
10
+ codes: {
11
+ error: 1,
12
+ pass: 2,
13
+ full: 3
14
+ },
15
+ labels: {
16
+ 0: 'never used',
17
+ 1: 'always error',
18
+ 2: 'always pass'
19
+ }
20
+ };
21
+
22
+
23
+ exports.setup = function (root) {
24
+
25
+ const trace = function () {
26
+
27
+ root._tracer = root._tracer || new internals.Tracer();
28
+ return root._tracer;
29
+ };
30
+
31
+ root.trace = trace;
32
+ root[Symbol.for('@hapi/lab/coverage/initialize')] = trace;
33
+
34
+ root.untrace = () => {
35
+
36
+ root._tracer = null;
37
+ };
38
+ };
39
+
40
+
41
+ exports.location = function (schema) {
42
+
43
+ return schema.$_setFlag('_tracerLocation', Pinpoint.location(2)); // base.tracer(), caller
44
+ };
45
+
46
+
47
+ internals.Tracer = class {
48
+
49
+ constructor() {
50
+
51
+ this.name = 'Joi';
52
+ this._schemas = new Map();
53
+ }
54
+
55
+ _register(schema) {
56
+
57
+ const existing = this._schemas.get(schema);
58
+ if (existing) {
59
+ return existing.store;
60
+ }
61
+
62
+ const store = new internals.Store(schema);
63
+ const { filename, line } = schema._flags._tracerLocation || Pinpoint.location(5); // internals.tracer(), internals.entry(), exports.entry(), validate(), caller
64
+ this._schemas.set(schema, { filename, line, store });
65
+ return store;
66
+ }
67
+
68
+ _combine(merged, sources) {
69
+
70
+ for (const { store } of this._schemas.values()) {
71
+ store._combine(merged, sources);
72
+ }
73
+ }
74
+
75
+ report(file) {
76
+
77
+ const coverage = [];
78
+
79
+ // Process each registered schema
80
+
81
+ for (const { filename, line, store } of this._schemas.values()) {
82
+ if (file &&
83
+ file !== filename) {
84
+
85
+ continue;
86
+ }
87
+
88
+ // Process sub schemas of the registered root
89
+
90
+ const missing = [];
91
+ const skipped = [];
92
+
93
+ for (const [schema, log] of store._sources.entries()) {
94
+
95
+ // Check if sub schema parent skipped
96
+
97
+ if (internals.sub(log.paths, skipped)) {
98
+ continue;
99
+ }
100
+
101
+ // Check if sub schema reached
102
+
103
+ if (!log.entry) {
104
+ missing.push({
105
+ status: 'never reached',
106
+ paths: [...log.paths]
107
+ });
108
+
109
+ skipped.push(...log.paths);
110
+ continue;
111
+ }
112
+
113
+ // Check values
114
+
115
+ for (const type of ['valid', 'invalid']) {
116
+ const set = schema[`_${type}s`];
117
+ if (!set) {
118
+ continue;
119
+ }
120
+
121
+ const values = new Set(set._values);
122
+ const refs = new Set(set._refs);
123
+ for (const { value, ref } of log[type]) {
124
+ values.delete(value);
125
+ refs.delete(ref);
126
+ }
127
+
128
+ if (values.size ||
129
+ refs.size) {
130
+
131
+ missing.push({
132
+ status: [...values, ...[...refs].map((ref) => ref.display)],
133
+ rule: `${type}s`
134
+ });
135
+ }
136
+ }
137
+
138
+ // Check rules status
139
+
140
+ const rules = schema._rules.map((rule) => rule.name);
141
+ for (const type of ['default', 'failover']) {
142
+ if (schema._flags[type] !== undefined) {
143
+ rules.push(type);
144
+ }
145
+ }
146
+
147
+ for (const name of rules) {
148
+ const status = internals.labels[log.rule[name] || 0];
149
+ if (status) {
150
+ const report = { rule: name, status };
151
+ if (log.paths.size) {
152
+ report.paths = [...log.paths];
153
+ }
154
+
155
+ missing.push(report);
156
+ }
157
+ }
158
+ }
159
+
160
+ if (missing.length) {
161
+ coverage.push({
162
+ filename,
163
+ line,
164
+ missing,
165
+ severity: 'error',
166
+ message: `Schema missing tests for ${missing.map(internals.message).join(', ')}`
167
+ });
168
+ }
169
+ }
170
+
171
+ return coverage.length ? coverage : null;
172
+ }
173
+ };
174
+
175
+
176
+ internals.Store = class {
177
+
178
+ constructor(schema) {
179
+
180
+ this.active = true;
181
+ this._sources = new Map(); // schema -> { paths, entry, rule, valid, invalid }
182
+ this._combos = new Map(); // merged -> [sources]
183
+ this._scan(schema);
184
+ }
185
+
186
+ debug(state, source, name, result) {
187
+
188
+ state.mainstay.debug && state.mainstay.debug.push({ type: source, name, result, path: state.path });
189
+ }
190
+
191
+ entry(schema, state) {
192
+
193
+ internals.debug(state, { type: 'entry' });
194
+
195
+ this._record(schema, (log) => {
196
+
197
+ log.entry = true;
198
+ });
199
+ }
200
+
201
+ filter(schema, state, source, value) {
202
+
203
+ internals.debug(state, { type: source, ...value });
204
+
205
+ this._record(schema, (log) => {
206
+
207
+ log[source].add(value);
208
+ });
209
+ }
210
+
211
+ log(schema, state, source, name, result) {
212
+
213
+ internals.debug(state, { type: source, name, result: result === 'full' ? 'pass' : result });
214
+
215
+ this._record(schema, (log) => {
216
+
217
+ log[source][name] = log[source][name] || 0;
218
+ log[source][name] |= internals.codes[result];
219
+ });
220
+ }
221
+
222
+ resolve(state, ref, to) {
223
+
224
+ if (!state.mainstay.debug) {
225
+ return;
226
+ }
227
+
228
+ const log = { type: 'resolve', ref: ref.display, to, path: state.path };
229
+ state.mainstay.debug.push(log);
230
+ }
231
+
232
+ value(state, by, from, to, name) {
233
+
234
+ if (!state.mainstay.debug ||
235
+ deepEqual(from, to)) {
236
+
237
+ return;
238
+ }
239
+
240
+ const log = { type: 'value', by, from, to, path: state.path };
241
+ if (name) {
242
+ log.name = name;
243
+ }
244
+
245
+ state.mainstay.debug.push(log);
246
+ }
247
+
248
+ _record(schema, each) {
249
+
250
+ const log = this._sources.get(schema);
251
+ if (log) {
252
+ each(log);
253
+ return;
254
+ }
255
+
256
+ const sources = this._combos.get(schema);
257
+ for (const source of sources) {
258
+ this._record(source, each);
259
+ }
260
+ }
261
+
262
+ _scan(schema, _path) {
263
+
264
+ const path = _path || [];
265
+
266
+ let log = this._sources.get(schema);
267
+ if (!log) {
268
+ log = {
269
+ paths: new Set(),
270
+ entry: false,
271
+ rule: {},
272
+ valid: new Set(),
273
+ invalid: new Set()
274
+ };
275
+
276
+ this._sources.set(schema, log);
277
+ }
278
+
279
+ if (path.length) {
280
+ log.paths.add(path);
281
+ }
282
+
283
+ const each = (sub, source) => {
284
+
285
+ const subId = internals.id(sub, source);
286
+ this._scan(sub, path.concat(subId));
287
+ };
288
+
289
+ schema.$_modify({ each, ref: false });
290
+ }
291
+
292
+ _combine(merged, sources) {
293
+
294
+ this._combos.set(merged, sources);
295
+ }
296
+ };
297
+
298
+
299
+ internals.message = function (item) {
300
+
301
+ const path = item.paths ? Errors.path(item.paths[0]) + (item.rule ? ':' : '') : '';
302
+ return `${path}${item.rule || ''} (${item.status})`;
303
+ };
304
+
305
+
306
+ internals.id = function (schema, { source, name, path, key }) {
307
+
308
+ if (schema._flags.id) {
309
+ return schema._flags.id;
310
+ }
311
+
312
+ if (key) {
313
+ return key;
314
+ }
315
+
316
+ name = `@${name}`;
317
+
318
+ if (source === 'terms') {
319
+ return [name, path[Math.min(path.length - 1, 1)]];
320
+ }
321
+
322
+ return name;
323
+ };
324
+
325
+
326
+ internals.sub = function (paths, skipped) {
327
+
328
+ for (const path of paths) {
329
+ for (const skip of skipped) {
330
+ if (deepEqual(path.slice(0, skip.length), skip)) {
331
+ return true;
332
+ }
333
+ }
334
+ }
335
+
336
+ return false;
337
+ };
338
+
339
+
340
+ internals.debug = function (state, event) {
341
+
342
+ if (state.mainstay.debug) {
343
+ event.path = state.debug ? [...state.path, state.debug] : state.path;
344
+ state.mainstay.debug.push(event);
345
+ }
346
+ };