UNPKG

10.3 kBJavaScriptView Raw
1'use strict';
2
3const Assert = require('@hapi/hoek/lib/assert');
4const Clone = require('@hapi/hoek/lib/clone');
5const Reach = require('@hapi/hoek/lib/reach');
6
7const Common = require('./common');
8
9let Template;
10
11
12const internals = {
13 symbol: Symbol('ref'), // Used to internally identify references (shared with other joi versions)
14 defaults: {
15 adjust: null,
16 in: false,
17 iterables: null,
18 map: null,
19 separator: '.',
20 type: 'value'
21 }
22};
23
24
25exports.create = function (key, options = {}) {
26
27 Assert(typeof key === 'string', 'Invalid reference key:', key);
28 Common.assertOptions(options, ['adjust', 'ancestor', 'in', 'iterables', 'map', 'prefix', 'render', 'separator']);
29 Assert(!options.prefix || typeof options.prefix === 'object', 'options.prefix must be of type object');
30
31 const ref = Object.assign({}, internals.defaults, options);
32 delete ref.prefix;
33
34 const separator = ref.separator;
35 const context = internals.context(key, separator, options.prefix);
36 ref.type = context.type;
37 key = context.key;
38
39 if (ref.type === 'value') {
40 if (context.root) {
41 Assert(!separator || key[0] !== separator, 'Cannot specify relative path with root prefix');
42 ref.ancestor = 'root';
43 if (!key) {
44 key = null;
45 }
46 }
47
48 if (separator &&
49 separator === key) {
50
51 key = null;
52 ref.ancestor = 0;
53 }
54 else {
55 if (ref.ancestor !== undefined) {
56 Assert(!separator || !key || key[0] !== separator, 'Cannot combine prefix with ancestor option');
57 }
58 else {
59 const [ancestor, slice] = internals.ancestor(key, separator);
60 if (slice) {
61 key = key.slice(slice);
62 if (key === '') {
63 key = null;
64 }
65 }
66
67 ref.ancestor = ancestor;
68 }
69 }
70 }
71
72 ref.path = separator ? (key === null ? [] : key.split(separator)) : [key];
73
74 return new internals.Ref(ref);
75};
76
77
78exports.in = function (key, options = {}) {
79
80 return exports.create(key, { ...options, in: true });
81};
82
83
84exports.isRef = function (ref) {
85
86 return ref ? !!ref[Common.symbols.ref] : false;
87};
88
89
90internals.Ref = class {
91
92 constructor(options) {
93
94 Assert(typeof options === 'object', 'Invalid reference construction');
95 Common.assertOptions(options, [
96 'adjust', 'ancestor', 'in', 'iterables', 'map', 'path', 'render', 'separator', 'type', // Copied
97 'depth', 'key', 'root', 'display' // Overridden
98 ]);
99
100 Assert([false, undefined].includes(options.separator) || typeof options.separator === 'string' && options.separator.length === 1, 'Invalid separator');
101 Assert(!options.adjust || typeof options.adjust === 'function', 'options.adjust must be a function');
102 Assert(!options.map || Array.isArray(options.map), 'options.map must be an array');
103 Assert(!options.map || !options.adjust, 'Cannot set both map and adjust options');
104
105 Object.assign(this, internals.defaults, options);
106
107 Assert(this.type === 'value' || this.ancestor === undefined, 'Non-value references cannot reference ancestors');
108
109 if (Array.isArray(this.map)) {
110 this.map = new Map(this.map);
111 }
112
113 this.depth = this.path.length;
114 this.key = this.path.length ? this.path.join(this.separator) : null;
115 this.root = this.path[0];
116
117 this.updateDisplay();
118 }
119
120 resolve(value, state, prefs, local, options = {}) {
121
122 Assert(!this.in || options.in, 'Invalid in() reference usage');
123
124 if (this.type === 'global') {
125 return this._resolve(prefs.context, state, options);
126 }
127
128 if (this.type === 'local') {
129 return this._resolve(local, state, options);
130 }
131
132 if (!this.ancestor) {
133 return this._resolve(value, state, options);
134 }
135
136 if (this.ancestor === 'root') {
137 return this._resolve(state.ancestors[state.ancestors.length - 1], state, options);
138 }
139
140 Assert(this.ancestor <= state.ancestors.length, 'Invalid reference exceeds the schema root:', this.display);
141 return this._resolve(state.ancestors[this.ancestor - 1], state, options);
142 }
143
144 _resolve(target, state, options) {
145
146 let resolved;
147
148 if (this.type === 'value' &&
149 state.mainstay.shadow &&
150 options.shadow !== false) {
151
152 resolved = state.mainstay.shadow.get(this.absolute(state));
153 }
154
155 if (resolved === undefined) {
156 resolved = Reach(target, this.path, { iterables: this.iterables, functions: true });
157 }
158
159 if (this.adjust) {
160 resolved = this.adjust(resolved);
161 }
162
163 if (this.map) {
164 const mapped = this.map.get(resolved);
165 if (mapped !== undefined) {
166 resolved = mapped;
167 }
168 }
169
170 if (state.mainstay) {
171 state.mainstay.tracer.resolve(state, this, resolved);
172 }
173
174 return resolved;
175 }
176
177 toString() {
178
179 return this.display;
180 }
181
182 absolute(state) {
183
184 return [...state.path.slice(0, -this.ancestor), ...this.path];
185 }
186
187 clone() {
188
189 return new internals.Ref(this);
190 }
191
192 describe() {
193
194 const ref = { path: this.path };
195
196 if (this.type !== 'value') {
197 ref.type = this.type;
198 }
199
200 if (this.separator !== '.') {
201 ref.separator = this.separator;
202 }
203
204 if (this.type === 'value' &&
205 this.ancestor !== 1) {
206
207 ref.ancestor = this.ancestor;
208 }
209
210 if (this.map) {
211 ref.map = [...this.map];
212 }
213
214 for (const key of ['adjust', 'iterables', 'render']) {
215 if (this[key] !== null &&
216 this[key] !== undefined) {
217
218 ref[key] = this[key];
219 }
220 }
221
222 if (this.in !== false) {
223 ref.in = true;
224 }
225
226 return { ref };
227 }
228
229 updateDisplay() {
230
231 const key = this.key !== null ? this.key : '';
232 if (this.type !== 'value') {
233 this.display = `ref:${this.type}:${key}`;
234 return;
235 }
236
237 if (!this.separator) {
238 this.display = `ref:${key}`;
239 return;
240 }
241
242 if (!this.ancestor) {
243 this.display = `ref:${this.separator}${key}`;
244 return;
245 }
246
247 if (this.ancestor === 'root') {
248 this.display = `ref:root:${key}`;
249 return;
250 }
251
252 if (this.ancestor === 1) {
253 this.display = `ref:${key || '..'}`;
254 return;
255 }
256
257 const lead = new Array(this.ancestor + 1).fill(this.separator).join('');
258 this.display = `ref:${lead}${key || ''}`;
259 }
260};
261
262
263internals.Ref.prototype[Common.symbols.ref] = true;
264
265
266exports.build = function (desc) {
267
268 desc = Object.assign({}, internals.defaults, desc);
269 if (desc.type === 'value' &&
270 desc.ancestor === undefined) {
271
272 desc.ancestor = 1;
273 }
274
275 return new internals.Ref(desc);
276};
277
278
279internals.context = function (key, separator, prefix = {}) {
280
281 key = key.trim();
282
283 if (prefix) {
284 const globalp = prefix.global === undefined ? '$' : prefix.global;
285 if (globalp !== separator &&
286 key.startsWith(globalp)) {
287
288 return { key: key.slice(globalp.length), type: 'global' };
289 }
290
291 const local = prefix.local === undefined ? '#' : prefix.local;
292 if (local !== separator &&
293 key.startsWith(local)) {
294
295 return { key: key.slice(local.length), type: 'local' };
296 }
297
298 const root = prefix.root === undefined ? '/' : prefix.root;
299 if (root !== separator &&
300 key.startsWith(root)) {
301
302 return { key: key.slice(root.length), type: 'value', root: true };
303 }
304 }
305
306 return { key, type: 'value' };
307};
308
309
310internals.ancestor = function (key, separator) {
311
312 if (!separator) {
313 return [1, 0]; // 'a_b' -> 1 (parent)
314 }
315
316 if (key[0] !== separator) { // 'a.b' -> 1 (parent)
317 return [1, 0];
318 }
319
320 if (key[1] !== separator) { // '.a.b' -> 0 (self)
321 return [0, 1];
322 }
323
324 let i = 2;
325 while (key[i] === separator) {
326 ++i;
327 }
328
329 return [i - 1, i]; // '...a.b.' -> 2 (grandparent)
330};
331
332
333exports.toSibling = 0;
334
335exports.toParent = 1;
336
337
338exports.Manager = class {
339
340 constructor() {
341
342 this.refs = []; // 0: [self refs], 1: [parent refs], 2: [grandparent refs], ...
343 }
344
345 register(source, target) {
346
347 if (!source) {
348 return;
349 }
350
351 target = target === undefined ? exports.toParent : target;
352
353 // Array
354
355 if (Array.isArray(source)) {
356 for (const ref of source) {
357 this.register(ref, target);
358 }
359
360 return;
361 }
362
363 // Schema
364
365 if (Common.isSchema(source)) {
366 for (const item of source._refs.refs) {
367 if (item.ancestor - target >= 0) {
368 this.refs.push({ ancestor: item.ancestor - target, root: item.root });
369 }
370 }
371
372 return;
373 }
374
375 // Reference
376
377 if (exports.isRef(source) &&
378 source.type === 'value' &&
379 source.ancestor - target >= 0) {
380
381 this.refs.push({ ancestor: source.ancestor - target, root: source.root });
382 }
383
384 // Template
385
386 Template = Template || require('./template');
387
388 if (Template.isTemplate(source)) {
389 this.register(source.refs(), target);
390 }
391 }
392
393 get length() {
394
395 return this.refs.length;
396 }
397
398 clone() {
399
400 const copy = new exports.Manager();
401 copy.refs = Clone(this.refs);
402 return copy;
403 }
404
405 reset() {
406
407 this.refs = [];
408 }
409
410 roots() {
411
412 return this.refs.filter((ref) => !ref.ancestor).map((ref) => ref.root);
413 }
414};