1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3'use strict';
4
5import {
6  valueIn
7} from '../common/util';
8
9import {
10  ChunkSource
11} from '../chunking';
12
13/**
14 * The different diff operations available
15 */
16export
17type DiffOp = 'add' | 'remove' | 'replace' | 'patch' |
18  'addrange' | 'removerange';
19
20
21/**
22 * Base interface for all diff entries
23 */
24export
25interface IDiffEntryBase {
26  /**
27   * The key of the diff entry: Either the field name in an object, or the
28   * index in a list/string.
29   */
30  key: string | number;
31
32  /**
33   * A string identifying the diff operation type, as defined by DiffOp.
34   */
35  op: DiffOp;
36
37  /**
38   * Optional: Source of diff, for use when merging.
39   *
40   * This should not need to be set manually.
41   */
42  source?: ChunkSource;
43}
44
45
46/**
47 * Diff representing an added sequence of list entries, or an added substring
48 */
49export
50interface IDiffAddRange extends IDiffEntryBase {
51  op: 'addrange';
52  key: number;
53  /**
54   * The sequence of values that were added
55   */
56  valuelist: string | any[];
57}
58
59/**
60 * Diff representing an added object entry
61 */
62export
63interface IDiffAdd extends IDiffEntryBase {
64  op: 'add';
65  key: string;
66  /**
67   * The value that was added
68   */
69  value: any;
70}
71
72
73/**
74 * Diff representing a removed object entry
75 */
76export
77interface IDiffRemove extends IDiffEntryBase {
78  op: 'remove';
79  key: string;
80}
81
82
83/**
84 * Diff representing a replaced object entry
85 */
86export
87interface IDiffReplace extends IDiffEntryBase {
88  op: 'replace';
89  key: string;
90  /**
91   * The new value
92   */
93  value: any;
94}
95
96
97/**
98 * Diff representing a removed sequence of list entries, or a removed substring
99 */
100export
101interface IDiffRemoveRange extends IDiffEntryBase {
102  op: 'removerange';
103  key: number;
104
105  /**
106   * The length of the sequence that was deleted
107   */
108  length: number;
109}
110
111
112/**
113 * Diff representing a patched entry (object entry or list entry)
114 */
115export
116interface IDiffPatch extends IDiffEntryBase {
117  op: 'patch';
118  /**
119   * The collection of sub-diffs describing the patch of the object
120   */
121  diff: IDiffEntry[] | null;
122}
123export
124interface IDiffPatchArray extends IDiffPatch {
125  key: number;
126}
127export
128interface IDiffPatchObject extends IDiffPatch {
129  key: string;
130}
131
132/**
133 * Describes a diff entry of a single JSON value (object, list, string)
134 */
135export
136type IDiffEntry = IDiffAddRange | IDiffRemoveRange | IDiffPatch | IDiffAdd | IDiffRemove | IDiffReplace;
137
138export
139type IDiffArrayEntry = IDiffAddRange | IDiffRemoveRange | IDiffPatchArray;
140
141export
142type IDiffObjectEntry = IDiffPatchObject | IDiffAdd | IDiffRemove | IDiffReplace;
143
144export
145type IDiffImmutableArrayEntry = IDiffAddRange | IDiffRemoveRange;
146
147export
148type IDiffImmutableObjectEntry = IDiffAdd | IDiffRemove | IDiffReplace;
149
150
151export
152type DiffCollection = (IDiffEntry[] | null)[];
153
154
155/** Create a replacement diff entry */
156export
157function opReplace(key: string, value: any): IDiffReplace {
158  return {op: 'replace', key: key, value: value};
159}
160
161/** Create an addition diff entry */
162export
163function opAdd(key: string, value: any): IDiffAdd {
164  return {op: 'add', key: key, value: value};
165}
166
167/** Create a removal diff entry */
168export
169function opRemove(key: string): IDiffRemove {
170  return {op: 'remove', key: key};
171}
172
173/** Create a removal diff entry */
174export
175function opAddRange(key: number, valuelist: string | any[]): IDiffAddRange {
176  return {op: 'addrange', key: key, valuelist: valuelist};
177}
178
179/** Create a range removal diff entry */
180export
181function opRemoveRange(key: number, length: number): IDiffRemoveRange {
182  return {op: 'removerange', key: key, length: length};
183}
184
185/** Create a range removal diff entry */
186export
187function opPatch(key: string | number, diff: IDiffEntry[] | null): IDiffPatch {
188  return {op: 'patch', key: key, diff: diff};
189}
190
191
192/**
193 * Validate that a diff operation is valid to apply on a given base sequence
194 */
195export
196function validateSequenceOp(base: ReadonlyArray<any> | string, entry: IDiffEntry): void {
197  if (typeof entry.key !== 'number') {
198    console.info('Invalid patch details', base, entry);
199    throw new TypeError(`Invalid patch sequence op: Key is not a number: ${entry.key}`);
200  }
201  let index = entry.key;
202  if (entry.op === 'addrange') {
203    if (index < 0 || index > base.length || isNaN(index)) {
204      throw new RangeError('Invalid add range diff op: Key out of range: ' + index);
205    }
206  } else if (entry.op === 'removerange') {
207    if (index < 0 || index >= base.length || isNaN(index)) {
208      throw new RangeError('Invalid remove range diff op: Key out of range: ' + index);
209    }
210    let skip = entry.length;
211    if (index + skip > base.length || isNaN(index)) {
212      throw new RangeError('Invalid remove range diff op: Range too long!');
213    }
214  } else if (entry.op === 'patch') {
215    if (index < 0 || index >= base.length || isNaN(index)) {
216      throw new RangeError('Invalid patch diff op: Key out of range: ' + index);
217    }
218  } else {
219    throw new Error('Invalid op: ' + entry.op);
220  }
221}
222
223/**
224 * Validate that a diff operation is valid to apply on a given base object
225 */
226export
227function validateObjectOp(base: any, entry: IDiffEntry, keys: string[]): void {
228  let op = entry.op;
229  if (typeof entry.key !== 'string') {
230    console.info('Invalid patch details', base, entry, keys);
231    throw new TypeError(`Invalid patch object op: Key is not a string: ${entry.key}`);
232  }
233  let key = entry.key;
234
235  if (op === 'add') {
236    if (valueIn(key, keys)) {
237      throw new Error('Invalid add key diff op: Key already present: ' + key);
238    }
239  } else if (op === 'remove') {
240    if (!valueIn(key, keys)) {
241      throw new Error('Invalid remove key diff op: Missing key: ' + key);
242    }
243  } else if (op === 'replace') {
244    if (!valueIn(key, keys)) {
245      throw new Error('Invalid replace key diff op: Missing key: ' + key);
246    }
247  } else if (op === 'patch') {
248    if (!valueIn(key, keys)) {
249      throw new Error('Invalid patch key diff op: Missing key: ' + key);
250    }
251  } else {
252    throw new Error('Invalid op: ' + op);
253  }
254}
255