UNPKG

glass-platform

Version:

Glass platform

324 lines (297 loc) 12.9 kB
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