Skip to content

Commit

Permalink
functional implementation of Store (see #32), completed resolution of…
Browse files Browse the repository at this point in the history
… event subscription propagation from Model to Store. addresses edge case issues regarding #33. the new Store should resolve cross module leaf references (see #31).
  • Loading branch information
sekur committed Aug 26, 2016
1 parent c6ed02f commit 37b9f3f
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 145 deletions.
8 changes: 5 additions & 3 deletions example/jukebox.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ module.exports = require('./jukebox.yang').bind {
"/jukebox/playlist[key() = '#{input.playlist}']/" +
"song[key() = '#{input['song-number']}']"
)
if song? and song.id not instanceof Error
resolve "ok"
else
unless song?
reject "selected song #{input['song-number']} not found in library"
else if song.id instanceof Error
reject song.id
else
resolve "ok"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "yang-js",
"version": "0.15.21",
"version": "0.15.22",
"description": "YANG parser and composer",
"keywords": [
"yang",
Expand Down
15 changes: 8 additions & 7 deletions src/element.litcoffee
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,12 @@
unless typeof attrs is 'object'
throw @error "must supply 'attrs' as an object"
super attrs.parent
@propagate 'change'
Object.defineProperties this,
kind: value: kind, enumerable: true
tag: value: tag, enumerable: true, writable: true
node: value: (attrs.node is true)
parent: value: attrs.parent, writable: true
scope: value: attrs.scope, writable: true
# auto-computed properties
Expand Down Expand Up @@ -74,6 +72,9 @@
'*': get: (-> @nodes ).bind this
'..': get: (-> @parent ).bind this
# publish 'change' event
super 'change'
## Instance-level methods

### clone
Expand Down Expand Up @@ -106,7 +107,6 @@ while performing `@scope` validations.
_merge = (item) ->
unless item.tag in @tags
@tags.push item.tag
@push item
true
else if replace is true
Expand All @@ -124,8 +124,9 @@ while performing `@scope` validations.
unless Array.isArray @[elem.kind]
exists = @[elem.kind]
@[elem.kind] = [ exists ]
Object.defineProperty @[elem.kind], 'tags', value: [ exists.tag ]
Object.defineProperty @[elem.kind], 'tags',
get: (-> @map (x) -> x.tag ).bind @[elem.kind]
unless _merge.call @[elem.kind], elem
throw @error "constraint violation for '#{elem.kind} #{elem.tag}' - cannot define more than once"
Expand All @@ -145,7 +146,7 @@ while performing `@scope` validations.
enumerable: true
value: []
Object.defineProperty @[elem.kind], 'tags',
value: []
get: (-> @map (x) -> x.tag ).bind @[elem.kind]
unless _merge.call @[elem.kind], elem
throw @error "constraint violation for '#{elem.kind} #{elem.tag}' - already defined"
when '0..1', '1'
Expand Down
21 changes: 13 additions & 8 deletions src/emitter.litcoffee
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@ You can reference the above classes for more information on how the
events = require 'events'
class Emitter extends events.EventEmitter
constructor: (parent) ->
constructor: (events...) ->
Object.defineProperties this,
parent: value: parent, writable: true
domain: writable: true
_events: writable: true
_eventsCount: writable: true
_maxListeners: writable: true
_publishes: value: events
_subscribers: value: [], writable: true
emit: (event) ->
if event in @_publishes ? []
for x in @_subscribers when x instanceof Emitter
console.debug? "Emitter.emit '#{event}' to '#{x.constructor.name}'"
x.emit arguments...
super
propagate: (events...) ->
propagate = (event, args...) ->
for x in [ @parent?.__, @parent ] when x instanceof Emitter
x.emit event, args...
events.forEach (event) => @on event, propagate.bind this, event
subscribe: (to) ->
console.debug? "subscribing '#{@name}' to '#{to.constructor.name}'"
@_subscribers.push to; return to
module.exports = Emitter
1 change: 1 addition & 0 deletions src/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ exports = module.exports = Yang
exports.Extension = Extension
exports.Typedef = Typedef
exports.Model = require './model'
exports.Store = require './store'
46 changes: 18 additions & 28 deletions src/model.litcoffee
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,16 @@ not known to itself can be added.

## Class Model

Stack = require 'stacktrace-parser'
Emitter = require './emitter'
XPath = require './xpath'
Expression = require './expression'
Property = require './property'
Stack = require 'stacktrace-parser'
Emitter = require './emitter'
Property = require './property'
class Model extends Emitter
constructor: (schema, props...) ->
super # become Emitter
return unless schema instanceof Expression
unless schema.kind is 'module'
schema = (new Expression 'module').extends schema
prop.join this for prop in props when prop.schema in schema.nodes
# create an 'unjoined' property into @__ (can be joined to Store)
new Property schema.tag, this, schema: schema
constructor: (props...) ->
props = ([].concat props...).filter (prop) ->
prop instanceof Property
super
prop.join this for prop in props
Object.preventExtensions this
## Instance-level methods
Expand Down Expand Up @@ -75,8 +66,6 @@ at most two times.
unless callback instanceof Function
throw new Error "must supply callback function to listen for events"
filters = filters.map (x) => XPath.parse x, @__.schema
recursive = (name) ->
seen = {}
frames = Stack.parse(new Error().stack)
Expand All @@ -90,14 +79,10 @@ at most two times.
return false
$$$ = (prop, args...) ->
console.log "$$$: check if '#{prop.path}' in '#{filters}'"
console.debug? "$$$: check if '#{prop.path}' in '#{filters}'"
if not filters.length or prop.path.contains filters...
unless recursive('$$$')
ctx =
type: event
model: this
ts: Date.now()
callback.apply ctx, [prop].concat args
callback.apply this, [prop].concat args
super event, $$$
Expand All @@ -110,13 +95,18 @@ 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).props
catch then return
return unless typeof pattern is 'string'
return (prop for k, prop of @__props__) if pattern is '/'
for k, prop of @__props__
try props = prop.find(pattern).props
catch then continue
return unless props?
return switch
when not props.length then null
when props.length > 1 then props
else props[0]
## Export Model Class

module.exports = Model
exports = module.exports = Model
exports.Property = Property
61 changes: 33 additions & 28 deletions src/property.litcoffee
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,21 @@ you are doing.
class Property extends Emitter
constructor: (name, value, opts={}) ->
unless name?
throw new Error "cannot create an unnamed Property"
@name = name
@configurable = opts.configurable
@configurable ?= true
@enumerable = opts.enumerable
@enumerable ?= value?
super opts.parent
Object.defineProperties this,
schema: value: opts.schema
parent: value: opts.parent, writable: true
root: value: opts.root
content: value: value, writable: true
props: get: (-> @content?.__props__ ).bind this
key: get: (-> switch
props: get: (-> @content?.__props__ ).bind this
key: get: (-> switch
when @content not instanceof Object then undefined
when @content.hasOwnProperty('@key') then @content['@key']
when Array.isArray @parent
Expand All @@ -46,18 +48,20 @@ you are doing.
path: get: (->
x = this
p = []
schema = @schema
loop
expr = x.name
key = x.key
if key?
expr += switch typeof key
when 'number' then "[#{key}]"
when 'string' then "[key() = #{key}]"
when 'string' then "[key() = '#{key}']"
else ''
x = x.parent?.__ # skip the list itself
p.unshift expr if expr?
break unless (x = x.parent?.__) and x.schema?.kind isnt 'module'
return XPath.parse "/#{p.join '/'}"
schema = x.schema
break unless (x = x.parent?.__) and x.parent not instanceof Emitter
return XPath.parse "/#{p.join '/'}", schema
).bind this
# Bind the get/set functions to call with 'this' bound to this
Expand All @@ -67,9 +71,9 @@ you are doing.
@set = @set.bind this
@get = @get.bind this
# setup 'update/create/delete' event propagation up the tree
@propagate 'update', 'create', 'delete' unless opts.detached
# publish 'update/create/delete' events
super 'update', 'create', 'delete'
if value instanceof Object
# setup direct property access
unless value.hasOwnProperty '__'
Expand All @@ -78,6 +82,13 @@ you are doing.
## Instance-level methods

emit: (event) ->
if event in @_publishes ? []
for x in @_subscribers when x.__ instanceof Emitter
console.debug? "Property.emit '#{event}' from '#{@name}' to '#{x.__.name}'"
x.__.emit arguments...
super
### valueOf (tag)

This call creates a new copy of the current `Property.content`
Expand All @@ -98,7 +109,7 @@ property's `@name`.
src.constructor.call src, src
value = copy @get()
value ?= [] if @schema?.kind is 'list'
if @name? and tag
if tag
"#{@name}": value
else value
Expand All @@ -112,16 +123,16 @@ attaches itself to the provided target `obj`. It registers itself into
join: (obj, opts={ replace: true, suppress: false }) ->
return obj unless obj instanceof Object
@parent = obj
@subscribe obj if @enumerable
unless Array.isArray(obj) and @schema is obj.__?.schema
unless @name?
throw new Error "cannot join unnamed property to an object"
console.debug? "updating containing object with new property #{@name}"
unless obj.hasOwnProperty '__props__'
Object.defineProperty obj, '__props__', value: {}
prev = obj.__props__[@name]
obj.__props__[@name] = this
Object.defineProperty obj, @name, this
for x in (prev?._subscribers ? []) when x isnt obj and x instanceof Emitter
@subscribe x
@emit 'update', this, prev unless opts.suppress
return obj
Expand Down Expand Up @@ -168,34 +179,28 @@ validations.
when opts.force is true then @content = val
when opts.merge is true then switch
when Array.isArray @content
val = val[@name] if val? and val.hasOwnProperty @name
val = [ val ] unless Array.isArray val
res = @schema.apply { "#{@name}": val }
res[@name].forEach (item) => item.__.join @content, opts
when (typeof @content is 'object') and (typeof val is 'object')
val = val[@name] if val.hasOwnProperty @name
@content[k] = v for k, v of val when @content.hasOwnProperty k
# TODO: need to reapply schema to self
else return @set val
when @schema?.apply? # should check if instanceof Expression
console.debug? "setting #{@name} with parent: #{@parent?}"
val = val[@name] if val? and val.hasOwnProperty @name
# this is an ugly conditional...
if @schema.kind is 'list' and val? and (not @content? or Array.isArray @content)
val = [ val ] unless Array.isArray val
data = switch
when @name? then "#{@name}": val
else val
# TODO: enable schema.eval on anonymous 'module' from Model
res = @schema.apply data
res = @schema.apply { "#{@name}": val }
@remove() if @key?
switch
when @name?
prop = res.__props__[@name]
if @parent? then prop.join @parent, opts
else @content = prop.content
return prop
when @content instanceof Object
for name, prop of res.__props__
prop.join @content, opts
else @content = res
prop = res.__props__[@name]
if @parent? then prop.join @parent, opts
else @content = prop.content
return prop
else @content = val
return this
Expand Down
Loading

0 comments on commit 37b9f3f

Please sign in to comment.