Skip to content

Commit 858ae48

Browse files
committed
feat(json-crdt): 🎸 add str_ins JSON Patch+ operation support
1 parent 8718818 commit 858ae48

File tree

4 files changed

+100
-3
lines changed

4 files changed

+100
-3
lines changed

src/json-crdt/json-patch/JsonPatch.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {Draft} from '../../json-crdt-patch/Draft';
21
import {JsonPatchDraft} from './JsonPatchDraft';
32
import type {Model} from '../model/Model';
43
import type {Operation} from '../../json-patch';

src/json-crdt/json-patch/JsonPatchDraft.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {ObjectType} from '../types/lww-object/ObjectType';
66
import {UNDEFINED_ID} from '../../json-crdt-patch/constants';
77
import {toPath} from '../../json-pointer/util';
88
import type {Model} from '../model';
9-
import type {Operation, OperationAdd, OperationRemove, OperationReplace, OperationMove, OperationCopy, OperationTest} from '../../json-patch';
9+
import type {Operation, OperationAdd, OperationRemove, OperationReplace, OperationMove, OperationCopy, OperationTest, OperationStrIns} from '../../json-patch';
1010

1111
export class JsonPatchDraft {
1212
public readonly draft = new Draft();
@@ -25,6 +25,7 @@ export class JsonPatchDraft {
2525
case 'move': this.applyMove(op); break;
2626
case 'copy': this.applyCopy(op); break;
2727
case 'test': this.applyTest(op); break;
28+
case 'str_ins': this.applyStrIns(op); break;
2829
}
2930
}
3031

@@ -107,6 +108,15 @@ export class JsonPatchDraft {
107108
if (!deepEqual(json, op.value)) throw new Error('TEST');
108109
}
109110

111+
public applyStrIns(op: OperationStrIns): void {
112+
const path = toPath(op.path);
113+
const {node} = this.model.api.str(path);
114+
const {builder} = this.draft;
115+
const length = node.length();
116+
const after = op.pos ? node.findId(length < op.pos ? length - 1 : op.pos - 1) : node.id;
117+
builder.insStr(node.id, after, op.str);
118+
}
119+
110120
private get(steps: Path): unknown {
111121
if (!steps.length) return this.model.toView();
112122
else {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {Operation} from '../../../json-patch';
2+
import {Model} from '../../model/Model';
3+
import {JsonPatch} from '../JsonPatch';
4+
5+
interface TestCase {
6+
name: string;
7+
doc1?: unknown;
8+
patches: Operation[][];
9+
doc2?: unknown;
10+
throws?: string;
11+
only?: true;
12+
}
13+
14+
const testCases: TestCase[] = [
15+
{
16+
name: 'can insert char in empty string',
17+
doc1: '',
18+
patches: [[{op: 'str_ins', path: '', pos: 0, str: 'a'}]],
19+
doc2: 'a',
20+
},
21+
{
22+
name: 'can insert char at the end of string',
23+
doc1: '1',
24+
patches: [[{op: 'str_ins', path: '', pos: 1, str: '2'}]],
25+
doc2: '12',
26+
},
27+
{
28+
name: 'can insert char beyond end of string',
29+
doc1: '1',
30+
patches: [[{op: 'str_ins', path: '', pos: 111, str: '2'}]],
31+
doc2: '12',
32+
},
33+
{
34+
name: 'can insert char beyond end of string - 2',
35+
doc1: '1',
36+
patches: [[{op: 'str_ins', path: '', pos: 2, str: '2'}]],
37+
doc2: '12',
38+
},
39+
{
40+
name: 'can insert char at the beginning of string',
41+
doc1: '1',
42+
patches: [[{op: 'str_ins', path: '', pos: 0, str: '0'}]],
43+
doc2: '01',
44+
},
45+
{
46+
name: 'can insert char in the middle of string',
47+
doc1: '25',
48+
patches: [[{op: 'str_ins', path: '', pos: 1, str: '.'}]],
49+
doc2: '2.5',
50+
},
51+
{
52+
name: 'can insert text in nested object',
53+
doc1: null,
54+
patches: [
55+
[{op: 'add', path: '', value: {foo: [{bar: 'baz'}]}}],
56+
[{op: 'str_ins', path: '/foo/0/bar', pos: 3, str: '!'}]
57+
],
58+
doc2: {foo: [{bar: 'baz!'}]},
59+
},
60+
{
61+
name: 'can insert text in nested object - 2',
62+
doc1: null,
63+
patches: [
64+
[{op: 'add', path: '', value: {foo: [{bar: 'baz'}]}}],
65+
[{op: 'str_ins', path: ['foo', 0, 'bar'], pos: 3, str: '!'}]
66+
],
67+
doc2: {foo: [{bar: 'baz!'}]},
68+
},
69+
];
70+
71+
for (const {only, name, doc1, doc2, patches, throws} of testCases) {
72+
(only ? test.only : test)(name, () => {
73+
const model = Model.withLogicalClock();
74+
const jsonPatch = new JsonPatch(model);
75+
if (doc1 !== undefined) model.api.root(doc1).commit();
76+
if (throws) {
77+
expect(() => {
78+
for (const patch of patches) jsonPatch.apply(patch).commit();
79+
}).toThrow(new Error(throws));
80+
} else {
81+
for (const patch of patches) jsonPatch.apply(patch).commit();
82+
expect(model.toView()).toEqual(doc2);
83+
}
84+
});
85+
}

src/json-crdt/types/rga-string/StringType.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ export class StringType implements JsonNode {
184184
return size;
185185
}
186186

187-
/** String length. */
187+
/**
188+
* String length.
189+
* @todo This could be cached same as .toJson().
190+
*/
188191
public length(): number {
189192
let curr: StringChunk | null = this.start;
190193
let size: number = 0;

0 commit comments

Comments
 (0)