docs: function presenter (args, req, res, next) {
      // if path is for base of model and missing trailing slash redirect
      if (args.baseRegExp && req.originalUrl.match(args.baseRegExp)) {
          // redirect to url with trailing slash
          res.redirect(301, './'+args.route.model.path+'/')
          return
      }
      // role used for selecting controller
      var activeRole
      // roles that client has
      var roles = req.session && req.session.roles
          ? req.session.roles
          : ['all']
      // get spec
      var spec = _.find(args.spec, controller => {
          // if controller is all then use controller
          if (controller.role === 'all') {
              activeRole = controller.role
              return true
          }
          // otherwise check if session has role
          else {
              // get number of roles
              var rolesLen = roles.length
              // check each roles
              for (var i=0; i < rolesLen; i++) {
                  if (controller.role === roles[i]) {
                      activeRole = controller.role
                      return true
                  }
              }
          }
      })
      // if client does not have a valid role for this method:path then 403
      if (!spec) {
          httpError(403)
      }
      // check access unless allowed for all
      if (!spec.allow) {
          // get global access control instance
          var accessControl = ImmutableAccessControl.getGlobal()
          // check access
          var allow = accessControl.allowRoute({
              method: req.method.toLowerCase(),
              path: req.originalUrl,
              session: req.session,
          })
          // throw access denied error if not allowed
          if (!allow) {
              httpError(403)
          }
      }
      // get controller method (if any)
      var method = spec.method
      // get template - either explicit or based on method name
      var template = spec.template || spec.methodName
      // resolve template name
      if (defined(args.route.templates[template])) {
          // use template for active role if found
          if (defined(args.route.templates[template].role[activeRole])) {
              template = args.route.templates[template].role[activeRole]
          }
          // use template for all if found
          else if (args.route.templates[template].role['all']) {
              template = args.route.templates[template].role['all']
          }
      }
      // append extension to template if not found
      if (!template.match(args.config.handlebars.templateRegExp)) {
          template = template + args.config.handlebars.ext
      }
      // response data - either template args or json
      var data = {}
      // if there is a controller method need to prep args and execute
      if (method) {
          // arguments for method call
          var methodArgs = getMethodArgs(args, req, spec)
          // call method
          method(methodArgs)
          // success
          .then(methodData => {
              // should response be json
              var json = spec.json
              // if json request in args or by content type then set
              // unless not allowed by spec
              if (methodArgs.json && json !== false) {
                  json = true
              }
              // set default args for template if not a json response
              if (!json) {
                  data.session = req.session
                  _.merge(data, args.config.assets)
              }
              // merge response into data
              _.mergeWith(data, methodData, mergeArrays)
              // if data has cookies set them
              if (data.cookies && spec.cookies !== false) {
                  // set cookies
                  _.each(data.cookies, (value, name) => {
                      setCookie(name, res, value)
                  })
                  // delete cookies from data
                  delete data.cookies
              }
              // if data has headers set them
              if (data.headers && spec.headers !== false) {
                  // set headers
                  _.each(data.headers, (value, name) => {
                      res.set(name, value)
                  })
                  // delete headers from data
                  delete data.headers
              }
              // if data has template then use it
              if (data.template) {
                  template = data.template
              }
              // if request is xhr or has json flag set then return json
              if (json) {
                  res.json(data)
              }
              else {
                  // set production flag for template only
                  if (!defined(data.production)) {
                      data.production = isProd
                  }
                  // if there are components then add component assets
                  if (defined(data.components)) {
                      data.js.push({src: 'https://cdn.jsdelivr.net/npm/handlebars@4.0.10/dist/handlebars.runtime.min.js'})
                      data.js.push({src: '/assets/immutable-app-component.js'})
                  }
                  // set layout if not defined
                  if (defined(spec.layout) && !defined(data.layout)) {
                      data.layout = spec.layout
                  }
                  // render template
                  res.render(template, data)
              }
          })
          // error
          .catch(next)
      }
      // if there is no method then only render template
      else {
          // require template
          assert.ok(template, 'no template found')
          // set default data for template
          data.session = req.session
          _.merge(data, args.config.assets)
          // set production flag for template only
          if (!defined(data.production)) {
              data.production = isProd
          }
          // render template
          res.render(template, data)
      }
  }