Skip to content

Commit

Permalink
Fixed tests, added package.json and generally made library work
Browse files Browse the repository at this point in the history
  • Loading branch information
josephg committed Jul 17, 2014
1 parent 1799f58 commit f3bd737
Show file tree
Hide file tree
Showing 10 changed files with 731 additions and 200 deletions.
78 changes: 78 additions & 0 deletions lib/bootstrapTransform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// These methods let you build a transform function from a transformComponent
// function for OT types like JSON0 in which operations are lists of components
// and transforming them requires N^2 work. I find it kind of nasty that I need
// this, but I'm not really sure what a better solution is. Maybe I should do
// this automatically to types that don't have a compose function defined.

// Add transform and transformX functions for an OT type which has
// transformComponent defined. transformComponent(destination array,
// component, other component, side)
module.exports = bootstrapTransform
function bootstrapTransform(type, transformComponent, checkValidOp, append) {
var transformComponentX = function(left, right, destLeft, destRight) {
transformComponent(destLeft, left, right, 'left');
transformComponent(destRight, right, left, 'right');
};

var transformX = type.transformX = function(leftOp, rightOp) {
checkValidOp(leftOp);
checkValidOp(rightOp);
var newRightOp = [];

for (var i = 0; i < rightOp.length; i++) {
var rightComponent = rightOp[i];

// Generate newLeftOp by composing leftOp by rightComponent
var newLeftOp = [];
var k = 0;
while (k < leftOp.length) {
var nextC = [];
transformComponentX(leftOp[k], rightComponent, newLeftOp, nextC);
k++;

if (nextC.length === 1) {
rightComponent = nextC[0];
} else if (nextC.length === 0) {
for (var j = k; j < leftOp.length; j++) {
append(newLeftOp, leftOp[j]);
}
rightComponent = null;
break;
} else {
// Recurse.
var pair = transformX(leftOp.slice(k), nextC);
for (var l = 0; l < pair[0].length; l++) {
append(newLeftOp, pair[0][l]);
}
for (var r = 0; r < pair[1].length; r++) {
append(newRightOp, pair[1][r]);
}
rightComponent = null;
break;
}
}

if (rightComponent != null) {
append(newRightOp, rightComponent);
}
leftOp = newLeftOp;
}
return [leftOp, newRightOp];
};

// Transforms op with specified type ('left' or 'right') by otherOp.
type.transform = function(op, otherOp, type) {
if (!(type === 'left' || type === 'right'))
throw new Error("type must be 'left' or 'right'");

if (otherOp.length === 0) return op;

if (op.length === 1 && otherOp.length === 1)
return transformComponent([], op[0], otherOp[0], type);

if (type === 'left')
return transformX(op, otherOp)[0];
else
return transformX(otherOp, op)[1];
};
};
7 changes: 7 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Only the JSON type is exported, because the text type is deprecated
// otherwise. (If you want to use it somewhere, you're welcome to pull it out
// into a separate module that json0 can depend on).

module.exports = {
type: require('./json0')
};
10 changes: 3 additions & 7 deletions lib/json0.js
Original file line number Diff line number Diff line change
Expand Up @@ -642,17 +642,13 @@ json.transformComponent = function(dest, c, otherC, type) {
return dest;
};

if (exports._bootstrapTransform) {
exports._bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append);
} else {
require('./helpers')._bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append);
}
require('./bootstrapTransform')(json, json.transformComponent, json.checkValidOp, json.append);

/**
* Register a subtype for string operations, using the text0 type.
*/
if (typeof text === 'undefined')
var text = typeof require !== "undefined" ? require('./text0') : window.ottypes.text;
var text = require('./text0');

json.registerSubtype(text);
module.exports = json;

17 changes: 5 additions & 12 deletions lib/text0.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// DEPRECATED!
//
// This type works, but is not exported, and will be removed in a future version of this library.
// This type works, but is not exported. Its included here because the JSON0
// embedded string operations use this library.


// A simple text implementation
//
// Operations are lists of components.
// Each component either inserts or deletes at a specified position in the document.
// Operations are lists of components. Each component either inserts or deletes
// at a specified position in the document.
//
// Components are either:
// {i:'str', p:100}: Insert 'str' at position 100 in the document
Expand All @@ -20,9 +21,6 @@
// is equivalent to this op:
// [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}]

// NOTE: The global scope here is shared with other sharejs files when built with closure.
// Be careful what ends up in your namespace.

var text = module.exports = {
name: 'text0',
uri: 'http://sharejs.org/types/textv0',
Expand Down Expand Up @@ -255,9 +253,4 @@ text.invert = function(op) {
return op;
};

if (exports._bootstrapTransform) {
exports._bootstrapTransform(text, transformComponent, checkValidOp, append);
} else {
require('./helpers')._bootstrapTransform(text, transformComponent, checkValidOp, append);
}

require('./bootstrapTransform')(text, transformComponent, checkValidOp, append);
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ot-json0",
"version": "1.0.0",
"description": "JSON OT type",
"main": "lib/index.js",
"directories": {
"test": "test"
},
"dependencies": {},
"devDependencies": {
"ot-fuzzer": "^1.0.0",
"mocha": "^1.20.1"
},
"scripts": {
"test": "mocha"
},
"repository": {
"type": "git",
"url": "git://github.com/ottypes/json0"
},
"keywords": [
"ot",
"json",
"sharejs",
"operational-transformation"
],
"author": "Joseph Gentle <[email protected]>",
"license": "ISC",
"bugs": {
"url": "https://github.com/ottypes/json0/issues"
},
"homepage": "https://github.com/ottypes/json0"
}
176 changes: 176 additions & 0 deletions test/json0-generator.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
json0 = require '../lib/json0'
{randomInt, randomReal, randomWord} = require 'ot-fuzzer'

# This is an awful function to clone a document snapshot for use by the random
# op generator. .. Since we don't want to corrupt the original object with
# the changes the op generator will make.
clone = (o) -> JSON.parse(JSON.stringify(o))

randomKey = (obj) ->
if Array.isArray(obj)
if obj.length == 0
undefined
else
randomInt obj.length
else
count = 0

for key of obj
result = key if randomReal() < 1/++count
result

# Generate a random new key for a value in obj.
# obj must be an Object.
randomNewKey = (obj) ->
# There's no do-while loop in coffeescript.
key = randomWord()
key = randomWord() while obj[key] != undefined
key

# Generate a random object
randomThing = ->
switch randomInt 6
when 0 then null
when 1 then ''
when 2 then randomWord()
when 3
obj = {}
obj[randomNewKey(obj)] = randomThing() for [1..randomInt(5)]
obj
when 4 then (randomThing() for [1..randomInt(5)])
when 5 then randomInt(50)

# Pick a random path to something in the object.
randomPath = (data) ->
path = []

while randomReal() > 0.85 and typeof data == 'object'
key = randomKey data
break unless key?

path.push key
data = data[key]

path


module.exports = genRandomOp = (data) ->
pct = 0.95

container = data: clone data

op = while randomReal() < pct
pct *= 0.6

# Pick a random object in the document operate on.
path = randomPath(container['data'])

# parent = the container for the operand. parent[key] contains the operand.
parent = container
key = 'data'
for p in path
parent = parent[key]
key = p
operand = parent[key]

if randomReal() < 0.4 and parent != container and Array.isArray(parent)
# List move
newIndex = randomInt parent.length

# Remove the element from its current position in the list
parent.splice key, 1
# Insert it in the new position.
parent.splice newIndex, 0, operand

{p:path, lm:newIndex}

else if randomReal() < 0.3 or operand == null
# Replace

newValue = randomThing()
parent[key] = newValue

if Array.isArray(parent)
{p:path, ld:operand, li:clone(newValue)}
else
{p:path, od:operand, oi:clone(newValue)}

else if typeof operand == 'string'
# String. This code is adapted from the text op generator.

if randomReal() > 0.5 or operand.length == 0
# Insert
pos = randomInt(operand.length + 1)
str = randomWord() + ' '

path.push pos
parent[key] = operand[...pos] + str + operand[pos..]
c = {p:path, si:str}
else
# Delete
pos = randomInt(operand.length)
length = Math.min(randomInt(4), operand.length - pos)
str = operand[pos...(pos + length)]

path.push pos
parent[key] = operand[...pos] + operand[pos + length..]
c = {p:path, sd:str}

if json0._testStringSubtype
# Subtype
subOp = {p:path.pop()}
if c.si?
subOp.i = c.si
else
subOp.d = c.sd

c = {p:path, t:'text0', o:[subOp]}

c

else if typeof operand == 'number'
# Number
inc = randomInt(10) - 3
parent[key] += inc
{p:path, na:inc}

else if Array.isArray(operand)
# Array. Replace is covered above, so we'll just randomly insert or delete.
# This code looks remarkably similar to string insert, above.

if randomReal() > 0.5 or operand.length == 0
# Insert
pos = randomInt(operand.length + 1)
obj = randomThing()

path.push pos
operand.splice pos, 0, obj
{p:path, li:clone(obj)}
else
# Delete
pos = randomInt operand.length
obj = operand[pos]

path.push pos
operand.splice pos, 1
{p:path, ld:clone(obj)}
else
# Object
k = randomKey(operand)

if randomReal() > 0.5 or not k?
# Insert
k = randomNewKey(operand)
obj = randomThing()

path.push k
operand[k] = obj
{p:path, oi:clone(obj)}
else
obj = operand[k]

path.push k
delete operand[k]
{p:path, od:clone(obj)}

[op, container.data]
Loading

0 comments on commit f3bd737

Please sign in to comment.