Promise = require 'promise'
class Meta
@__meta__: synth: 'meta'
@__version__: 3
tokenize = (key) -> ((key?.split? '.')?.filter (e) -> !!e) ? []
@instanceof: (obj) ->
(obj?.instanceof is arguments.callee or obj?.hasOwnProperty? '__meta__')
@synthesized: (obj) ->
(@instanceof obj) and (obj.get 'synth') is (@get 'synth')
@copy: (dest={}, src, append=false) ->
for p of src
switch
when src[p]?.constructor is Object
dest[p] ?= {}
unless dest[p] instanceof Object
k = dest[p]
dest[p] = {}
dest[p][k] = null
arguments.callee dest[p], src[p], append
when append is true and dest[p]?
unless dest[p] instanceof Object
k = dest[p]
dest[p] = {}
dest[p][k] = null
dest[p][src[p]] = null
else dest[p] = src[p]
return dest
@objectify: (key, val) ->
return key if key instanceof Object
composite = tokenize key
unless composite.length
return val ? {}
obj = root = {}
while (k = composite.shift())
last = r: root, k: k
root = root[k] = {}
last.r[last.k] = val
obj
@configure: (f, args...) -> f?.apply? this, args; this
@extend: (obj) ->
@[k] = v for k, v of obj when k isnt '__super__' and k not in Object.keys Meta
this
@include: (obj) ->
@::[k] = v for k, v of obj when k isnt 'constructor' and k not in Object.keys Meta.prototype
this
The mixin
convenience function essentially fuses the target class
obj(s) into itself.
@mixin: (objs...) ->
for obj in objs when obj instanceof Object
@extend obj
@include obj.prototype
continue unless Meta.instanceof obj
# when mixing in another Meta object, merge the 'bindings'
# as well
@merge obj.extract 'bindings'
this
The following get/extract/match
provide meta data retrieval mechanisms.
@get: (key) ->
return unless key? and typeof key is 'string'
root = @__meta__ ? this
composite = tokenize key
root = root?[key] while (key = composite.shift())
root
@extract: (keys...) ->
return Meta.copy {}, (@__meta__ ? this) unless keys.length > 0
res = {}
Meta.copy res, Meta.objectify key, (@get? key) ? @[key] for key in keys
res
@match: (regex) ->
root = @__meta__ ? this
obj = {}
obj[k] = v for k, v of root when (k.match regex)
obj
The following clear/delete
provides meta data removal mechanisms
unwindObject = (obj, key) ->
[ pre..., key ] = tokenize key
return unless obj? and key?
obj = obj[k] while k = pre.shift() when obj instanceof Object
return root: obj, key: key if obj?
@clear: (key) ->
o = unwindObject (@__meta__ ? this), key
return unless o?
val = o.root[o.key]
o.root[o.key] = switch
when val instanceof Array then []
when val instanceof Object then {}
else undefined
@delete: (key) ->
o = unwindObject (@__meta__ ? this), key
return unless o?
orig = o.root[o.key]
delete o.root[o.key]
return orig
The following set/merge
provide meta data update mechanisms.
@set: (key, val) ->
obj = Meta.objectify key, val
@__meta__ = Meta.copy (Meta.copy {}, @__meta__), obj
this
@merge: (key, obj) ->
return this unless key?
unless typeof key is 'string'
(@merge k, v) for k, v of (key.__meta__ ? key) when k isnt 'synth'
return this
target = @get key
switch
when not target? then @set key, obj
when (Meta.instanceof target) and (Meta.instanceof obj)
target.merge obj
when target instanceof Function and obj instanceof Function
target.mixin? obj
when target instanceof Array and obj instanceof Array
@set key, target.concat obj...
when target instanceof Object and obj instanceof Object
@set "#{key}.#{k}", v for k, v of obj
when typeof target is typeof obj
@set key, obj
else
console.log "performing merge for '#{key}' with existing value type (#{typeof target}) conflicting with passed-in value (#{typeof obj})"
@set key, obj
this
The bind
function associates the passed in key/object into the meta
class so that when this class object is instantiated, all the bound
objects are actualized during construction. It protects the key under
question so that the binding can only take place once for a given key.
Nested bindings are also supported but only if nested keys each
resolve to a pre-existing instance of Meta class that supports bind
function.
@bind: (key, obj) ->
return this unless key?
unless typeof key is 'string'
(@bind k, v) for k, v of key
return this
[ key, rest... ] = tokenize key
if rest.length > 0
res = (@get "bindings.#{key}")?.bind? (rest.join '.'), obj
unless res?
throw new Error "unable to bind to non-existent prefix #{key}"
else
unless (@get "bindings.#{key}")? then @set "bindings.#{key}", obj
this
@unbind: (key) ->
unless key? then @clear 'bindings'; return this
[ key, rest... ] = tokenize key
if rest.length > 0
(@get "bindings.#{key}")?.unbind? (rest.join '.')
else
@delete "bindings.#{key}"
@rebind: (key, target) ->
prev = @unbind key
if target instanceof Function
@bind key, target.call this, prev
else
@bind key, target
@override: (key, obj) ->
return this unless key?
if typeof key is 'object'
(@override k, v) for k, v of key
return this
[ key, rest... ] = tokenize key
if rest.length > 0
(@get "bindings.#{key}")?.override? (rest.join '.'), obj
else
obj = [ obj ] unless obj instanceof Array
@merge "overrides.#{key}", obj
this
The following reduce
provides meta data extrapolation by collapsing
nested Meta
instances into object format for singular JS object
output
@reduce: (opts={}) ->
meta = @extract()
o = meta: meta
if not opts.depth? or opts.depth-- > 0
for key, val of meta.bindings
o[key] = switch
when (@instanceof val) then val.reduce opts
else val
delete meta.bindings
for key, val of meta
meta[key] = switch
when (@instanceof val) then val.reduce opts
else val
return o
constructor: (value, parent) ->
return class extends Meta if @constructor is Object
@parent = parent if parent?
bindings = (@constructor.extract 'bindings').bindings
bindings ?= {}
for k, overrides of (@constructor.get 'overrides')
console.debug? "overriding #{k}"
for override in overrides
bindings[k] = switch
when override instanceof Function
override.call @constructor, bindings[k]
else override
@attach k, v for k, v of bindings
@set value if value?
attach: (key, val) ->
switch
when (Meta.instanceof val)
@properties ?= {}
@properties[key] = new val undefined, this
when val instanceof Function
@methods ?= {}
@methods[key] = val
when val?.constructor is Object
(@attach "#{key}:#{k}", v) for k,v of val
else
@properties ?= {}
@properties[key] = val
detach: (key) ->
match = @access key
return unless match?
[ rest..., key ] = tokenize key
if match?.parent?.properties?.hasOwnProperty key
delete match.parent.properties[key]
return match
fork: (f, args...) -> f?.apply? (new @constructor @get()), args
meta: (key) -> @constructor.get key
access: (key) ->
[ key, rest... ] = tokenize key
return unless key? and typeof key is 'string'
prop = @properties?[key]
return unless prop?
switch
when rest.length is 0 then prop
else prop?.access? (rest.join '.')
seek: (query, meta=true) ->
return unless typeof query is 'object'
for k, v of query
value = switch
when (this instanceof Meta) then (if meta then @meta k else @get k)
else @[k]
unless (switch
when v instanceof Function then (v.call this, value)
else value is v)
return unless @parent?
return arguments.callee.call @parent, query, meta
return this
get: (key) ->
[ key, rest... ] = tokenize key
switch
when @properties? and key?
p = @access key
if p?.get? then p.get (if rest.length then (rest.join '.') else undefined)
else p
when @properties?
@value = {}
for k, v of @properties
@value[k] = if v.get? then v.get?() else v
@value
when key? then rest.unshift key; Meta.get.call @value, rest.join '.'
else @value
set: (key, val) ->
if typeof key is 'string' and val?
key = Meta.objectify key, val
if @properties? and key instanceof Object
for k, v of key when @properties.hasOwnProperty k
p = @access k
if p?.set? then p.set v else @properties[k] = v
else
@value = key
this
invoke: (input, args...) ->
if input instanceof Array
# a magical one-liner...
return Promise.all input.map (f) => @invoke ([f].concat args)...
method = input if input instanceof Function
method ?= @methods?[input]
unless method instanceof Function
return Promise.reject "cannot invoke undefined '#{input}' method"
new Promise (resolve, reject) =>
method.apply this, args.concat [ resolve, reject ]
valueOf: -> @constructor.extract()
toString: -> @meta 'name' ? @meta 'synth'
module.exports = Meta