msgflo
Version:
Polyglot FBP runtime based on message queues
206 lines (175 loc) • 7.28 kB
text/coffeescript
fs = require 'fs'
path = require 'path'
debug = require('debug')('msgflo:library')
EventEmitter = require('events').EventEmitter
common = require './common'
defaultHandlers =
".yml": "msgflo-register-foreign --forever=true --role #ROLE #FILENAME"
".js": "msgflo-nodejs --name #ROLE #FILENAME"
".coffee": "msgflo-nodejs --name #ROLE #FILENAME"
".py": "msgflo-python #FILENAME #ROLE"
".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'
extensionToLanguage = {}
for lang, ext of languageExtensions
extensionToLanguage[".#{ext}"] = lang
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, filename) ->
variables = common.clone config.variables
componentName = component.split('/')[1]
componentName = component if not componentName
variables['FILENAME'] = filename if filename
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, filename
componentsFromConfig = (config) ->
components = {}
for component, cmd of config.components
components[component] =
language: null # XXX: Could try to guess from cmd/template??
command: 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
ext = path.extname filename
lang = extensionToLanguage[ext]
component = path.basename(filename, ext)
if config.namespace
component = "#{config.namespace}/#{component}"
debug 'loading component from file', filename, component
filepath = path.join directory, filename
components[component] =
language: lang
command: componentCommandForFile config, filepath
return callback null, components
normalizeConfig = (config) ->
config = {} if not config
namespace = config.name or null
if config.repository?.url
# package.json convention
config.repository = config.repository.url
config = config.msgflo if config.msgflo # Migth be under a .msgflo key, for instance in package.json
config.namespace = namespace if not config.namespace?
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 extends EventEmitter
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
= {} # "name" -> { command: "", language: ''|null }. lazy-loaded using load()
getComponent: (name) ->
# Direct match
return [name] if [name]
withoutNamespace = path.basename name
return [withoutNamespace] if [withoutNamespace]
if name.indexOf '/' == -1 and .config.namespace
withNamespace = .config.namespace + '/' + name
return [withNamespace] if [withNamespace]
return null
_updateComponents: (components) ->
names = Object.keys components
for name, comp of components
if not comp
# removed
[name] = null
continue
existing = name
unless existing
# added
[name] = comp
continue
# update
for k, v of comp
existing[k] = v
'components-changed', names,
load: (callback) ->
componentsFromDirectory .componentdir, .config, (err, components) =>
return callback err if err
components
componentsFromConfig(.config)
return callback null
# call when MsgFlo discovery message has come in
_updateDefinition: (name, def) ->
return if not def # Ignore participants being removed
changes = {}
changes[name] =
definition: def
changes
getSource: (name, callback) ->
debug 'requesting component source', name
component = name
return callback new Error "Component not found for #{name}" unless component
lang = component.language
ext = languageExtensions[lang]
basename = name
library = null
if name.indexOf('/') isnt -1
# FBP protocol component:getsource unfortunately bakes in library in this case
[library, basename] = name.split '/'
else if .config?.namespace?
library = .config?.namespace
filename = path.join .componentdir, "#{basename}.#{ext}"
fs.readFile filename, 'utf-8', (err, code) ->
debug 'component source file', filename, lang, err
return callback new Error "Could not find component source for #{name}" if err
source =
name: basename
library: library
code: code
language: component.language
return callback null, source
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
filename = path.join .componentdir, "#{path.basename(name)}.#{ext}"
if name.indexOf('/') == -1 and .config?.namespace
name = "#{@options.config.namespace}/#{name}"
fs.writeFile filename, code, (err) =>
return callback err if err
changes = {}
changes[name] =
language: language
command: componentCommandForFile .config, filename
changes
return callback null
componentCommand: (component, role, iips={}) ->
cmd = ?.command
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