Formatting

Formatting

    fs = require 'fs'
    path = require 'path'
    {Transform} = require 'stream'

    _ = require 'lodash'
    {LineStream} = require 'byline'
    glob = require 'grunt/node_modules/glob'
    Promise = require 'bluebird'

    class HookInvocations extends Transform

      constructor: ->
        super

        @list = []

      _transform: (chunk, encoding, done) ->
        line = chunk.toString('utf8')
        if matches = line.match /^\#\#\#\# Invoke hook `([^`]+)`/
          @list.push matches[1]

        done()

    class HookImplementations extends Transform

      constructor: ->
        super

        @list = []

      _transform: (chunk, encoding, done) ->
        line = chunk.toString('utf8')
        if matches = line.match /^\#\#\#\# Implements hook `([^`]+)`/
          @list.push matches[1]

        done()

    class Todos extends Transform

      @context = 4

      constructor: ->
        super

        @lines = []
        @todos = []

      _transform: (chunk, encoding, done) ->
        @lines.push line = chunk.toString('utf8')
        if matches = line.match /^\#\#\#\#\#\# TODO/
          @todos.push @lines.length - 1

        done()

      withContext: ->

        for todo in @todos

          start = Math.max 0, todo - Todos.context
          end = Math.min @lines.length - 1, todo + Todos.context

          index: todo
          lines: @lines.slice start, end

    _getFiles = (spec) ->
      new Promise (resolve, reject) ->
        glob(
          '{{client,custom,packages,server}/**/*.litcoffee,*.litcoffee}'
          (error, files) ->
            return reject error if error?
            resolve files
        )

    _getFilesByType = (spec) -> _getFiles(spec).then (files) ->

      client = []
      server = []

      for file in files
        parts = file.split '/'

        if parts[0] is 'client'
          client.push file
        else if parts[0] is 'custom' and parts[2] is 'client'
          client.push file
        else if parts[0] is 'packages' and parts[2] is 'client'
          client.push file
        else
          server.push file

      client: client, server: server

    _hookToId = (hook) -> hook.replace(
      /[^0-9A-Za-z-]+/g, '-'
    ).toLowerCase()

    _removeExtension = (filename) ->

        dirname = path.dirname filename
        if dirname is '.' then dirname = '' else dirname += '/'
        extname = path.extname filename
        filename = "#{dirname}#{path.basename filename, extname}"

        parts = filename.split '/'
        parts.pop() if parts[parts.length - 1] is 'index'
        return parts.join '/'

    fileStatsListPromise = _getFilesByType().then (allFiles) ->

      allFilesPromises = for type, files of allFiles

        typePromises = for file in files

          do (type, file) -> new Promise (resolve, reject) ->

            fstream = fs.createReadStream file
            fstream.pipe lineStream = new LineStream keepEmptyLines: true

            lineStream.pipe hookImplementations = new HookImplementations()
            lineStream.pipe hookInvocations = new HookInvocations()
            lineStream.pipe todos = new Todos()

            fstream.on 'error', reject

            fstream.on 'end', ->
              resolve(
                type: type
                file: file
                implementations: hookImplementations.list
                invocations: hookInvocations.list
                todos: todos.withContext()
              )

        Promise.all typePromises

      Promise.all(allFilesPromises).then (fileStatsLists) ->
        _.flatten fileStatsLists

    fileStatsListPromise.then((fileStatsList) ->

      hooksIndex = {}
      implementations = {}
      invocations = {}

      for fileStats in fileStatsList

        for implementation in fileStats.implementations
          impl = implementations[implementation] ?= {}
          (impl[fileStats.file] ?= []).push fileStats.type

          hooksIndex[implementation] = true

        for invocation in fileStats.invocations
          invo = invocations[invocation] ?= {}
          (invo[fileStats.file] ?= []).push fileStats.type

          hooksIndex[invocation] = true

      for hook, stats of implementations
        for file of stats
          stats[file] = _.unique stats[file] ? []

      for hook, stats of invocations
        for file of stats
          stats[file] = _.unique stats[file] ? []

      hooks = (hook for hook of hooksIndex).sort()

      hookFiles = {}
      for hook in hooks
        hookFiles[hook] = try
          fs.readFileSync "docs/hook/#{hook}.md", 'utf8'
        catch error
          ''

      hookFiles: hookFiles
      hooks: hooks
      implementations: implementations
      invocations: invocations

    ).then(({hookFiles, hooks, implementations, invocations}) ->

      render = '''
    # Hook system

    Hooks are how Shrub allows packages to implement (or invoke) dynamic
    behavior. Hooks can also serve as a form of message passing between
    packages.

    To implement a hook, export a `pkgmanRegister` method which takes a
    `registrar` argument, and use the registrar to register your hook:

    ```javascript

    exports.pkgmanRegister = function(registrar) {

      registrar.registerHook('someHook', function() {
        doStuff();
      }

      registrar.registerHook('someOtherHook', function(arg) {
        doOtherStuffWith(arg);
      }
    }

    ```

    To invoke a hook, require `pkgman` and use the `invoke` method:

    ```javascript

    var pkgman = require('pkgman');

    var results = pkgman.invoke('someOtherHook', arg);

    ```

    Any arguments following the hook name will be passed along to the
    implementations. Hooks are invoked synchronously. For more information
    about `pkgman`, see

    ###### TODO: Link to `pkgman` documentation when complete.

    '''

      for hook in hooks

        render += "## #{hook}\n\n"
        render += hookFiles[hook] + '\n\n' if hookFiles[hook]

        if implementations[hook]?

          implementationCount = Object.keys(implementations[hook]).length
          render += "### #{implementationCount} implementation(s)\n\n"

          for file, types of implementations[hook]

            file = _removeExtension file

            types = types.map (type) -> "[#{type}](source/#{file}#implements-hook-#{hook.toLowerCase()})"

            render += "* #{file} (#{types.join ','})\n"

          render += '\n'

        if invocations[hook]?

          invocationCount = Object.keys(invocations[hook]).length
          render += "### #{invocationCount} invocation(s)\n\n"

          for file, types of invocations[hook]

            file = _removeExtension file

            types = types.map (type) -> "[#{type}](source/#{file}#invoke-hook-#{hook.toLowerCase()})"

            render += "* #{file} (#{types.join ','})\n"

          render += '\n'

      new Promise (resolve, reject) ->
        fs.writeFile 'docs/hooks.md', render, (error) ->
          return reject error if error?
          resolve()

    ).done()

    fileStatsListPromise.then((fileStatsList) ->

      render = '''
      # TODO list

      Shrub — like any project — always presents a path for improvement. This
      is a dynamically generated list of TODO items, each with context.

      '''

      for fileStats in fileStatsList

        idMap = {}

        for todo in fileStats.todos

          render += '\n'

          id = ''
          for line, index in todo.lines
            render += '>'

            if index is Todos.context
              id = _hookToId(line).slice 1, -1

              render += line.slice 4
            else
              render += " #{line}"
            render += '\n'

          render += '\n'

          filename = _removeExtension fileStats.file

          if idMap[id]?
            idMap[id] += 1
            id += "_#{idMap[id]}"
          else
            idMap[id] = 0

          render += "###### the above found in [#{fileStats.file}:#{todo.index}](source/#{filename}##{id})\n"

      new Promise (resolve, reject) ->
        fs.writeFile 'docs/todos.md', render, (error) ->
          return reject error if error?
          resolve()

    ).done()

    _getFiles().then (files) ->

      yml = fs.readFileSync 'docs/mkdocs.yml.template', 'utf8'

      for file in files
        parts = file.split '/'

        for i in [0...parts.length]
          try
            fs.mkdirSync "docs/source/#{parts.slice(0, i).join '/'}"
          catch error

        fs.createReadStream(file).pipe(
          fs.createWriteStream("docs/source/#{file}")
        )

        yml += "- [source/#{file}, 'Source code', '#{file}']\n"

      fs.writeFileSync 'mkdocs.yml', yml

      return

    fs = require 'fs'
    path = require 'path'
    {Transform} = require 'stream'

    _ = require 'lodash'
    {LineStream} = require 'byline'
    glob = require 'grunt/node_modules/glob'
    Promise = require 'bluebird'

    class HookInvocations extends Transform

      constructor: ->
        super

        @list = []

      _transform: (chunk, encoding, done) ->
        line = chunk.toString('utf8')
        if matches = line.match /^\#\#\#\# Invoke hook `([^`]+)`/
          @list.push matches[1]

        done()

    class HookImplementations extends Transform

      constructor: ->
        super

        @list = []

      _transform: (chunk, encoding, done) ->
        line = chunk.toString('utf8')
        if matches = line.match /^\#\#\#\# Implements hook `([^`]+)`/
          @list.push matches[1]

        done()

    class Todos extends Transform

      @context = 4

      constructor: ->
        super

        @lines = []
        @todos = []

      _transform: (chunk, encoding, done) ->
        @lines.push line = chunk.toString('utf8')
        if matches = line.match /^\#\#\#\#\#\# TODO/
          @todos.push @lines.length - 1

        done()

      withContext: ->

        for todo in @todos

          start = Math.max 0, todo - Todos.context
          end = Math.min @lines.length - 1, todo + Todos.context

          index: todo
          lines: @lines.slice start, end

    _getFiles = (spec) ->
      new Promise (resolve, reject) ->
        glob(
          '{{client,custom,packages,server}/**/*.litcoffee,*.litcoffee}'
          (error, files) ->
            return reject error if error?
            resolve files
        )

    _getFilesByType = (spec) -> _getFiles(spec).then (files) ->

      client = []
      server = []

      for file in files
        parts = file.split '/'

        if parts[0] is 'client'
          client.push file
        else if parts[0] is 'custom' and parts[2] is 'client'
          client.push file
        else if parts[0] is 'packages' and parts[2] is 'client'
          client.push file
        else
          server.push file

      client: client, server: server

    _hookToId = (hook) -> hook.replace(
      /[^0-9A-Za-z-]+/g, '-'
    ).toLowerCase()

    _removeExtension = (filename) ->

        dirname = path.dirname filename
        if dirname is '.' then dirname = '' else dirname += '/'
        extname = path.extname filename
        filename = "#{dirname}#{path.basename filename, extname}"

        parts = filename.split '/'
        parts.pop() if parts[parts.length - 1] is 'index'
        return parts.join '/'

    fileStatsListPromise = _getFilesByType().then (allFiles) ->

      allFilesPromises = for type, files of allFiles

        typePromises = for file in files

          do (type, file) -> new Promise (resolve, reject) ->

            fstream = fs.createReadStream file
            fstream.pipe lineStream = new LineStream keepEmptyLines: true

            lineStream.pipe hookImplementations = new HookImplementations()
            lineStream.pipe hookInvocations = new HookInvocations()
            lineStream.pipe todos = new Todos()

            fstream.on 'error', reject

            fstream.on 'end', ->
              resolve(
                type: type
                file: file
                implementations: hookImplementations.list
                invocations: hookInvocations.list
                todos: todos.withContext()
              )

        Promise.all typePromises

      Promise.all(allFilesPromises).then (fileStatsLists) ->
        _.flatten fileStatsLists

    fileStatsListPromise.then((fileStatsList) ->

      hooksIndex = {}
      implementations = {}
      invocations = {}

      for fileStats in fileStatsList

        for implementation in fileStats.implementations
          impl = implementations[implementation] ?= {}
          (impl[fileStats.file] ?= []).push fileStats.type

          hooksIndex[implementation] = true

        for invocation in fileStats.invocations
          invo = invocations[invocation] ?= {}
          (invo[fileStats.file] ?= []).push fileStats.type

          hooksIndex[invocation] = true

      for hook, stats of implementations
        for file of stats
          stats[file] = _.unique stats[file] ? []

      for hook, stats of invocations
        for file of stats
          stats[file] = _.unique stats[file] ? []

      hooks = (hook for hook of hooksIndex).sort()

      hookFiles = {}
      for hook in hooks
        hookFiles[hook] = try
          fs.readFileSync "docs/hook/#{hook}.md", 'utf8'
        catch error
          ''

      hookFiles: hookFiles
      hooks: hooks
      implementations: implementations
      invocations: invocations

    ).then(({hookFiles, hooks, implementations, invocations}) ->

      render = '''
    # Hook system

    Hooks are how Shrub allows packages to implement (or invoke) dynamic
    behavior. Hooks can also serve as a form of message passing between
    packages.

    To implement a hook, export a `pkgmanRegister` method which takes a
    `registrar` argument, and use the registrar to register your hook:

    ```javascript

    exports.pkgmanRegister = function(registrar) {

      registrar.registerHook('someHook', function() {
        doStuff();
      }

      registrar.registerHook('someOtherHook', function(arg) {
        doOtherStuffWith(arg);
      }
    }

    ```

    To invoke a hook, require `pkgman` and use the `invoke` method:

    ```javascript

    var pkgman = require('pkgman');

    var results = pkgman.invoke('someOtherHook', arg);

    ```

    Any arguments following the hook name will be passed along to the
    implementations. Hooks are invoked synchronously. For more information
    about `pkgman`, see

    ###### TODO: Link to `pkgman` documentation when complete.

    '''

      for hook in hooks

        render += "## #{hook}\n\n"
        render += hookFiles[hook] + '\n\n' if hookFiles[hook]

        if implementations[hook]?

          implementationCount = Object.keys(implementations[hook]).length
          render += "### #{implementationCount} implementation(s)\n\n"

          for file, types of implementations[hook]

            file = _removeExtension file

            types = types.map (type) -> "[#{type}](source/#{file}#implements-hook-#{hook.toLowerCase()})"

            render += "* #{file} (#{types.join ','})\n"

          render += '\n'

        if invocations[hook]?

          invocationCount = Object.keys(invocations[hook]).length
          render += "### #{invocationCount} invocation(s)\n\n"

          for file, types of invocations[hook]

            file = _removeExtension file

            types = types.map (type) -> "[#{type}](source/#{file}#invoke-hook-#{hook.toLowerCase()})"

            render += "* #{file} (#{types.join ','})\n"

          render += '\n'

      new Promise (resolve, reject) ->
        fs.writeFile 'docs/hooks.md', render, (error) ->
          return reject error if error?
          resolve()

    ).done()

    fileStatsListPromise.then((fileStatsList) ->

      render = '''
      # TODO list

      Shrub -- like any project -- always presents a path for improvement. This
      is a dynamically generated list of TODO items, each with context.

      '''

      for fileStats in fileStatsList

        idMap = {}

        for todo in fileStats.todos

          render += '\n'

          id = ''
          for line, index in todo.lines
            render += '>'

            if index is Todos.context
              id = _hookToId(line).slice 1, -1

              render += line.slice 4
            else
              render += " #{line}"
            render += '\n'

          render += '\n'

          filename = _removeExtension fileStats.file

          if idMap[id]?
            idMap[id] += 1
            id += "_#{idMap[id]}"
          else
            idMap[id] = 0

          render += "###### the above found in [#{fileStats.file}:#{todo.index}](source/#{filename}##{id})\n"

      new Promise (resolve, reject) ->
        fs.writeFile 'docs/todos.md', render, (error) ->
          return reject error if error?
          resolve()

    ).done()

    _getFiles().then (files) ->

      yml = fs.readFileSync 'docs/mkdocs.yml.template', 'utf8'

      for file in files
        parts = file.split '/'

        for i in [0...parts.length]
          try
            fs.mkdirSync "docs/source/#{parts.slice(0, i).join '/'}"
          catch error

        fs.createReadStream(file).pipe(
          fs.createWriteStream("docs/source/#{file}")
        )

        yml += "- [source/#{file}, 'Source code', '#{file}']\n"

      fs.writeFileSync 'mkdocs.yml', yml

      return


Removed unnecessary code snippet

Removed unnecessary code snippet

Sandbox
=======

    vm = require "vm"
    path = require "path"
    _ = require "underscore"
    Membrane = require "./membrane"

    class Sandbox
        constructor: (@fileName, @policyObj) ->

            @origRequire = require
            @glob =
                __filename: @fileName
                __dirname: path.dirname(@fileName)
                console: console
                module:
                    exports: {}
                Buffer: Buffer
                require: undefined
                process: process

            @glob.global = @glob
            @glob.GLOBAL = @glob
            @glob.exports = @glob.module.exports

        buildContext: () ->
            ctxt = vm.createContext @glob
            customRequire = @buildMembranedRequire()
            ctxt.require = (libName) -> customRequire libName
            return ctxt

        buildMembranedRequire: ->
            (libName) =>
                try
                    # first try to load library as a global library or add cwd if local file
                    libName = path.join(@glob.__dirname, libName) if @glob.__dirname? and libName.indexOf(".js") > -1
                    libexports = @origRequire libName
                catch e
                    # otherwise try to look in cwd node_modules directory
                    libName = path.join(path.join(@glob.__dirname, "node_modules"), libName)
                    libexports = @origRequire libName

If the library is only used within the (outer) membrane, i.e., it is not
referenced in the policy, we return immediately.

                try
                    return libexports unless @policyObj.mustWrap(libName)
                catch e
                    return libexports

The last step is to create a wrapper function that will carefully wrap each
exported API call within a membrane.

                f =  @buildMembraneWrapper libName
                return _.object(_.map libexports, f)

        buildMembraneWrapper: (libName) =>
            (propObj, propName) => [propName, new Membrane(propObj, @policyObj)]

        context: => @sandboxedContext ?= @buildContext()
        global: => @glob

    module.exports = Sandbox

Sandbox
=======

    vm = require "vm"
    path = require "path"
    _ = require "underscore"
    Membrane = require "./membrane"

    class Sandbox
        constructor: (@fileName, @policyObj) ->

            @origRequire = require
            @glob =
                __filename: @fileName
                __dirname: path.dirname(@fileName)
                console: console
                module:
                    exports: {}
                Buffer: Buffer
                require: undefined
                process: process

            @glob.global = @glob
            @glob.GLOBAL = @glob
            @glob.exports = @glob.module.exports

        buildContext: () ->
            ctxt = vm.createContext @glob
            customRequire = @buildMembranedRequire()
            ctxt.require = (libName) -> customRequire libName
            return ctxt

        buildMembranedRequire: ->
            (libName) =>
                try
                    # first try to load library as a global library or add cwd if local file
                    libName = path.join(@glob.__dirname, libName) if @glob.__dirname? and libName.indexOf(".js") > -1
                    libexports = @origRequire libName
                catch e
                    # otherwise try to look in cwd node_modules directory
                    libName = path.join(path.join(@glob.__dirname, "node_modules"), libName)
                    libexports = @origRequire libName

If the library is only used within the (outer) membrane, i.e., it is not
referenced in the policy, we return immediately.

                try
                    return libexports unless @policyObj.mustWrap(libName)
                catch e
                    return libexports

The last step is to create a wrapper function that will carefully wrap each
exported API call within a membrane.

                f =  @buildMembraneWrapper libName
                return _.object(_.map libexports, f)

        buildMembraneWrapper: (libName) =>
            (propObj, propName) =>
                name = "#{libName}.#{propName}"
                [propName, new Membrane(propObj, @policyObj)]

        context: => @sandboxedContext ?= @buildContext()
        global: => @glob

    module.exports = Sandbox


fix sort...

fix sort...

# Super secret queue methods

    @QueueMethods =
      popAndServeNextTicket: ->

Move the current connection to the processed queue

TODO: get postID

        queueMeta = QueueMeta.findOne()

Update object, extended at various point for updating the QueueMeta in one swoop

        update =
          $set: {}

If `theOnlyConnectionAllowedIn` is set to something, check if the connection
has expired and process the connection if it has.

        if queueMeta.theOnlyConnectionAllowedIn isnt undefined

          previousConnection = queueMeta.theOnlyConnectionAllowedIn
          previousConnectionDidntPosted = not queueMeta.hasCurrentConnectionPosted

Return if timeCurrentTicketExpires is in the future (hasn't expired)

          hasntExpired = queueMeta.timeCurrentTicketExpires > moment.utc().toDate()
          return if hasntExpired

Okay then, the current connection has expired, time to get bumped.
Insert the processed connection into the ProcessedQueue

          try

Calculate how long the connection had to wait in line.

            waitTimeSeconds = moment.utc().diff (moment.utc queueMeta.theOnlyConnectionAllowedIn.timeEnqueued), 'seconds'

Make the processed object.

            processed =
              connection: queueMeta.theOnlyConnectionAllowedIn
              waitTimeSeconds: waitTimeSeconds

Insert the processed connection to `QueueProcessed`

            QueueProcessed.insert processed
          catch err
            console.log err

Remove the connection from the `Queue`

          criteria = connectionId: queueMeta.theOnlyConnectionAllowedIn.connectionId
          Queue.remove criteria

Update `QueueMeta` stats and ticket number.

          try
            totalWaitTimeSeconds = queueMeta.totalWaitTimeSeconds + waitTimeSeconds
            currentlyServingTicketNumber = queueMeta.currentlyServingTicketNumber + 1
            _.extend update.$set,
              currentlyServingTicketNumber: currentlyServingTicketNumber
              hasCurrentConnectionPosted: no
              totalWaitTimeSeconds: totalWaitTimeSeconds
          catch err
            console.log err

Lookup the lowest ticket # waiting in the `Queue`

        nextConnection = Queue.findOne {}, { sort: { 'ticketNumber': 1 } }

Set the allowed connection and expireTime if one exists

If there is a nextConnection (i.e. someone waiting in line), then serve them.
Otherwise clear out theOnlyConnectionAllowedIn.

        if nextConnection?
          timeCurrentTicketExpires = moment.utc()
          timeCurrentTicketExpires.add 1, 'minutes'
          _.extend update.$set,
            theOnlyConnectionAllowedIn: nextConnection
            timeCurrentTicketExpires: timeCurrentTicketExpires.toDate()
            currentlyServingTicketNumber: nextConnection.ticketNumber

If there is no nextConnection, unset theOnlyConnectionAllowedIn.
Note, fake data is filled in to pass schema, even though we're $unset'ing

        else
          update.$unset = theOnlyConnectionAllowedIn: ""

        criteria = _id: queueMeta._id

Delete `update.$set` if it's empty

        if _.isEmpty update.$set
          delete update.$set

If `update` isn't empty, update!

        if not _.isEmpty update
          QueueMeta.update criteria, update

TODO: keep it DRY; logic isn't exactly shared and want to keep update atomic to
avoid extraneous DDP messages.

## Disconnect, remove from queue

    Meteor.onConnection (connection) ->
      connection.onClose ->

Remove the connection from the `Queue`

        criteria = connectionId: connection.id
        Queue.remove criteria

# Super secret queue methods

    @QueueMethods =
      popAndServeNextTicket: ->

Move the current connection to the processed queue

TODO: get postID

        queueMeta = QueueMeta.findOne()

Update object, extended at various point for updating the QueueMeta in one swoop

        update =
          $set: {}

If `theOnlyConnectionAllowedIn` is set to something, check if the connection
has expired and process the connection if it has.

        if queueMeta.theOnlyConnectionAllowedIn isnt undefined

          previousConnection = queueMeta.theOnlyConnectionAllowedIn
          previousConnectionDidntPosted = not queueMeta.hasCurrentConnectionPosted

Return if timeCurrentTicketExpires is in the future (hasn't expired)

          hasntExpired = queueMeta.timeCurrentTicketExpires > moment.utc().toDate()
          return if hasntExpired

Okay then, the current connection has expired, time to get bumped.
Insert the processed connection into the ProcessedQueue

          try

Calculate how long the connection had to wait in line.

            waitTimeSeconds = moment.utc().diff (moment.utc queueMeta.theOnlyConnectionAllowedIn.timeEnqueued), 'seconds'

Make the processed object.

            processed =
              connection: queueMeta.theOnlyConnectionAllowedIn
              waitTimeSeconds: waitTimeSeconds

Insert the processed connection to `QueueProcessed`

            QueueProcessed.insert processed
          catch err
            console.log err

Remove the connection from the `Queue`

          criteria = connectionId: queueMeta.theOnlyConnectionAllowedIn.connectionId
          Queue.remove criteria

Update `QueueMeta` stats and ticket number.

          try
            totalWaitTimeSeconds = queueMeta.totalWaitTimeSeconds + waitTimeSeconds
            currentlyServingTicketNumber = queueMeta.currentlyServingTicketNumber + 1
            _.extend update.$set,
              currentlyServingTicketNumber: currentlyServingTicketNumber
              hasCurrentConnectionPosted: no
              totalWaitTimeSeconds: totalWaitTimeSeconds
          catch err
            console.log err

Lookup the lowest ticket # waiting in the `Queue`

        nextConnection = Queue.findOne {}, { $sort: { 'ticketNumber': 1 } }

Set the allowed connection and expireTime if one exists

If there is a nextConnection (i.e. someone waiting in line), then serve them.
Otherwise clear out theOnlyConnectionAllowedIn.

        if nextConnection?
          timeCurrentTicketExpires = moment.utc()
          timeCurrentTicketExpires.add 1, 'minutes'
          _.extend update.$set,
            theOnlyConnectionAllowedIn: nextConnection
            timeCurrentTicketExpires: timeCurrentTicketExpires.toDate()
            currentlyServingTicketNumber: nextConnection.ticketNumber

If there is no nextConnection, unset theOnlyConnectionAllowedIn.
Note, fake data is filled in to pass schema, even though we're $unset'ing

        else
          update.$unset = theOnlyConnectionAllowedIn: ""

        criteria = _id: queueMeta._id

Delete `update.$set` if it's empty

        if _.isEmpty update.$set
          delete update.$set

If `update` isn't empty, update!

        if not _.isEmpty update
          QueueMeta.update criteria, update

TODO: keep it DRY; logic isn't exactly shared and want to keep update atomic to
avoid extraneous DDP messages.

## Disconnect, remove from queue

    Meteor.onConnection (connection) ->
      connection.onClose ->

Remove the connection from the `Queue`

        criteria = connectionId: connection.id
        Queue.remove criteria


fix indention for caching

fix indention for caching

# Application Routes

The purpose of a route is to connect URL resources with underlying code.  I
also do explicit data normalization (input -> dictionary to pass to method/dict
from method -> view) at this stage.

To configure routes, the only object we need access to is `app`.  This module
can be called with `require("./lib/routes")(app)` as long as `app` is an
already initialized ExpressJS application.

Also note, app.models contians:

- **app.models.db** - Mongoose MongoDB connection
- **app.models.MODELNAME** - model objects

    module.exports = (app) ->

## Function cache(res, seconds)

Set cache headers on this response.

Usage: `cache(res,86400)`

      cache = (res,seconds) ->
        res.header 'Cache-control', 'Cache-Control:public, max-age='+seconds

## Function nocache(res)

Set uncacheable headers on this response

Usage: `nocache(res)`

      nocache = (res) ->
        res.header 'Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'

## GET /

Render the main index page.

      app.get '/', (req, res) ->
        cache res, 2592000
        res.redirect '/page/index'

## GET /page/:name

Render static pages at /view/page/name.html

      app.get '/page/:name', (req, res) ->
        app.models.page.get req.params.name, (err, page) ->
          if err
            console.error 'page '+req.params.name+' not found in db, showing 500'
            nocache res
            res.render 'error/500'
          else
            cache res, 86400
            res.render 'page/dynamic', page

## POST /page/_post

Post a page into the database

WARNING no security! .. then again, it doesn't read input at the moment either.
<!-- ' -->

      app.post '/page/_post', (req, res) ->
        nocache res
        sample =
          name: 'test'
          title: 'Test Page'
          content: 'Hello world, from inside the DB'
        app.models.page sample, (err, page) ->
          if err
            page =
              name: 'error'
              title: 'Error'
              content: 'There was an error posting your page, please try again.'
          res.render 'page/dynamic', page

## GET /login

Render static pages at /view/login/index.html

And never cache the login page, it breaks csrf

      app.get '/login', (req, res) ->
        nocache res
        res.render 'login'

## POST /login/_post

Do Login action, and redirect as appropriate.

      app.post '/login/_post', (req, res) ->
        nocache res
        require('../login').checkUser req.body.username, req.body.password, (user, securityLevel) ->
          console.info 'login callback = '+user+', '+securityLevel
          req.session.user = user
          req.session.securityLevel = securityLevel
          res.cookie 'nocache', '1'
          if securityLevel
            res.redirect '/'
          else
            res.redirect '/login'

## GET /login/clear

Logout from the website by clearing the session

      app.get '/login/clear', (req, res) ->
        nocache res
        req.session.destroy (err) ->
          res.clearCookie 'nocache'
          res.redirect '/login'

## GET /directory

Download PDF to browser

      app.get '/directory', (req, res) ->
        nocache res
        if req.session.securityLevel
          request = require 'request'
          request app.directory_url, (err, cbres, data) ->
            if err or cbres.statusCode != 200
              res.render 500, 'Error retrieving full directory'
            else
              dir = JSON.parse data
              page =
                name: 'Full Directory'
                title: 'Full Directory'
                directory: dir.data
                helpers:
                  month: (num) ->
                    return [null, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][num]
              res.render 'directory', page
        else
          res.redirect '/login'

## GET /directory/directory.pdf

Download PDF to browser

      app.get '/directory/directory.pdf', (req, res) ->
        nocache res
        if req.session.securityLevel
          res.download 'protected/directory.pdf'
        else
          res.redirect '/login'

## Copying

This software is released under the ISC License.

Copyright (c) 2013, Cameron King <cking@ecc12.com>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

<!-- vim: ts=2 sts=2 sw=2 expandtab
     CoffeeScript-friendly tabstops in vim. -->


router window.onpopstate

router window.onpopstate

#Weya.Router.litcoffee

    @Weya = {} unless @Weya?
    Weya = @Weya

    if exports?
     Weya = require './weya'
     Weya.Base = require './weya_base'

    if not Weya.Base?
     throw new Error 'Weya.Base not found'

##Weya.Router

Routing with hash tags or pushState

    class Router extends Weya.Base
     optionalParam: /\((.*?)\)/g
     namedParam: /(\(\?)?:\w+/g
     splatParam: /\*\w+/g
     escapeRegExp: /[\-{}\[\]+?.,\\\^$|#\s]/g

     @extend()

     @initialize ->
      @_bindRoutes()
      @_event = null
      @_history = []

     _routes: {}

####Register router

`'analyse/:analysis': 'onAnalyse'`

`dashboard': ['auth', 'dashboard']`

This will match urls of the form `analyse/\*` and call method @onAnalyse
with the parameter. Second route will first call @auth if it returns
`true` @dashboard will be called.

The most specific route should be at bottom. Routers can added or overridden
in subclasses

     @routes: (routes) ->
      temp = @::_routes
      @::_routes = {}
      @::_routes[k] = v for k, v of temp
      @::_routes[k] = v for k, v of routes

####Starts routing
Option silent will not trigger an event for the current url

     start: (options) ->
      Weya.history.start options
      fragment = Weya.history.getFragment()

      if options?.silent is on
       @_history.push fragment: fragment, title: document.title

####Goes to previous page if exists

     back: ->
      if @_history.length > 1
       Weya.history.back()

####Whether it is possible to go to previous page

     canBack: ->
      if @_history.length > 1 and Weya.history.canBack()
       return true
      else
       return false

####Registers a route

     route: (route, name) ->
      if (Object.prototype.toString.call route) isnt '[object RegExp]'
       route = @_routeToRegExp route

Registers the route with Weya.history

      Weya.history.route route, (fragment, event) =>
       args = @_extractParameters route, fragment
       @_event = event

Handle state

       if @_event?.type is "popstate"
        @_history.pop()
        if @_history.length is 0
         @_history.push
          fragment: fragment
          title: document.title
          state: @getState()
       else
        @_history.push
         fragment: fragment
         title: document.title
         state: @getState()

Calls callbacks in order

       callbacks = name
       callbacks = [callbacks] if not Array.isArray callbacks

       for callback in callbacks
        if (typeof callback) is 'string'
         callback = @[callback]
        break unless callback.apply this, args

####Gets the current HTML5 History state

     getState: ->
      if @_event?.originalEvent?.state?
       return @_event.originalEvent.state
      else
       return null

####Navigate to a new URL, while setting HTML5 History state

     navigate: (fragment, options) ->
      options = {} unless options
      if options.replace
       @_history.pop()
      if not options.trigger
       @_history.push
        fragment: fragment
        title: options.title
        state: options.state

      Weya.history.navigate fragment, options

Bind routes

     _bindRoutes: ->
      for route, name of @_routes
       @route route, name

Convert a route string into a regular expression, suitable for matching

     _routeToRegExp: (route) ->
      route = route.replace @escapeRegExp, '\\$&'
                   .replace @noptionalParam, '(?:$1)?'
                   .replace @namedParam, (match, optional) ->
                    if optional then match else '([^\/]+)'
                   .replace @splatParam, '(.*?)'

       return new RegExp "^#{route}$"

Extract parameters from a route regex and URL

     _extractParameters: (route, fragment) ->
      params = route.exec(fragment).slice(1)
      for p, i in params when p?
       params[i] = decodeURIComponent p

## History class

    class History extends Weya.Base
     @extend()

Strip leading hash/slash and trailing space

     routeStripper: /^[#\/]|\s+$/g

Strip leading and trailing slashes

     rootStripper: /^\/+|\/+$/g

Remove trailing slash

     trailingSlash: /\/$/

Strip urls of hash and query

     pathStripper: /[?#].*$/

     @initialize ->
      @handlers = []
      @checkUrl = @checkUrl.bind this
      @history = window.history
      @location = window.location
      @stateList = []

Hash check interval

     interval: 50

Get hash value

     getHash: ->
      match = @location.href.match /#(.*)$/
      return (if match? then match[1] else '')

Get emulated state

     getEmulateState: ->
      if @stateList.length > 0
       return @stateList[@stateList.length - 1]
      else
       return {fragment: ""}

     popEmulateState: -> @stateList.pop()

     pushEmulateState: (state, title, fragment) ->
      @stateList.push
       state: state
       title: title
       fragment: fragment

Get the URL fragment

     getFragment: (fragment, forcePushState) ->
      if not fragment?
       if @_emulateState
        fragment = @getEmulateState().fragment
       else if @_hasPushState or not @_wantsHashChange or forcePushState
        fragment = @location.pathname
        root = @root.replace @trailingSlash, ''
        if (fragment.indexOf root) is 0
         fragment = fragment.slice root.length
       else
        fragment = @getHash()

      return fragment.replace @routeStripper, ''

####Goto the previous page

     back: ->
      if @_emulateState is on
       @popEmulateState()
       @loadUrl null, null
      else
       @history?.back?()

####Can back?

     canBack: ->
      if @_emulateState is on
       return @stateList.length > 1
      else
       return @history?.back?

####Start listening to events

     start: (options) ->
      History.started = true

      opt = {root: '/'}
      opt[k] = v for k, v of @options
      opt[k] = v for k, v of options
      @options = opt
      @root = @options.root
      @_emulateState = @options.emulateState is on
      @_wantsHashChange = @_emulateState is off and @options.hashChange isnt off
      @_wantsPushState = @_emulateState is off and @options.pushState is on
      @_hasPushState = @_wantsPushState is on and @history?.pushState?
      if @_emulateState and @options.start?
       @pushEmulateState @options.start.state,
        @options.start.title
        @options.start.fragment

      @fragment = @getFragment()

Normalize root to always include a leading and trailing slash.

      @root = "/#{@root}/".replace @rootStripper, '/'

Depending on whether we're using pushState or hashes, and whether
'onhashchange' is supported, determine how we check the URL state.

      if @_hasPushState
       window.onpopstate = @checkUrl
      else if @_wantsHashChange and window.onhashchange?
       window.onhashchange = @checkUrl
      else if @_wantsHashChange
       @_checkUrlInterval = setInterval @checkUrl, @interval

      if not @options.silent
       @loadUrl null, null

####Add a listener to a router

     route: (route, callback) ->
      @handlers.unshift route: route, callback: callback

Check current url for changes

     checkUrl: (e) ->
      fragment = @getFragment()
      return if fragment is @fragment
      @loadUrl fragment, e

Call callbacks of matching route

     loadUrl: (fragment, e) ->
      fragment = @fragment = @getFragment fragment
      for handler in @handlers
       if handler.route.test fragment
        return handler.callback fragment, e

      return

####Navigate to a URL
Triggers a route is option trigger is on
Replaces the url is option replace is on
Sets state and title

     navigate: (fragment, options) ->
      return false if not History.started

      fragment = @getFragment(fragment or '')
      url = @root + fragment

Strip the fragment of the query and hash for matching.

      fragment = fragment.replace @pathStripper, ''

      return if @fragment is fragment
      @fragment = fragment

Don't include a trailing slash on the root.

      if fragment is '' and url isnt '/'
       url = url.slice 0, -1

If pushState is available, we use it to set the fragment as a real URL.

      if @_emulateState
       if options.replace is on
        @popEmulateState()
       state = {}
       state = options.state if options.state?
       title = ''
       title = options.title if options.title?
       @pushEmulateState state, title, fragment
      else if @_hasPushState
       method = if options.replace then 'replaceState' else 'pushState'
       state = {}
       state = options.state if options.state?
       title = ''
       title = options.title if options.title?
       @history[method] state, title, url

If hash changes haven't been explicitly disabled, update the hash
fragment to store history.

      else if @_wantsHashChange
       @_updateHash @location, fragment, options.replace

If you've told us that you explicitly don't want fallback hashchange-
based history, then `navigate` becomes a page refresh.

      else
       return @location.assign url
      if options.trigger
       return @loadUrl fragment, null

Update the hash

     _updateHash: (location, fragment, replace) ->
      if replace
       href = location.href.replace /(javascript:|#).*$/, ''
       location.replace "#{href}##{fragment}"
      else
       location.hash = "##{fragment}"

    Weya.Router = Router
    Weya.history = new History

    if module?
     module.exports = Weya.Router

#Weya.Router.litcoffee

    @Weya = {} unless @Weya?
    Weya = @Weya

    if exports?
     Weya = require './weya'
     Weya.Base = require './weya_base'

    if not Weya.Base?
     throw new Error 'Weya.Base not found'

##Weya.Router

Routing with hash tags or pushState

    class Router extends Weya.Base
     optionalParam: /\((.*?)\)/g
     namedParam: /(\(\?)?:\w+/g
     splatParam: /\*\w+/g
     escapeRegExp: /[\-{}\[\]+?.,\\\^$|#\s]/g

     @extend()

     @initialize ->
      @_bindRoutes()
      @_event = null
      @_history = []

     _routes: {}

####Register router

`'analyse/:analysis': 'onAnalyse'`

`dashboard': ['auth', 'dashboard']`

This will match urls of the form `analyse/\*` and call method @onAnalyse
with the parameter. Second route will first call @auth if it returns
`true` @dashboard will be called.

The most specific route should be at bottom. Routers can added or overridden
in subclasses

     @routes: (routes) ->
      temp = @::_routes
      @::_routes = {}
      @::_routes[k] = v for k, v of temp
      @::_routes[k] = v for k, v of routes

####Starts routing
Option silent will not trigger an event for the current url

     start: (options) ->
      Weya.history.start options
      fragment = Weya.history.getFragment()

      if options?.silent is on
       @_history.push fragment: fragment, title: document.title

####Goes to previous page if exists

     back: ->
      if @_history.length > 1
       Weya.history.back()

####Whether it is possible to go to previous page

     canBack: ->
      if @_history.length > 1 and Weya.history.canBack()
       return true
      else
       return false

####Registers a route

     route: (route, name) ->
      if (Object.prototype.toString.call route) isnt '[object RegExp]'
       route = @_routeToRegExp route

Registers the route with Weya.history

      Weya.history.route route, (fragment, event) =>
       args = @_extractParameters route, fragment
       @_event = event

Handle state

       if @_event?.type is "popstate"
        @_history.pop()
        if @_history.length is 0
         @_history.push
          fragment: fragment
          title: document.title
          state: @getState()
       else
        @_history.push
         fragment: fragment
         title: document.title
         state: @getState()

Calls callbacks in order

       callbacks = name
       callbacks = [callbacks] if not Array.isArray callbacks

       for callback in callbacks
        if (typeof callback) is 'string'
         callback = @[callback]
        break unless callback.apply this, args

####Gets the current HTML5 History state

     getState: ->
      if @_event?.originalEvent?.state?
       return @_event.originalEvent.state
      else
       return null

####Navigate to a new URL, while setting HTML5 History state

     navigate: (fragment, options) ->
      options = {} unless options
      if options.replace
       @_history.pop()
      if not options.trigger
       @_history.push
        fragment: fragment
        title: options.title
        state: options.state

      Weya.history.navigate fragment, options

Bind routes

     _bindRoutes: ->
      for route, name of @_routes
       @route route, name

Convert a route string into a regular expression, suitable for matching

     _routeToRegExp: (route) ->
      route = route.replace @escapeRegExp, '\\$&'
                   .replace @noptionalParam, '(?:$1)?'
                   .replace @namedParam, (match, optional) ->
                    if optional then match else '([^\/]+)'
                   .replace @splatParam, '(.*?)'

       return new RegExp "^#{route}$"

Extract parameters from a route regex and URL

     _extractParameters: (route, fragment) ->
      params = route.exec(fragment).slice(1)
      for p, i in params when p?
       params[i] = decodeURIComponent p

## History class

    class History extends Weya.Base
     @extend()

Strip leading hash/slash and trailing space

     routeStripper: /^[#\/]|\s+$/g

Strip leading and trailing slashes

     rootStripper: /^\/+|\/+$/g

Remove trailing slash

     trailingSlash: /\/$/

Strip urls of hash and query

     pathStripper: /[?#].*$/

     @initialize ->
      @handlers = []
      @checkUrl = @checkUrl.bind this
      @history = window.history
      @location = window.location
      @stateList = []

Hash check interval

     interval: 50

Get hash value

     getHash: ->
      match = @location.href.match /#(.*)$/
      return (if match? then match[1] else '')

Get emulated state

     getEmulateState: ->
      if @stateList.length > 0
       return @stateList[@stateList.length - 1]
      else
       return {fragment: ""}

     popEmulateState: -> @stateList.pop()

     pushEmulateState: (state, title, fragment) ->
      @stateList.push
       state: state
       title: title
       fragment: fragment

Get the URL fragment

     getFragment: (fragment, forcePushState) ->
      if not fragment?
       if @_emulateState
        fragment = @getEmulateState().fragment
       else if @_hasPushState or not @_wantsHashChange or forcePushState
        fragment = @location.pathname
        root = @root.replace @trailingSlash, ''
        if (fragment.indexOf root) is 0
         fragment = fragment.slice root.length
       else
        fragment = @getHash()

      return fragment.replace @routeStripper, ''

####Goto the previous page

     back: ->
      if @_emulateState is on
       @popEmulateState()
       @loadUrl null, null
      else
       @history?.back?()

####Can back?

     canBack: ->
      if @_emulateState is on
       return @stateList.length > 1
      else
       return @history?.back?

####Start listening to events

     start: (options) ->
      History.started = true

      opt = {root: '/'}
      opt[k] = v for k, v of @options
      opt[k] = v for k, v of options
      @options = opt
      @root = @options.root
      @_emulateState = @options.emulateState is on
      @_wantsHashChange = @_emulateState is off and @options.hashChange isnt off
      @_wantsPushState = @_emulateState is off and @options.pushState is on
      @_hasPushState = @_wantsPushState is on and @history?.pushState?
      if @_emulateState and @options.start?
       @pushEmulateState @options.start.state,
        @options.start.title
        @options.start.fragment

      @fragment = @getFragment()

Normalize root to always include a leading and trailing slash.

      @root = "/#{@root}/".replace @rootStripper, '/'

Depending on whether we're using pushState or hashes, and whether
'onhashchange' is supported, determine how we check the URL state.

      if @_hasPushState
       $(window).on 'popstate', @checkUrl
      else if @_wantsHashChange and window.onhashchange?
       $(window).on 'hashchange', @checkUrl
      else if @_wantsHashChange
       @_checkUrlInterval = setInterval @checkUrl, @interval

      if not @options.silent
       @loadUrl null, null

####Add a listener to a router

     route: (route, callback) ->
      @handlers.unshift route: route, callback: callback

Check current url for changes

     checkUrl: (e) ->
      fragment = @getFragment()
      return if fragment is @fragment
      @loadUrl fragment, e

Call callbacks of matching route

     loadUrl: (fragment, e) ->
      fragment = @fragment = @getFragment fragment
      for handler in @handlers
       if handler.route.test fragment
        return handler.callback fragment, e

      return

####Navigate to a URL
Triggers a route is option trigger is on
Replaces the url is option replace is on
Sets state and title

     navigate: (fragment, options) ->
      return false if not History.started

      fragment = @getFragment(fragment or '')
      url = @root + fragment

Strip the fragment of the query and hash for matching.

      fragment = fragment.replace @pathStripper, ''

      return if @fragment is fragment
      @fragment = fragment

Don't include a trailing slash on the root.

      if fragment is '' and url isnt '/'
       url = url.slice 0, -1

If pushState is available, we use it to set the fragment as a real URL.

      if @_emulateState
       if options.replace is on
        @popEmulateState()
       state = {}
       state = options.state if options.state?
       title = ''
       title = options.title if options.title?
       @pushEmulateState state, title, fragment
      else if @_hasPushState
       method = if options.replace then 'replaceState' else 'pushState'
       state = {}
       state = options.state if options.state?
       title = ''
       title = options.title if options.title?
       @history[method] state, title, url

If hash changes haven't been explicitly disabled, update the hash
fragment to store history.

      else if @_wantsHashChange
       @_updateHash @location, fragment, options.replace

If you've told us that you explicitly don't want fallback hashchange-
based history, then `navigate` becomes a page refresh.

      else
       return @location.assign url
      if options.trigger
       return @loadUrl fragment, null

Update the hash

     _updateHash: (location, fragment, replace) ->
      if replace
       href = location.href.replace /(javascript:|#).*$/, ''
       location.replace "#{href}##{fragment}"
      else
       location.hash = "##{fragment}"

    Weya.Router = Router
    Weya.history = new History

    if module?
     module.exports = Weya.Router


weya elem fix

weya elem fix

Gist of SVG elements

    Tags =
     svg: 'a altGlyph altGlyphDef altGlyphItem animate animateColor
      animateMotion animateTransform circle clipPath color-profile cursor
      defs desc ellipse feBlend feColorMatrix feComponentTransfer
      feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
      feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
      feImage feMerge feMergeNode feMorphology feOffset fePointLight
      feSpecularLighting feSpotLight feTile feTurbulence
      filter font font-face font-face-format font-face-name font-face-src
      font-face-uri foreignObject g glyph glyphRef hkern image line
      linearGradient marker mask metadata missing-glyph mpath path pattern
      polygon polyline radialGradient rect script set stop style svg symbol
      text textPath title tref tspan use view vkern switch foreignObject'

List of HTML elements

     html: 'a abbr address article aside audio b bdi bdo blockquote body button
      canvas caption cite code colgroup datalist dd del details dfn div dl dt
      em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header
      hgroup html i iframe ins kbd label legend li main map mark menu meter
      nav noscript object ol optgroup option output p pre progress q rp rt
      ruby s samp script section select small span strong style sub summary
      sup table tbody td textarea tfoot th thead time title tr u ul video'

     htmlVoid: 'area base br col command embed hr img input keygen link meta
      param source track wbr'

Wrapper for browser API

    Api =
     document: @document

Parse id and class string

    parseIdClass = (str) ->
     res =
      id: null
      class: []

     for c, i in str.split "."
      if c.indexOf("#") is 0
       res.id = c.substr 1
      else if c isnt ""
       if not res.class?
        res.class = [c]
       else
        res.class.push c

     return res

    getParameters = (args) ->
     params =
      idClass: null
      text: null
      attrs: null
      func: null

     for arg, i in args
      switch typeof arg
       when 'function' then params.func = arg
       when 'object' then params.attrs = arg
       when 'string'
        if args.length is 1
         params.text = arg
        else
         c = arg.charAt 0

         if i is 0 and (c is '#' or c is '.')
          params.idClass = parseIdClass arg
         else
          params.text = arg

     return params

#Weya DOM

    weyaDomCreate = ->

Weya object to be passed as `this`

     weya =
      _elem: null

##Manipulating dom objects

Set styles

     setStyles = (elem, styles) ->
      for k, v of styles
       if v?
        elem.style.setProperty k, v
       else
        elem.style.removeProperty k

Set events

     setEvents = (elem, events) ->
      for k, v of events
       elem.addEventListener k, v, false

Set data

     setData = (elem, data) ->
      for k, v of data
       elem[k] = v

Set attributes

     setAttributes = (elem, attrs) ->
      for k, v of attrs
       switch k
        when 'style' then setStyles elem, v
        when 'on' then setEvents elem, v
        when 'data' then setData elem, v
        else
         if v?
          elem.setAttribute k, v
         else
          elem.removeAttribute k

Set ID and Class with `classList`

     setIdClass = (elem, idClass) ->
      if idClass.id?
       elem.id = idClass.id

      if idClass.class?
       for c in idClass.class
        elem.classList.add c

Set ID and Class with setAttributes

     setIdClassFallback = (elem, idClass) ->
      if idClass.id?
       elem.id = idClass.id

      if idClass.class?
       className = idClass.class.join ' '
       elem.setAttribute 'class', className

Check whether classList is available and switch to fallback otherwise

     switchIdClass = ->
      elem = Api.document.createElementNS "http://www.w3.org/2000/svg", 'g'
      if not elem.classList
       setIdClass = setIdClassFallback

Switch to fall back if no classList

     switchIdClass() if Api.document?

Append a child element

     append = (ns, name, args) ->
      params = getParameters args

Keep a reference to parent element

      pElem = @_elem

Keep a reference of `elem` to return at the end of the function

      if ns?
       elem = @_elem = Api.document.createElementNS ns, name
      else
       elem = @_elem = Api.document.createElement name

      if params.idClass?
       setIdClass elem, params.idClass

      if params.attrs?
       setAttributes elem, params.attrs

      if pElem?
       pElem.appendChild elem

      if params.func?
       params.func.call this
      else if params.text?
       elem.textContent = params.text

      @_elem = pElem
      return elem

Wrap `append`

     wrapAppend = (ns, name) ->
      ->
       append.call this, ns, name, arguments

Initialize

     for name in Tags.svg.split ' '
      weya[name] = wrapAppend "http://www.w3.org/2000/svg", name

     for name in Tags.html.split ' '
      weya[name] = wrapAppend "http://www.w3.org/1999/xhtml", name

     for name in Tags.htmlVoid.split ' '
      weya[name] = wrapAppend null, name

     return weya

#Weya Markup

    weyaMarkupCreate = ->

Weya object to be passed as `this`

     weya =
      _buf: null
      _indent: 0

Render components

     setStyles = (buf, styles) ->
      buf.push " style=\""
      for k, v of styles
       buf.push "#{k}:#{v};"
      buf.push "\""

     setEvents = (buf, events) ->
      for k, v of events
       buf.push " on#{k}=\"#{v}\""

     setData = (buf, data) ->
      for k, v of data
       buf.push " data-#{k}=\"#{v}\""

     setAttributes = (buf, attrs) ->
      for k, v of attrs
       switch k
        when 'style' then setStyles buf, v
        when 'on' then setEvents buf, v
        when 'data' then setData buf, v
        else
         buf.push " #{k}=\"#{v}\""

     setIndent = (buf, indent) ->
      for i in [0...indent]
       buf.push " "

Append a child element

     append = (ns, name, isVoid, args) ->
      params = getParameters args

      buf = @_buf

      setIndent buf, @_indent

      buf.push "<#{name}"

      if params.idClass?
       if params.idClass.id?
        buf.push " id=\"#{params.idClass.id}\""
       if params.idClass.class?
        className = params.idClass.class.join ' '
        buf.push " class=\"#{className}\""

      if params.attrs?
       setAttributes buf, params.attrs

Can close void elements (element that self close) with a `/>`

      if isVoid
       buf.push "/>\n"
       if params.func? or params.text?
        throw new Error 'Void tags cannot have content'

      else
       buf.push ">\n"
       @_indent++

       if params.func?
        params.func.call this
       else if params.text?
        setIndent buf, @_indent
        buf.push params.text
        buf.push "\n"

       @_indent--
       setIndent buf, @_indent
       buf.push "</#{name}>\n"

Wrap `append`

     wrapAppend = (ns, name, isVoid) ->
      ->
       append.call this, ns, name, isVoid, arguments

Initialize

     for name in Tags.svg.split ' '
      weya[name] = wrapAppend "http://www.w3.org/2000/svg", name, false

     for name in Tags.html.split ' '
      weya[name] = wrapAppend null, name, false

     for name in Tags.htmlVoid.split ' '
      weya[name] = wrapAppend null, name, true

     return weya

#Weya API

    weyaDom = weyaDomCreate()
    weyaMarkup = weyaMarkupCreate()

Create and append to `options.elem`. If `options.context` is provied it can be
accessed via `@$`. If `options.elem` is `null`, the element is created but
not appended.

    @Weya = Weya = (options, func) ->
     weya = weyaDom
     pContext = weya.$
     weya.$ = options.context
     helpersAdded = []

     if options.helpers?
      for name, helper of options.helpers
       if not weya[name]?
        helpersAdded.push name
        weya[name] = helper.bind weya

     pElem = weya._elem
     weya._elem = options.elem
     r = func?.call weya
     weya._elem = pElem
     weya.$ = pContext

     for name in helpersAdded
      delete weya[name]

     return r

    Weya.markup = (options, func) ->
     weya = weyaMarkup
     pContext = weya.$
     weya.$ = options.context
     helpersAdded = []

     if options.helpers?
      for name, helper of options.helpers
       if not weya[name]?
        helpersAdded.push name
        weya[name] = helper.bind weya

     pBuf = weya._buf
     weya._buf = []
     r = func?.call weya
     buf = weya._buf
     weya._buf = pBuf
     weya.$ = pContext

     for name in helpersAdded
      delete weya[name]

     return buf.join ''

    Weya.setApi = (api) ->
     for k, v of api
      Api[k] = v
     weyaDom = weyaDomCreate()
     weyaMarkup = weyaMarkupCreate()

    if module?
     module.exports = Weya

Gist of SVG elements

    Tags =
     svg: 'a altGlyph altGlyphDef altGlyphItem animate animateColor
      animateMotion animateTransform circle clipPath color-profile cursor
      defs desc ellipse feBlend feColorMatrix feComponentTransfer
      feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
      feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
      feImage feMerge feMergeNode feMorphology feOffset fePointLight
      feSpecularLighting feSpotLight feTile feTurbulence
      filter font font-face font-face-format font-face-name font-face-src
      font-face-uri foreignObject g glyph glyphRef hkern image line
      linearGradient marker mask metadata missing-glyph mpath path pattern
      polygon polyline radialGradient rect script set stop style svg symbol
      text textPath title tref tspan use view vkern switch foreignObject'

List of HTML elements

     html: 'a abbr address article aside audio b bdi bdo blockquote body button
      canvas caption cite code colgroup datalist dd del details dfn div dl dt
      em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header
      hgroup html i iframe ins kbd label legend li main map mark menu meter
      nav noscript object ol optgroup option output p pre progress q rp rt
      ruby s samp script section select small span strong style sub summary
      sup table tbody td textarea tfoot th thead time title tr u ul video'

     htmlVoid: 'area base br col command embed hr img input keygen link meta
      param source track wbr'

Wrapper for browser API

    Api =
     document: @document

Parse id and class string

    parseIdClass = (str) ->
     res =
      id: null
      class: []

     for c, i in str.split "."
      if c.indexOf("#") is 0
       res.id = c.substr 1
      else if c isnt ""
       if not res.class?
        res.class = [c]
       else
        res.class.push c

     return res

    getParameters = (args) ->
     params =
      idClass: null
      text: null
      attrs: null
      func: null

     for arg, i in args
      switch typeof arg
       when 'function' then params.func = arg
       when 'object' then params.attrs = arg
       when 'string'
        if args.length is 1
         params.text = arg
        else
         c = arg.charAt 0

         if i is 0 and (c is '#' or c is '.')
          params.idClass = parseIdClass arg
         else
          params.text = arg

     return params

#Weya DOM

    weyaDomCreate = ->

Weya object to be passed as `this`

     weya =
      _elem: null

Manipulating dom objects

     setStyles = (elem, styles) ->
      for k, v of styles
       if v?
        elem.style.setProperty k, v
       else
        elem.style.removeProperty k

     setEvents = (elem, events) ->
      for k, v of events
       elem.addEventListener k, v, false

     setData = (elem, data) ->
      for k, v of data
       elem[k] = v

     setAttributes = (elem, attrs) ->
      for k, v of attrs
       switch k
        when 'style' then setStyles elem, v
        when 'on' then setEvents elem, v
        when 'data' then setData elem, v
        else
         if v?
          elem.setAttribute k, v
         else
          elem.removeAttribute k

     setIdClass = (elem, idClass) ->
      if idClass.id?
       elem.id = idClass.id

      if idClass.class?
       for c in idClass.class
        elem.classList.add c

     setIdClassFallback = (elem, idClass) ->
      if idClass.id?
       elem.id = idClass.id

      if idClass.class?
       className = idClass.class.join ' '
       elem.setAttribute 'class', className

     if Api.document?
      elem = Api.document.createElementNS "http://www.w3.org/2000/svg", 'g'
      if not elem.classList
       setIdClass = setIdClassFallback

Append a child element

     append = (ns, name, args) ->
      params = getParameters args

Keep a reference to parent element

      pElem = @_elem

Keep a reference of `elem` to return at the end of the function

      if ns?
       elem = @_elem = Api.document.createElementNS ns, name
      else
       elem = @_elem = Api.document.createElement name

      if params.idClass?
       setIdClass elem, params.idClass

      if params.attrs?
       setAttributes elem, params.attrs

      if pElem?
       pElem.appendChild elem

      if params.func?
       params.func.call this
      else if params.text?
       elem.textContent = params.text

      @_elem = pElem
      return elem

Wrap `append`

     wrapAppend = (ns, name) ->
      ->
       append.call this, ns, name, arguments

Initialize

     for name in Tags.svg.split ' '
      weya[name] = wrapAppend "http://www.w3.org/2000/svg", name

     for name in Tags.html.split ' '
      weya[name] = wrapAppend "http://www.w3.org/1999/xhtml", name

     for name in Tags.htmlVoid.split ' '
      weya[name] = wrapAppend null, name

     return weya

#Weya Markup

    weyaMarkupCreate = ->

Weya object to be passed as `this`

     weya =
      _buf: null
      _indent: 0

Render components

     setStyles = (buf, styles) ->
      buf.push " style=\""
      for k, v of styles
       buf.push "#{k}:#{v};"
      buf.push "\""

     setEvents = (buf, events) ->
      for k, v of events
       buf.push " on#{k}=\"#{v}\""

     setData = (buf, data) ->
      for k, v of data
       buf.push " data-#{k}=\"#{v}\""

     setAttributes = (buf, attrs) ->
      for k, v of attrs
       switch k
        when 'style' then setStyles buf, v
        when 'on' then setEvents buf, v
        when 'data' then setData buf, v
        else
         buf.push " #{k}=\"#{v}\""

     setIndent = (buf, indent) ->
      for i in [0...indent]
       buf.push " "

Append a child element

     append = (ns, name, isVoid, args) ->
      params = getParameters args

      buf = @_buf

      setIndent buf, @_indent

      buf.push "<#{name}"

      if params.idClass?
       if params.idClass.id?
        buf.push " id=\"#{params.idClass.id}\""
       if params.idClass.class?
        className = params.idClass.class.join ' '
        buf.push " class=\"#{className}\""

      if params.attrs?
       setAttributes buf, params.attrs

Can close void elements (element that self close) with a `/>`

      if isVoid
       buf.push "/>\n"
       if params.func? or params.text?
        throw new Error 'Void tags cannot have content'

      else
       buf.push ">\n"
       @_indent++

       if params.func?
        params.func.call this
       else if params.text?
        setIndent buf, @_indent
        buf.push params.text
        buf.push "\n"

       @_indent--
       setIndent buf, @_indent
       buf.push "</#{name}>\n"

Wrap `append`

     wrapAppend = (ns, name, isVoid) ->
      ->
       append.call this, ns, name, isVoid, arguments

Initialize

     for name in Tags.svg.split ' '
      weya[name] = wrapAppend "http://www.w3.org/2000/svg", name, false

     for name in Tags.html.split ' '
      weya[name] = wrapAppend null, name, false

     for name in Tags.htmlVoid.split ' '
      weya[name] = wrapAppend null, name, true

     return weya

#Weya API

    weyaDom = weyaDomCreate()
    weyaMarkup = weyaMarkupCreate()

Create and append to `options.elem`. If `options.context` is provied it can be
accessed via `@$`. If `options.elem` is `null`, the element is created but
not appended.

    @Weya = Weya = (options, func) ->
     weya = weyaDom
     pContext = weya.$
     weya.$ = options.context
     helpersAdded = []

     if options.helpers?
      for name, helper of options.helpers
       if not weya[name]?
        helpersAdded.push name
        weya[name] = helper.bind weya

     pElem = weya._elem
     weya._elem = options.elem
     r = func?.call weya
     weya._elem = pElem
     weya.$ = pContext

     for name in helpersAdded
      delete weya[name]

     return r

    Weya.markup = (options, func) ->
     weya = weyaMarkup
     pContext = weya.$
     weya.$ = options.context
     helpersAdded = []

     if options.helpers?
      for name, helper of options.helpers
       if not weya[name]?
        helpersAdded.push name
        weya[name] = helper.bind weya

     pBuf = weya._buf
     weya._buf = []
     r = func?.call weya
     buf = weya._buf
     weya._buf = pBuf
     weya.$ = pContext

     for name in helpersAdded
      delete weya[name]

     return buf.join ''

    Weya.setApi = (api) ->
     for k, v of api
      Api[k] = v
     weyaDom = weyaDomCreate()
     weyaMarkup = weyaMarkupCreate()

    if module?
     module.exports = Weya


fix memory leak with action/rpc. method should be a property and ignore update() propagation from input/output schema evaluations

# Method - controller of functions

## Class Method

    debug = require('debug')('yang:method')
    Property = require('./property')

    class Method extends Property
      debug: -> debug @uri, arguments...

      @property 'value',
        get: -> @state.value ? @do.bind this

      get: (pattern) -> switch
        when pattern? then super arguments...
        when @binding? then @do.bind this
        else @data

### do ()

A convenience wrap to a Property instance that holds a function to
perform a Promise-based execution.

Always returns a Promise.

      do: (input={}, opts={}) ->
        unless (@binding instanceof Function) or (@data instanceof Function)
          return Promise.reject @error "cannot perform action on a property without function"
        @debug "[do] executing method: #{@name}"
        @debug input
        ctx = @parent?.context ? @context
        ctx = ctx.with opts, path: @path
        try
          # calling context is the parent node of the method
          input = @schema.input.apply input, this, force: true

          # first apply schema bound function (if availble), otherwise
          # execute assigned function (if available and not 'missing')
          if @binding?
            @debug "[do] calling bound function with: #{Object.keys(input)}"
            @debug @binding.toString()
            output = @binding? ctx, input
          else
            @debug "[do] calling assigned function: #{@data.name}"
            @debug @value
            @debug @data
            output = @data.call @parent.data, input, ctx

          output = await output
          { output } = @schema.output.eval { output }, this, force: true
          return output
        catch e
          @debug e
          return e

      update: (value, opts) ->
        super value, opts unless value instanceof Property

### toJSON

This call always returns undefined for the Method node.

      toJSON: (key) ->
        value = undefined
        value = "#{@name}": value if key is true
        return value

    module.exports = Method

# Method - controller of functions

## Class Method

    debug = require('debug')('yang:method')
    Container = require('./container')

    class Method extends Container
      debug: -> debug @uri, arguments...

      @property 'value',
        get: -> @state.value ? @do.bind this

      get: (pattern) -> switch
        when pattern? then super arguments...
        when @binding? then @do.bind this
        else @data

### do ()

A convenience wrap to a Property instance that holds a function to
perform a Promise-based execution.

Always returns a Promise.

      do: (input={}, opts={}) ->
        unless (@binding instanceof Function) or (@data instanceof Function)
          return Promise.reject @error "cannot perform action on a property without function"
        @debug "[do] executing method: #{@name}"
        @debug input
        ctx = @parent?.context ? @context
        ctx = ctx.with opts, path: @path
        try
          # calling context is the parent node of the method
          input = @schema.input.apply input, this, force: true

          # first apply schema bound function (if availble), otherwise
          # execute assigned function (if available and not 'missing')
          if @binding?
            @debug "[do] calling bound function with: #{Object.keys(input)}"
            @debug @binding.toString()
            output = @binding? ctx, input
          else
            @debug "[do] calling assigned function: #{@data.name}"
            output = @data.call @parent.data, input, ctx

          output = await Promise.resolve output
          { output } = @schema.output.eval { output }, this, force: true
          return output
        catch e
          @debug e
          return Promise.reject e

### toJSON

This call always returns undefined for the Method node.

      toJSON: (key) ->
        value = undefined
        value = "#{@name}": value if key is true
        return value

    module.exports = Method


Remove TIFF from supported images

Remove TIFF from supported images

    url          = require 'url'

    compression  = require 'compression'
    express      = require 'express'
    morgan       = require 'morgan'
    responseTime = require 'response-time'

    multer       = require 'multer'
    async        = require 'async'
    dms2dec      = require 'dms2dec'

    raven        = require 'raven'
    sentry       = require './sentry'
    librato      = require './librato'

    app = express()
    app.use morgan 'combined'
    app.use compression()
    app.use responseTime()

    Upload = require 's3-uploader'
    s3 = new Upload process.env.AWS_BUCKET_NAME,
      awsBucketUrl: "https://s3-eu-west-1.amazonaws.com/#{process.env.AWS_BUCKET_NAME}/"
      awsBucketPath: process.env.AWS_BUCKET_PATH
      awsBucketAcl: 'public-read'
      awsHttpTimeout: 60000
      returnExif: true

      versions: [{
        original: true
        awsImageAcl: 'private'
      },{
        suffix: '-large'
        quality: 80
        maxHeight: 1040
        maxWidth: 1040
      },{
        suffix: '-medium'
        maxHeight: 780
        maxWidth: 780
      },{
        suffix: '-small'
        maxHeight: 320
        maxWidth: 320
      }]

    origins = process.env.ALLOW_ORIGINS?.split(',') or []

    app.all '/upload', (req, res, next) ->
      res.once 'close', -> sentry.captureResClose res

      origin = url.parse req.get('Origin') or ''

      if origin.hostname not in origins
        error = new Error "Bad Origin Header #{req.get('Origin')}"
        error.status = 403
        return next error

      res.set 'Access-Control-Allow-Origin', req.get('Origin')
      res.set 'Access-Control-Allow-Methods', 'POST'
      res.set 'Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'
      res.set 'Access-Control-Expose-Headers', 'X-Response-Time'
      res.set 'Access-Control-Allow-Max-Age', 0

      return res.send 200 if req.method is 'OPTIONS'
      return next()

    app.post '/upload', multer(dest: require('os').tmpdir()), (req, res, next) ->
      librato.measure 'image.upload', Object.keys(req.files).length, {}

      async.mapSeries Object.keys(req.files), (key, cb) ->
        if req.files[key].extension.toLowerCase() not in ['jpg', 'jpeg', 'png', 'gif']
          error = new Error "Invalid Image #{req.files[key].extension}"
          error.status = 422
          return cb error

        s3.upload req.files[key].path, {}, (err, images, meta) ->
          return cb err if err

          if meta.exif['exif:GPSLatitude'] and meta.exif['exif:GPSLongitude']
            meta.geojson =
              type: 'Point'
              coordinates: dms2dec \
                meta.exif['exif:GPSLatitude'], \
                meta.exif['exif:GPSLatitudeRef'], \
                meta.exif['exif:GPSLongitude'], \
                meta.exif['exif:GPSLongitudeRef']
              .reverse()

          return cb null, versions: images.splice(1), meta: meta
      , (err, files) ->
        return next err if err
        sentry.captureHeaderSent req, files if res._headerSent
        return res.status(201).json files

    app.use (req, res, next) ->
      res.status(404).json message: 'Not Found'

    app.use raven.middleware.express sentry
    app.use (err, req, res, next) ->
      if not err.status or err.status >= 500
        sentry.captureError err

        console.error err.message
        console.error err.stack

        res.status(err.status or 500).json message: 'Internal Server Error'
      else
        sentry.captureMessage err.message, level: 'warning', req: req
        res.status(err.status).json message: err.message

    if not module.parent
      app.listen process.env.PORT_WWW
      console.log "Server listening on port #{process.env.PORT_WWW}"
    else
      module.exports = app

    url          = require 'url'

    compression  = require 'compression'
    express      = require 'express'
    morgan       = require 'morgan'
    responseTime = require 'response-time'

    multer       = require 'multer'
    async        = require 'async'
    dms2dec      = require 'dms2dec'

    raven        = require 'raven'
    sentry       = require './sentry'
    librato      = require './librato'

    app = express()
    app.use morgan 'combined'
    app.use compression()
    app.use responseTime()

    Upload = require 's3-uploader'
    s3 = new Upload process.env.AWS_BUCKET_NAME,
      awsBucketUrl: "https://s3-eu-west-1.amazonaws.com/#{process.env.AWS_BUCKET_NAME}/"
      awsBucketPath: process.env.AWS_BUCKET_PATH
      awsBucketAcl: 'public-read'
      awsHttpTimeout: 60000
      returnExif: true

      versions: [{
        original: true
        awsImageAcl: 'private'
      },{
        suffix: '-large'
        quality: 80
        maxHeight: 1040
        maxWidth: 1040
      },{
        suffix: '-medium'
        maxHeight: 780
        maxWidth: 780
      },{
        suffix: '-small'
        maxHeight: 320
        maxWidth: 320
      }]

    origins = process.env.ALLOW_ORIGINS?.split(',') or []

    app.all '/upload', (req, res, next) ->
      res.once 'close', -> sentry.captureResClose res

      origin = url.parse req.get('Origin') or ''

      if origin.hostname not in origins
        error = new Error "Bad Origin Header #{req.get('Origin')}"
        error.status = 403
        return next error

      res.set 'Access-Control-Allow-Origin', req.get('Origin')
      res.set 'Access-Control-Allow-Methods', 'POST'
      res.set 'Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'
      res.set 'Access-Control-Expose-Headers', 'X-Response-Time'
      res.set 'Access-Control-Allow-Max-Age', 0

      return res.send 200 if req.method is 'OPTIONS'
      return next()

    app.post '/upload', multer(dest: require('os').tmpdir()), (req, res, next) ->
      librato.measure 'image.upload', Object.keys(req.files).length, {}

      async.mapSeries Object.keys(req.files), (key, cb) ->
        if req.files[key].extension.toLowerCase() not in ['jpg', 'jpeg', 'png', 'gif', 'tiff']
          error = new Error "Invalid Image #{req.files[key].extension}"
          error.status = 422
          return cb error

        s3.upload req.files[key].path, {}, (err, images, meta) ->
          return cb err if err

          if meta.exif['exif:GPSLatitude'] and meta.exif['exif:GPSLongitude']
            meta.geojson =
              type: 'Point'
              coordinates: dms2dec \
                meta.exif['exif:GPSLatitude'], \
                meta.exif['exif:GPSLatitudeRef'], \
                meta.exif['exif:GPSLongitude'], \
                meta.exif['exif:GPSLongitudeRef']
              .reverse()

          return cb null, versions: images.splice(1), meta: meta
      , (err, files) ->
        return next err if err
        sentry.captureHeaderSent req, files if res._headerSent
        return res.status(201).json files

    app.use (req, res, next) ->
      res.status(404).json message: 'Not Found'

    app.use raven.middleware.express sentry
    app.use (err, req, res, next) ->
      if not err.status or err.status >= 500
        sentry.captureError err

        console.error err.message
        console.error err.stack

        res.status(err.status or 500).json message: 'Internal Server Error'
      else
        sentry.captureMessage err.message, level: 'warning', req: req
        res.status(err.status).json message: err.message

    if not module.parent
      app.listen process.env.PORT_WWW
      console.log "Server listening on port #{process.env.PORT_WWW}"
    else
      module.exports = app


pass @pending queue with 'commit' event, see #37

# Model - instance of schema-driven data

The `Model` class aggregates [Property](./property.litcoffee)
attachments to provide the *adaptive* and *event-driven* data
interactions.

It is typically not instantiated directly, but is generated as a
result of [Yang::eval](./yang.litcoffee#eval-data-opts) for a YANG
`module` schema.

```javascript
var schema = Yang.parse('module foo { container bar { leaf a { type uint8; } } }');
var model = schema.eval({ 'foo:bar': { a: 7 } });
// model is { 'foo:bar': [Getter/Setter] }
```

The generated `Model` is a hierarchical composition of
[Property](./property.litcoffee) instances. The instance itself uses
`Object.preventExtensions` to ensure no additional properties that are
not known to itself can be added.

It is designed to provide *stand-alone* interactions on a per-module
basis. For flexible management of multiple modules (such as hotplug
modules) and data persistence, please take a look at the
[yang-store](http://github.com/corenova/yang-store) project.

## Class Model

    Stack    = require 'stacktrace-parser'
    Yang     = require './yang'
    Property = require './property'
    XPath    = require './core/xpath'

    class Model extends Property

      @Store = {}
      @MaxTransactions = 100

      constructor: (schema, data={}) ->
        unless schema?.kind is 'module'
          throw new Error "cannot create Model without YANG 'module' schema"
        data = expr.apply data for expr in schema.exprs
        super schema.tag, data, schema: schema

        if schema.import?
          new Model dep.module for dep in schema.import when dep.tag not of Model.Store

        @transactable = false
        @on 'update', (prop, prev) =>
          return unless @transactable
          if @pending.length > Model.MaxTransactions
            throw @error "exceeded max transactions of #{Model.MaxTransactions}, forgot to save()?"
          @pending.push prev
        Object.defineProperty this, 'pending', value: []
        Object.preventExtensions this
        # register this instance in the Model class singleton instance
        @join Model.Store

      valueOf: -> super false

### save

This routine triggers a 'commit' event for listeners to handle any
persistence operations. It also clears the `@pending` transaction
queue so that future [rollback](#rollback) will reset back to this state.

      save: ->
        @emit 'commit', @pending
        @pending.splice(0, @pending.length) # clear
        this

### rollback

This routine will replay tracked updates in reverse order when
`@transactable` is set to `true`. It will restore any `@pending`
changes to the Model instance back to the last [save](#save-opts)
state.

      rollback: ->
        while prop = @pending.pop()
          prop.join prop.parent, suppress: true
        this

### set (path..., value)

This routine allows `set` operation to reference an optional XPATH
location to update with the passed in `value`. Also, it restricts the
direct `set` operation on a Model to always peform a `merge: true`.

      set: (path..., value) ->
        if path.length
          @in(path[0])?.set? value
        else
          value ?= {}
          super value, merge: true

### find (pattern)

This routine enables *cross-model* property search when the `Model` is
joined to another object (such as a datastore). The schema-bound model
restricts *cross-model* property access to only those modules that are
`import` dependencies of the current model instance.

      find: (pattern='.', opts={}) ->
        return super unless @parent?

        console.debug? "[Model:#{@name}] find #{pattern}"
        match = super pattern, root: true
        return match if match?.length or opts.root

        # here we have a @parent that likely has a collectin of Models
        opts.root = true
        for k, model of @parent.__props__ when k isnt @name
          console.debug? "[Model:#{@name}] looking at #{k}.find"
          try match = model.find pattern, opts
          catch then continue
          return match if match?.length
        return []

### invoke (path, input)

Executes a `Property` holding a function found at the `path` using the
`input` data.

      invoke: (path, input) -> @in(path)?.invoke? input

### on (event)

The `Model` instance is an `EventEmitter` and you can attach various
event listeners to handle events generated by the `Model`:

event | arguments | description
--- | --- | ---
update | (prop, prev) | fired when an update takes place within the data tree
change | (elems...) | fired when the schema is modified
create | (items...) | fired when one or more `list` element is added
delete | (items...) | fired when one or more `list` element is deleted

It also accepts optional XPATH/YPATH expressions which will *filter*
for granular event subscription to specified events from only the
elements of interest.

The event listeners to the `Model` can handle any customized behavior
such as saving to database, updating read-only state, scheduling
background tasks, etc.

This operation is protected from recursion, where operations by the
`callback` may result in the same `callback` being executed multiple
times due to subsequent events triggered due to changes to the
`Model`. Currently, it will allow the same `callback` to be executed
at most two times.

      on: (event, filters..., callback) ->
        unless callback instanceof Function
          throw new Error "must supply callback function to listen for events"

        recursive = (name) ->
          seen = {}
          frames = Stack.parse(new Error().stack)
          for frame, i in frames when ~frame.methodName.indexOf(name)
            { file, lineNumber, column } = frames[i-1]
            callee = "#{file}:#{lineNumber}:#{column}"
            seen[callee] ?= 0
            if ++seen[callee] > 1
              console.warn "detected recursion for '#{callee}'"
              return true
          return false

        $$$ = (prop, args...) ->
          console.debug? "$$$: check if '#{prop.path}' in '#{filters}'"
          if not filters.length or prop.path.contains filters...
            unless recursive('$$$')
              callback.apply this, [prop].concat args

        super event, $$$

Please refer to [Model Events](../TUTORIAL.md#model-events) section of
the [Getting Started Guide](../TUTORIAL.md) for usage examples.

### in (pattern)

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]

## Export Model Class

    module.exports = Model

# Model - instance of schema-driven data

The `Model` class aggregates [Property](./property.litcoffee)
attachments to provide the *adaptive* and *event-driven* data
interactions.

It is typically not instantiated directly, but is generated as a
result of [Yang::eval](./yang.litcoffee#eval-data-opts) for a YANG
`module` schema.

```javascript
var schema = Yang.parse('module foo { container bar { leaf a { type uint8; } } }');
var model = schema.eval({ 'foo:bar': { a: 7 } });
// model is { 'foo:bar': [Getter/Setter] }
```

The generated `Model` is a hierarchical composition of
[Property](./property.litcoffee) instances. The instance itself uses
`Object.preventExtensions` to ensure no additional properties that are
not known to itself can be added.

It is designed to provide *stand-alone* interactions on a per-module
basis. For flexible management of multiple modules (such as hotplug
modules) and data persistence, please take a look at the
[yang-store](http://github.com/corenova/yang-store) project.

## Class Model

    Stack    = require 'stacktrace-parser'
    Yang     = require './yang'
    Property = require './property'
    XPath    = require './core/xpath'

    class Model extends Property

      @Store = {}
      @MaxTransactions = 100

      constructor: (schema, data={}) ->
        unless schema?.kind is 'module'
          throw new Error "cannot create Model without YANG 'module' schema"
        data = expr.apply data for expr in schema.exprs
        super schema.tag, data, schema: schema

        if schema.import?
          new Model dep.module for dep in schema.import when dep.tag not of Model.Store

        @transactable = false
        @on 'update', (prop, prev) =>
          return unless @transactable
          if @pending.length > Model.MaxTransactions
            throw @error "exceeded max transactions of #{Model.MaxTransactions}, forgot to save()?"
          @pending.push prev
        Object.defineProperty this, 'pending', value: []
        Object.preventExtensions this
        # register this instance in the Model class singleton instance
        @join Model.Store

      valueOf: -> super false

### save

This routine triggers a 'commit' event for listeners to handle any
persistence operations. It also clears the `@pending` transaction
queue so that future [rollback](#rollback) will reset back to this state.

      save: ->
        @emit 'commit'
        @pending.splice(0, @pending.length) # clear
        this

### rollback

This routine will replay tracked updates in reverse order when
`@transactable` is set to `true`. It will restore any `@pending`
changes to the Model instance back to the last [save](#save-opts)
state.

      rollback: ->
        while prop = @pending.pop()
          prop.join prop.parent, suppress: true
        this

### set (path..., value)

This routine allows `set` operation to reference an optional XPATH
location to update with the passed in `value`. Also, it restricts the
direct `set` operation on a Model to always peform a `merge: true`.

      set: (path..., value) ->
        if path.length
          @in(path[0])?.set? value
        else
          value ?= {}
          super value, merge: true

### find (pattern)

This routine enables *cross-model* property search when the `Model` is
joined to another object (such as a datastore). The schema-bound model
restricts *cross-model* property access to only those modules that are
`import` dependencies of the current model instance.

      find: (pattern='.', opts={}) ->
        return super unless @parent?

        console.debug? "[Model:#{@name}] find #{pattern}"
        match = super pattern, root: true
        return match if match?.length or opts.root

        # here we have a @parent that likely has a collectin of Models
        opts.root = true
        for k, model of @parent.__props__ when k isnt @name
          console.debug? "[Model:#{@name}] looking at #{k}.find"
          try match = model.find pattern, opts
          catch then continue
          return match if match?.length
        return []

### invoke (path, input)

Executes a `Property` holding a function found at the `path` using the
`input` data.

      invoke: (path, input) -> @in(path)?.invoke? input

### on (event)

The `Model` instance is an `EventEmitter` and you can attach various
event listeners to handle events generated by the `Model`:

event | arguments | description
--- | --- | ---
update | (prop, prev) | fired when an update takes place within the data tree
change | (elems...) | fired when the schema is modified
create | (items...) | fired when one or more `list` element is added
delete | (items...) | fired when one or more `list` element is deleted

It also accepts optional XPATH/YPATH expressions which will *filter*
for granular event subscription to specified events from only the
elements of interest.

The event listeners to the `Model` can handle any customized behavior
such as saving to database, updating read-only state, scheduling
background tasks, etc.

This operation is protected from recursion, where operations by the
`callback` may result in the same `callback` being executed multiple
times due to subsequent events triggered due to changes to the
`Model`. Currently, it will allow the same `callback` to be executed
at most two times.

      on: (event, filters..., callback) ->
        unless callback instanceof Function
          throw new Error "must supply callback function to listen for events"

        recursive = (name) ->
          seen = {}
          frames = Stack.parse(new Error().stack)
          for frame, i in frames when ~frame.methodName.indexOf(name)
            { file, lineNumber, column } = frames[i-1]
            callee = "#{file}:#{lineNumber}:#{column}"
            seen[callee] ?= 0
            if ++seen[callee] > 1
              console.warn "detected recursion for '#{callee}'"
              return true
          return false

        $$$ = (prop, args...) ->
          console.debug? "$$$: check if '#{prop.path}' in '#{filters}'"
          if not filters.length or prop.path.contains filters...
            unless recursive('$$$')
              callback.apply this, [prop].concat args

        super event, $$$

Please refer to [Model Events](../TUTORIAL.md#model-events) section of
the [Getting Started Guide](../TUTORIAL.md) for usage examples.

### in (pattern)

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]

## Export Model Class

    module.exports = Model