estate/alexa-router

View on GitHub
lib/response.js

Summary

Maintainability
D
2 days
Test Coverage
'use strict'

let _ = require('lodash')
let Joi = require('joi')
let helpers = require('./helpers')

/**
 * Manipulates an Alexa's compatible reply
 */
class Response {
  /**
   * Instantiates a new Response
   * @param {Request} request An instance of Request
   * @param {Object} patchRaw Overrides the default raw reply
   */
  constructor (request, patchRaw) {
    this.request = request
    this.raw = _.merge({
      version: '1.0',
      response: { shouldEndSession: false },
      sessionAttributes: {}
    }, patchRaw)
  }

  /**
   * Detects if a text has SSML tags in it
   * @param {String} text The text to be analyzed
   * @return {Boolean}
   * @private
   */
  _isSSML (text) {
    return text.match(/<(.|\n)*?>/g)
  }

  /**
   * Sets, patches or retrieve the session's attributes
   * @param {String|Object} [key] If a string is present and value is also present
   * then it will set a single key into the session. If only the key is present
   * and it's a string then the key's value will be returned. If the key is an
   * object then it will be merged into the session. If neither the key nor the
   * value is present then the whole session will be returned
   * @param {String} [value] The value for the desired attributed
   * @return {String|Object} An string if only the key is defined and is a string
   * or an object representing the whole session if neither the key nor the value
   * are present
   */
  session (key, value) {
    if (typeof key === 'object') {
      this.raw.sessionAttributes = _.merge(this.raw.sessionAttributes, key)
    } else if (typeof key === 'string' && typeof value === 'undefined') {
      return this.raw.sessionAttributes[key]
    } else if (typeof key === 'string' && typeof value !== 'undefined') {
      this.raw.sessionAttributes[key] = value
    } else {
      return this.raw.sessionAttributes
    }
  }

  /**
   * Sets up the output speech and automatically detects SSML
   * @param {String} text The text that Alexa will say to the user
   * @return {Object} An object with the speech if the parameter is undefined
   */
  speech (text) {
    if (text) {
      // Unset all conflicting keys for precaution
      _.unset(this.raw.response, 'outputSpeech')
      _.unset(this.raw.response, 'reprompt')

      // Check if the speech is in the SSML format
      this.raw.response.outputSpeech = this._isSSML(text)
        ? { type: 'SSML', ssml: text }
        : { type: 'PlainText', text }
    } else {
      // The callee just want to retrieve the speech
      return this.raw.response.outputSpeech
        ? this.raw.response.outputSpeech.ssml || this.raw.response.outputSpeech.text
        : undefined
    }
  }

  /**
   * Makes another question to the user and automatically detects SSML
   * @param {String} text The text that Alexa will say to the user
   * @return {Object} An object with the reprompt speech if the parameter is undefined
   */
  reprompt (text) {
    if (text) {
      // Unset all conflicting keys for precaution
      _.unset(this.raw.response, 'outputSpeech')
      _.unset(this.raw.response, 'reprompt')

      // Detects SSML
      this.raw.response.reprompt = this._isSSML(text)
        ? { type: 'SSML', ssml: text }
        : { type: 'PlainText', text }
    } else {
      // The callee only wants to know the reprompt
      return this.raw.response.reprompt
        ? this.raw.response.reprompt.ssml || this.raw.response.reprompt.text
        : undefined
    }
  }

  /**
   * Sets the card attribute or returns
   * @param {Object} [card] An Alexa's response compatible card object
   * @return {Object} Returns the whole card object if the card param isn't present
   */
  card (card) {
    if (card) {
      // Unset any previous card
      _.unset(this.raw.response, 'card')
      this.raw.response.card = card
    } else {
      return this.raw.response.card
    }
  }

  /**
   * Clears the whole session data
   */
  clearSession () {
    this.raw.sessionAttributes = {}
  }

  /**
   * Sets or returns the shouldEndSession attribute
   * @param {Boolean} [shouldEndSession]
   * @return {Boolean}
   */
  endSession (shouldEndSession) {
    if (typeof shouldEndSession === 'boolean') {
      this.raw.response.shouldEndSession = shouldEndSession
    } else {
      return this.raw.response.shouldEndSession
    }
  }

  /**
   * Configures the next flow
   * @param {Object[]} config An object with a flow to be inserted
   */
  next (config) {
    // Load any previously added next flow
    let next = this.session('alexa-router-next')
      ? JSON.parse(this.session('alexa-router-next'))
      : []

    if (Array.isArray(config)) {
      config.forEach(config => this.next(config))
    } else {
      config = helpers.validate(config, Joi.object({
        type: Joi.only('intent', 'sessionEnded', 'launch', 'unexpected').required(),
        intent: Joi.when('type', {
          is: 'intent',
          then: Joi.string().required(),
          otherwise: Joi.forbidden()
        }),
        action: Joi.only(Object.keys(this.request.router.actions)).required(),
        params: Joi.any().optional()
      }))

      next.push(config)
      this.session('alexa-router-next', JSON.stringify(next))
    }
  }

  /**
   * Overrides the default toJSON attribute to return only the raw response
   * @return {Object} The raw response
   */
  toJSON () {
    return this.raw
  }
}

module.exports = Response