metalman

Composes configurable methods which are based on middlewares.

Usage no npm install needed!

<script type="module">
  import metalman from 'https://cdn.skypack.dev/metalman';
</script>

README

metalman

Greenkeeper badge

A recursion-free middleware framework for composable methods with native promise support. Middlewares are factory functions that should return a command handler function.

Middlewares receive the command config which can be used for initialization and to register command handlers if they like to.

A parameter passed during command invocation runs through all the middlewares until a middleware stops execution using an error. Returning undefined in a middleware handler means it should continue with the execution of the next middleware without doing any changes on the input parameter.

Example method

const metalman = require('metalman')

// Load middlewares
// registers a `schema` handler, see the `schema` attribute in the example below
const schema = require('metalman-schema')

// registers an `action` handler, see the `action` attribute in the example below
// Basically it just proxies the `action` attribute of a command to the middleware factory
//   e.g. `function action (command) { return command.action }`
const action = metalman.action

// Pass the middlewares to the command factory
const command = metalman([schema, metalman.action])

module.exports = {
  doSomething: command({
    async action (cmd) {
      return somethingAsync(cmd)
    }
  }),
  createUser: command({
    schema: {
      required: ['id', 'name'],
      properties: {
        id: {type: 'string', maxLength: 36},
        name: {type: 'string'}
      }
    },

    // This middleware framework only supports
    // one input argument and an optional callback
    // We like to keep it simple (and fast), so we can stream objects into those methods
    action (cmd) {
      // cmd.id is definitely a string
      // cmd.name is also a string

      // By returning a value, you can change what gets passed
      // into the next middleware as input parameter.
      // If you don't need to change the output parameter,
      // and just go to the next middleware, you can return `undefined`
      return {id: cmd.id, name: cmd.name}
    }
  })
}

// module.exports.createUser({id: 'some-id', name: 'foobar'}, console.log)

Api

metalman([middlewares])

Instatiates a new object where you can pass some middlewares.

const metalman = require('metalman')
const commands = metalman([metalman.action])

A middleware is a factory that should return a command handler. It receives the config object of a command that's passed using instance(config).

metalman.action

The simplest middleware there is. It executes the provided function passed as action property on a command config.

const metalman = require('metalman')
const commands = metalman([metalman.action])
const someCommand = commands({action (param) { throw new Error(param) }})

// `someCommand()` returns a callback and therefore `awaiting` it
// will throw an error 'Hello' as we're just proxying it to the error instance
// in the example.
await someCommand('Hello')

instance(config)

Constructs a new function using the provided config which gets passed to the middlewares that conditionally can register a middleware.

The config parameter must be an object.

instance.object({...methods})

A helper to create multiple methods and return an object

e.g.

const metalman = require('metalman')
const commands = metalman([metalman.action])

// The following declaration will basically return an object
module.exports = commands.object({
  ping: {
    action (cmd) { return 'pong' }
  },
  someAsyncFunction: {
    async action (cmd) { await 'something' }
  }
})

// The returned methods are automatically promisified and callbackified
// Just provide the optional callback as parameter, to execute it as callback
// It will look like something similar like that:
module.exports = {
  ping (cmd, [callback]) { return 'pong' },
  someAsyncFunction (cmd, [callback]) { await 'something' }
}

e.g. if you want to mix in your custom functions, just use a spread operator

// The following declaration will basically return an object
module.exports = {
  someOtherMethod () { return 'Hello World' },
  ...commands.object({
    ping: {
      action (cmd) { return 'pong' }
    },
    someAsyncFunction: {
      async action (cmd) { await 'something' }
    }
  })
}

instance.define('name', config)

Just another helper to declare some commands. e.g.

module.exports = commands
  .define('ping', {action () { return 'pong' }})
  .define('someAsyncFunction', {async action () { await 'something' }})

Example middleware

The action middleware exposed as require('metalman').action.

function actionMiddleware (config) { return config.action }
module.exports = schemaValidation

// The command config directly gets passed to the factory
function schemaValidation (commandConfig) {
  // In case the command doesn't need a middleware, just return a falsy value
  if (!commandConfig.schema) return
  const validator = require('ajv')(commandConfig.schema)

  return function validate (command) {
    // returning `undefined` continues the middleware execution
    if (validator(command)) return
    else throw new Error(JSON.stringify(validator.errors)
  }
}

Example websocket server

Check out /examples/server.js and /examples/client.js

const schema = require('metalman-schema')
const action = require('metalman-action')
const Methodman = require('methodman')
const metalman = require('metalman')
const websocket = require('websocket-stream')

const command = metalman([schema, action])
const commands = {
  echo: command({
    schema: {type: 'string'},
    action (str) {
      return str
    }
  })
}

function onWebsocketStream (stream) {
  const methodman = Methodman(stream)
  methodman.commands(commands)
}

const http = require('http')
const server = http.createServer(function (req, res) { res.end() })
websocket.createServer({server}, onWebsocketStream)

const port = process.env.PORT || 0
server.listen(port, function (err) {
  if (err) return console.error(err)
  console.log('Listening on http://localhost:%s', server.address().port)
})