printer.js

const Config = require('@bespoken-sdk/shared/lib/config')
const debug = require('debug')('PRINTER')
const fs = require('fs')
const Job = require('./job')
const path = require('path')
const stringify = require('csv-stringify')

/**
 * The printer class is responsible for outputting results in a human-readable format
 * The default implementation creates a CSV file
 */
class Printer {
  /**
   * @param {string} outputPath
   * @returns {Printer}
   */
  static instance (outputPath) {
    return Config.instance('printer', 'printer', outputPath)
  }

  /**
   * @param {string} outputPath
   */
  constructor (outputPath = 'results') {
    this.outputPath = outputPath.endsWith('.csv') ? `output/${outputPath}` : `output/${outputPath}.csv`
    // Make the output director if it does not exist
    const outputDirectory = path.dirname(this.outputPath)
    if (!fs.existsSync(outputDirectory)) {
      fs.mkdirSync(outputDirectory)
    }

    // If there is already an output file, remove it
    if (fs.existsSync(this.outputPath)) {
      fs.unlinkSync(this.outputPath)
    }
  }

  /**
   * Prints out the results for a job
   * @param {Job} job
   * @returns {Promise<void>}
   */
  async print (job) {
    let successCount = 0
    let ignoreCount = 0
    const outputHeaders = ['UTTERANCE']
    job.expectedFieldNames().forEach(h => outputHeaders.push('ACTUAL ' + h.toUpperCase()))
    outputHeaders.push('SUCCESS')
    job.expectedFieldNames().forEach(h => outputHeaders.push('EXPECTED ' + h.toUpperCase()))
    job.outputFieldNames().forEach(h => outputHeaders.push(h.toUpperCase()))
    outputHeaders.push('ERROR')
    outputHeaders.push('RAW DATA URL')

    const resultsArray = [outputHeaders]
    job.results.forEach(async (result) => {
      const ignore = result.error !== undefined
      if (ignore) {
        ignoreCount++
      } else if (result.success) {
        successCount++
      }

      // Print out a line of the CSV
      // First is the utterance
      // Then the actual values
      // Then TRUE or FALSE for success
      // Then the expected values
      const resultArray = [result.record.utteranceRaw]

      const expectedFieldNames = job.expectedFieldNames()
      for (const fieldName of expectedFieldNames) {
        const actual = result.actualFields[fieldName]
        resultArray.push(actual)
      }

      resultArray.push(result.success + '')
      for (const fieldName of expectedFieldNames) {
        const expected = result.record.expectedFields[fieldName]
        resultArray.push(expected)
      }

      // Add extra output fields
      for (const fieldName of job.outputFieldNames()) {
        const expected = result.outputField(fieldName)
        resultArray.push(expected)
      }

      // Add errors to output
      if (result.error) {
        resultArray.push(result.error)
      } else {
        resultArray.push('')
      }

      // Push a link to the logs
      const index = resultsArray.length - 1
      resultArray.push(`${job.logURL(index)}`)

      resultsArray.push(resultArray)
    })

    debug(`PRINT Success: ${successCount} Ignore: ${ignoreCount} Total: ${job.results.length}`)
    // Create the CSV and the output file asynchronously
    return new Promise((resolve, reject) => {
      stringify(resultsArray, {
        cast: {
          boolean: (v) => v ? 'TRUE' : 'FALSE'
        }
      }, (error, output) => {
        if (error) {
          reject(error)
        } else {
          fs.writeFile(this.outputPath, output, (error) => {
            if (error) {
              reject(error)
            } else {
              resolve()
            }
          })
        }
      })
    })
  }
}

module.exports = Printer