Refactors our parsers.
authordsc <dsc@wikimedia.org>
Fri, 9 Mar 2012 19:19:27 +0000 (11:19 -0800)
committerdsc <dsc@wikimedia.org>
Fri, 9 Mar 2012 19:19:27 +0000 (11:19 -0800)
lib/chart/chart-library.co [new file with mode: 0644]
lib/chart/dygraphs-library.co [new file with mode: 0644]
lib/chart/index.co [new file with mode: 0644]
lib/main.co
lib/underscore/array.co
lib/util/aliasdict.co [new file with mode: 0644]
lib/util/backbone.co
lib/util/cascade.co [new file with mode: 0644]
lib/util/index.co
lib/util/parser.co [new file with mode: 0644]
www/modules.yaml

diff --git a/lib/chart/chart-library.co b/lib/chart/chart-library.co
new file mode 100644 (file)
index 0000000..04c053c
--- /dev/null
@@ -0,0 +1,118 @@
+_  = require 'kraken/underscore'
+op = require 'kraken/util/op'
+{EventEmitter} = require 'events'
+{Parsers, ParserMixin}  = require 'kraken/util/parser'
+
+
+/**
+ * @class Specification for an option.
+ */
+class exports.ChartOption
+    SPEC_KEYS : <[ name type default desc tags examples ]>
+    
+    name     : null
+    type     : 'String'
+    default  : null
+    desc     : ''
+    tags     : null
+    examples : null
+    
+    
+    (@library, @spec) ->
+        throw new Error('Each ChartOption requires a name!') unless @spec.name
+        
+        for k of @SPEC_KEYS
+            v = @spec[k]
+            @[k] = v if v?
+        @tags or= []
+        @parse = @library.getParser @type
+    
+    parse : Parsers.parseString
+    
+    isDefault: (v) ->
+        @default is v
+    
+    toString: -> "(#{@name}: #{@type})"
+
+
+/**
+ * @class Abstraction of a charting library, encapsulating its logic and options.
+ */
+class exports.ChartLibrary extends EventEmitter
+    /**
+     * Ordered ChartOption objects.
+     * @type ChartOption[]
+     */
+    options_ordered : null
+    
+    /**
+     * Map of option name to ChartOption objects.
+     * @type { name:ChartOption, ... }
+     */
+    options : null
+    
+    
+    
+    /**
+     * @constructor
+     * @param {String} name Library name.
+     * @param {Array} options List of options objects, each specifying the
+     *  name, type, default, description (etc) of a chart library option.
+     */
+    (@name, options) ->
+        @options_ordered = _.map options, (opt) ~> new ChartOption this, opt
+        @options = _.synthesize @options, -> [it.name, it]
+    
+    
+    /**
+     * @returns {ChartOption} Get an option's spec by name.
+     */
+    get: (name, def) ->
+        @options[name] or def
+    
+    /**
+     * @returns {Array} List of values found at the given attr on each 
+     *  option spec object.
+     */
+    pluck: (attr) ->
+        _.pluck @spec, attr
+    
+    /**
+     * @returns {Object} An object, mapping from option.name to the
+     *  result of the supplied function.
+     */
+    map: (fn, context=this) ->
+        _.synthesize @spec, ~> [it.name, fn.call(context, it, it.name, this)]
+    
+    
+    /**
+     * @returns {Boolean} Whether the supplied value is the same as
+     * the default value for the given key.
+     */
+    isDefault: (name, value) ->
+        @get name .isDefault value
+    
+    
+    serialize: (v, k) ->
+        # if v!?
+        #     v = ''
+        if _.isBoolean v
+            v =  Number v
+        else if _.isObject v
+            v = JSON.stringify v
+        String v
+    
+    
+    /**
+     * When implementing a ChartLibrary, you can add or override parsers
+     * merely by subclassing.
+     */
+    this:: import ParserMixin::
+    
+    getParserFor: (name) ->
+        @getParser @get(name).type
+    
+    
+    
+
+
diff --git a/lib/chart/dygraphs-library.co b/lib/chart/dygraphs-library.co
new file mode 100644 (file)
index 0000000..fc80796
--- /dev/null
@@ -0,0 +1,13 @@
+_ = require 'kraken/underscore'
+{ ChartLibrary, ChartOption,
+} = require 'kraken/chart/chart-library'
+
+
+class exports.DygraphsLibrary extends ChartLibrary
+    
+    (name, options) ->
+        super ...
+    
+    render: ->
+        ...
+    
diff --git a/lib/chart/index.co b/lib/chart/index.co
new file mode 100644 (file)
index 0000000..fe6f0b8
--- /dev/null
@@ -0,0 +1,3 @@
+library  = require 'kraken/chart/chart-library'
+dygraphs = require 'kraken/chart/dygraphs-library'
+exports import library import dygraphs
index e9e6960..e2d6793 100644 (file)
@@ -46,7 +46,7 @@ main = ->
     if match = /\/graph\/(?!view)([^\/?]+)/i.exec loc
         data.slug = match[1]
     
-    vis   = root.vis   = new VisModel data
+    vis   = root.vis   = new VisModel data, {+parse}
     graph = root.graph = new VisView do
         graph_spec : root.CHART_OPTIONS_SPEC
         model      : vis
index 0d5f1d3..76e078a 100644 (file)
@@ -17,7 +17,7 @@ _array = do
         _.reduce do
             o
             (acc, [k, v], idx) ->
-                if k and filter(v, k)
+                if k and (not filter or filter(v, k))
                     acc[k] = v
                 acc
             {}
@@ -28,10 +28,11 @@ _array = do
      * @param {Function} [fn=I] Transformation function. Defaults to the identity transform.
      * @param {Function} [filter=defined] Optional filter function. If omitted, will 
      *  exclude `undefined` and `null` values.
+     * @param {Object} [context=o] Function context.
      * @return {Object} Transformed result.
      */
-    synthesize : (o, fn=I, filter=defined) ->
-        _array.generate _.map(o, fn), filter
+    synthesize : (o, fn=I, filter=defined, context) ->
+        _array.generate _.map(o, fn, context), filter
     
     
     /**
diff --git a/lib/util/aliasdict.co b/lib/util/aliasdict.co
new file mode 100644 (file)
index 0000000..966c000
--- /dev/null
@@ -0,0 +1,158 @@
+_ = require 'kraken/underscore'
+
+/**
+ * @class A mapping of key-value pairs supporting key-aliases.
+ */
+class AliasDict
+    
+    /**
+     * Data store.
+     * @type Object
+     * @private
+     */
+     _data : null
+    
+    /**
+     * Mapping from keys to an array of [potentially nested] alias-keys.
+     * @type Object<String, Array<String>>
+     * @private
+     */
+    _aliases : null
+    
+    
+    /**
+     * @constructor
+     */
+    ->
+        @_data    = {}
+        @_aliases = {}
+        @extend ...
+    
+    
+    /**
+     * @returns {Number} Number of real keys in the Dict.
+     */
+    size : ->
+        _.keys @_data .length
+    
+    /**
+     * @returns {AliasDict} A copy of the AliasDict, including aliases as well as data.
+     */
+    clone: ->
+        d = new AliasDict @_data
+        _.each @_aliases, (v, k) ->
+            d.setAlias k, v.slice()
+        d
+    
+    
+    
+    ### Value Accessors ###
+    
+    /**
+     * @returns {Boolean} Whether there is a value at the given key.
+     */
+    has : (key) ->
+        (@get key)?
+    
+    /**
+     * @returns {*} Ignores aliases, returning the value at key or `undefined`.
+     */
+    getValue : (key) ->
+        prop = _.getNested @_data, key
+        prop.value if prop?
+    
+    get : (key, def) ->
+        aliases = @_aliases[key] or [key]
+        val = aliases.reduce do
+            (val, alias) ->
+                return val if val? is not undefined
+                prop = _.getNested @_data, alias
+                prop.value if prop?
+            undefined
+        
+        if val is not undefined
+            val
+        else
+            def
+    
+    set : (key, val) ->
+        _.setNested @_data, key, val, true
+        val
+    
+    del : (key) ->
+        prop = _.getNestedMeta key
+        if prop
+            delete prop.obj[prop.key]
+            prop.value
+    
+    
+    
+    ### Alias Methods ###
+    
+    hasAlias : (key) ->
+        @_aliases[key]?
+    
+    getAlias : (key, def=[]) ->
+        @_aliases[key] or def
+    
+    setAlias: (key, aliases) ->
+        @_aliases[key] = if _.isArray aliases then aliases else [aliases]
+        this
+    
+    addAlias : (key, ...aliases) ->
+        @_aliases[key] = _.flatten @getAlias(key, [key]).concat(aliases)
+        this
+    
+    delAlias : (key) ->
+        delete @_aliases[key]
+    
+    
+    
+    ### Collection Methods ###
+    
+    toObject: ->
+        _.clone @_data
+    
+    keys: ->
+        _.keys @_data
+    
+    values: ->
+        _.values @_data
+    
+    extend : (...args) ->
+        for o of args
+            for k,v in o then @set k, v
+        this
+    
+    reduce : (fn, acc, context=this) ->
+        _.reduce @_data, fn, acc, context
+    
+    map : (fn, context=this) ->
+        _.map @_data, fn, context
+    
+    filter: (fn, context=this) ->
+        _.filter @_data, fn, context
+    
+    each : (fn, context=this) ->
+        _.each @_data, fn, context
+        this
+    
+    invoke : (name, ...args) ->
+        _.invoke @_data, name, ...args
+    
+    pluck : (attr) ->
+        _.pluck @_data, attr
+    
+    find: (fn, context=this) ->
+        _.find @_data, fn, context
+    
+    
+    toString: ->
+        Cls = @.constructor
+        "#{Cls.displayName or Cls.name}()"
+
+
+module.exports = exports = AliasDict
+
+
+
index 4dbe1ed..8246d1d 100644 (file)
@@ -7,9 +7,9 @@ _backbone = do
     /**
      * @returns {Array<Class>} The list of all superclasses for this class or object.
      */
-    getSuperClasses: function getSuperClasses(cls)
-        cls .= constructor if cls and typeof cls is not 'function'
-        if superclass = cls?.__super__?.constructor
+    getSuperClasses: function getSuperClasses(Cls)
+        Cls .= constructor if cls and typeof Cls is not 'function'
+        if superclass = Cls?.__super__?.constructor
             [superclass].concat getSuperClasses superclass
         else
             []
diff --git a/lib/util/cascade.co b/lib/util/cascade.co
new file mode 100644 (file)
index 0000000..6258ade
--- /dev/null
@@ -0,0 +1,130 @@
+_ = require 'kraken/underscore'
+
+/**
+ * @class A mapping of key-value pairs supporting lookup fallback across multiple objects.
+ */
+class Cascade
+    
+    /**
+     * List of objects for lookups.
+     * @type Array
+     * @private
+     */
+    _dicts : null
+    
+    
+    /**
+     * @constructor
+     */
+    ->
+        @_dicts = []
+        @extend ...
+    
+    
+    /**
+     * @returns {Number} Number of real keys in the Dict.
+     */
+    size : ->
+        _.keys @_dicts .length
+    
+    /**
+     * @returns {Cascade} A copy of the dict, including fallbacks as well as data.
+     */
+    clone: ->
+        d = new Cascade
+        _.each @_dicts, (v, k) ->
+            d.setAlias k, v.slice()
+        d
+    
+    
+    
+    ### Value Accessors ###
+    
+    /**
+     * @returns {Boolean} Whether there is a value at the given key.
+     */
+    has : (key) ->
+        (@get key, null)?
+    
+    /**
+     * @returns {*} Ignores aliases, returning the value at key or `undefined`.
+     */
+    getValue : (key) ->
+        prop = _.getNested @_dicts, key
+        prop.value if prop?
+    
+    get : (key, def) ->
+        aliases = @_aliases[key] or [key]
+        val = aliases.reduce do
+            (val, alias) ->
+                return val if val? is not undefined
+                prop = _.getNested @_dicts, alias
+                prop.value if prop?
+            undefined
+        
+        if val is not undefined
+            val
+        else
+            def
+    
+    set : (key, val) ->
+        _.setNested @_dicts, key, val, true
+        val
+    
+    del : (key) ->
+        prop = _.getNestedMeta key
+        if prop
+            delete prop.obj[prop.key]
+            prop.value
+    
+    
+    
+    
+    ### Collection Methods ###
+    
+    toObject: ->
+        _.extend {}, ...@_dicts
+    
+    # XXX: Merge keys from all objects?
+    keys: ->
+        _.keys @_data
+    
+    values: ->
+        _.values @_data
+    
+    extend : (...args) ->
+        for o of args
+            for k,v in o then @set k, v
+        this
+    
+    reduce : (fn, acc, context=this) ->
+        _.reduce @_data, fn, acc, context
+    
+    map : (fn, context=this) ->
+        _.map @_data, fn, context
+    
+    filter: (fn, context=this) ->
+        _.filter @_data, fn, context
+    
+    each : (fn, context=this) ->
+        _.each @_data, fn, context
+        this
+    
+    invoke : (name, ...args) ->
+        _.invoke @_data, name, ...args
+    
+    pluck : (attr) ->
+        _.pluck @_data, attr
+    
+    find: (fn, context=this) ->
+        _.find @_data, fn, context
+    
+    
+    
+    toString: ->
+        Cls = @.constructor
+        "#{Cls.displayName or Cls.name}()"
+
+
+
+module.exports = exports = Cascade
index 745238e..285a8f2 100644 (file)
@@ -13,6 +13,7 @@ backbone  = require 'kraken/util/backbone'
 # HashSet   = require 'kraken/util/hashset'
 # BitString = require 'kraken/util/bitstring'
 # {crc32}   = require 'kraken/util/crc'
+parser    = require 'kraken/util/parser'
 
 
 ## Debug
@@ -27,5 +28,5 @@ _.dump = (o, label='dump') ->
     o
 
 
-exports import { root, _, op, backbone }
-# exports import { root, _, op, HashSet, BitString, crc32, }
+exports import { root, _, op, backbone, parser, }
+# exports import { root, _, op, HashSet, BitString, crc32, parser, }
diff --git a/lib/util/parser.co b/lib/util/parser.co
new file mode 100644 (file)
index 0000000..e1f9744
--- /dev/null
@@ -0,0 +1,71 @@
+_  = require 'kraken/underscore'
+op = require 'kraken/util/op'
+
+
+/**
+ * @namespace Parsers by type.
+ */
+Parsers = exports.Parsers =
+    
+    parseBoolean: (v) ->
+        op.toBool v
+    
+    parseInteger: (v) ->
+        r = op.toInt v
+        unless isNaN r then r else null
+    
+    parseFloat: (v) ->
+        r = op.toFloat v
+        unless isNaN r then r else null
+    
+    parseString: (v) ->
+        if v? then op.toStr v else null
+    
+    parseArray: (v) ->
+        if v then op.toObject v else null
+    
+    parseObject: (v) ->
+        if v then op.toObject v else null
+    
+    parseFunction: (v) ->
+        if v and _.startswith String(v), 'function'
+            try eval "(#v)" catch err then null
+        else
+            null
+    
+
+
+class exports.ParserMixin
+    this:: import Parsers
+    
+    
+    parse: (v, type) ->
+        @getParser(type)(v)
+    
+    getParser: (type='String') ->
+        # If this is a known type and we have a parser for it, return that
+        fn = @["parse#type"]
+        return fn if typeof fn is 'function'
+        
+        # Handle compound/optional types
+        # XXX: handle 'or' by returning an array of parsers?
+        type = _ String(type).toLowerCase()
+        for t of <[ Integer Float Boolean Object Array Function ]>
+            if type.startsWith t.toLowerCase()
+                return @["parse#t"]
+        @defaultParser or @parseString
+    
+    getParserFromExample: (v) ->
+        return null unless v?
+        type = typeof v
+        
+        if type is not 'object'
+            @getParser type
+        else if _.isArray v
+            @getParser 'Array'
+        else
+            @getParser 'Object'
+    
+    
+
+
index 4dfc388..5510f5c 100644 (file)
@@ -42,8 +42,13 @@ dev:
             - util:
                 - op
                 - backbone
+                - parser
                 - index
             - base
+            - chart:
+                - chart-library
+                - dygraphs-library
+                - index
             - template:
                 - graph.jade
                 - graph-option.jade