values.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. 'use strict';
  2. const Assert = require('@hapi/hoek/lib/assert');
  3. const DeepEqual = require('@hapi/hoek/lib/deepEqual');
  4. const Common = require('./common');
  5. const internals = {};
  6. module.exports = internals.Values = class {
  7. constructor(values, refs) {
  8. this._values = new Set(values);
  9. this._refs = new Set(refs);
  10. this._lowercase = internals.lowercases(values);
  11. this._override = false;
  12. }
  13. get length() {
  14. return this._values.size + this._refs.size;
  15. }
  16. add(value, refs) {
  17. // Reference
  18. if (Common.isResolvable(value)) {
  19. if (!this._refs.has(value)) {
  20. this._refs.add(value);
  21. if (refs) { // Skipped in a merge
  22. refs.register(value);
  23. }
  24. }
  25. return;
  26. }
  27. // Value
  28. if (!this.has(value, null, null, false)) {
  29. this._values.add(value);
  30. if (typeof value === 'string') {
  31. this._lowercase.set(value.toLowerCase(), value);
  32. }
  33. }
  34. }
  35. static merge(target, source, remove) {
  36. target = target || new internals.Values();
  37. if (source) {
  38. if (source._override) {
  39. return source.clone();
  40. }
  41. for (const item of [...source._values, ...source._refs]) {
  42. target.add(item);
  43. }
  44. }
  45. if (remove) {
  46. for (const item of [...remove._values, ...remove._refs]) {
  47. target.remove(item);
  48. }
  49. }
  50. return target.length ? target : null;
  51. }
  52. remove(value) {
  53. // Reference
  54. if (Common.isResolvable(value)) {
  55. this._refs.delete(value);
  56. return;
  57. }
  58. // Value
  59. this._values.delete(value);
  60. if (typeof value === 'string') {
  61. this._lowercase.delete(value.toLowerCase());
  62. }
  63. }
  64. has(value, state, prefs, insensitive) {
  65. return !!this.get(value, state, prefs, insensitive);
  66. }
  67. get(value, state, prefs, insensitive) {
  68. if (!this.length) {
  69. return false;
  70. }
  71. // Simple match
  72. if (this._values.has(value)) {
  73. return { value };
  74. }
  75. // Case insensitive string match
  76. if (typeof value === 'string' &&
  77. value &&
  78. insensitive) {
  79. const found = this._lowercase.get(value.toLowerCase());
  80. if (found) {
  81. return { value: found };
  82. }
  83. }
  84. if (!this._refs.size &&
  85. typeof value !== 'object') {
  86. return false;
  87. }
  88. // Objects
  89. if (typeof value === 'object') {
  90. for (const item of this._values) {
  91. if (DeepEqual(item, value)) {
  92. return { value: item };
  93. }
  94. }
  95. }
  96. // References
  97. if (state) {
  98. for (const ref of this._refs) {
  99. const resolved = ref.resolve(value, state, prefs, null, { in: true });
  100. if (resolved === undefined) {
  101. continue;
  102. }
  103. const items = !ref.in || typeof resolved !== 'object'
  104. ? [resolved]
  105. : Array.isArray(resolved) ? resolved : Object.keys(resolved);
  106. for (const item of items) {
  107. if (typeof item !== typeof value) {
  108. continue;
  109. }
  110. if (insensitive &&
  111. value &&
  112. typeof value === 'string') {
  113. if (item.toLowerCase() === value.toLowerCase()) {
  114. return { value: item, ref };
  115. }
  116. }
  117. else {
  118. if (DeepEqual(item, value)) {
  119. return { value: item, ref };
  120. }
  121. }
  122. }
  123. }
  124. }
  125. return false;
  126. }
  127. override() {
  128. this._override = true;
  129. }
  130. values(options) {
  131. if (options &&
  132. options.display) {
  133. const values = [];
  134. for (const item of [...this._values, ...this._refs]) {
  135. if (item !== undefined) {
  136. values.push(item);
  137. }
  138. }
  139. return values;
  140. }
  141. return Array.from([...this._values, ...this._refs]);
  142. }
  143. clone() {
  144. const set = new internals.Values(this._values, this._refs);
  145. set._override = this._override;
  146. return set;
  147. }
  148. concat(source) {
  149. Assert(!source._override, 'Cannot concat override set of values');
  150. const set = new internals.Values([...this._values, ...source._values], [...this._refs, ...source._refs]);
  151. set._override = this._override;
  152. return set;
  153. }
  154. describe() {
  155. const normalized = [];
  156. if (this._override) {
  157. normalized.push({ override: true });
  158. }
  159. for (const value of this._values.values()) {
  160. normalized.push(value && typeof value === 'object' ? { value } : value);
  161. }
  162. for (const value of this._refs.values()) {
  163. normalized.push(value.describe());
  164. }
  165. return normalized;
  166. }
  167. };
  168. internals.Values.prototype[Common.symbols.values] = true;
  169. // Aliases
  170. internals.Values.prototype.slice = internals.Values.prototype.clone;
  171. // Helpers
  172. internals.lowercases = function (from) {
  173. const map = new Map();
  174. if (from) {
  175. for (const value of from) {
  176. if (typeof value === 'string') {
  177. map.set(value.toLowerCase(), value);
  178. }
  179. }
  180. }
  181. return map;
  182. };