API/request.js

import { Promise } from 'es6-promise'
import EasyXDM from '../lib/easyXDM'
import { each, isString, map } from '../util'
import { emptyCleanups, guardCreateEasyXDM, KakaoError, KAKAO_AGENT, logDebug, processRules, serializeFile, URL } from '../common'

import rules from './rules'
import { accessToken } from './authType'

/**
 * 호스트가 `https://dapi.kakao.com`인 API (검색, 로컬, 비전, 번역)는 제외되며, Ajax를 통해 직접 요청할 수 있습니다.
 * Admin 키를 사용하는 API (인증, 푸시, 페이)는 제외됩니다.
 * 푸시 알림 기능은 지원되지 않습니다.
 * @typedef {string} APIUrl
 * @alias APIUrl
 * @example /v2/user/me
 * @see {@link https://developers.kakao.com/tool/demo/login/userme|데모 보러가기}
 * @memberof Kakao.API
 */
/**
 * 카카오 API를 호출할 수 있습니다.
 * @function request
 * @param {Object} settings API 호출과 관련된 설정을 key/value로 전달합니다.
 * @param {APIUrl} settings.url 호출할 API URL
 * @param {Object} [settings.data] API에 전달할 파라미터
 * @param {FileList|File[]|Blob[]} [settings.files] 파일 첨부가 필요한 API에서 이용하는 파일 파라미터
 * @param {Function} [settings.success] API 호출이 성공할 경우 결과를 받을 콜백 함수
 * @param {Function} [settings.fail] API 호출이 실패할 경우 결과를 받을 콜백 함수
 * @param {Function} [settings.always] API 호출이 성공하거나 실패할 경우 항상 호출할 콜백 함수
 * @returns {Promise}
 * @memberof Kakao.API
 */
let proxyForRequest = null
export function request(settings) {
  settings = processRules(settings, rules.request, 'API.request')

  const url = settings.url
  const urlRule = rules.api[url].data
  if (urlRule) {
    settings.data = processRules(settings.data, urlRule, `API.request - ${url}`)
  }

  if (!proxyForRequest) {
    proxyForRequest = getProxy()

    cleanups.push(() => {
      proxyForRequest.destroy()
      proxyForRequest = null
    })
  }

  return new Promise((resolve, reject) => {
    getConfig(settings).then(
      config => {
        proxyForRequest.request(
          config,
          res => {
            settings.success(res)
            settings.always(res)

            resolve(res)
          },
          xdmError => {
            const error = parseXdmError(xdmError)
            settings.fail(error)
            settings.always(error)

            reject(error)
          },
        )
      },
      error => {
        reject(error)
      },
    )
  })
}

function getProxy() {
  return guardCreateEasyXDM(() => {
    return new EasyXDM.Rpc(
      {
        remote: URL.apiRemote,
      },
      {
        remote: {
          request: {},
        },
      },
    )
  })
}

function parseXdmError(xdmError) {
  try {
    logDebug(xdmError)

    return JSON.stringify(xdmError.message.responseText)
  } catch(e) {
    return {
      code: -777,
      msg: 'Unknown error',
    }
  }
}

function getConfig(settings) {
  const { url } = settings
  const urlSpec = rules.api[url]

  const stringifiedData = {}
  each(settings.data, (value, key) => {
    stringifiedData[key] = isString(value) ? value : JSON.stringify(value)
  })

  const config = {
    url,
    method: urlSpec.method,
    headers: {
      KA: KAKAO_AGENT,
      Authorization: (urlSpec.authType || accessToken)(),
      'Cache-Control': 'no-cache',
      Pragma: 'no-cache', // IE 11에서 일부 응답(/v2/user/me)을 캐시하는 것을 방지
    },
    data: stringifiedData,
  }

  return new Promise((resolve, reject) => {
    if (isFileRequired(url) || settings.data.file) {
      const files = settings.files || settings.data.file
      if (!files) {
        throw new KakaoError(`'files' parameter should be set for ${url}`)
      }

      getFileConfig(files).then(
        fileConfig => {
          const searchParams = []
          for (let prop in stringifiedData) {
            if (prop !== 'file') {
              searchParams.push(`${prop}=${encodeURIComponent(stringifiedData[prop])}`)
            }
          }

          if (searchParams.length > 0) {
            config.url += `?${searchParams.join('&')}`
          }

          config.file = fileConfig
          resolve(config)
        },
        error => {
          reject(error)
        },
      )
    } else {
      resolve(config)
    }
  })
}

function isFileRequired(url) {
  return url === '/v1/api/story/upload/multi' ||
    url === '/v2/api/talk/message/image/upload'
}

function getFileConfig(files) {
  const serializePromises = map(files, file => {
    return serializeFile(file).then(serialized => {
      return {
        name: file.name,
        type: file.type,
        str: serialized,
      }
    })
  })

  return new Promise((resolve, reject) => {
    Promise.all(serializePromises).then(
      serializedFiles => {
        resolve({
          paramName: 'file',
          data: serializedFiles,
        })
      },
      error => {
        reject(error)
      },
    )
  })
}

const cleanups = []
export function cleanup() {
  emptyCleanups(cleanups)
}