Added cool json features (JsonWrapper)

This commit is contained in:
Kevin Jahns
2014-08-04 23:35:50 +02:00
parent 5ba735701c
commit 6c34d97432
72 changed files with 2237 additions and 889 deletions

View File

@@ -41,7 +41,7 @@ createIwcConnector = (callback)->
sendRootElement = ()=>
json = {
root_element : @yatta.getRootElement()
HB : @yatta.getHistoryBuffer().toJson()
HB : @yatta.getHistoryBuffer()._encode()
}
@sendIwcIntent "Yatta_push_root_element", json
@iwcHandler["Yatta_get_root_element"] = [sendRootElement]
@@ -94,5 +94,5 @@ createIwcConnector = (callback)->
undefined
module.exports = createIwcConnector
window?.createIwcConnector = createIwcConnector
window?.createConnector = createIwcConnector

View File

@@ -17,7 +17,7 @@ module.exports = (user_list)->
@applied_operations.push o
@execution_listener.push appliedOperationsListener
if not (user_list?.length is 0)
@engine.applyOps user_list[0].getHistoryBuffer().toJson()
@engine.applyOps user_list[0].getHistoryBuffer()._encode()
@unexecuted = {}

View File

@@ -22,9 +22,17 @@ class Engine
for o in ops
if not o.execute()
@unprocessed_ops.push o
@cleanUp()
@tryUnprocessed()
cleanUp: ()->
applyOp: (op_json)->
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
@HB.addOperation o
if not o.execute()
@unprocessed_ops.push o
@tryUnprocessed()
tryUnprocessed: ()->
while true
old_length = @unprocessed_ops.length
unprocessed = []
@@ -35,15 +43,7 @@ class Engine
if @unprocessed_ops.length is old_length
break
applyOp: (op_json)->
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
@HB.addOperation o
if not o.execute()
@unprocessed_ops.push o
@cleanUp()
module.exports = Engine

View File

@@ -4,7 +4,11 @@ HistoryBuffer = require "../HistoryBuffer.coffee"
Engine = require "../Engine.coffee"
#
# Framework for arbitrary Json data-structures.
# Framework for Json data-structures.
# Known values that are supported:
# * String
# * Integer
# * Array
#
class JsonYatta
constructor: (user_id, Connector)->
@@ -21,23 +25,55 @@ class JsonYatta
else
@root_element = @HB.getOperation(root_elem)
#
# @result JsonType
#
getRootElement: ()->
@root_element
#
# @see Engine
#
getEngine: ()->
@engine
#
# Get the initialized connector.
#
getConnector: ()->
@connector
#
# @see HistoryBuffer
#
getHistoryBuffer: ()->
@HB
#
# @see JsonType.setMutableDefault
#
setMutableDefault: (mutable)->
@root_element.setMutableDefault(mutable)
#
# Get the UserId from the HistoryBuffer object.
# In most cases this will be the same as the user_id value with which
# JsonYatta was initialized (Depending on the HistoryBuffer implementation).
#
getUserId: ()->
@HB.getUserId()
val: (name, content)->
@root_element.val(name, content)
#
# @see JsonType.val
#
val : (name, content, mutable)->
@root_element.val(name, content, mutable)
#
# @see JsonType.value
#
value : ()->
@root_element.value
window?.JsonYatta = JsonYatta
module.exports = JsonYatta

View File

@@ -30,12 +30,12 @@ class HistoryBuffer
res[user] = ctn
res
toJson: ()->
_encode: ()->
json = []
for u_name,user of @buffer
for o_number,o of user
if not isNaN(parseInt(o_number))
json.push o.toJson()
json.push o._encode()
json
#

View File

@@ -7,35 +7,44 @@ module.exports = (HB)->
# A generic interface to operations.
#
# An operation has the following methods:
# toJson: encodes an operation (needed only if instance of this operation is sent).
# _encode: encodes an operation (needed only if instance of this operation is sent).
# execute: execute the effects of this operations. Good examples are Insert-type and AddName-type
# val: in the case that the operation holds a value
#
# Furthermore an encodable operation has a parser.
#
class Operation
# @param {Object} uid A unique identifier
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @see HistoryBuffer.getNextOperationIdentifier
#
constructor: (uid)->
if not uid?
uid = HB.getNextOperationIdentifier()
{
'creator': @creator
'op_number' : @op_number
} = uid
# Computes a unique identifier (uid).
#
# Computes a unique identifier (uid) that identifies this operation.
#
getUid: ()->
{ 'creator': @creator, 'op_number': @op_number }
#
# @private
# Notify the all the listeners.
#
execute: ()->
@is_executed = true
for l in execution_listener
l @toJson()
l @_encode()
@
#
# @private
# Operations may depend on other operations (linked lists, etc.).
# The saveOperation and validateSavedOperations methods provide
# an easy way to refer to these operations via an uid or object reference.
@@ -68,6 +77,7 @@ module.exports = (HB)->
@unchecked[name] = op
#
# @private
# After calling this function all not instantiated operations will be accessible.
# @see Operation.saveOperation
#
@@ -94,15 +104,21 @@ module.exports = (HB)->
# A simple Delete-type operation that deletes an Insert-type operation.
#
class Delete extends Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} deletes UID or reference of the operation that this to be deleted.
#
constructor: (uid, deletes)->
@saveOperation 'deletes', deletes
super uid
#
# @private
# Convert all relevant information of this operation to the json-format.
# This result can be sent to other clients.
#
toJson: ()->
_encode: ()->
{
'type': "Delete"
'uid': @getUid()
@@ -110,6 +126,7 @@ module.exports = (HB)->
}
#
# @private
# Apply the deletion.
#
execute: ()->
@@ -142,8 +159,7 @@ module.exports = (HB)->
class Insert extends Operation
#
# @param {Object} creator A unique user identifier
# @param {Integer} op_number This Number was assigned via getNextOperationIdentifier().
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
#
@@ -158,6 +174,9 @@ module.exports = (HB)->
@saveOperation 'origin', prev_cl
super uid
#
# @private
#
applyDelete: (o)->
@deleted_by ?= []
@deleted_by.push o
@@ -169,6 +188,7 @@ module.exports = (HB)->
@deleted_by?.length > 0
#
# @private
# The amount of positions that $this operation was moved to the right.
#
getDistanceToOrigin: ()->
@@ -185,6 +205,7 @@ module.exports = (HB)->
d
#
# @private
# Update the short list
# TODO (Unused)
update_sl: ()->
@@ -203,6 +224,7 @@ module.exports = (HB)->
#
# @private
# Include this operation in the associative lists.
#
execute: ()->
@@ -263,8 +285,47 @@ module.exports = (HB)->
super # notify the execution_listeners
@
val: ()->
throw new Error "Implement this function!"
#
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
#
class ImmutableObject extends Insert
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} content
#
constructor: (uid, @content="", prev, next, origin)->
super uid, prev, next, origin
#
# @return [String] The content of this operation.
#
val : ()->
@content
_encode: ()->
json = {
'type': "ImmutableObject"
'uid' : @getUid()
'content' : @content
}
if @prev_cl?
json['prev'] = @prev_cl.getUid()
if @next_cl?
json['next'] = @next_cl.getUid()
if @origin? and @origin isnt @prev_cl
json["origin"] = @origin.getUid()
json
parser['ImmutableObject'] = (json)->
{
'uid' : uid
'content' : content
'prev': prev
'next': next
'origin' : origin
} = json
new ImmutableObject uid, content, prev, next, origin
#
# A delimiter is placed at the end and at the beginning of the associative lists.
@@ -273,21 +334,21 @@ module.exports = (HB)->
#
class Delimiter extends Insert
isDeleted: ()->
false
getDistanceToOrigin: ()->
0
#
# @private
#
execute: ()->
if @validateSavedOperations()
for l in execution_listener
l @toJson()
l @_encode()
@
else
false
toJson: ()->
#
# @private
#
_encode: ()->
{
'type' : "Delimiter"
'uid' : @getUid()
@@ -310,6 +371,7 @@ module.exports = (HB)->
'Insert' : Insert
'Delimiter': Delimiter
'Operation': Operation
'ImmutableObject' : ImmutableObject
'parser' : parser
'execution_listener' : execution_listener
}

View File

@@ -5,48 +5,179 @@ module.exports = (HB)->
types = text_types.types
parser = text_types.parser
createJsonWrapper = (_jsonType)->
#
# A JsonWrapper was intended to be a convenient wrapper for the JsonType.
# But it can make things more difficult than they are.
# @see JsonType
#
# @example create a JsonWrapper
# # You get a JsonWrapper from a JsonType by calling
# w = yatta.value
#
# It creates Javascripts -getter and -setter methods for each property that JsonType maintains.
# @see getter https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
#
# @example Getter Example
# # you can access the x property of yatta by calling
# w.x
# # instead of
# yatta.val('x')
#
# @note You can only overwrite existing values! Setting a new property won't have any effect!
#
# @example Setter Example
# # you can set an existing x property of yatta by calling
# w.x = "text"
# # instead of
# yatta.val('x', "text")
#
# In order to set a new property you have to overwrite an existing property.
# Therefore the JsonWrapper supports a special feature that should make things more convenient
# (we can argue about that, use the JsonType if you don't like it ;).
# If you overwrite an object property of the JsonWrapper with a new object, it will result in a merged version of the objects.
# Let w.p the property that is to be overwritten and o the new value. E.g. w.p = o
# * The result has all properties of o
# * The result has all properties of w.p if they don't occur under the same property-name in o.
#
# @example Conflict Example
# yatta.value = {a : "string"}
# w = yatta.value
# console.log(w) # {a : "string"}
# w.a = {a : {b : "string"}}
# console.log(w) # {a : {b : "String"}}
# w.a = {a : {c : 4}}
# console.log(w) # {a : {b : "String", c : 4}}
#
# @example Common Pitfalls
# w = yatta.value
# # Setting a new property
# w.newProperty = "Awesome"
# console.log(w.newProperty == "Awesome") # false, w.newProperty is undefined
# # overwrite the w object
# w = {newProperty : "Awesome"}
# console.log(w.newProperty == "Awesome") # true!, but ..
# console.log(yatta.value.newProperty == "Awesome") # false, you are only allowed to set properties!
# # The solution
# yatta.value = {newProperty : "Awesome"}
# console.log(w.newProperty == "Awesome") # true!
#
class JsonWrapper
constructor: (jsonType)->
for name, obj of jsonType.map
do (name, obj)->
Object.defineProperty JsonWrapper.prototype, name,
get : ->
x = obj.val()
if x instanceof JsonType
createJsonWrapper x
else if x instanceof types.ImmutableObject
x.val()
else
x
set : (o)->
if o.constructor is {}.constructor
overwrite = jsonType.val(name)
for o_name,o_obj of o
overwrite.val(o_name, o_obj, 'immutable')
else
jsonType.val(name, o, 'immutable')
enumerable: true
configurable: false
new JsonWrapper _jsonType
#
# Manages Object-like values.
#
class JsonType extends types.MapManager
constructor: (uid, initial_value)->
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} initial_value Create this operation with an initial value.
# @param {String|Boolean} Whether the initial_value should be created as mutable. (Optional - see setMutableDefault)
#
constructor: (uid, initial_value, mutable)->
super uid
if initial_value?
if typeof initial_value isnt "object"
throw new Error "The initial value of JsonTypes must be of type Object! (current type: #{typeof initial_value})"
for name,o of initial_value
@val name, o
@val name, o, mutable
mutable_default:
true
setMutableDefault: (mutable)->
if mutable is true or mutable is 'mutable'
JsonType.prototype.mutable_default = true
else if mutable is false or mutable is 'immutable'
JsonType.prototype.mutable_default = false
else
throw new Error 'Set mutable either "mutable" or "immutable"!'
'OK'
#
# Get this as a Json object. Note that none of the values of the result is of type Operation.
# @overload val()
# @results [Json]
# Get this as a Json object.
# @return [Json]
#
# Get value of a property.
# @overload val(name)
# Get value of a property.
# @param {String} name Name of the object property.
# @results [JsonType|WordType]
# @return [JsonType|Word|String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
#
# Set a new property.
# @overload val(name, content)
# Set a new property.
# @param {String} name Name of the object property.
# @param {Object|String} content Content of the object property.
# @return [JsonType] This object. (supports chaining)
#
val: (name, content)->
if name? and content?
if typeof content is 'string'
word = HB.addOperation(new types.Word HB.getNextOperationIdentifier(), content).execute()
super name, word
else if typeof content is 'object'
json = HB.addOperation(JsonType HB.getNextOperationIdentifier(), content).execute()
super name, json
else
throw new Error "You must not set #{typeof content}-types in collaborative Json-objects!"
val: (name, content, mutable)->
if typeof name is 'object'
# Special case. First argument is an object. Then the second arg is mutable.
# Keep that in mind when reading the following..
for o_name,o of name
@val(o_name,o,content)
@
else if name? and content?
if mutable?
if mutable is true or mutable is 'mutable'
mutable = true
else
mutable = false
else
mutable = @mutable_default
if typeof content is 'function'
@ # Just do nothing
else if ((not mutable) or typeof content is 'number') and content.constructor isnt Object
obj = HB.addOperation(new types.ImmutableObject undefined, content).execute()
super name, obj
else
if typeof content is 'string'
word = HB.addOperation(new types.Word HB.getNextOperationIdentifier(), content).execute()
super name, word
else if content.constructor is Object
json = HB.addOperation(new JsonType HB.getNextOperationIdentifier(), content, mutable).execute()
super name, json
else
throw new Error "You must not set #{typeof content}-types in collaborative Json-objects!"
else
super name, content
toJson: ()->
Object.defineProperty JsonType.prototype, 'value',
get : -> createJsonWrapper @
set : (o)->
if o.constructor is {}.constructor
for o_name,o_obj of o
@val(o_name, o_obj, 'immutable')
else
throw new Error "You must only set Object values!"
#
# @private
#
_encode: ()->
{
'type' : "JsonType"
'uid' : @getUid()
@@ -59,6 +190,8 @@ module.exports = (HB)->
new JsonType uid
types['JsonType'] = JsonType
text_types

View File

@@ -9,6 +9,10 @@ module.exports = (HB)->
# Manages map like objects. E.g. Json-Type and XML attributes.
#
class MapManager extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (uid)->
@map = {}
super uid
@@ -18,12 +22,20 @@ module.exports = (HB)->
if not @map[name]?
HB.addOperation(new AddName HB.getNextOperationIdentifier(), @, name).execute()
@map[name].replace content
@
else if name?
@map[name]?.val()
obj = @map[name]?.val()
if obj instanceof types.ImmutableObject
obj.val()
else
obj
else
result = {}
for name,o of @map
result[name] = o.val()
obj = o.val()
if obj instanceof types.ImmutableObject or obj instanceof MapManager
obj = obj.val()
result[name] = obj
result
#
@@ -33,6 +45,12 @@ module.exports = (HB)->
# only one will AddName operation will be executed.
#
class AddName extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} map_manager Uid or reference to the MapManager.
# @param {String} name Name of the property that will be added.
#
constructor: (uid, map_manager, @name)->
@saveOperation 'map_manager', map_manager
super uid
@@ -54,7 +72,7 @@ module.exports = (HB)->
@map_manager.map[@name] = HB.addOperation(new ReplaceManager undefined, uid_r, beg, end).execute()
super
toJson: ()->
_encode: ()->
{
'type' : "AddName"
'uid' : @getUid()
@@ -74,6 +92,12 @@ module.exports = (HB)->
# Manages a list of Insert-type operations.
#
class ListManager extends types.Insert
#
# A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (uid, beginning, end, prev, next, origin)->
if beginning? and end?
@saveOperation 'beginning', beginning
@@ -129,6 +153,11 @@ module.exports = (HB)->
# @see Word
#
class ReplaceManager extends ListManager
#
# @param {Operation} initial_content Initialize this with a Replaceable that holds the initial_content.
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (initial_content, uid, beginning, end, prev, next, origin)->
super uid, beginning, end, prev, next, origin
if initial_content?
@@ -145,7 +174,7 @@ module.exports = (HB)->
throw new Error "dtrn"
o.val()
toJson: ()->
_encode: ()->
json =
{
'type': "ReplaceManager"
@@ -178,6 +207,12 @@ module.exports = (HB)->
# @see ReplaceManager
#
class Replaceable extends types.Insert
#
# @param {Operation} content The value that this Replaceable holds.
# @param {ReplaceManager} parent Used to replace this Replaceable with another one.
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (content, parent, uid, prev, next, origin)->
@saveOperation 'content', content
@saveOperation 'parent', parent
@@ -203,7 +238,7 @@ module.exports = (HB)->
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
toJson: ()->
_encode: ()->
json =
{
'type': "Replaceable"

View File

@@ -16,6 +16,10 @@ module.exports = (HB)->
# Extends the basic Insert type to an operation that holds a text value
#
class TextInsert extends types.Insert
#
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (@content, uid, prev, next, origin)->
if not (prev? and next?)
throw new Error "You must define prev, and next for TextInsert-types!"
@@ -44,7 +48,7 @@ module.exports = (HB)->
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
toJson: ()->
_encode: ()->
json =
{
'type': "TextInsert"
@@ -71,6 +75,11 @@ module.exports = (HB)->
# Handles a Text-like data structures with support for insertText/deleteText at a word-position.
#
class Word extends types.ListManager
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {String} initial_content
#
constructor: (uid, initial_content, beginning, end, prev, next, origin)->
super uid, beginning, end, prev, next, origin
if initial_content?
@@ -97,7 +106,7 @@ module.exports = (HB)->
if o instanceof types.Delimiter
throw new Error "You can't delete more than there is.."
o = o.next_cl
d.toJson()
d._encode()
#
# Replace the content of this word with another one. Concurrent replacements are not merged!
@@ -133,7 +142,7 @@ module.exports = (HB)->
@saveOperation 'replace_manager', op
@validateSavedOperations
toJson: ()->
_encode: ()->
json = {
'type': "Word"
'uid' : @getUid()
@@ -162,7 +171,6 @@ module.exports = (HB)->
types['TextInsert'] = TextInsert
types['TextDelete'] = TextDelete
types['Word'] = Word
structured_types