Skip to content

Commit 97819c3

Browse files
committed
🚩 Add strict flag
This change adds a `strict` flag to control whether we have the stricter type checking introduced in #40 Strict mode will be off by default (to maintain compatibility with old `json0` versions). In order to add this flag, we also add a new `options` object to the `type`. Strict mode is enabled on the type by setting the flag: ```js type.options.strict = true ``` Note that `text0` will share the same options as `json0` (ie enabling strict mode for `json0` also enables it for `text0`). In this change we also tidy up some unused utility functions from a previous commit.
1 parent b89153c commit 97819c3

5 files changed

Lines changed: 71 additions & 43 deletions

File tree

‎README.md‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Lists and objects have the same set of operations (*Insert*, *Delete*,
9696
*Replace*, *Move*) but their semantics are very different. List operations
9797
shuffle adjacent list items left or right to make space (or to remove space).
9898
Object operations do not. You should pick the data structure which will give
99-
you the behaviour you want when you design your data model.
99+
you the behaviour you want when you design your data model.
100100

101101
To make it clear what the semantics of operations will be, list operations and
102102
object operations are named differently. (`li`, `ld`, `lm` for lists and `oi`,
@@ -201,9 +201,9 @@ There is (unfortunately) no equivalent for list move with objects.
201201
### Subtype operations
202202

203203
Usage:
204-
204+
205205
{p:PATH, t:SUBTYPE, o:OPERATION}
206-
206+
207207
`PATH` is the path to the object that will be modified by the subtype.
208208
`SUBTYPE` is the name of the subtype, e.g. `"text0"`.
209209
`OPERATION` is the subtype operation itself.
@@ -242,15 +242,15 @@ the subtype operation.
242242
Usage:
243243

244244
{p:PATH, t:'text0', o:[{p:OFFSET, i:TEXT}]}
245-
245+
246246
Insert `TEXT` to the string specified by `PATH` at the position specified by `OFFSET`.
247247

248248
##### Delete from a string
249249

250250
Usage:
251251

252252
{p:PATH, t:'text0', o:[{p:OFFSET, d:TEXT}]}
253-
253+
254254
Delete `TEXT` in the string specified by `PATH` at the position specified by `OFFSET`.
255255

256256
---
@@ -292,6 +292,18 @@ Usage:
292292
Delete `TEXT` at the location specified by `PATH`. The path must specify an
293293
offset in a string. `TEXT` must be contained at the location specified.
294294

295+
## Options
296+
297+
### Strict
298+
299+
`json0` supports a "strict" mode which performs additional type checking of the
300+
ops on `apply()`. This mode is off by default to preserve backwards-compatibility,
301+
but can be enabled by setting the flag:
302+
303+
```js
304+
type.options.strict = true
305+
```
306+
295307
---
296308

297309
# Commentary

‎lib/json0.js‎

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,14 @@ var clone = function(o) {
4343
return JSON.parse(JSON.stringify(o));
4444
};
4545

46-
var validateListIndex = function(key) {
47-
if (typeof key !== 'number')
48-
throw new Error('List index must be a number');
49-
};
50-
51-
var validateObjectKey = function (key) {
52-
if (typeof key !== 'string')
53-
throw new Error('Object key must be a number');
54-
};
55-
5646
/**
5747
* JSON OT Type
5848
* @type {*}
5949
*/
6050
var json = {
6151
name: 'json0',
62-
uri: 'http://sharejs.org/types/JSONv0'
52+
uri: 'http://sharejs.org/types/JSONv0',
53+
options: require('./options')
6354
};
6455

6556
// You can register another OT type as a subtype in a JSON document using
@@ -172,10 +163,10 @@ json.apply = function(snapshot, op) {
172163
elem = elem[key];
173164
key = p;
174165

175-
if (isArray(elem) && typeof key !== 'number')
166+
if (json.options.strict && isArray(elem) && typeof key !== 'number')
176167
throw new Error('List index must be a number');
177168

178-
if (isObject(elem) && typeof key !== 'string')
169+
if (json.options.strict && isObject(elem) && typeof key !== 'string')
179170
throw new Error('Object key must be a string');
180171

181172
if (parent == null)
@@ -191,7 +182,7 @@ json.apply = function(snapshot, op) {
191182
if (typeof elem[key] != 'number')
192183
throw new Error('Referenced element not a number');
193184

194-
if (typeof c.na !== 'number')
185+
if (json.options.strict && typeof c.na !== 'number')
195186
throw new Error('Number addition is not a number');
196187

197188
elem[key] += c.na;
@@ -219,7 +210,7 @@ json.apply = function(snapshot, op) {
219210

220211
// List move
221212
else if (c.lm !== void 0) {
222-
if (typeof c.lm !== 'number')
213+
if (json.options.strict && typeof c.lm !== 'number')
223214
throw new Error('List move target index must be a number');
224215

225216
json.checkList(elem);

‎lib/options.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
strict: false
3+
};

‎lib/text0.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
var text = module.exports = {
2525
name: 'text0',
2626
uri: 'http://sharejs.org/types/textv0',
27+
options: require('./options'),
2728
create: function(initial) {
2829
if ((initial != null) && typeof initial !== 'string') {
2930
throw new Error('Initial data must be a string');
@@ -61,7 +62,7 @@ text.apply = function(snapshot, op) {
6162
var deleted;
6263

6364
var type = typeof snapshot;
64-
if (type !== 'string')
65+
if (text.options.strict && type !== 'string')
6566
throw new Error('text0 operations cannot be applied to type: ' + type);
6667

6768
checkValidOp(op);

‎test/json0.coffee‎

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,18 @@ genTests = (type) ->
6464
c_s = type.apply leftHas, right_
6565
assert.deepEqual s_c, c_s
6666

67-
it 'throws when adding a string to a number', ->
68-
assert.throws -> type.apply 1, [{p: [], na: 'a'}]
67+
describe 'strict', ->
68+
beforeEach ->
69+
type.options.strict = true
6970

70-
it 'throws when adding a number to a string', ->
71-
assert.throws -> type.apply 'a', [{p: [], na: 1}]
71+
afterEach ->
72+
type.options.strict = false
73+
74+
it 'throws when adding a string to a number', ->
75+
assert.throws -> type.apply 1, [{p: [], na: 'a'}]
76+
77+
it 'throws when adding a number to a string', ->
78+
assert.throws -> type.apply 'a', [{p: [], na: 1}]
7279

7380
# Strings should be handled internally by the text type. We'll just do some basic sanity checks here.
7481
describe 'string', ->
@@ -138,23 +145,30 @@ genTests = (type) ->
138145
assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'a', 'c'], [{p:[1], lm:0}]
139146
assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'a', 'c'], [{p:[0], lm:1}]
140147

141-
it 'throws when keying a list replacement with a string', ->
142-
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], li: 'x', ld: 'a'}]
148+
describe 'strict', ->
149+
beforeEach ->
150+
type.options.strict = true
151+
152+
afterEach ->
153+
type.options.strict = false
143154

144-
it 'throws when keying a list insertion with a string', ->
145-
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], li: 'x'}]
155+
it 'throws when keying a list replacement with a string', ->
156+
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], li: 'x', ld: 'a'}]
146157

147-
it 'throws when keying a list deletion with a string', ->
148-
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], ld: 'a'}]
158+
it 'throws when keying a list insertion with a string', ->
159+
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], li: 'x'}]
149160

150-
it 'throws when keying a list move with a string', ->
151-
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], lm: 0}]
161+
it 'throws when keying a list deletion with a string', ->
162+
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], ld: 'a'}]
152163

153-
it 'throws when specifying a string as a list move target', ->
154-
assert.throws -> type.apply ['a', 'b', 'c'], [{p: [1], lm: '0'}]
164+
it 'throws when keying a list move with a string', ->
165+
assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], lm: 0}]
155166

156-
it 'throws when an array index part-way through the path is a string', ->
157-
assert.throws -> type.apply {arr:[{x:'a'}]}, [{p:['arr', '0', 'x'], od: 'a'}]
167+
it 'throws when specifying a string as a list move target', ->
168+
assert.throws -> type.apply ['a', 'b', 'c'], [{p: [1], lm: '0'}]
169+
170+
it 'throws when an array index part-way through the path is a string', ->
171+
assert.throws -> type.apply {arr:[{x:'a'}]}, [{p:['arr', '0', 'x'], od: 'a'}]
158172

159173
###
160174
'null moves compose to nops', ->
@@ -418,14 +432,21 @@ genTests = (type) ->
418432
assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'left'
419433
assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'right'
420434

421-
it 'throws when the insertion key is a number', ->
422-
assert.throws -> type.apply {'1':'a'}, [{p:[2], oi: 'a'}]
435+
describe 'strict', ->
436+
beforeEach ->
437+
type.options.strict = true
438+
439+
afterEach ->
440+
type.options.strict = false
441+
442+
it 'throws when the insertion key is a number', ->
443+
assert.throws -> type.apply {'1':'a'}, [{p:[2], oi: 'a'}]
423444

424-
it 'throws when the deletion key is a number', ->
425-
assert.throws -> type.apply {'1':'a'}, [{p:[1], od: 'a'}]
445+
it 'throws when the deletion key is a number', ->
446+
assert.throws -> type.apply {'1':'a'}, [{p:[1], od: 'a'}]
426447

427-
it 'throws when an object key part-way through the path is a number', ->
428-
assert.throws -> type.apply {'1': {x: 'a'}}, [{p:[1, 'x'], od: 'a'}]
448+
it 'throws when an object key part-way through the path is a number', ->
449+
assert.throws -> type.apply {'1': {x: 'a'}}, [{p:[1, 'x'], od: 'a'}]
429450

430451
describe 'randomizer', ->
431452
@timeout 20000

0 commit comments

Comments
 (0)