Collection.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. 'use strict';
  2. var createNode = require('../doc/createNode.js');
  3. var identity = require('./identity.js');
  4. var Node = require('./Node.js');
  5. function collectionFromPath(schema, path, value) {
  6. let v = value;
  7. for (let i = path.length - 1; i >= 0; --i) {
  8. const k = path[i];
  9. if (typeof k === 'number' && Number.isInteger(k) && k >= 0) {
  10. const a = [];
  11. a[k] = v;
  12. v = a;
  13. }
  14. else {
  15. v = new Map([[k, v]]);
  16. }
  17. }
  18. return createNode.createNode(v, undefined, {
  19. aliasDuplicateObjects: false,
  20. keepUndefined: false,
  21. onAnchor: () => {
  22. throw new Error('This should not happen, please report a bug.');
  23. },
  24. schema,
  25. sourceObjects: new Map()
  26. });
  27. }
  28. // Type guard is intentionally a little wrong so as to be more useful,
  29. // as it does not cover untypable empty non-string iterables (e.g. []).
  30. const isEmptyPath = (path) => path == null ||
  31. (typeof path === 'object' && !!path[Symbol.iterator]().next().done);
  32. class Collection extends Node.NodeBase {
  33. constructor(type, schema) {
  34. super(type);
  35. Object.defineProperty(this, 'schema', {
  36. value: schema,
  37. configurable: true,
  38. enumerable: false,
  39. writable: true
  40. });
  41. }
  42. /**
  43. * Create a copy of this collection.
  44. *
  45. * @param schema - If defined, overwrites the original's schema
  46. */
  47. clone(schema) {
  48. const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this));
  49. if (schema)
  50. copy.schema = schema;
  51. copy.items = copy.items.map(it => identity.isNode(it) || identity.isPair(it) ? it.clone(schema) : it);
  52. if (this.range)
  53. copy.range = this.range.slice();
  54. return copy;
  55. }
  56. /**
  57. * Adds a value to the collection. For `!!map` and `!!omap` the value must
  58. * be a Pair instance or a `{ key, value }` object, which may not have a key
  59. * that already exists in the map.
  60. */
  61. addIn(path, value) {
  62. if (isEmptyPath(path))
  63. this.add(value);
  64. else {
  65. const [key, ...rest] = path;
  66. const node = this.get(key, true);
  67. if (identity.isCollection(node))
  68. node.addIn(rest, value);
  69. else if (node === undefined && this.schema)
  70. this.set(key, collectionFromPath(this.schema, rest, value));
  71. else
  72. throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
  73. }
  74. }
  75. /**
  76. * Removes a value from the collection.
  77. * @returns `true` if the item was found and removed.
  78. */
  79. deleteIn(path) {
  80. const [key, ...rest] = path;
  81. if (rest.length === 0)
  82. return this.delete(key);
  83. const node = this.get(key, true);
  84. if (identity.isCollection(node))
  85. return node.deleteIn(rest);
  86. else
  87. throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
  88. }
  89. /**
  90. * Returns item at `key`, or `undefined` if not found. By default unwraps
  91. * scalar values from their surrounding node; to disable set `keepScalar` to
  92. * `true` (collections are always returned intact).
  93. */
  94. getIn(path, keepScalar) {
  95. const [key, ...rest] = path;
  96. const node = this.get(key, true);
  97. if (rest.length === 0)
  98. return !keepScalar && identity.isScalar(node) ? node.value : node;
  99. else
  100. return identity.isCollection(node) ? node.getIn(rest, keepScalar) : undefined;
  101. }
  102. hasAllNullValues(allowScalar) {
  103. return this.items.every(node => {
  104. if (!identity.isPair(node))
  105. return false;
  106. const n = node.value;
  107. return (n == null ||
  108. (allowScalar &&
  109. identity.isScalar(n) &&
  110. n.value == null &&
  111. !n.commentBefore &&
  112. !n.comment &&
  113. !n.tag));
  114. });
  115. }
  116. /**
  117. * Checks if the collection includes a value with the key `key`.
  118. */
  119. hasIn(path) {
  120. const [key, ...rest] = path;
  121. if (rest.length === 0)
  122. return this.has(key);
  123. const node = this.get(key, true);
  124. return identity.isCollection(node) ? node.hasIn(rest) : false;
  125. }
  126. /**
  127. * Sets a value in this collection. For `!!set`, `value` needs to be a
  128. * boolean to add/remove the item from the set.
  129. */
  130. setIn(path, value) {
  131. const [key, ...rest] = path;
  132. if (rest.length === 0) {
  133. this.set(key, value);
  134. }
  135. else {
  136. const node = this.get(key, true);
  137. if (identity.isCollection(node))
  138. node.setIn(rest, value);
  139. else if (node === undefined && this.schema)
  140. this.set(key, collectionFromPath(this.schema, rest, value));
  141. else
  142. throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
  143. }
  144. }
  145. }
  146. exports.Collection = Collection;
  147. exports.collectionFromPath = collectionFromPath;
  148. exports.isEmptyPath = isEmptyPath;