glass-platform
Version:
Glass platform
324 lines (297 loc) • 12.9 kB
text/coffeescript
require './global'
generateId = (parent, type) ->
name = type.name
throw new Error "type did not have a name: #{type}" unless name?
counts = parent._Component_generateId_counts ?= {}
count = counts[name] ?= 0
count++
counts[name] = count
return "#{name}_#{count}"
module.exports = Component = class glass_Component
constructor: (properties) ->
@initialize properties
@
isPrimitive = (object) ->
Object.isNumber(object) or Object.isBoolean(object) or Object.isString(object)
Component.normalizeProperties = (properties={}, definingClass) ->
for name, property of properties
if Object.isFunction(property)
property =
writable: false
value: property
else if not property? or isPrimitive(property) or Object.isArray(property)
property =
value: property
if not property.get? and not property.set? and not property.hasOwnProperty('value')
property.value = null
if property.hasOwnProperty 'value'
# default property values to writable
property.writable ?= true
# set the id of functions to their property name
if Object.isFunction property.value
property.value.id ?= name
if definingClass?
property.definingClass ?= definingClass
if Object.isFunction property.value
property.value.definingClass ?= definingClass
properties[name] = property
properties
Component.defineProperties = (object, properties, definingClass) ->
properties = Component.normalizeProperties properties, definingClass
Object.defineProperties object, properties
properties
Component.disposeProperties = (object) ->
for key, value of object
if value? and value.parent is object and Object.isFunction value.dispose
value.dispose()
return
Component.id = "glass.Component"
Component.toString = -> @id
Component.valueOf = -> @value ? @id
properties =
# composition
id:
get: -> @_id
set: (value) ->
throw new Error "id has already been set to #{@_id}" if @_id?
@_id = value
parent:
get: -> @_parent
set: (value) ->
throw new Error "parent has already been set to #{@_parent}" if @_parent?
@_parent = value
inner:
description: "Calls the subclass defined function if present."
value: (fn, args...)->
# get and cache the name of the underride function
innerName = fn.innerName ?= getUnderrideName fn.definingClass, fn.id
@[innerName]?.apply @, args
# lifecycle
initialize: initialize = (properties) ->
throw new Error "properties object is required #{properties}" unless properties?
throw new Error "parent is required" unless properties.parent?
# first set ourselves onto parent
parent = properties.parent
id = properties.id ?= generateId parent, @constructor
parent[id] = @
# second we set all properties in case setters access parent
for key, value of properties
@[key] = value
# finally we call subclass initializer
@inner initialize
dispose: dispose = ->
if @_parent?
Component.disposeProperties @
if @_parent is global
delete @_parent[@id]
else
@_parent[@id] = null
@_parent = null
@inner dispose
return
disposed:
get: -> @_parent is null
# discovery
get: (id, parsed) ->
throw new Error "id is required" unless id?
value = @[id]
if value?
if value.disposed is true
value = null
else
return value
# we only throw error from the original method call.
throwError = not parsed?
# now we have to look for it
# parse the id if it matches our format
if not parsed?
colon = id.indexOf ':'
if colon > 0
parsed =
type: id.substring 0, colon
properties: JSON.parse id.substring colon + 1
else
parsed = false
# if we recognize the parse object then
# check for a local factory of its type
if parsed
type = parsed.type
properties = parsed.properties
# look for a factory method with that type
factory = this[type]
if Object.isFunction factory
# set this as the parent on the properties
properties.parent = @
isClass = factory.properties?
if isClass
value = new factory properties
else
value = factory properties
# if we don't have a value, try to get from parent
value ?= @parent.get?(id, parsed)
if value?
# cache the result locally for next time it's requested
this[id] = value
else if throwError
throw new Error "Component not found: #{id}"
return value
Component.properties = Component.defineProperties Component.prototype, properties, Component
getUnderrideName = (baseDefiningClass, name) ->
"#{baseDefiningClass.name}_subclass_#{name}"
getBaseDefiningClass = (classDefinition, properties, name) ->
baseProperty = properties[name]
# now traverse the underride chain.
while true
baseFunction = baseProperty.value
baseDefiningClass = baseProperty.definingClass
# search the text for a call to the underride function
underrideName = getUnderrideName baseDefiningClass, name
callsUnderride =
baseFunction.toString().has(underrideName) or
baseFunction.toString().has(/\binner\b/)
if not callsUnderride
throw new Error "#{classDefinition.name}.#{name} cannot be defined because #{baseDefiningClass.name}.#{name} does not call #{underrideName}."
# now check to see if it has already been underridden
underrideProperty = properties[underrideName]
if underrideProperty?
baseProperty = underrideProperty
else
return baseDefiningClass
return
underride = (classDefinition, properties, rootDefiningClass, name, fn) ->
baseDefiningClass = getBaseDefiningClass classDefinition, properties, name
properties[getUnderrideName baseDefiningClass, name] = fn
return
extend = (baseClass, subClassDefinition) ->
throw new Error "missing id property" unless Object.isString subClassDefinition?.id
subClassDefinition.name = subClassDefinition.id.replace /[\.\/]/g, '_'
subClass = eval """
(function #{subClassDefinition.name}(properties) {
this.initialize(properties);
})
"""
subProperties = subClassDefinition.properties = Component.normalizeProperties subClassDefinition.properties, subClass
prototype = subClass.prototype
properties = Object.clone baseClass.properties
for name, property of subProperties
baseProperty = properties[name]
if Object.isFunction baseProperty?.value
if not Object.isFunction property.value
throw new Error "Functions can only be overridden with other functions: #{property.value}"
# if method defined in A, but not overrode in B, then C
# must override the correct name from A
underride subClassDefinition, properties, baseProperty.definingClass, name, property.value
else
properties[name] = property
subClassDefinition.properties = properties
Object.merge subClass, subClassDefinition
Component.defineProperties prototype, properties, subClass
# add an extend method to the subclass
subClass.extend = (subClassDefinition) -> extend subClass, subClassDefinition
subClass
Component.extend = (subClassDefinition) ->
extend Component, subClassDefinition
if typeof describe is 'function'
assert = require 'assert'
describe 'glass.Component', ->
it "should have an id", ->
assert Object.isString Component.id
it "its toString should return it's id", ->
assert.equal Component.toString(), "glass.Component"
it "should have a name", ->
assert.equal Component.name, "glass_Component"
describe '#dispose', ->
it 'should mark self disposed', ->
a = new Component parent:global
a.dispose()
assert a.disposed
it 'should dispose of children', ->
a = new Component parent:global
b = new Component parent:a
a.dispose()
assert b.disposed
it 'should remove property from parent', ->
a = new Component parent:global
a.dispose()
assert not global[a.id]?
describe '#defineProperties', ->
it "should allow primitive values", ->
object = {}
Component.defineProperties object,
f: -> "function"
i: 2
b: true
a: []
s: "hello"
assert Object.isFunction object.f
assert.equal object.f(), "function"
assert.equal object.i, 2
assert.equal object.b, true
assert Object.equal object.a, []
assert.equal object.s, "hello"
describe '#Constructor', ->
it 'should set itself as property on parent', ->
a = new Component parent:global
assert Object.isString a.id
assert.equal global[a.id], a
a.dispose()
it 'should require parent', ->
assert.throws -> a = new Component
it 'should generate a missing id', ->
a = new Component parent:global
assert Object.isString a.id
a.dispose()
describe "#get", ->
it 'should throw exception if instance not found', ->
a = new Component parent:global
assert.throws -> a.get "foo"
a.dispose()
it 'should create instances with factory', ->
a = new Component parent:global
b = new Component parent: a
# register a factory on the parent a
a[Component] = Component
c = b.get 'glass.Component:{"x":2,"y":3}'
assert.equal c.x, 2
assert.equal c.y, 3
assert.equal c.parent, a
a.dispose()
describe 'extend', ->
it 'should inherit base properties', ->
SubComponent = Component.extend
id: 'SubComponent'
assert SubComponent.properties.id?
it 'should allow underriding constructors and functions', ->
SubComponent = Component.extend
id: 'SubComponent'
properties:
initialize: ->
@constructorCalled = true
@
dispose: ->
@disposeCalled = true
return
sub = new SubComponent parent:global
assert sub.constructorCalled
sub.dispose()
assert sub.disposed
assert sub.disposeCalled
it 'should allow recursive extension', ->
AComponent = Component.extend
id: 'AComponent'
properties:
dispose: -> # does not call AComponent_subclass_dispose
BComponent = AComponent.extend
id: 'BComponent'
properties:
foo: ->
it 'should not allow final functions to be underridden', ->
AComponent = Component.extend
id: 'AComponent'
properties:
dispose: -> # does not call AComponent_subclass_dispose
assert.throws ->
BComponent = AComponent.extend
id: 'BComponent'
properties:
dispose: -> # cannot underride this method