The Property
class is the secretive shadowy element that governs
Object
behavior and are bound to the Object
via
Object.defineProperty
. It acts like a shadow Proxy/Reflector
to
the Object
instance and provides tight control via the
Getter/Setter
interfaces.
Below are list of properties available to every instance of Property
:
property | type | mapping | description |
---|---|---|---|
name | string | direct | name of the property |
schema | object | direct | a schema instance (usually Yang) |
state | object | direct | private object holding internal state |
configurable | boolean | getter(state) | defines whether this property can be redefined |
enumerable | boolean | getter(state) | defines whether this property is enumerable |
content | any | computed | getter/setter for state.value |
context | object | computed | dynamically generated using context |
root | Property | computed | dynamically returns the root Property instance |
path | XPath | computed | dynamically generate XPath for this Property from root |
debug = require('debug')
delegate = require 'delegates'
context = require './context'
Yang = require './yang'
XPath = require './xpath'
class Property
@property: (prop, desc) ->
Object.defineProperty @prototype, prop, desc
logger: debug('yang:property')
debug: (f) -> switch
when debug.enabled @logger.namespace then switch
when typeof f is 'function' then @logger @uri, [].concat(f())...
else @logger @uri, arguments...
constructor: (spec={}) ->
# NOTE: ES6/CS2 does not support below
# unless this instanceof Property then return new Property arguments...
# 1. parse if spec is YANG definition (string)
spec = Yang.parse spec if typeof spec is 'string'
# 2. assign if spec is an instance of Yang schema
schema = spec if spec instanceof Yang
# 3. destructure spec as an object if not schema as instance
{ name, schema } = spec unless schema?
# 4. parse if schema is YANG definition (string)
schema = Yang.parse schema if typeof schema is 'string'
schema ?= kind: 'anydata'
# 5. initialize property instance
@name = name ? schema.datakey
@schema = schema
@state =
strict: false
private: false
mutable: `schema.config != false`
attached: false
replaced: false
changed: false
locked: false
prior: undefined
value: undefined
parent: null
# 6. soft freeze this instance
Object.preventExtensions this
delegate @prototype, 'state'
.access 'parent'
.access 'strict'
.getter 'private'
.getter 'mutable'
.getter 'attached'
.getter 'replaced'
.getter 'changed'
.getter 'locked'
.getter 'prior'
.getter 'value'
delegate @prototype, 'schema'
.getter 'tag'
.getter 'kind'
.getter 'type'
.getter 'default'
.getter 'external'
.getter 'binding'
.method 'locate'
.method 'lookup'
@property 'key',
get: -> @name
@property 'value',
get: -> @state.value
@property 'data',
set: (value) -> @set value, { force: true }
get: -> switch
when @binding?.get? then @binding.get @context
else @value
@property 'enumerable',
get: -> not @private and (@value? or @binding?)
@property 'active',
get: -> @enumerable and @value?
@property 'change',
get: -> switch
when @changed and not @active then null
when @changed then @data
@property 'context',
get: ->
ctx = Object.create(context)
ctx.opts = {}
ctx.node = this
Object.preventExtensions ctx
return ctx
@property 'root',
get: ->
return this if @kind is 'module'
root = switch
when @parent is this then this
when @parent instanceof Property then @parent.root
else this
@state.path = undefined unless @state.root is root
return @state.root = root
@property 'path',
get: ->
if this is @root
entity = switch
when @kind is 'module' then '/'
else '.'
return XPath.parse entity, @schema
@state.path ?= @parent.path.clone().append @key
return @state.path
@property 'uri',
get: -> switch
when @parent? and @parent.uri? then "#{@parent.uri}/#{@key}"
when @parent? then @key
else @schema.datapath ? @schema.uri
clone: (state = {}) ->
throw @error 'must clone with state typeof object' unless typeof state is 'object'
copy = new @constructor this
copy.state = Object.assign(Object.create(@state), state, origin: this)
return copy
equals: (a, b) -> switch @kind
when 'leaf-list'
return false unless a and b
a = Array.from(new Set([].concat(a)))
b = Array.from(new Set([].concat(b)))
return false unless a.length is b.length
a.every (x) => b.some (y) => x is y
else a is b
This is the main Getter
for the target object's property value. When
called with optional key
it will perform an internal
find operation to traverse/locate that value being
requested instead of returning its own @data
.
It also provides special handling based on different types of
@data
currently held.
get: (key) -> switch
when key?
try match = @find key
return unless match? and match.length
switch
when match.length is 1 then match[0].data
when match.length > 1 then match.map (x) -> x.data
else undefined
else @data
This is the main Setter
for the target object's property value. It
utilizes internal @schema
attribute if available to enforce schema
validations.
set: (value, opts={}) ->
opts.origin ?= this
# @debug => "[set] enter..."
unless @mutable or not value? or opts.force
throw @error "cannot set data on read-only (config false) element", 'set'
try value = @binding.set @context.with(opts), value if @binding?.set?
catch e
throw @error e, 'set'
return this if value? and @equals value, @value # return if same value
bypass = opts.bypass and @kind in ["leaf", "leaf-list"]
@debug => "[set] applying schema..."
value = switch
when @schema.apply? and not bypass
subopts = Object.assign {}, opts, inner: true
try @schema.apply value, this, subopts
catch e then throw @error e, 'set'
else value
@debug => "[set] done applying schema..."
return this if value instanceof Error
return this if value? and @equals value, @value # return if same value
@debug => "[set] replaced? #{@state.value?}"
@state.replaced = @state.prior? or @state.value?
@update value, opts
Performs a granular merge of value
into existing @value
if
available, otherwise performs set operation.
merge: (value, opts={}) ->
opts.origin ?= this
return @delete opts if value is null
return @set value, opts
delete: (opts={}) ->
opts.origin ?= this
#return this if @state.value is null
return this unless @state.value?
if not opts.force and @binding?.delete?
try @binding.delete @context.with(opts), null
catch e
throw @error e, 'delete'
@state.replaced = @state.prior? # revisit if we need to do this
@update null, opts
Updates the value to the data model. Called once for each node that is part of the change branch.
update: (value, opts={}) ->
opts.origin ?= this
@state.changed or= @state.value isnt value
@state.value = value
@parent?.update this, opts
return this
Commits the changes to the data model. Called once for each node that is part of the change branch.
lock: (opts={}) ->
@state.locked = true
@state.delta = @change
opts.lock = this
return this
unlock: (opts={}) ->
@state.locked = false
# @state.delta = undefined
delete opts.lock
return this
commit: (opts={}) ->
return this unless @changed
try
await @lock opts
# 1. perform the bound commit transaction
if not opts.sync and @binding?.commit?
(@debug => "[commit] execute commit binding...")
await @binding?.commit? @context.with(opts)
# 2. wait for the parent to commit unless called by parent
await @parent?.commit? opts unless opts.inner
# 3. self-clean only if no parent
@clean opts if not @parent?
catch err
@debug => "[commit] revert due to #{err.message}"
await @revert opts
throw @error err, 'commit'
finally
@unlock opts
return this
revert: (opts={}) ->
return unless @changed
id = opts.seq
@debug "[revert:#{id}] changing back from:", @state.value
@debug "[revert:#{id}] changing back to:", @state.prior
temp = @state.value
@state.value = @state.prior
@state.prior = temp # preserve what we were trying to change to within commit context
@debug "[revert:#{id}] execute binding..." unless opts.sync
try
await @binding?.commit? @context.with(opts) unless opts.sync
catch err
@debug "[revert:#{id}] failed due to: #{err.message}"
# throw @error err, 'revert'
@debug "[revert:#{id}] cleaning up..."
@clean opts
clean: (opts={}) ->
# @state.prior = @state.value # save current value as prior
@state.prior = @toJSON() # save copy of current data as prior
@state.changed = false
@state.replaced = false
@debug "[clean:#{opts.seq}] finalized commit, persisted:", @state.prior
This call is the primary mechanism via which the Property
instance
attaches itself to the provided target obj
. It defines itself in the
target obj
via Object.defineProperty
.
attach: (obj, parent, opts) ->
return obj unless obj instanceof Object
opts ?= { replace: false, force: false }
@parent = parent
# if joining for the first time, apply existing data unless explicit replace
unless @attached
# @debug "[attach] applying existing data for #{@name} (external: #{@external}) to:"
# @debug obj
name = switch
when @parent?.external and @tag of obj then @tag
when @external then @name
when @name of obj then @name
else "#{@root.name}:#{@name}" # should we ensure root is kind = module?
@set obj[name], Object.assign {}, opts, inner: true
unless opts.preserve
try Object.defineProperty obj, @name,
configurable: true
enumerable: @enumerable
get: (args...) => @get args...
set: (args...) => @set args...
@state.attached = true
@debug => "[attach] attached into #{obj.constructor.name} container"
return obj
This helper routine can be used to allow traversal to other elements
in the data tree from the relative location of the current Property
instance. It returns matching Property
instances based on the
provided pattern
in the form of XPATH or YPATH.
It is internally used via get and generally used inside controller logic bound inside the Yang expression as well as event handler listening on Model events.
It always returns an array (empty to denote no match) unless it encounters an error, in which case it will throw an Error.
find: (pattern='.', opts={}) ->
@debug => "[find] #{pattern}"
pattern = XPath.parse pattern, @schema
switch
when pattern.tag is '/' and this isnt @root then @root.find(pattern)
when pattern.tag is '..' then @parent?.find pattern.xpath
else pattern.apply(this) ? []
A convenience routine to locate one or more matching Property
instances based on pattern
(XPATH or YPATH) from this Model.
in: (pattern) ->
try props = @find pattern
return unless props? and props.length
return switch
when props.length > 1 then props
else props[0]
Optionally defer setting the value to the property until root has been updated.
defer: (value) ->
@debug => "deferring '#{@kind}(#{@name})' until update at #{@root.name}"
@root.once 'update', =>
@debug => "applying deferred data (#{typeof value})"
@data = value
return value
Provides more contextual error message pertaining to the Property instance.
error: (err, ctx) ->
err = new Error err unless err instanceof Error
err.uri ?= @uri
err.src ?= this
err.ctx ?= ctx
return switch
when @binding?.error? then @binding.error err
else err
This call creates a new copy of the current Property.content
completely detached/unbound to the underlying data schema. It's main
utility is to represent the current data state for subsequent
serialization/transmission. It accepts optional argument tag
which
when called with true
will tag the produced object with the current
property's @name
.
toJSON: (key, state = true) ->
value = switch
when @kind is 'anydata' then undefined
when state isnt true and not @mutable then undefined
else @data
value = "#{@name}": value if key is true
return value
inspect: ->
return {
name: @name
kind: @kind
path: @path.toString()
active: @active
private: @private
mutable: @mutable
changed: @changed
schema: switch
when @schema.uri?
uri: @schema.uri
summary: @schema.description?.tag
datakey: @schema.datakey
datapath: @schema.datapath
external: @schema.external
children: @schema.children.map (x) -> x.uri
else false
value: @toJSON()
}
module.exports = Property