ref.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. 'use strict';
  2. const Assert = require('@hapi/hoek/lib/assert');
  3. const Clone = require('@hapi/hoek/lib/clone');
  4. const Reach = require('@hapi/hoek/lib/reach');
  5. const Common = require('./common');
  6. let Template;
  7. const internals = {
  8. symbol: Symbol('ref'), // Used to internally identify references (shared with other joi versions)
  9. defaults: {
  10. adjust: null,
  11. in: false,
  12. iterables: null,
  13. map: null,
  14. separator: '.',
  15. type: 'value'
  16. }
  17. };
  18. exports.create = function (key, options = {}) {
  19. Assert(typeof key === 'string', 'Invalid reference key:', key);
  20. Common.assertOptions(options, ['adjust', 'ancestor', 'in', 'iterables', 'map', 'prefix', 'render', 'separator']);
  21. Assert(!options.prefix || typeof options.prefix === 'object', 'options.prefix must be of type object');
  22. const ref = Object.assign({}, internals.defaults, options);
  23. delete ref.prefix;
  24. const separator = ref.separator;
  25. const context = internals.context(key, separator, options.prefix);
  26. ref.type = context.type;
  27. key = context.key;
  28. if (ref.type === 'value') {
  29. if (context.root) {
  30. Assert(!separator || key[0] !== separator, 'Cannot specify relative path with root prefix');
  31. ref.ancestor = 'root';
  32. if (!key) {
  33. key = null;
  34. }
  35. }
  36. if (separator &&
  37. separator === key) {
  38. key = null;
  39. ref.ancestor = 0;
  40. }
  41. else {
  42. if (ref.ancestor !== undefined) {
  43. Assert(!separator || !key || key[0] !== separator, 'Cannot combine prefix with ancestor option');
  44. }
  45. else {
  46. const [ancestor, slice] = internals.ancestor(key, separator);
  47. if (slice) {
  48. key = key.slice(slice);
  49. if (key === '') {
  50. key = null;
  51. }
  52. }
  53. ref.ancestor = ancestor;
  54. }
  55. }
  56. }
  57. ref.path = separator ? (key === null ? [] : key.split(separator)) : [key];
  58. return new internals.Ref(ref);
  59. };
  60. exports.in = function (key, options = {}) {
  61. return exports.create(key, { ...options, in: true });
  62. };
  63. exports.isRef = function (ref) {
  64. return ref ? !!ref[Common.symbols.ref] : false;
  65. };
  66. internals.Ref = class {
  67. constructor(options) {
  68. Assert(typeof options === 'object', 'Invalid reference construction');
  69. Common.assertOptions(options, [
  70. 'adjust', 'ancestor', 'in', 'iterables', 'map', 'path', 'render', 'separator', 'type', // Copied
  71. 'depth', 'key', 'root', 'display' // Overridden
  72. ]);
  73. Assert([false, undefined].includes(options.separator) || typeof options.separator === 'string' && options.separator.length === 1, 'Invalid separator');
  74. Assert(!options.adjust || typeof options.adjust === 'function', 'options.adjust must be a function');
  75. Assert(!options.map || Array.isArray(options.map), 'options.map must be an array');
  76. Assert(!options.map || !options.adjust, 'Cannot set both map and adjust options');
  77. Object.assign(this, internals.defaults, options);
  78. Assert(this.type === 'value' || this.ancestor === undefined, 'Non-value references cannot reference ancestors');
  79. if (Array.isArray(this.map)) {
  80. this.map = new Map(this.map);
  81. }
  82. this.depth = this.path.length;
  83. this.key = this.path.length ? this.path.join(this.separator) : null;
  84. this.root = this.path[0];
  85. this.updateDisplay();
  86. }
  87. resolve(value, state, prefs, local, options = {}) {
  88. Assert(!this.in || options.in, 'Invalid in() reference usage');
  89. if (this.type === 'global') {
  90. return this._resolve(prefs.context, state, options);
  91. }
  92. if (this.type === 'local') {
  93. return this._resolve(local, state, options);
  94. }
  95. if (!this.ancestor) {
  96. return this._resolve(value, state, options);
  97. }
  98. if (this.ancestor === 'root') {
  99. return this._resolve(state.ancestors[state.ancestors.length - 1], state, options);
  100. }
  101. Assert(this.ancestor <= state.ancestors.length, 'Invalid reference exceeds the schema root:', this.display);
  102. return this._resolve(state.ancestors[this.ancestor - 1], state, options);
  103. }
  104. _resolve(target, state, options) {
  105. let resolved;
  106. if (this.type === 'value' &&
  107. state.mainstay.shadow &&
  108. options.shadow !== false) {
  109. resolved = state.mainstay.shadow.get(this.absolute(state));
  110. }
  111. if (resolved === undefined) {
  112. resolved = Reach(target, this.path, { iterables: this.iterables, functions: true });
  113. }
  114. if (this.adjust) {
  115. resolved = this.adjust(resolved);
  116. }
  117. if (this.map) {
  118. const mapped = this.map.get(resolved);
  119. if (mapped !== undefined) {
  120. resolved = mapped;
  121. }
  122. }
  123. if (state.mainstay) {
  124. state.mainstay.tracer.resolve(state, this, resolved);
  125. }
  126. return resolved;
  127. }
  128. toString() {
  129. return this.display;
  130. }
  131. absolute(state) {
  132. return [...state.path.slice(0, -this.ancestor), ...this.path];
  133. }
  134. clone() {
  135. return new internals.Ref(this);
  136. }
  137. describe() {
  138. const ref = { path: this.path };
  139. if (this.type !== 'value') {
  140. ref.type = this.type;
  141. }
  142. if (this.separator !== '.') {
  143. ref.separator = this.separator;
  144. }
  145. if (this.type === 'value' &&
  146. this.ancestor !== 1) {
  147. ref.ancestor = this.ancestor;
  148. }
  149. if (this.map) {
  150. ref.map = [...this.map];
  151. }
  152. for (const key of ['adjust', 'iterables', 'render']) {
  153. if (this[key] !== null &&
  154. this[key] !== undefined) {
  155. ref[key] = this[key];
  156. }
  157. }
  158. if (this.in !== false) {
  159. ref.in = true;
  160. }
  161. return { ref };
  162. }
  163. updateDisplay() {
  164. const key = this.key !== null ? this.key : '';
  165. if (this.type !== 'value') {
  166. this.display = `ref:${this.type}:${key}`;
  167. return;
  168. }
  169. if (!this.separator) {
  170. this.display = `ref:${key}`;
  171. return;
  172. }
  173. if (!this.ancestor) {
  174. this.display = `ref:${this.separator}${key}`;
  175. return;
  176. }
  177. if (this.ancestor === 'root') {
  178. this.display = `ref:root:${key}`;
  179. return;
  180. }
  181. if (this.ancestor === 1) {
  182. this.display = `ref:${key || '..'}`;
  183. return;
  184. }
  185. const lead = new Array(this.ancestor + 1).fill(this.separator).join('');
  186. this.display = `ref:${lead}${key || ''}`;
  187. }
  188. };
  189. internals.Ref.prototype[Common.symbols.ref] = true;
  190. exports.build = function (desc) {
  191. desc = Object.assign({}, internals.defaults, desc);
  192. if (desc.type === 'value' &&
  193. desc.ancestor === undefined) {
  194. desc.ancestor = 1;
  195. }
  196. return new internals.Ref(desc);
  197. };
  198. internals.context = function (key, separator, prefix = {}) {
  199. key = key.trim();
  200. if (prefix) {
  201. const globalp = prefix.global === undefined ? '$' : prefix.global;
  202. if (globalp !== separator &&
  203. key.startsWith(globalp)) {
  204. return { key: key.slice(globalp.length), type: 'global' };
  205. }
  206. const local = prefix.local === undefined ? '#' : prefix.local;
  207. if (local !== separator &&
  208. key.startsWith(local)) {
  209. return { key: key.slice(local.length), type: 'local' };
  210. }
  211. const root = prefix.root === undefined ? '/' : prefix.root;
  212. if (root !== separator &&
  213. key.startsWith(root)) {
  214. return { key: key.slice(root.length), type: 'value', root: true };
  215. }
  216. }
  217. return { key, type: 'value' };
  218. };
  219. internals.ancestor = function (key, separator) {
  220. if (!separator) {
  221. return [1, 0]; // 'a_b' -> 1 (parent)
  222. }
  223. if (key[0] !== separator) { // 'a.b' -> 1 (parent)
  224. return [1, 0];
  225. }
  226. if (key[1] !== separator) { // '.a.b' -> 0 (self)
  227. return [0, 1];
  228. }
  229. let i = 2;
  230. while (key[i] === separator) {
  231. ++i;
  232. }
  233. return [i - 1, i]; // '...a.b.' -> 2 (grandparent)
  234. };
  235. exports.toSibling = 0;
  236. exports.toParent = 1;
  237. exports.Manager = class {
  238. constructor() {
  239. this.refs = []; // 0: [self refs], 1: [parent refs], 2: [grandparent refs], ...
  240. }
  241. register(source, target) {
  242. if (!source) {
  243. return;
  244. }
  245. target = target === undefined ? exports.toParent : target;
  246. // Array
  247. if (Array.isArray(source)) {
  248. for (const ref of source) {
  249. this.register(ref, target);
  250. }
  251. return;
  252. }
  253. // Schema
  254. if (Common.isSchema(source)) {
  255. for (const item of source._refs.refs) {
  256. if (item.ancestor - target >= 0) {
  257. this.refs.push({ ancestor: item.ancestor - target, root: item.root });
  258. }
  259. }
  260. return;
  261. }
  262. // Reference
  263. if (exports.isRef(source) &&
  264. source.type === 'value' &&
  265. source.ancestor - target >= 0) {
  266. this.refs.push({ ancestor: source.ancestor - target, root: source.root });
  267. }
  268. // Template
  269. Template = Template || require('./template');
  270. if (Template.isTemplate(source)) {
  271. this.register(source.refs(), target);
  272. }
  273. }
  274. get length() {
  275. return this.refs.length;
  276. }
  277. clone() {
  278. const copy = new exports.Manager();
  279. copy.refs = Clone(this.refs);
  280. return copy;
  281. }
  282. reset() {
  283. this.refs = [];
  284. }
  285. roots() {
  286. return this.refs.filter((ref) => !ref.ancestor).map((ref) => ref.root);
  287. }
  288. };