snowyu/property-manager.js

View on GitHub
src/properties/index.coffee

Summary

Maintainability
Test Coverage
setPrototypeOf  = require 'inherits-ex/lib/setPrototypeOf'
isString        = require 'util-ex/lib/is/type/string'
isArray         = require 'util-ex/lib/is/type/array'
isBoolean       = require 'util-ex/lib/is/type/boolean'
isFunction      = require 'util-ex/lib/is/type/function'
isObject        = require 'util-ex/lib/is/type/object'
defineProperty  = require 'util-ex/lib/defineProperty'
cloneObject     = require 'util-ex/lib/clone-object'
deepEqual       = require 'deep-equal'
assignValue     = require '../assign-value'
getObjectKeys   = Object.keys
getOwnPropertyNames = Object.getOwnPropertyNames
getPropertyDescriptor = Object.getOwnPropertyDescriptor

module.exports = class Properties
  privated = defineProperty.bind(null, Properties::)

  @SMART_ASSIGN: '' #deprecated
  privated 'nonExported1stChar', defaultNonExported1stChar = '$'

  # the attrs inhertis from itself
  privated 'extends', (attrs, nonExported1stChar)->
    nonExported1stChar?= this.nonExported1stChar
    result = Properties(attrs, nonExported1stChar)
    setPrototypeOf(result, this)
    result.updateNames()
    result
  privated 'merge', (attrs)->@mergeTo attrs, @
  privated 'mergeTo', (attrs, dest)->
    return dest unless attrs
    dest = {} unless dest
    if attrs instanceof Properties
      keys = (k for k of attrs.names)
    else
      keys = getObjectKeys attrs
    for name in keys
      attr = attrs[name]
      @mergePropertyTo dest, name, attr
    dest.updateNames() if dest.updateNames
    return dest
  privated 'mergePropertyTo', (dest, name, attr)->
    #attr = type:attr if isString attr
    attr = value:attr unless !isArray(attr) and isObject attr
    if !attr.enumerable? and attr.assigned is false and attr.exported is false
      attr.enumerable = false
    else
      attr.enumerable = attr.enumerable isnt false
    vEnumerable = attr.enumerable isnt false
    vWritable = attr.writable isnt false or isFunction(attr.set)
    attr.assigned?= vEnumerable and vWritable
    attr.exported?= vEnumerable and name[0] isnt @nonExported1stChar and vWritable
    vAttr = dest[name]
    if vAttr is undefined
      dest[name] = attr
    else
      vAttr[k] = v for k, v of attr
    return
  privated '_initialize', (aOptions)-> @merge(aOptions)
  privated 'initialize', (aOptions)->
    @_initialize(aOptions)
    return
  constructor: (aOptions, nonExported1stChar)->
    if not (this instanceof Properties)
      return new Properties aOptions, nonExported1stChar
    defineProperty @, 'names', {}
    defineProperty @, 'ixNames', {}
    unless isString(nonExported1stChar) and nonExported1stChar.length is 1
      nonExported1stChar = defaultNonExported1stChar
    defineProperty @ , 'nonExported1stChar', nonExported1stChar
    @initialize(aOptions)

  privated 'updateNames', ->
    @names = {}
    @ixNames = {}
    for k,v of @
      # v = @[k]
      continue if !isObject(v) or !v.enumerable?
      @names[k] = v.name || k
      @ixNames[v.name|| k] = k

      vAlias = v.alias
      if vAlias
        if isArray vAlias
          for n in vAlias
            @ixNames[n] = k
        else if isString vAlias
          @ixNames[vAlias] = k
    return
  privated 'initializeTo', (dest, src = {}, aOptions = {})->
    {skipNull, skipUndefined, overwrite} = aOptions
    nonExported1stChar = @nonExported1stChar
    for k,v of @names
      continue if k is 'name'
      continue if !overwrite && dest[k] isnt undefined
      vAttr = @[k]
      value = src[k]
      value = vAttr.value if value is undefined
      continue if skipNull && value is null
      continue if skipUndefined && value is undefined
      if isString(vAttr.assigned) and !vAttr.get and !vAttr.set
        # Smart assignment:
        vAttr = cloneObject(vAttr)
        vAttrName = vAttr.assigned || nonExported1stChar+k
        defineProperty dest, vAttrName, value
        ((name, assign)->
          vAttr.get = ->@[name]
          if (vAttr.writable || isFunction(assign))
            if isFunction(assign)
              vAttr.set = (v)->
                @[name] = assign(v, @, @, name)
                return
            else
              vAttr.set = (v)->@[name] = v
        )(vAttrName, vAttr.assign)
      if !vAttr.get and !vAttr.set and vAttr.clone isnt false and
         isObject(value)
        try
          value = cloneObject(value)
        catch err
          err.message = 'the attribute "'+k+'" can not be cloned, set descriptor "clone" to false.\n' +
            err.message
          throw err
      value = assignValue(value, vAttr.type)
      defineProperty dest, k, value, vAttr
      # call set function to assign the initialization value after define property.
      dest[k] = value if vAttr.set
    return
  privated 'getRealAttrName', (name)->
    name = @ixNames[name] unless @hasOwnProperty name
    name
  privated 'validatePropertyValue', (name, value, attr, raiseError)->
    if isBoolean attr
      raiseError = attr
      attr = null
    attr = @[name] unless attr
    result = attr?
    raiseError ?= true
    throw new TypeError('no such property name:'+name) unless result
    if @Type and value? and attr.type? and value isnt attr.value
      vType = @Type attr.type
      if vType
        result = vType.isValid value
        if !result and raiseError
          k = "assign attribute '#{name}' error"
          if vType.errors.length
            k += ": the value '#{value}'"
            for v in vType.errors
              k += "\n #{v.name}: #{v.message}"
            dest.errors = vType.errors if dest.errors
          throw new TypeError k
    result
  privated 'assignPropertyTo', (dest, src, name, value, options)->
    # isExported means exportedOnly
    {skipDefault, isExported, skipExists} = options if options
    name = @getRealAttrName name
    if name
      vAttr = @[name]
      return if skipExists and dest[name] != undefined
      vIsAssigned = vAttr.assigned || isString(vAttr.assigned)
      return unless (vIsAssigned and !isExported) or (vAttr.exported and isExported)
      return if skipDefault and (vAttr.exported != true or vAttr.writable != false) and deepEqual vAttr.value, value
      vCanAssign = (!isExported and vIsAssigned) or value isnt undefined
      if name is 'name' and vCanAssign and value isnt dest.name
        dest.name = value
        return
      @validatePropertyValue name, value, vAttr if !isExported
      if isFunction(vAttr.assign)
        value = vAttr.assign(value, dest, src, name, options)
        #vCanAssign = false if value is undefined
      name = vAttr.name || name if isExported
      value = vAttr.value if value is undefined and vAttr.value != undefined
      if vCanAssign

        vAttrName = vAttr.assigned || @nonExported1stChar+name if isString vAttr.assigned
        unless vAttrName and !vAttr.get and !vAttr.set and
           isFunction(vAttr.assign) and dest.hasOwnProperty(vAttrName)
          # avoid duplication assignment.
          # dest[vAttrName] = assignValue(value, vAttr.type)
          # else
          vAttrName = name
          # dest[name] = assignValue(value, vAttr.type)

        if isExported
          if value?
            if isFunction value.toObject
              value = value.toObject(options)
            else if isFunction value.toJSON
              value = value.toJSON()
          dest[vAttrName] = value
        else
          dest[vAttrName] = assignValue(value, vAttr.type)
    return
  privated 'assignTo', (dest, src, aOptions = {})->
    {exclude, skipReadOnly, skipNull, skipUndefined, overwrite} = aOptions
    if isString exclude
      exclude = [exclude]
    else if not isArray exclude
      exclude = []
    dest?={}

    vNames = @names
    for k, v of vNames
      continue if (v in exclude) or (k in exclude)
      continue if skipReadOnly and v.writable is false or (v.get && !v.set)
      vAttr = @[k]
      vValue = src[v] || src[k]
      continue if skipNull and vValue is null
      continue if skipUndefined and vValue is undefined
      if overwrite || dest[k] is undefined
        @assignPropertyTo dest, src, k, vValue, aOptions
    return dest
  privated 'isDefaultObject', (aObject)->
    result = true
    for k,v of @names
      attr = @[k]
      continue if k is 'name' or attr.writable is false or attr.enumerable is false
      value = @getValue(aObject, k)
      #continue unless aObject.hasOwnProperty(k) or aObject.hasOwnProperty(v)
      unless value is undefined or value is attr.value
        result = false
        break
    result
  privated 'getValue', (aOptions, aName)->
    result = aOptions[aName]
    if not result?
      attr = @[aName]
      if attr?
        result = aOptions[attr.name]
        if not result? and attr.alias
          if isString attr.alias
            result = aOptions[attr.alias]
          else if isArray attr.alias
            for aName in attr.alias
              result = aOptions[aName]
              break if result?
    result
  # getNames: ->
  #   result = {}
  #   for k in getObjectKeys @
  #     v = @[k]
  #     result[k] = v.name || k
  #   result
module.exports.default = module.exports;