summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/utils/yaml.js
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 09:45:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 09:45:46 +0000
commita7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch)
tree7452bd5c3545c2fa67a28aa013835fb4fa071baf /app/assets/javascripts/lib/utils/yaml.js
parentee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff)
downloadgitlab-ce-3cc9186904540cc13152bba687400d46ea51d15d.tar.gz
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'app/assets/javascripts/lib/utils/yaml.js')
-rw-r--r--app/assets/javascripts/lib/utils/yaml.js121
1 files changed, 121 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/utils/yaml.js b/app/assets/javascripts/lib/utils/yaml.js
new file mode 100644
index 00000000000..9270d388342
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/yaml.js
@@ -0,0 +1,121 @@
+/**
+ * This file adds a merge function to be used with a yaml Document as defined by
+ * the yaml@2.x package: https://eemeli.org/yaml/#yaml
+ *
+ * Ultimately, this functionality should be merged upstream into the package,
+ * track the progress of that effort at https://github.com/eemeli/yaml/pull/347
+ * */
+
+import { visit, Scalar, isCollection, isDocument, isScalar, isNode, isMap, isSeq } from 'yaml';
+
+function getPath(ancestry) {
+ return ancestry.reduce((p, { key }) => {
+ return key !== undefined ? [...p, key.value] : p;
+ }, []);
+}
+
+function getFirstChildNode(collection) {
+ let firstChildKey;
+ let type;
+ switch (collection.constructor.name) {
+ case 'YAMLSeq': // eslint-disable-line @gitlab/require-i18n-strings
+ return collection.items.find((i) => isNode(i));
+ case 'YAMLMap': // eslint-disable-line @gitlab/require-i18n-strings
+ firstChildKey = collection.items[0]?.key;
+ if (!firstChildKey) return undefined;
+ return isScalar(firstChildKey) ? firstChildKey : new Scalar(firstChildKey);
+ default:
+ type = collection.constructor?.name || typeof collection;
+ throw Error(`Cannot identify a child Node for type ${type}`);
+ }
+}
+
+function moveMetaPropsToFirstChildNode(collection) {
+ const firstChildNode = getFirstChildNode(collection);
+ const { comment, commentBefore, spaceBefore } = collection;
+ if (!(comment || commentBefore || spaceBefore)) return;
+ if (!firstChildNode)
+ throw new Error('Cannot move meta properties to a child of an empty Collection'); // eslint-disable-line @gitlab/require-i18n-strings
+ Object.assign(firstChildNode, { comment, commentBefore, spaceBefore });
+ Object.assign(collection, {
+ comment: undefined,
+ commentBefore: undefined,
+ spaceBefore: undefined,
+ });
+}
+
+function assert(isTypeFn, node, path) {
+ if (![isSeq, isMap].includes(isTypeFn)) {
+ throw new Error('assert() can only be used with isSeq() and isMap()');
+ }
+ const expectedTypeName = isTypeFn === isSeq ? 'YAMLSeq' : 'YAMLMap'; // eslint-disable-line @gitlab/require-i18n-strings
+ if (!isTypeFn(node)) {
+ const type = node?.constructor?.name || typeof node;
+ throw new Error(
+ `Type conflict at "${path.join(
+ '.',
+ )}": Destination node is of type ${type}, the node to be merged is of type ${expectedTypeName}.`,
+ );
+ }
+}
+
+function mergeCollection(target, node, path) {
+ // In case both the source and the target node have comments or spaces
+ // We'll move them to their first child so they do not conflict
+ moveMetaPropsToFirstChildNode(node);
+ if (target.hasIn(path)) {
+ const targetNode = target.getIn(path, true);
+ assert(isSeq(node) ? isSeq : isMap, targetNode, path);
+ moveMetaPropsToFirstChildNode(targetNode);
+ }
+}
+
+function mergePair(target, node, path) {
+ if (!isScalar(node.value)) return undefined;
+ if (target.hasIn([...path, node.key.value])) {
+ target.setIn(path, node);
+ } else {
+ target.addIn(path, node);
+ }
+ return visit.SKIP;
+}
+
+function getVisitorFn(target, options) {
+ return {
+ Map: (_, node, ancestors) => {
+ mergeCollection(target, node, getPath(ancestors));
+ },
+ Pair: (_, node, ancestors) => {
+ mergePair(target, node, getPath(ancestors));
+ },
+ Seq: (_, node, ancestors) => {
+ const path = getPath(ancestors);
+ mergeCollection(target, node, path);
+ if (options.onSequence === 'replace') {
+ target.setIn(path, node);
+ return visit.SKIP;
+ }
+ node.items.forEach((item) => target.addIn(path, item));
+ return visit.SKIP;
+ },
+ };
+}
+
+/** Merge another collection into this */
+export function merge(target, source, options = {}) {
+ const opt = {
+ onSequence: 'replace',
+ ...options,
+ };
+ const sourceNode = target.createNode(isDocument(source) ? source.contents : source);
+ if (!isCollection(sourceNode)) {
+ const type = source?.constructor?.name || typeof source;
+ throw new Error(`Cannot merge type "${type}", expected a Collection`);
+ }
+ if (!isCollection(target.contents)) {
+ // If the target doc is empty add the source to it directly
+ Object.assign(target, { contents: sourceNode });
+ return;
+ }
+ visit(sourceNode, getVisitorFn(target, opt));
+}