msgflo
Version:
Polyglot FBP runtime based on message queues
123 lines (98 loc) • 4.34 kB
text/coffeescript
fs = require 'fs'
path = require 'path'
debug = require('debug')('msgflo:library')
common = require './common'
replaceMarker = (str, marker, value) ->
marker = '#'+marker.toUpperCase()
str.replace(new RegExp(marker, 'g'), value)
exports.replaceVariables = replaceVariables = (str, variables) ->
for marker, value of variables
str = replaceMarker str, marker, value
return str
baseComponentCommand = (config, component, cmd) ->
variables = common.clone config.variables
componentName = component.split('/')[1]
componentName = component if not componentName
variables['COMPONENTNAME'] = componentName
variables['COMPONENT'] = component
return replaceVariables cmd, variables
componentCommandForFile = (config, filename) ->
ext = path.extname filename
component = path.basename filename, ext
cmd = config.handlers[ext]
return baseComponentCommand config, component, cmd
componentsFromConfig = (config) ->
components = {}
for component, cmd of config.components
components[component] = baseComponentCommand config, component, cmd
return components
componentsFromDirectory = (directory, config, callback) ->
components = {}
extensions = Object.keys config.handlers
fs.exists directory, (exists) ->
return callback null, {} if not exists
fs.readdir directory, (err, filenames) ->
return callback err if err
supported = filenames.filter (f) -> path.extname(f) in extensions
unsupported = filenames.filter (f) -> not (path.extname(f) in extensions)
debug 'unsupported component files', unsupported if unsupported.length
for filename in supported
component = path.basename(filename, path.extname(filename))
debug 'loading component from file', filename, component
components[component] = componentCommandForFile config, filename
return callback null, components
# TODO: also add msgflo-python
defaultHandlers =
".yml": "msgflo-register-foreign --role #ROLE participants/#COMPONENT.yml"
".js": "msgflo-nodejs --name #ROLE participants/#COMPONENT.js"
".coffee": "msgflo-nodejs --name #ROLE participants/#COMPONENT.coffee"
".json": "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS"
".fbp": "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS"
languageExtensions =
'python': 'py'
'coffeescript': 'coffee'
'javascript': 'js'
'yaml': 'yml'
normalizeConfig = (config) ->
config = {} if not config
config = config.msgflo if config.msgflo # Migth be under a .msgflo key, for instance in package.json
config.components = {} if not config.components
config.variables = {} if not config.variables
config.handlers = {} if not config.handlers
for k, v of defaultHandlers
config.handlers[k] = defaultHandlers[k] if not config.handlers[k]
return config
class Library
constructor: (options) ->
options.config = JSON.parse(fs.readFileSync options.configfile, 'utf-8') if options.configfile
options.componentdir = 'participants' if not options.componentdir
options.config = normalizeConfig options.config
= options
= {} # lazy-loaded using load()
load: (callback) ->
componentsFromDirectory .componentdir, .config, (err, components) =>
return callback err if err
for k,v of components
[k] = v
for k,v of componentsFromConfig .config
[k] = v
return callback null
addComponent: (name, language, code, callback) ->
debug 'adding component', name, language
ext = languageExtensions[language]
ext = ext or language # default to input lang for open-ended extensibility
name = path.basename name # TODO: support multiple libraries?
filename = path.join .componentdir, "#{name}.#{ext}"
fs.writeFile filename, code, (err) =>
return callback err if err
[name] = componentCommandForFile .config, filename
return callback null
componentCommand: (component, role, iips={}) ->
cmd = [component]
throw new Error "No component #{component} defined for role #{role}" if not cmd
vars =
'ROLE': role
'IIPS': "'#{JSON.stringify(iips)}'"
cmd = replaceVariables cmd, vars
return cmd
exports.Library = Library