debug = require('debug')
logger = debug('yang:element')
delegate = require('delegates')
Emitter = require('events').EventEmitter
Emitter.defaultMaxListeners = 100
kIndex = Symbol.for('element:index');
kCache = Symbol.for('element:cache');
class Element
@property: (prop, desc) ->
Object.defineProperty @prototype, prop, desc
@use: ->
res = [].concat(arguments...)
.filter (x) -> x?
.map (elem) =>
exists = Element::match.call this, elem.kind, elem.tag
if exists?
@debug => "use: using previously loaded '#{elem.kind}:#{elem.tag}'"
return exists
try Element::merge.call this, elem
catch e
throw @error "use: unable to merge '#{elem.kind}:#{elem.tag}'", e
return switch
when res.length > 1 then res
when res.length is 1 then res[0]
else undefined
@logger: logger
@debug: (f) -> switch
when debug.enabled @logger.namespace then switch
when typeof f is 'function' then @logger @uri, [].concat(f())...
else @logger @uri, arguments...
@error: (err, ctx) ->
err = new Error err unless err instanceof Error
err.uri = @uri
err.src = this
err.ctx = ctx
return err
logger: @logger
debug: @debug
error: @error
constructor: (@kind, @tag, scope) ->
unless @kind?
throw @error "must supply 'kind' to create a new Element"
@scope = scope if scope?
Object.defineProperties this,
parent: value: null, writable: true
origin: value: null, writable: true
state: value: {}, writable: true
[kIndex]: value: 0, writable: true
emitter: value: new Emitter
delegate @prototype, 'emitter'
.method 'emit'
.method 'once'
.method 'on'
.method 'off' @property 'datakey',
get: -> @tag ? @kind
@property 'uri',
get: -> switch
when @parent instanceof Element
mark = @kind
mark += "(#{@tag})" if @tag? and @parent.scope?[@kind] in [ '0..n', '1..n', '*' ]
"#{@parent.uri}/#{mark}"
when @tag?
"#{@kind}(#{@tag})"
else
@kind
@property 'root',
get: -> switch
when @parent instanceof Element then @parent.root
when @origin instanceof Element then @origin.root
else this
@property 'children',
get: ->
unless this[kCache]?
elements = (v for own k, v of this when k not in [ 'parent', 'origin', 'tag', kCache, 'source' ])
.reduce ((a,b) -> switch
when b instanceof Element then a.concat b
when b instanceof Array
a.concat b.filter (x) -> x instanceof Element
else a
), []
this[kCache] = elements.sort (a,b) -> a[kIndex] - b[kIndex]
return this[kCache];
@property '*', get: -> @children
@property '..', get: -> @parent clone: (opts={}) ->
{ origin = @origin, relative = true } = opts
@debug => "cloning #{@kind}:#{@tag} with #{@children.length} elements"
copy = (new @constructor @kind, @tag, @source).extends @children.map (x) -> x.clone opts
copy.state = Object.create(@state)
copy.state.relative = relative
copy.origin = origin ? this
return copyThis is the primary mechanism for defining sub-elements to become part of the element tree
extends: ->
elems = ([].concat arguments...).filter (x) -> x? and !!x
return this unless elems.length > 0
elems.forEach (expr) => @merge expr
@emit 'change', elems...
return thisThis helper method merges a specific Element into current Element
while performing @scope validations.
merge: (elem, opts={}) ->
unless elem instanceof Element
throw @error "cannot merge invalid element into Element", elem
elem.parent = this
elem[kIndex] = @children.length if @children?
this[kCache] = null
_merge = (item) ->
if not item.node or opts.append or item.datakey not in (@keys ? [])
@push item
true
else if opts.replace is true
for x, i in this when x.datakey is item.datakey
@splice i, 1, item
break
true
else false
unless @scope?
unless @hasOwnProperty elem.kind
@[elem.kind] = elem
return elem
unless Array.isArray @[elem.kind]
exists = @[elem.kind]
@[elem.kind] = [ exists ]
Object.defineProperty @[elem.kind], 'keys',
get: (-> @map (x) -> x.datakey ).bind @[elem.kind]
unless _merge.call @[elem.kind], elem
throw @error "constraint violation for '#{elem.kind} #{elem.datakey}' - cannot define more than once"
return elem
unless elem.kind of @scope
if elem.scope? and (not elem.source.state.unbound and not @source.state.unbound)
@debug => @scope
throw @error "scope violation - invalid '#{elem.kind}' extension found"
else
@scope[elem.kind] = '*' # this is hackish...
switch @scope[elem.kind]
when '0..n', '1..n', '*'
unless @hasOwnProperty elem.kind
@[elem.kind] = []
Object.defineProperty @[elem.kind], 'keys',
get: (-> @map (x) -> x.datakey ).bind @[elem.kind]
unless Array.isArray @[elem.kind]
exists = @[elem.kind]
@[elem.kind] = [ exists ]
Object.defineProperty @[elem.kind], 'keys',
get: (-> @map (x) -> x.datakey ).bind @[elem.kind]
unless _merge.call @[elem.kind], elem
throw @error "constraint violation for '#{elem.kind} #{elem.datakey}' - already defined"
when '0..1', '1'
unless @hasOwnProperty elem.kind
@[elem.kind] = elem
else if opts.replace is true
@debug => "replacing pre-existing #{elem.kind}"
@[elem.kind] = elem
else
throw @error "constraint violation for '#{elem.kind}' - cannot define more than once"
else
throw @error "unrecognized scope constraint defined for '#{elem.kind}' with #{@scope[elem.kind]}"
return elem
removes: ->
elems = ([].concat arguments...).filter (x) -> x? and !!x
return this unless elems.length > 0
elems.forEach (expr) => @remove expr
@emit 'change', elems...
return this
remove: (elem) ->
unless elem instanceof Element
throw @error "cannot remove a non-Element from an Element", elem
exists = Element::match.call this, elem.kind, elem.datakey
return this unless exists?
if Array.isArray @[elem.kind]
@[elem.kind] = @[elem.kind].filter (x) -> x.datakey isnt elem.datakey
delete @[elem.kind] unless @[elem.kind].length
else
delete @[elem.kind]
this[kCache] = null
return thisThis alternative form of merge performs conditional merge based on existence check. It is considered safer alternative to direct merge call.
# performs conditional merge based on existence
update: (elem) ->
unless elem instanceof Element
throw @error "cannot update a non-Element into an Element", elem
#@debug => "update with #{elem.kind}/#{elem.tag}"
exists = switch
when elem.tag? then Element::match.call this, elem.kind, elem.datakey
else Element::match.call this, elem.kind
return @merge elem unless exists?
#@debug => "update #{exists.kind} in-place for #{elem.children.length} elements"
exists.update target for target in elem.children
return exists
# Looks for matching Elements using kind and tag
# Direction: up the hierarchy (towards root)
lookup: (kind, tag) ->
#@debug => "lookup: #{kind}(#{tag})..."
res = switch
when this not instanceof Object then undefined
when this instanceof Element then @match kind, tag
else Element::match.call this, kind, tag
res ?= switch
when @origin? then Element::lookup.apply @origin, arguments
when @parent? then Element::lookup.apply @parent, arguments
else Element::match.call @constructor, kind, tag
#@debug => "lookup: #{kind}(#{tag}) got result: #{res?}"
return res
# Looks for matching Elements using YPATH notation
# Direction: down the hierarchy (away from root)
at: -> @locate arguments...
locate: (ypath) ->
return unless ypath?
if typeof ypath is 'string'
@debug => "locate: #{ypath}"
ypath = ypath.replace /\s/g, ''
if (/^\//.test ypath) and this isnt @root
return @root.locate ypath
[ key, rest... ] = ypath.split('/').filter (e) -> !!e
else
@debug => "locate: #{ypath.join('/')}"
[ key, rest... ] = ypath
return this unless key?
match = switch
when key is '..' then @match key
else @match '*', key
match ?= @match key if @scope[key] in ['0..1', '1']
return switch
when rest.length > 0 then match?.locate rest
else match
# Looks for a matching Element(s) in immediate sub-elements
match: (kind, tag) ->
return unless kind? and @[kind]?
return @[kind] unless tag?
match = @[kind]
match = [ match ] unless match instanceof Array
return match if tag is '*'
for elem in match when elem instanceof Element
return elem if tag is elem.datakey or tag is elem.tag
return undefinedConverts the Element into a JS object
toJSON: (opts={ tag: true, extended: false }) ->
#@debug => "converting #{@kind} toJSON with #{@children.length}"
sub =
@children
.filter (x) => opts.extended or x.parent is this
.reduce ((a,b) ->
for k, v of b.toJSON()
if a[k] instanceof Object
a[k][kk] = vv for kk, vv of v if v instanceof Object
else
a[k] = v
return a
), {}
if opts.tag
"#{@kind}": switch
when Object.keys(sub).length > 0
if @tag? then "#{@tag}": sub else sub
when @tag instanceof Object then "#{@tag}"
else @tag
else submodule.exports = Element