Initial merge.

This commit is contained in:
Julien Fontanet 2014-01-16 14:07:31 +01:00
parent 5b91e6aa59
commit 60c46d529c
11 changed files with 954 additions and 2120 deletions

View File

@ -1,387 +1,492 @@
# Low level tools.
{EventEmitter: $EventEmitter} = require 'events'
#----------------------------------------------------------------------
$_ = require 'underscore'
#=====================================================================
class $DynamicProperty
# TODO: move these helpers in a dedicated module.
constructor: (@value, @hooks) ->
$done = {}
# Similar to `$_.each()` but can be interrupted by returning the
# special value `done` provided as the forth argument.
$each = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
# Array-like object.
i = 0
while i < n and (iterator.call ctx, col[i], "#{i}", col, $done) isnt $done
++i
else
for key of col
break if (iterator.call ctx, col[key], key, $done) is $done
# For performance.
undefined
$makeFunction = (val) -> -> val
# Similar to `$_.map()` for array and `$_.mapValues()` for objects.
#
# Note: can be interrupted by returning the special value `done`
# provided as the forth argument.
$map = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
result = []
# Array-like object.
i = 0
while i < n
value = iterator.call ctx, col[i], "#{i}", col, $done
break if value is $done
result.push value
++i
else
result = {}
for key of col
value = iterator.call ctx, col[key], key, $done
break if value is $done
result.push value
# The new collection is returned.
result
# Similar to `$map()` but change the current collection.
#
# Note: can be interrupted by returning the special value `done`
# provided as the forth argument.
$mapInPlace = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
# Array-like object.
i = 0
while i < n
value = iterator.call ctx, col[i], "#{i}", col, $done
break if value is $done
col[i] = value
++i
else
for key of col
value = iterator.call ctx, col[key], key, $done
break if value is $done
col[key] = value
# The collection is returned.
col
#=====================================================================
$noop = ->
class $MappedCollection extends $EventEmitter
$copyDeep = (value) ->
if value instanceof Array
return ($copyDeep item for item in value)
if value instanceof Object
result = {}
result[key] = $copyDeep item for key, item of value
return result
return value
$getDeep = (obj, path) ->
return obj if path.length is 0
current = obj
i = 0
n = path.length - 1
while i < n
current = current[path[i++]]
throw new Error 'invalid path component' if current is undefined
current[path[i]]
$setDeep = (obj, path, value) ->
throw new Error 'invalid path' if path.length is 0
current = obj
i = 0
n = path.length - 1
while i < n
current = current[path[i++]]
throw new Error 'invalid path component' if current is undefined
current[path[i]] = value
# @param rule Rule of the current item.
# @param item Current item.
# @param value Value of the generator item.
$computeValue = (collection, rule, item) ->
value = item.generator
# @param parent The parent object of this entry (necessary for
# assignment).
# @param name The name of this entry.
# @param spec Specification for the current entry.
# The dispatch function is called whenever a new item has to be
# processed and returns the name of the rule to use.
#
# @returns The generated value for this entry.
helper = (parent, name, spec) ->
if not $_.isObject spec
parent[name] = spec
else if spec instanceof $DynamicProperty
# If there was no previous value use $DynamicProperty.value,
# otherwise, just keep the previous value.
if parent[name] is undefined
# Helper is re-called for the initial value.
helper parent, name, spec.value
else if $_.isFunction spec
ctx = {collection, rule}
ctx.__proto__ = item # Links to the current item.
parent[name] = spec.call ctx, value, item.key
else if $_.isArray spec
current = parent[name] or= new Array spec.length
for entry, index in spec
helper current, index, entry
else
# It's a plain object.
current = parent[name] or= {}
for key, property of spec
helper current, key, property
# To change the way it is dispatched, just override this it.
dispatch: ->
(@genval and (@genval.rule ? @genval.type)) ? 'unknown'
helper item, 'value', rule.value
# This function is called when an item has been dispatched to a
# missing rule.
#
# The default behavior is to throw an error but you may instead
# choose to create a rule:
#
# collection.missingRule = collection.rule
missingRule: (name) ->
throw new Error "undefined rule “#{name}"
######################################################################
class $MappedCollection
constructor: (spec) ->
# If spec is a function, it is called with various helpers in
# `this`.
if $_.isFunction spec
ctx =
dynamic: (initialValue, hooks) ->
new $DynamicProperty initialValue, hooks
noop: $noop
spec = spec.call ctx
# This key function returns the identifier used to map the
# generator to the generated item.
constructor: ->
# Items are stored here indexed by key.
#
# The default function uses the index if the generator collection
# is an array or the property name if it is an object.
# The prototype of this object is set to `null` to avoid pollution
# from enumerable properties of `Object.prototype` and the
# performance hit of `hasOwnProperty o`.
@_byKey = Object.create null
# Hooks are stored here indexed by moment.
@_hooks = {
beforeDispatch: []
beforeUpdate: []
beforeSave: []
afterRule: []
}
# Rules are stored here indexed by name.
#
# /!\: This entry MUST be overridden in rules for new items.
if spec.key?
@_key = spec.key
throw new Error 'key must be a function' unless $_.isFunction @_key
# The prototype of this object is set to `null` to avoid pollution
# from enumerable properties of `Object.prototype` and to be able
# to use the `name of @_rules` syntax.
@_rules = Object.create null
# Register a hook to run at a given point.
#
# A hook receives as parameter an event object with the following
# properties:
# - `preventDefault()`: prevents the next default action from
# happening;
# - `stopPropagation()`: prevents other hooks from being run.
#
# Note: if a hook throws an exception, `event.stopPropagation()`
# then `event.preventDefault()` will be called and the exception
# will be forwarded.
#
# # Item hook
#
# Valid items related moments are:
# - beforeDispatch: even before the item has been dispatched;
# - beforeUpdate: after the item has been dispatched but before
# updating its value.
# - beforeSave: after the item has been updated.
#
# An item hook is run in the context of the current item.
#
# # Rule hook
#
# Valid rules related moments are:
# - afterRule: just after a new rule has been defined (even
# singleton).
#
# An item hook is run in the context of the current rule.
hook: (name, hook) ->
# Allows a nicer syntax for CoffeeScript.
if $_.isObject name
# Extracts the name and the value from the first property of the
# object.
do ->
object = name
return for own name, hook of object
hooks = @_hooks[name]
@_assert(
hooks?
"invalid hook moment “#{name}"
)
hooks.push hook
# Register a new singleton rule.
#
# See the `rule()` method for more information.
item: (name, definition) ->
# Creates the corresponding rule.
rule = @rule name, definition, true
# Creates the singleton.
item = {
rule: rule.name
key: rule.key() # No context because there is not generator.
val: undefined
}
@_updateItems [item], true
# Register a new rule.
#
# If the definition is a function, it will be run in the context of
# an item-like object with the following properties:
# - `key`: the definition for the key of this item;
# - `val`: the definition for the value of this item.
#
# Warning: The definition function is run only once!
rule: (name, definition, singleton = false) ->
# Allows a nicer syntax for CoffeeScript.
if $_.isObject name
# Extracts the name and the definition from the first property
# of the object.
do ->
object = name
return for own name, definition of object
@_assert(
name not of @_rules
"the rule “#{name}” is already defined"
)
# Extracts the rule definition.
if $_.isFunction definition
ctx = {
name
key: undefined
val: undefined
singleton
}
definition.call ctx
else
@_key = (_, key) -> key
ctx = {
name
key: definition?.key
val: definition?.val
singleton
}
spec.rules or= {}
# Runs the `afterRule` hook and returns if the registration has
# been prevented.
return unless @_runHook 'afterRule', ctx
# Rules are the core of $MappedCollection, they allow to categorize
# objects and to treat them differently.
@_rules = {}
{key, val} = ctx
# Hooks are functions which are run when a item of a given rule
# enters, exists or is updated.
@_hooks = {}
# The default key.
key ?= if singleton then -> name else -> @genkey
# Initially the collection is empty.
@_byKey = {}
# The default value.
val ?= -> @genval
# For performance concerns, items are also categorized by rules.
@_byRule = {}
# Makes sure `key` is a function for uniformity.
key = $makeFunction key unless $_.isFunction key
# Rules are checked for conformity and created in the system.
for name, def of spec.rules
# If it's a function, runs it.
def = def() if $_.isFunction def
# Register the new rule.
@_rules[name] = {
name
key
val
singleton
}
unless $_.isObject def
throw new Error "#{name} definition must be an object"
#--------------------------------
# A rule can extends another (not recursive for now!).
if def.extends?
unless $_.isString def.extends
throw new Error "#{name}.extends must be a string"
get: (keys) ->
if keys is undefined
items = $_.map @_byKey, (item) -> item.val
else
items = $mapInPlace (@_fetchItems keys), (item) -> item.val
if spec.rules[def.extends] is undefined
throw new Error "#{name}.extends must reference a valid rule (#{def.extends})"
if $_.isString keys then items[0] else items
$_.defaults def, spec.rules[def.extends]
getRaw: (keys) ->
if keys is undefined
item for _, item of @_byKey
else
items = @_fetchItems keys
rule = {name}
if $_.isString keys then items[0] else items
if def.key?
# Static rule, used to create a new item (without generator).
remove: (keys) ->
@_removeItems (@_fetchItems keys)
if def.test?
throw new Error "both #{name}.key and #{name}.test cannot be defined"
set: (items, {add, update, remove} = {}) ->
add = true unless add?
update = true unless update?
remove = false unless remove?
unless $_.isString def.key
throw new Error "#{name}.key must be a string"
itemsToAdd = {}
itemsToUpdate = {}
rule.key = if $_.isFunction def.key then def.key() else def.key
else if def.test?
# Dynamic rule, used to create new items from generator items.
itemsToRemove = {}
$_.extend itemsToRemove, @_byKey if remove
unless $_.isFunction def.test
throw new Error "#{name}.test must be a function"
$each items, (genval, genkey) =>
item = {
rule: undefined
key: undefined
val: undefined
genkey
genval
}
rule.test = def.test
else
# Invalid rule!
throw new Error "#{name} must have either a key or a test entry"
return unless @_runHook 'beforeDispatch', item
# A rule must have a value.
throw new Error "#{name}.value must be defined" unless def.value?
rule.value = def.value
rule.private = !!def.private
@_rules[name] = rule
@_hooks[name] =
enter: []
update: []
exit: []
@_byRule[name] = {}
# For each rules, values are browsed and hooks are created when
# necessary (dynamic properties).
for name, rule of @_rules or {}
# Browse the value searching for dynamic properties/entries.
#
# An immediately invoked function is used to easily handle
# recursion.
browse = (value, path) =>
# Unless the value is an object, there is nothing to browse.
return unless $_.isObject value
# If the value is a function, it is a factory which will be
# called later, when an item will be created.
return if $_.isFunction value
# If the value is a dynamic property, grabs the initial value
# and registers its hooks.
if value instanceof $DynamicProperty
hooks = value.hooks
# Browse hooks for each rules.
for name_, hooks_ of hooks
# Wraps a hook.
#
# A hook is run with a defined environment
wrap = (hook, rule, items) =>
# Last two parameters are here to be protected from the
# environment.
# FIXME: @_rules[name] and @_byRule are not defined at
# this point.
rule = @_rules[name]
items = @_byRule[name]
(value, key) ->
# The current hook is runs for all items of the
# current rule.
for _, item of items
# Value of the current field.
field = $getDeep item.value, path
ctx = {rule, field}
ctx.__proto__ = item # Links to the current item.
hook.call ctx, value, key
# Updates the value if it changed.
$setDeep item.value, path, ctx.field if ctx.field isnt field
# Checks each hook is correctly defined.
{enter, update, exit} = hooks_
enter ?= update
@_hooks[name_].enter.push wrap(enter) if enter?
if not update? and exit? and enter?
update = (args...) ->
exit.apply this, args
enter.apply this, args
@_hooks[name_].update.push wrap(update) if update?
@_hooks[name_].exit.push wrap(exit) if exit?
# OPTIMIZE: do not register hooks if they are `noop`.
# FIXME: Hooks must be associated to the rule (because they
# must be run for each object of this type), and to the
# field (because it must be available through @field).
return
# If the value is an array, browse each entry.
if $_.isArray value
for entry, index in value
browse entry, path.concat(index)
return
# The value is an object, browse each property.
for key, property of value
browse property, path.concat(key)
browse rule.value, []
# If it is a static rule, creates its item right now.
if rule.key?
# Adds the item.
item = @_byKey[rule.key] = @_byRule[rule.name][rule.key] =
_ruleName: rule.name
key: rule.key
value: undefined
# Computes the value.
$computeValue this, rule, item
# No events for static items.
get: (key) -> @_byKey[key]?.value
getAll: ->
items = {}
for ruleName, ruleItems of @_byRule
# Searches for a rule to handle it.
ruleName = @dispatch.call item
rule = @_rules[ruleName]
# Items of private rules are not exported.
continue if rule.private
unless rule?
@missingRule ruleName
for key, {value} of ruleItems
items[key] = value
# If `missingRule()` has not created the rule, just keep this
# item.
rule = @_rules[ruleName]
return unless rule?
items
# Checks if this is a singleton.
@_assert(
not rule.singleton
"cannot add items to singleton rule “#{rule.name}"
)
remove: (items) ->
itemsToRemove = {}
$_.each items, (value, key) =>
key = @_key key
item = @_byKey[key]
if item?
itemsToRemove[key] = item
# Computes its key.
key = rule.key.call item
@_remove items
@_assert(
$_.isString key
"the key “#{key}” is not a string"
)
# Adds, updates or removes items from the collections. Items not
# present are added, present are updated, and present in the
# generated collection but not in the generator are removed.
set: (items, {add, update, remove} = {}) ->
add = true if add is undefined
update = true if update is undefined
remove = false if remove is undefined
# Updates known values.
item.rule = rule.name
item.key = key
itemsToRemove = {}
if remove
$_.extend(itemsToRemove, @_byKey)
$_.each items, (value, generatorKey) =>
key = @_key value, generatorKey
# If the item already existed.
if @_byKey[key]?
if key of @_byKey
# Marks this item as not to be removed.
delete itemsToRemove[key] if remove
delete itemsToRemove[key]
if update
item = @_byKey[key]
rule = @_rules[item._ruleName]
# Fetches the existing entry.
prev = @_byKey[key]
# Compute the new value.
item.generator = value
item.generatorKey = generatorKey
$computeValue this, rule, item
# Checks if there is a conflict in rules.
@_assert(
item.rule is prev.rule
"the key “#{key}” cannot be of rule “#{item.rule}”, "
"already used by “#{prev.rule}"
)
# Runs related hooks.
for hook in @_hooks[rule.name]?.update or []
hook item.value, item.key
else if add
# First we have to find to which rule this item belongs to.
rule = do =>
for _, rule of @_rules
ctx = {rule}
return rule if rule.test? and rule.test.call ctx, value, key
# Gets its previous value.
item.val = prev.val
# If no rule has been found, just stops.
return unless rule
# Registers the item to be updated.
itemsToUpdate[key] = item
# Adds the item.
item = @_byKey[key] = @_byRule[rule.name][key] = {
_ruleName: rule.name
key
value: undefined
generator: value
generatorKey
}
# Note: an item will be updated only once per `set()` and
# only the last generator will be used.
else
if add
# Registers the item to be added.
itemsToAdd[key] = item
# Computes the value.
$computeValue this, rule, item
# Adds items.
@_updateItems itemsToAdd, true
# Runs related hooks.
for hook in @_hooks[rule.name]?.enter or []
hook item.value, item.key
# Updates items.
@_updateItems itemsToUpdate
# There are keys inside only if remove is `true`.
@_remove itemsToRemove if remove
# Removes any items not seen (iff `remove` is true).
@_removeItems itemsToRemove
_remove: (items) ->
for {_ruleName: ruleName, value}, key in items
# If there are some hooks registered, runs them.
for hook in @_hooks[ruleName]?.remove or []
hook value, key
# Forces items to update their value.
touch: (keys) ->
@_updateItems (@_fetchItems keys, true)
# Removes effectively the item.
delete @_byKey[key] @_byRule[ruleName][key]
#--------------------------------
_assert: (cond, message) ->
throw new Error message unless cond
# Emits item related event.
_emitEvent: (event, items) ->
byRule = {}
# One per item.
$each items, (item) =>
@emit "key=#{item.key}", event, item
(byRule[item.rule] ?= []).push item
# One per rule.
@emit "rule=#{rule}", event, items for rule, items of byRule
# One for everything.
@emit "any", event, items
_fetchItems: (keys, ignoreMissingItems = false) ->
unless $_.isArray keys
keys = if $_.isObject keys then $_.keys keys else [keys]
items = []
for key in keys
item = @_byKey[key]
if item?
items.push item
else
@_assert(
ignoreMissingItems
"no item with key “#{key}"
)
items
_removeItems: (items) ->
return if $_.isEmpty items
$each items, (item) => delete @_byKey[item.key]
@_emitEvent 'exit', items
# Runs hooks for the moment `name` with the given context and
# returns false if the default action has been prevented.
_runHook: (name, ctx) ->
hooks = @_hooks[name]
# If no hooks, nothing to do.
return true unless hooks? and (n = hooks.length) isnt 0
# Flags controlling the run.
notStopped = true
actionNotPrevented = true
# Creates the event object.
event = {
stopPropagation: -> notStopped = false
# TODO: Should `preventDefault()` imply `stopPropagation()`?
preventDefault: -> actionNotPrevented = false
}
i = 0
while notStopped and i < n
hooks[i++].call ctx, event
# TODO: Is exception handling necessary to have the wanted
# behavior?
return actionNotPrevented
_updateItems: (items, areNew) ->
return if $_.isEmpty items
# An update is similar to an exit followed by an enter.
@_emitEvent 'exit', items unless areNew
$each items, (item) =>
return unless @_runHook 'beforeUpdate', item
{rule: ruleName} = item
# Computes its value.
do =>
# Item is not passed directly to function to avoid direct
# modification.
#
# This is not a true security but better than nothing.
proxy = Object.create item
updateValue = (parent, prop, def) ->
if not $_.isObject def
parent[prop] = def
else if $_.isFunction def
parent[prop] = def.call proxy, parent[prop]
else if $_.isArray def
i = 0
n = def.length
current = parent[prop] ?= new Array n
while i < n
updateValue current, i, def[i]
++i
else
# It's a plain object.
current = parent[prop] ?= {}
for i of def
updateValue current, i, def[i]
updateValue item, 'val', @_rules[ruleName].val
return unless @_runHook 'beforeSave', item
# Registers the new item.
@_byKey[item.key] = item
@_emitEvent 'enter', items
# TODO: checks for loops.
#=====================================================================
module.exports = $MappedCollection
module.exports = {$MappedCollection}

View File

@ -4,17 +4,17 @@ $sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection2} = require './MappedCollection2.coffee'
{$MappedCollection} = require './MappedCollection.coffee'
#=====================================================================
describe '$MappedCollection2', ->
describe '$MappedCollection', ->
# Shared variables.
collection = null
beforeEach ->
collection = new $MappedCollection2()
collection = new $MappedCollection()
#-------------------------------------------------------------------

View File

@ -1,491 +0,0 @@
{EventEmitter: $EventEmitter} = require 'events'
#----------------------------------------------------------------------
$_ = require 'underscore'
#=====================================================================
# TODO: move these helpers in a dedicated module.
$done = {}
# Similar to `$_.each()` but can be interrupted by returning the
# special value `done` provided as the forth argument.
$each = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
# Array-like object.
i = 0
while i < n and (iterator.call ctx, col[i], "#{i}", col, $done) isnt $done
++i
else
for key of col
break if (iterator.call ctx, col[key], key, $done) is $done
# For performance.
undefined
$makeFunction = (val) -> -> val
# Similar to `$_.map()` for array and `$_.mapValues()` for objects.
#
# Note: can be interrupted by returning the special value `done`
# provided as the forth argument.
$map = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
result = []
# Array-like object.
i = 0
while i < n
value = iterator.call ctx, col[i], "#{i}", col, $done
break if value is $done
result.push value
++i
else
result = {}
for key of col
value = iterator.call ctx, col[key], key, $done
break if value is $done
result.push value
# The new collection is returned.
result
# Similar to `$map()` but change the current collection.
#
# Note: can be interrupted by returning the special value `done`
# provided as the forth argument.
$mapInPlace = (col, iterator, ctx) ->
# The default context is inherited.
ctx ?= this
if (n = col.length)?
# Array-like object.
i = 0
while i < n
value = iterator.call ctx, col[i], "#{i}", col, $done
break if value is $done
col[i] = value
++i
else
for key of col
value = iterator.call ctx, col[key], key, $done
break if value is $done
col[key] = value
# The collection is returned.
col
#=====================================================================
class $MappedCollection2 extends $EventEmitter
# The dispatch function is called whenever a new item has to be
# processed and returns the name of the rule to use.
#
# To change the way it is dispatched, just override this it.
dispatch: ->
(@genval and (@genval.rule ? @genval.type)) ? 'unknown'
# This function is called when an item has been dispatched to a
# missing rule.
#
# The default behavior is to throw an error but you may instead
# choose to create a rule:
#
# collection.missingRule = collection.rule
missingRule: (name) ->
throw new Error "undefined rule “#{name}"
constructor: ->
# Items are stored here indexed by key.
#
# The prototype of this object is set to `null` to avoid pollution
# from enumerable properties of `Object.prototype` and the
# performance hit of `hasOwnProperty o`.
@_byKey = Object.create null
# Hooks are stored here indexed by moment.
@_hooks = {
beforeDispatch: []
beforeUpdate: []
beforeSave: []
afterRule: []
}
# Rules are stored here indexed by name.
#
# The prototype of this object is set to `null` to avoid pollution
# from enumerable properties of `Object.prototype` and to be able
# to use the `name of @_rules` syntax.
@_rules = Object.create null
# Register a hook to run at a given point.
#
# A hook receives as parameter an event object with the following
# properties:
# - `preventDefault()`: prevents the next default action from
# happening;
# - `stopPropagation()`: prevents other hooks from being run.
#
# Note: if a hook throws an exception, `event.stopPropagation()`
# then `event.preventDefault()` will be called and the exception
# will be forwarded.
#
# # Item hook
#
# Valid items related moments are:
# - beforeDispatch: even before the item has been dispatched;
# - beforeUpdate: after the item has been dispatched but before
# updating its value.
# - beforeSave: after the item has been updated.
#
# An item hook is run in the context of the current item.
#
# # Rule hook
#
# Valid rules related moments are:
# - afterRule: just after a new rule has been defined (even
# singleton).
#
# An item hook is run in the context of the current rule.
hook: (name, hook) ->
# Allows a nicer syntax for CoffeeScript.
if $_.isObject name
# Extracts the name and the value from the first property of the
# object.
do ->
object = name
return for own name, hook of object
hooks = @_hooks[name]
@_assert(
hooks?
"invalid hook moment “#{name}"
)
hooks.push hook
# Register a new singleton rule.
#
# See the `rule()` method for more information.
item: (name, definition) ->
# Creates the corresponding rule.
rule = @rule name, definition, true
# Creates the singleton.
item = {
rule: rule.name
key: rule.key() # No context because there is not generator.
val: undefined
}
@_updateItems [item], true
# Register a new rule.
#
# If the definition is a function, it will be run in the context of
# an item-like object with the following properties:
# - `key`: the definition for the key of this item;
# - `val`: the definition for the value of this item.
#
# Warning: The definition function is run only once!
rule: (name, definition, singleton = false) ->
# Allows a nicer syntax for CoffeeScript.
if $_.isObject name
# Extracts the name and the definition from the first property
# of the object.
do ->
object = name
return for own name, definition of object
@_assert(
name not of @_rules
"the rule “#{name}” is already defined"
)
# Extracts the rule definition.
if $_.isFunction definition
ctx = {
name: name
key: undefined
val: undefined
singleton
}
definition.call ctx
else
ctx = {
key: definition?.key
val: definition?.val
singleton
}
# Runs the `afterRule` hook and returns if the registration has
# been prevented.
return unless @_runHook 'afterRule', ctx
{key, val} = ctx
# The default key.
key ?= if singleton then -> name else -> @genkey
# The default value.
val ?= -> @genval
# Makes sure `key` is a function for uniformity.
key = $makeFunction key unless $_.isFunction key
# Register the new rule.
@_rules[name] = {
name
key
val
singleton
}
#--------------------------------
get: (keys) ->
if keys is undefined
items = $_.map @_byKey, (item) -> item.val
else
items = $mapInPlace (@_fetchItems keys), (item) -> item.val
if $_.isString keys then items[0] else items
getRaw: (keys) ->
if keys is undefined
items = item for _, item of @_byKey
else
items = @_fetchItems keys
if $_.isString keys then items[0] else items
remove: (keys) ->
@_removeItems (@_fetchItems keys)
set: (items, {add, update, remove} = {}) ->
add = true unless add?
update = true unless update?
remove = false unless remove?
itemsToAdd = {}
itemsToUpdate = {}
itemsToRemove = {}
$_.extend itemsToRemove, @_byKey if remove
$each items, (genval, genkey) =>
item = {
rule: undefined
key: undefined
val: undefined
genkey
genval
}
return unless @_runHook 'beforeDispatch', item
# Searches for a rule to handle it.
ruleName = @dispatch.call item
rule = @_rules[ruleName]
unless rule?
@missingRule ruleName
# If `missingRule()` has not created the rule, just keep this
# item.
rule = @_rules[ruleName]
return unless rule?
# Checks if this is a singleton.
@_assert(
not rule.singleton
"cannot add items to singleton rule “#{rule.name}"
)
# Computes its key.
key = rule.key.call item
@_assert(
$_.isString key
"the key “#{key}” is not a string"
)
# Updates known values.
item.rule = rule.name
item.key = key
if key of @_byKey
# Marks this item as not to be removed.
delete itemsToRemove[key]
if update
# Fetches the existing entry.
prev = @_byKey[key]
# Checks if there is a conflict in rules.
@_assert(
item.rule is prev.rule
"the key “#{key}” cannot be of rule “#{item.rule}”, "
"already used by “#{prev.rule}"
)
# Gets its previous value.
item.val = prev.val
# Registers the item to be updated.
itemsToUpdate[key] = item
# Note: an item will be updated only once per `set()` and
# only the last generator will be used.
else
if add
# Registers the item to be added.
itemsToAdd[key] = item
# Adds items.
@_updateItems itemsToAdd, true
# Updates items.
@_updateItems itemsToUpdate
# Removes any items not seen (iff `remove` is true).
@_removeItems itemsToRemove
# Forces items to update their value.
touch: (keys) ->
@_updateItems (@_fetchItems keys, true)
#--------------------------------
_assert: (cond, message) ->
throw new Error message unless cond
# Emits item related event.
_emitEvent: (event, items) ->
byRule = {}
# One per item.
$each items, (item) =>
@emit "key=#{item.key}", event, item
(byRule[item.rule] ?= []).push item
# One per rule.
@emit "rule=#{rule}", event, items for rule, items of byRule
# One for everything.
@emit "any", event, items
_fetchItems: (keys, ignoreMissingItems = false) ->
unless $_.isArray keys
keys = if $_.isObject keys then $_.keys keys else [keys]
items = []
for key in keys
item = @_byKey[key]
if item?
items.push item
else
@_assert(
ignoreMissingItems
"no item with key “#{key}"
)
items
_removeItems: (items) ->
return if $_.isEmpty items
$each items, (item) => delete @_byKey[item.key]
@_emitEvent 'exit', items
# Runs hooks for the moment `name` with the given context and
# returns false if the default action has been prevented.
_runHook: (name, ctx) ->
hooks = @_hooks[name]
# If no hooks, nothing to do.
return true unless hooks? and (n = hooks.length) isnt 0
# Flags controlling the run.
notStopped = true
actionNotPrevented = true
# Creates the event object.
event = {
stopPropagation: -> notStopped = false
# TODO: Should `preventDefault()` imply `stopPropagation()`?
preventDefault: -> actionNotPrevented = false
}
i = 0
while notStopped and i < n
hooks[i++].call ctx, event
# TODO: Is exception handling necessary to have the wanted
# behavior?
return actionNotPrevented
_updateItems: (items, areNew) ->
return if $_.isEmpty items
# An update is similar to an exit followed by an enter.
@_emitEvent 'exit', items unless areNew
$each items, (item) =>
return unless @_runHook 'beforeUpdate', item
{rule: ruleName} = item
# Computes its value.
do =>
# Item is not passed directly to function to avoid direct
# modification.
#
# This is not a true security but better than nothing.
proxy = Object.create item
updateValue = (parent, prop, def) ->
if not $_.isObject def
parent[prop] = def
else if $_.isFunction def
parent[prop] = def.call proxy, parent[prop]
else if $_.isArray def
i = 0
n = def.length
current = parent[prop] ?= new Array n
while i < n
updateValue current, i, def[i]
++i
else
# It's a plain object.
current = parent[prop] ?= {}
for i of def
updateValue current, i, def[i]
updateValue item, 'val', @_rules[ruleName].val
return unless @_runHook 'beforeSave', item
# Registers the new item.
@_byKey[item.key] = item
@_emitEvent 'enter', items
# TODO: checks for loops.
#=====================================================================
module.exports = {$MappedCollection2}

View File

@ -221,7 +221,7 @@ Api.fn = $requireTree('./api');
$register('api.getVersion', '0.1');
$register('xo.getAllObjects', function () {
return this.xo.xobjs.getAll();
return this.xo.xobjs.get();
});
// Returns the list of available methods similar to XML-RPC

View File

@ -1,5 +1,7 @@
$_ = require 'underscore'
# FIXME: This file name should reflect what's inside!
#=====================================================================
$asArray = (val) -> if $_.isArray val then val else [val]
@ -25,7 +27,7 @@ $removeValue = (array, value) ->
#---------------------------------------------------------------------
# TODO: currently the watch can be updated multiple times per
# $MappedCollection2.set() which is inefficient: there should be
# $MappedCollection.set() which is inefficient: it should be
# possible to address that.
$watch = (collection, {
@ -263,7 +265,7 @@ $set = (options) ->
changed = false
$each entered, (value) =>
if @value.indexOf value is -1
if (@value.indexOf value) is -1
@value.push value
changed = true

View File

@ -4,7 +4,7 @@ $sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection2} = require './MappedCollection2.coffee'
{$MappedCollection} = require './MappedCollection.coffee'
$nonBindedHelpers = require './helpers'
@ -16,7 +16,7 @@ describe 'Helper', ->
collection = $set = $sum = $val = null
beforeEach ->
# Creates the collection.
collection = new $MappedCollection2()
collection = new $MappedCollection()
# Dispatcher used for tests.
collection.dispatch = -> (@genkey.split '.')[0]

File diff suppressed because it is too large Load Diff

View File

@ -4,36 +4,28 @@ $sinon = require 'sinon'
#---------------------------------------------------------------------
{$MappedCollection2} = require './MappedCollection2.coffee'
$helpers = require './helpers'
{$MappedCollection} = require './MappedCollection.coffee'
# Helpers for dealing with fibers.
{$promisify} = require './fibers-utils'
#=====================================================================
describe 'spec2', ->
describe 'spec', ->
collection = null
before $promisify ->
# Creates the collection.
collection = new $MappedCollection2()
# Binds the helpers to the collection.
collection.helpers = do ->
helpers = {}
helpers[name] = fn.bind collection for name, fn of $helpers
helpers
collection = new $MappedCollection()
# Loads the spec.
(require './spec2').call collection
(require './spec').call collection
# Skips missing rules.
collection.missingRule = ( -> )
# Loads the mockup data.
collection.set (require './spec2.spec-data')
collection.set (require './spec.spec-data')
#console.log collection.get()

View File

@ -1,558 +0,0 @@
$_ = require 'underscore'
#---------------------------------------------------------------------
$xml2js = require 'xml2js'
#---------------------------------------------------------------------
# Helpers for dealing with fibers.
{$synchronize} = require './fibers-utils'
#=====================================================================
$isVMRunning = ->
switch @genval.power_state
when 'Paused', 'Running'
true
else
false
$isHostRunning = ->
@val.power_state is 'Running'
$isTaskLive = ->
@genval.status is 'pending' or @genval.status is 'cancelling'
# $xml2js.parseString() uses callback for synchronous code.
$parseXML = (XML) ->
options = {
mergeAttrs: true
explicitArray: false
}
result = null
$xml2js.parseString XML, options, (error, result_) ->
throw error if error?
result = result_
result
$retrieveTags = -> [] # TODO
#=====================================================================
module.exports = ->
{
$map
$set
$sum
$val
} = @helpers
# Shared watchers.
UUIDsToKeys = $map {
if: -> 'UUID' of @val
val: -> [@val.UUID, @key]
loopDetected: ( -> )
}
messages = $set {
rule: 'message'
bind: -> @val.$object
}
# Defines which rule should be used for this item.
#
# Note: If the rule does not exists, a temporary item is created. FIXME
@dispatch = ->
{$type: type} = @genval
# Subtypes handling for VMs.
if type is 'VM'
return 'VM-controller' if @genval.is_control_domain
return 'VM-snapshot' if @genval.is_a_snapshot
return 'VM-template' if @genval.is_a_template
type
# Missing rules should be created.
@missingRule = @rule
# Used to apply common definition to rules.
@hook afterRule: ->
return unless @val?
unless $_.isObject @val
throw new Error 'the value should be an object'
# Injects various common definitions.
@val.type = @name
unless @singleton
# This definition are for non singleton items only.
@key = -> @genval.$ref
@val.UUID = -> @genval.uuid
@val.ref = -> @genval.$ref
@val.poolRef = -> @genval.$poolRef
# Main objects all can have associated messages and tags.
if @name in ['host', 'pool', 'SR', 'VM', 'VM-controller']
@val.messages = messages
@val.$messages = -> @val.messages # Deprecated.
@val.tags = $retrieveTags
# Helper to create multiple rules with the same definition.
rules = (rules, definition) =>
@rule rule, definition for rule in rules
# An item is equivalent to a rule but one and only one instance of
# this rule is created without any generator.
@item xo: ->
@key = '00000000-0000-0000-0000-000000000000'
@val = {
# TODO: Maybe there should be high-level hosts: those who do not
# belong to a pool.
pools: $set {
rule: 'pool'
}
$CPUs: $sum {
rule: 'host'
val: -> +(@val.CPUs.cpu_count)
}
$running_VMs: $set {
rule: 'VM'
if: $isVMRunning
}
$vCPUs: $sum {
rule: 'VM'
val: -> @val.CPUs.number
if: $isVMRunning
}
# Do not work due to problem in host rule.
# $memory: $sum {
# rule: 'host'
# val: -> @val.memory
# init: {
# usage: 0
# size: 0
# }
# }
# Maps the UUIDs to keys (i.e. opaque references).
$UUIDsToKeys: UUIDsToKeys
}
@rule pool: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
SRs: $set {
rule: 'SR'
bind: -> @val.$container
}
HA_enabled: -> @genval.ha_enabled
hosts: $set {
rule: 'host'
bind: -> @genval.$poolRef
}
master: -> @genval.master
VMs: $set {
rule: 'VM'
bind: -> @val.$container
}
$running_hosts: $set {
rule: 'host'
bind: -> @genval.$poolRef
if: $isHostRunning
}
$running_VMs: $set {
rule: 'VM'
bind: -> @genval.$poolRef
if: $isHostRunning
}
$VMs: $set {
rule: 'VM'
bind: -> @genval.$poolRef
}
}
@rule host: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
address: -> @genval.address
controller: $val {
rule: 'VM-controller'
bind: -> @val.$container
val: -> @key
}
CPUs: -> @genval.cpu_info
enabled: -> @genval.enabled
hostname: -> @genval.hostname
iSCSI_name: -> @genval.other_config?.iscsi_iqn ? null
# memory: $sum {
# key: -> @genval.metrics # FIXME
# val: -> {
# usage: +@val.memory_total - @val.memory_free
# size: +@val.memory_total
# }
# init: {
# usage: 0
# size: 0
# }
# }
# TODO
power_state: 'Running'
# Local SRs are handled directly in `SR.$container`.
SRs: $set {
rule: 'SR'
bind: -> @val.$container
}
# Local VMs are handled directly in `VM.$container`.
VMs: $set {
rule: 'VM'
bind: -> @val.$container
}
$PBDs: -> @genval.PBDs
PIFs: -> @genval.PIFs
$PIFs: -> @val.PIFs
tasks: $set {
rule: 'task'
bind: -> @val.$container
if: $isTaskLive
}
$tasks: -> @val.tasks # Deprecated.
$running_VMs: $set {
rule: 'VM'
bind: -> @val.$container
if: $isVMRunning
}
$vCPUs: $sum {
rule: 'VM'
bind: -> @val.$container
if: $isVMRunning
val: -> @val.CPUs.number
}
}
# This definition is shared.
VMdef = ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
# address: {
# ip: $val {
# key: -> @genval.guest_metrics # FIXME
# val: -> @val.networks
# default: null
# }
# }
# consoles: $set {
# key: -> @genval.consoles # FIXME
# }
memory: {
usage: null
# size: $val {
# key: -> @genval.guest_metrics # FIXME
# val: -> +@val.memory_actual
# default: +@genval.memory_dynamic_min
# }
}
power_state: -> @genval.power_state
CPUs: {
number: 0
# number: $val {
# key: -> @genval.metrics # FIXME
# val: -> +@genval.VCPUs_number
# # FIXME: must be evaluated in the context of the current object.
# if: -> @gen
# }
}
$CPU_usage: null #TODO
# FIXME: $container should contains the pool UUID when the VM is
# not on a host.
$container: ->
if $isVMRunning.call this
@genval.resident_on
else
# TODO: Handle local VMs.
@genval.$poolRef
snapshots: -> @genval.snapshots
# TODO: Replace with a UNIX timestamp.
snapshot_time: -> @genval.snapshot_time
$VBDs: -> @genval.VBDs
VIFs: -> @genval.VIFs
$VIFs: -> @val.VIFs # Deprecated
}
@rule VM: VMdef
@rule 'VM-controller': VMdef
@rule 'VM-snapshot': VMdef
# VM-template starts with the same definition but extends it.
@rule 'VM-template': ->
VMdef.call this
@val.template_info = {
arch: -> @genval.other_config?['install-arch']
disks: ->
disks = @genval.other_config?.disks
return [] unless disks?
disks = ($parseXML disks)?.provision?.disk
return [] unless disks?
disks = [disks] unless $_.isArray disks
# Normalize entries.
for disk in disks
disk.bootable = disk.bootable is 'true'
disk.size = +disk.size
disk.SR = disk.sr
delete disk.sr
disks
install_methods: ->
methods = @genval.other_config?['install-methods']
return [] unless methods?
methods.split ','
}
@rule SR: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
SR_type: -> @genval.type
content_type: -> @genval.content_type
physical_usage: -> +@genval.physical_utilisation
usage: -> +@genval.virtual_allocation
size: -> +@genval.physical_size
$container: ->
if @genval.shared
@genval.$poolRef
else
null # TODO
$PBDs: -> @genval.PBDs
VDIs: -> @genval.VDIs
$VDIs: -> @val.VDIs # Deprecated
}
@rule PBD: ->
@val = {
attached: -> @genval.currently_attached
host: -> @genval.host
SR: -> @genval.SR
}
@rule PIF: ->
@val = {
attached: -> @genval.currently_attached
device: -> @genval.device
IP: -> @genval.IP
ip: -> @val.IP # Deprecated
$host: -> @genval.host
#host: -> @val.$host # Deprecated
MAC: -> @genval.MAC
mac: -> @val.MAC # Deprecated
# TODO: Find a more meaningful name.
management: -> @genval.management
mode: -> @genval.ip_configuration_mode
MTU: -> +@genval.MTU
mtu: -> @val.MTU # Deprecated
netmask: -> @genval.netmask
$network: -> @genval.network
# TODO: What is it?
#
# Could it mean is this a physical interface?.
# How could a PIF not be physical?
#physical: -> @genval.physical
}
@rule VDI: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
# TODO: determine whether or not tags are required for a VDI.
#tags: $retrieveTags
usage: -> +@genval.physical_utilisation
size: -> +@genval.virtual_size
$snapshot_of: ->
original = @genval.snapshot_of
if original is 'OpaqueRef:NULL'
null
else
original
snapshot_of: -> @val.$snapshot_of # Deprecated
snapshots: -> @genval.snapshots
# TODO: Does the name fit?
#snapshot_time: -> @genval.snapshot_time
$SR: -> @genval.SR
SR: -> @val.$SR # Deprecated
$VBDs: -> @genval.VBDs
$VBD: -> # Deprecated
{VBDs} = @genval
if VBDs.length is 0 then null else VBDs[0]
}
@rule VBD: ->
@val = {
attached: -> @genval.currently_attached
VDI: -> @genval.VDI
VM: -> @genval.VM
}
@rule VIF: ->
@val = {
attached: -> @genval.currently_attached
# TODO: Should it be cast to a number?
device: -> @genval.device
MAC: -> @genval.MAC
mac: -> @val.MAC # Deprecated
MTU: -> +@genval.MTU
mtu: -> @val.MTU # Deprecated
$network: -> @genval.network
$VM: -> @genval.VM
VM: -> @val.$VM # Deprecated
}
@rule network: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
# TODO: determine whether or not tags are required for a VDI.
#tags: $retrieveTags
bridge: -> @genval.bridge
MTU: -> +@genval.MTU
PIFs: -> @genval.PIFs
VIFs: -> @genval.VIFs
}
@rule message: ->
@val = {
# TODO: UNIX timestamp?
time: -> @genval.timestamp
$object: ->
# If the key of the concerned object has already be resolved
# returns the known value.
return @val.$object if @val.$object?
# Tries to resolve the key of the concerned object.
object = (UUIDsToKeys.call this)[@genval.obj_uuid]
# If resolved, unregister from the watcher.
UUIDsToKeys.unregister.call this if object?
object
# TODO: Are these names meaningful?
name: -> @genval.name
body: -> @genval.body
}
@rule task: ->
@val = {
name_label: -> @genval.name_label
name_description: -> @genval.name_description
progress: -> +@genval.progress
result: -> @genval.result
$host: -> @genval.resident_on
$container: -> @val.$host # Deprecated
created: -> @genval.created
finished: -> @genval.finished
current_operations: -> @genval.current_operations
status: -> @genval.status
}

View File

@ -19,7 +19,7 @@ $createRedisClient = (require 'then-redis').createClient
# A mapped collection is generated from another collection through a
# specification.
$MappedCollection = require './MappedCollection'
{$MappedCollection} = require './MappedCollection'
# Collection where models are stored in a Redis DB.
$RedisCollection = require './collection/redis'
@ -150,13 +150,8 @@ class $XO extends $EventEmitter
@tokens.remove (token.id for token in tokens)
# Collections of XAPI objects mapped to XO API.
refsToUUIDs = { # Needed for the mapping.
'OpaqueRef:NULL': null
}
@xobjs = do ->
spec = (require './spec') refsToUUIDs
new $MappedCollection spec
@xobjs = new $MappedCollection()
(require './spec').call @xobjs
# XAPI connections.
@xapis = {}
@ -189,11 +184,8 @@ class $XO extends $EventEmitter
types.push type
types
# This helper normalizes a record by inserting its type and by
# storing its UUID in the `refsToUUIDs` map if any.
# This helper normalizes a record by inserting its type.
normalizeObject = (object, ref, type) ->
refsToUUIDs[ref] = object.uuid if object.uuid? # TODO: remove
object.$pool = poolUUID unless type is 'pool' # TODO: remove
object.$poolRef = poolRef unless type is 'pool'
object.$ref = ref
object.$type = type
@ -290,11 +282,14 @@ class $XO extends $EventEmitter
throw error unless error[0] is 'SESSION_NOT_REGISTERED'
# Prevents errors from stopping the server.
connectSafe = $fiberize (server) =>
connectSafe = $fiberize (server) ->
try
connect server
catch error
console.log "[WARN] #{server.host}:", error[0] ? error.code ? error
console.error(
"[WARN] #{server.host}:"
error[0] ? error.stack ? error.code ? error
)
# Connects to existing servers.
connectSafe server for server in $wait @servers.get()