snowyu/abstract-type.js

View on GitHub
src/index.coffee

Summary

Maintainability
Test Coverage
properties      = require 'property-manager/ability'
factory         = require 'custom-factory'
createObject    = require 'inherits-ex/lib/createObject'
isInheritedFrom = require 'inherits-ex/lib/isInheritedFrom'
isObject        = require 'util-ex/lib/is/type/object'
isFunction      = require 'util-ex/lib/is/type/function'
isString        = require 'util-ex/lib/is/type/string'
isArray         = require 'util-ex/lib/is/type/array'
isUndefined     = require 'util-ex/lib/is/type/undefined'
defineProperty  = require 'util-ex/lib/defineProperty'
extend          = require 'util-ex/lib/_extend'
Attributes      = require './attributes/'

attributes      = new Attributes()
objectToString  = Object::toString
getObjectKeys   = Object.keys

class Value
  @tryGetTypeName: (aValue)->
    if aValue instanceof Object
      result = objectToString.call aValue
      i = result.lastIndexOf ' '
      result = result.substring(i+1, result.length-1) if i >= 0
    else
      result = typeof aValue
      result =result.charAt(0).toUpperCase() + result.substring(1)
    result
  constructor: (aValue, aType, aOptions)->
    if not (aType instanceof Type)
      aOptions = aType
      vTypeName = Value.tryGetTypeName(aValue)
      if aOptions
        # TODO: whether cache this type?
        aType = Type.create vTypeName, aOptions
      else
        aType = Type vTypeName
      throw new TypeError 'can not determine the value type.' unless aType
    if not (this instanceof Value)
      return createObject aType.ValueType, aValue, aType, aOptions
    defineProperty @, '$type', aType
    @_initialize(aValue, aType, aOptions)
    @assign(aValue, aOptions)
  isValid: (aOptions)->@$type.isValid(@valueOf(), aOptions)
  _initialize: (aValue, aType, aOptions)->
    defineProperty @, 'value', null
  _assign:(aValue)->
    @value = aValue
    return
  assign: (aValue, aOptions)->
    checkValidity = aOptions.checkValidity if aOptions
    vType  = @$type
    if isFunction vType.toValue
      aValue = vType.toValue aValue, aOptions
    else if aValue instanceof Value
      aValue = aValue.valueOf()
    @$type.validate(aValue, checkValidity) if checkValidity isnt false
    @_assign aValue
    @
  create: (aValue, aOptions)->
    @$type.createValue aValue, aOptions
  clone: (aOptions) ->
    @create @valueOf(), aOptions
  toString: -> String(@valueOf())
  valueOf: ->@value
  _toObject: (aOptions)->@valueOf()
  toObject: (aOptions)->
    if aOptions and aOptions.withType
      delete aOptions.withType
      result = @toObjectWithType(aOptions)
    else
      result = @_toObject(aOptions)
    result
  toObjectWithType: (aOptions)->
    value = @_toObject(aOptions)
    #aOptions = {} unless aOptions
    result = @$type.toObject(aOptions)
    result.value = value
    result
  # assign value from JSON string.
  fromJson: (aString)->
    aString = JSON.parse aString
    @assign aString
  # create a new value object from JSON string.
  createFromJson: (aString)->
    aString = JSON.parse aString
    vType  = @$type
    aString = vType.toValue aString if vType.toValue
    createObject @$type.ValueType, aString, vType
  toJSON: (aOptions)->
    result = @toObject(aOptions)
    vType  = @$type
    if vType.valueToString
      if result.hasOwnProperty 'value'
        result.value = vType.valueToString result.value
      else
        result = vType.valueToString result
    result
  # convert a Json string.
  toJson: (aOptions)->
    result = @toJSON(aOptions)
    JSON.stringify result
  inspect: ->
    '<type '+@$type._inspect(value:@_toObject())+'>'
#End Value class

module.exports  = class Type
  factory Type
  properties Type, name: 'advance'

  @ROOT_NAME: 'type'

  # export the Value Class from here
  @Value: Value
  # override for inherited type class:
  ValueType: Value
  $attributes: attributes

  constructor: (aTypeName, aOptions)->
    # create a new instance object if aOptions is not the original
    # options of the type.
    if not (this instanceof Type) and not (aTypeName instanceof Type)
      if aTypeName
        if isObject aTypeName
          aOptions = aTypeName
          aTypeName = attributes.getValue aOptions, 'name'
        else if not isString aTypeName
          aOptions = aTypeName
          aTypeName = undefined
      if not aTypeName
        # arguments.callee is forbidden if strict mode enabled.
        try vCaller = arguments.callee.caller
        if vCaller and isInheritedFrom vCaller, Type
          aTypeName = vCaller
          vCaller = vCaller.caller
          #get farest hierarchical registered class
          while isInheritedFrom vCaller, aTypeName
            aTypeName = vCaller
            vCaller = vCaller.caller
          aTypeName = Type.getNameFromClass(aTypeName) if aTypeName
        return unless aTypeName
      vTypeClass = Type.registeredClass aTypeName
      if vTypeClass and aOptions and
         not vTypeClass::$attributes.isDefaultObject(aOptions)
        return createObject vTypeClass, aOptions
    return super

  initialize: (aOptions)->
    defineProperty @, 'errors', null
    @$attributes.initializeTo @ if @$attributes
    @_initialize aOptions if @_initialize
    @assign(aOptions) if aOptions?

  finalize: (aOptions)->
    @errors = null if @errors
    @_finalize(aOptions) if @_finalize

  @validators: {}
  # aValidatorFn  = (aValue, aOptions)->
  @registerValidator: (aValidator, aValidatorFn)->
    vIsFn = null
    if isObject aValidator
      aValidatorFn = aValidator.validate if vIsFn = isFunction aValidator.validate
      aValidator = aValidator.name if aValidator.name
    vIsFn  = isFunction(aValidatorFn) if vIsFn is null
    result = vIsFn and isString(aValidator) and !Type.validators[aValidator]
    Type.validators[aValidator] = aValidatorFn if result
    return result
  @unregisterValidator: (aValidatorName)->
    delete Type.validators[aValidatorName]
  _checkValidator: (aValue, aOptions)->
    aOptions = @ unless isObject aOptions
    result = true
    for vName, vFn of Type.validators
      if aOptions[vName]? or @$attributes.getRealAttrName vName
        # u can push errors in the validation function
        result = vFn.call @, aValue, aOptions
        break unless result
    result

  oldAssign = @::assign
  assign: (aOptions, aExclude)->
    @errors = []

    return oldAssign.call @, aOptions, aExclude

  _validate: (aValue, aOptions)->true
  error: (aMessage, aOptions)->
    name = (aOptions && @$attributes.getValue(aOptions, 'name')) || String(@)
    @errors.push name: name, message: aMessage
    return
  isRequired: (aValue, aOptions)->
    aOptions = @ unless isObject aOptions
    vRequired = @$attributes.getValue aOptions, 'required'
    result = not vRequired or (vRequired is true and aValue?)
  validateRequired: (aValue, aOptions)->
    result = @isRequired aValue, aOptions
    @error 'is required', aOptions unless result
    result
  validate: (aValue, raiseError, aOptions)->
    @errors = []
    if isObject raiseError
      aOptions    = raiseError
      raiseError  = aOptions.raiseError
    customValidate = @$attributes.getValue(aOptions, 'customValidate') if aOptions
    aOptions = @mergeTo(aOptions, 'name')
    customValidate = aOptions.customValidate unless customValidate
    aOptions.raiseError = true if raiseError
    result = @validateRequired aValue, aOptions
    result = @_checkValidator(aValue, aOptions) if result
    result = customValidate.call(@, aValue, aOptions) if result and isFunction customValidate
    result = @_validate(aValue, aOptions) if result and aValue?
    if raiseError isnt false and not result
      throw new TypeError('"'+aValue + '" is an invalid ' + @name)
    result
  isValid: (aValue, aOptions) ->
    @validate(aValue, false, aOptions)

  createValue: (aValue, aOptions)->
    if aOptions and not @isSame(aOptions)
      aOptions = @mergeTo(aOptions, 'name')
      # TODO: seperate the cache-able ability
      if isFunction Type.getCacheItem
        # this Type Factory is cache-able.
        aOptions.cached = true unless aOptions.cached?
        vType = Factory.getCacheItem @Class, aOptions
      else
        vType = @createType aOptions
    else
      vType = @
    createObject vType.ValueType, aValue, vType, aOptions
  create: @::createValue
  createType: (aOptions)->
    delete aOptions.value if aOptions
    result = createObject @Class, aOptions
    result
  cloneType: (aOptions)->
    aOptions = @exportTo(aOptions, 'name', true, true)
    aOptions.name = @name unless aOptions.name
    @createType aOptions
  clone: @::cloneType
  # Get(create) a global Type class or create new Value from the json string.
  # it will create a new type object if options is not the original type
  # options.
  @fromJson: (aString)->
    #aString = JSON.parse aString
    Type.from JSON.parse(aString)
  # create a new Type instance  or create new Value from json string.
  @createFromJson: (aString)->
    Type.createFrom JSON.parse aString

  # Get(create) a global Type class or create new Value from the parametric
  # type object.
  # it will create a new type object if options is not the original(default)
  # type options.
  @from: (aObject) ->
    result = Type aObject
    if aObject.value? and result
      result = result.createValue aObject.value
    result
  @createFrom: (aObject)->
    value   = aObject.value
    result  = Type.create aObject.name, aObject
    result  = result.createValue value if value? and result
    result

  toString: (aOptions)->
    '[type '+ @name+']'

  _inspect: (aOptions, aNameRequired = false)->
    result = '"' + @name + '"'
    vAttrs = @toJson(aOptions, aNameRequired).slice(1,-1)
    result += ': ' + vAttrs if vAttrs
    result
  inspect: ->
    '<type ' + @_inspect()+ '>'
  toJson: (aOptions, aNameRequired)->
    result = @toObject(aOptions, aNameRequired)
    result = JSON.stringify result
    result
  _toObject:(aOptions, aNameRequired = true)->
    vExclude = 'name' unless aNameRequired
    result = @exportTo aOptions, vExclude, true, true
    result
  toObject: (aOptions, aNameRequired)->
    if aOptions
      unless aOptions.typeOnly or isUndefined aOptions.value
        value = aOptions.value
      aOptions = extend({}, aOptions)
      delete aOptions.typeOnly
      delete aOptions.value
    result = @_toObject(aOptions, aNameRequired)
    result.value = value unless isUndefined value
    result