sphereio/sphere-product-type-json-generator

View on GitHub
src/coffee/product-type-generator.coffee

Summary

Maintainability
Test Coverage
_ = require 'underscore'
_.mixin require('underscore.string').exports()
Promise = require 'bluebird'
fs = Promise.promisifyAll require('fs')

# TODO: missing types
ATTRIBUTE_TYPES =
  text: 'text'
  ltext: 'ltext'
  enum: 'enum'
  lenum: 'lenum'
  set: 'set'
  reference: 'reference'

MASTER_SKU_NAME = 'mastersku'

isTrue = (val) ->
  val is 'true' or val is 'TRUE'

###*
 * Class for generating JSON product-type representations from CSV files
 * @class ProductTypeGenerator
###
class ProductTypeGenerator

  ###*
   * Main exposed function that runs the program
   * @param  {Array} types The parsed CSV records of product-types
   * @param  {Array} attributes The parsed CSV records of product-type attributes
   * @param  {String} target The path to the folder where to write files
   * @param  {Boolean} withRetailer Wheter to generate extra files for master<->retailer support
   * @return {Promise} A promise resolved with summary report
  ###
  run: (types, attributes, target, withRetailer) ->
    new Promise (resolve, reject) =>
      try
        # build object with all attribute defintions for later usage
        attributeDefinitions = @_createAttributeDefinitions attributes

        # build product type definitions
        productTypeDefinitions = @_createProductTypesDefinitions types, attributeDefinitions

        # handle master/retailer product types
        if withRetailer
          # create attribute definition 'mastersku'
          attributeDefinitionMastersku = @_createAttributeDefinitionMastersku()
          # build product type definitions used in retailer projects
          productTypeDefinitionsRetailers = @_createProductTypesDefinitions types, attributeDefinitions, attributeDefinitionMastersku

        resolve
          productTypes: productTypeDefinitions
          productTypesForRetailer: productTypeDefinitionsRetailers
      catch e
        reject e

  ###*
   * Map all attribute definitions in the correct format
   * @param  {Array} attributes The parsed CSV records of product-type attributes
   * @return {Object} A JSON object that maps attribute definitions in correct format using name as key
  ###
  _createAttributeDefinitions: (attributes) ->
    attributeDefinitions = {}
    lastProcessedAttributeDefinition = null

    for row in attributes
      # check if attribute name is empty
      attrName = row['name']
      if !!attrName
        attributeDefinition =
          name: attrName
          label: @_i18n row, 'label'
          type:
            name: @_type row['type']
          attributeConstraint: row['attributeConstraint']
          isRequired: isTrue row['isRequired']
          isSearchable: isTrue row['isSearchable']

        inputTip = @_i18n row, 'inputTip'
        if _.keys(inputTip).length > 0
          attributeDefinition.inputTip = inputTip

        # store attribute definition using name as key for easy access
        attributeDefinitions[attrName] = attributeDefinition
        # store last processed attribute for further usage (reading next rows)
        lastProcessedAttributeDefinition = attributeDefinition
      else
        # process additional attribute rows
        attributeDefinition = lastProcessedAttributeDefinition

      @_attributeDefinition row, attributeDefinition, attributeDefinition['type'], row['type']

    attributeDefinitions

  ###
  Builds an attribute definition instance (called recursivly for each part in given raw type name ('set:<type>').
  @param {object} row The row object containing key/value pairs (header/value).
  @param {object} attributeDefinition The object containing attribute definition
  @param {object} type The attribute type instance.
  @param {string} rawTypeName The raw attribute type name (e.g. 'set:text')
  ###
  _attributeDefinition: (row, attributeDefinition, type, rawTypeName) ->

    switch type.name
      when ATTRIBUTE_TYPES.text, ATTRIBUTE_TYPES.ltext
        attributeDefinition['inputHint'] = row['textInputHint']
      when ATTRIBUTE_TYPES.enum
        type['values'] = (type['values'] or []).concat [
          key: row['enumKey'].trim()
          label: row["#{ATTRIBUTE_TYPES.enum}Label"].trim()
        ]
      when ATTRIBUTE_TYPES.lenum
        type['values'] = (type['values'] or []).concat [
          key: row['enumKey'].trim()
          label: @_i18n row, "#{ATTRIBUTE_TYPES.enum}Label"
        ]
      when ATTRIBUTE_TYPES.reference
        if row['type.referenceTypeId']
          type['referenceTypeId'] = row['type.referenceTypeId']
        else if row['type']
          type['referenceTypeId'] = @_type(@_typeOrElementType(rawTypeName))
      when ATTRIBUTE_TYPES.set
        # TODO: ensure set is correctly build
        # e.g.: it will generate a wrong attribute definition for
        #   name,description,set_set_gender
        #   women,Woman Product Type,x
        attributeDefinition['isRequired'] = false

        if row['type']
          type['elementType'] = {name: @_type(@_typeOrElementType(rawTypeName))}

        @_attributeDefinition row, attributeDefinition, type['elementType'], @_typeOrElementType rawTypeName

  ###*
   * Split the raw attribute type and return the attribute element type or the type itself
   * @param  {String} rawAttributeType The raw attribute type (e.g. 'text' or 'set:text')
   * @return {String} The mapped elements
   * @example
   *   'set:set:type' => 'set:type'
   *   'set:type'     => 'type'
   *   'text'         => 'text'
  ###
  _typeOrElementType: (rawAttributeType) ->
    parts = rawAttributeType.split(':')
    parts = parts[1..] unless parts.length == 1
    parts.join(':')

  ###*
   * Split the raw attribute type and return the first element, which is the real attribute type
   * @param  {String} rawAttributeType The raw attribute type (e.g. 'text' or 'set:text')
   * @return {String} The mapped elements types
   * @example
   *   'set:type' => 'set'
   *   'text'     => 'text'
  ###
  _type: (rawAttributeType) -> _.first rawAttributeType.split(':')

  ###*
   * Create an attribute definition 'masterSku' for master<->retailer support
  ###
  _createAttributeDefinitionMastersku: ->
    name: MASTER_SKU_NAME
    label:
      en: 'Master SKU'
    type:
      name: 'text'
    attributeConstraint: 'Unique'
    isRequired: true
    isSearchable: false
    inputHint: 'SingleLine'

  ###*
   * Map languages used for given attribute
   * @param  {String} name The name of the attribute header
   * @param  {Array} headers The list of headers from the CSV
   * @return {Array} The mapped list of languages
  ###
  _languages: (name, headers) ->
    regexp = new RegExp("^#{name}\.([a-zA-Z]{2})(-[a-zA-Z]{2})?", 'i')
    languages = (header) ->
      # `match` will return null if there is no match, otherwise it returns an array with the matched group
      # In this case it will output this
      # 'label.de'.match(regexp) => ["label.de"]
      matched = header.match(regexp)
      # here we can safely access `matched` and split it since we know it matched what we wanted
      _.last(matched[0].split(".")) if matched

    # this will iterate over the array and execute the function if the condition is passed, returning a "filtered" array
    # see http://coffeescript.org/#loops
    (languages(header) for header in headers when header.match(regexp))

  ###*
   * Map key/value pairs for each language
   * @param  {Object} row The row containing key/value pairs (header/value)
   * @param  {String} header The attribute header
   * @return {Object} The mapped keys
  ###
  _i18n: (row, header) ->
    i18n = {}
    languages = @_languages header, _.keys row
    for language in languages
      i18n[language] = row["#{header}.#{language}"].trim()
    i18n

  ###*
   * Create product-type definition objects
   * @param  {Array} types The parsed CSV records of product-types
   * @param  {Object} attributeDefinitions The mapped attribute definitions
   * @param  {Object} [defaultAttributeDefinition] Some attribute definitions added to the resulting product type definitions
   * @return {Array} The list of generated product-type definitions
  ###
  _createProductTypesDefinitions: (types, attributeDefinitions, defaultAttributeDefinition) ->

    productTypeDefinitions = []

    for row in types
      # enforce the key field - see issue #52
      if not row['key']
        console.error "[ERR] No key given for productType '#{row['name']}', skipping..."
        continue

      productTypeDefinition =
        name: row['name']
        description: row['description']
        key: row['key']
        attributes: []

      for header, value of row
        continue if header in ['name', 'description', 'key']

        if _.isString(value) and value.length > 0
          if attributeDefinitions[header]
            attributeDefinition = attributeDefinitions[header]
            attributeDefinition.name = value unless value.toLowerCase() is 'x'
            productTypeDefinition.attributes.push attributeDefinition
          else
            console.log "[WARN] No attribute definition found with name '#{header}', skipping..."

      # add default attribute definition to attributes
      if defaultAttributeDefinition
        productTypeDefinition.attributes.push defaultAttributeDefinition

      productTypeDefinitions.push productTypeDefinition
    productTypeDefinitions

module.exports = ProductTypeGenerator