Converts the hodgepodge of server endpoints to RESTful resources.
authordsc <dsc@wikimedia.org>
Sun, 1 Apr 2012 08:24:55 +0000 (01:24 -0700)
committerdsc <dsc@wikimedia.org>
Sun, 1 Apr 2012 08:24:55 +0000 (01:24 -0700)
lib/server/controller.co [new file with mode: 0644]
lib/server/controllers/dashboard.co [new file with mode: 0644]
lib/server/controllers/datasource.co [new file with mode: 0644]
lib/server/controllers/graph.co [new file with mode: 0644]
lib/server/server.co
package.co
package.json

diff --git a/lib/server/controller.co b/lib/server/controller.co
new file mode 100644 (file)
index 0000000..15b6885
--- /dev/null
@@ -0,0 +1,94 @@
+express  = require 'express'
+Resource = require 'express-resource'
+
+hasOwn = Object::hasOwnProperty
+
+/**
+ * @class Resource controller for easily subclassing an express Resource.
+ */
+class Controller extends Resource
+    /**
+     * Singular, lowercase resource-noun.
+     * @optional
+     * @type String
+     * @example "user"
+     */
+    id : null
+    
+    /**
+     * Plural, lowercase resource-noun.
+     * @required
+     * @type String
+     * @example "users"
+     */
+    name : null
+    
+    /**
+     * Resource routing prefix.
+     * @optional
+     * @type String
+     */
+    base : '/'
+    
+    /**
+     * Default format.
+     * @type String
+     */
+    format : null
+    
+    /**
+     * Hash of sub-routes. Keys are routes, and values are either:
+     *  - String: the name of a method to be used for used for all HTTP-methods
+     *    for this sub-route.
+     *  - Object: Hash of HTTP-method (get, put, post, del, or all) to the name
+     *    of a method on this Controller.
+     * @type Object
+     */
+    mapping : null
+    
+    
+    
+    /**
+     * @constructor
+     */
+    (@app, name) ->
+        
+        # Bind all methods and make actions object
+        actions = {}
+        for k, fn in this
+            continue unless typeof fn is 'function' and k is not 'constructor'
+            actions[k] = this[k] = fn.bind this
+        
+        # Replace/remove .load, as by default it's Resource#load
+        delete actions.load
+        if typeof actions.autoload is 'function'
+            actions.load = actions.autoload
+        
+        super (name or @name), actions, @app
+        (@app.resources or= {})[@name] = this
+        @applyControllerMapping()
+    
+    
+    /**
+     * Apply the contents of a mapping hash.
+     * @private
+     */
+    applyControllerMapping: (mapping=@mapping) ->
+        for subroute, methods in mapping
+            if typeof methods is 'string'
+                methods = { all:methods }
+            for verb, method in methods
+                @map verb, subroute, @[method]
+        this
+    
+    toString: ->
+        "#{@.constructor.name}('#{@name}', base='#{@base}', app=#{@app})"
+
+
+express.HTTPServer::controller  = \
+express.HTTPSServer::controller = (name, ControllerClass, opts) ->
+    [opts, ControllerClass, name] = [ControllerClass, name, null] if typeof name is 'function'
+    new ControllerClass this, name
+
+
+module.exports = exports = Controller
diff --git a/lib/server/controllers/dashboard.co b/lib/server/controllers/dashboard.co
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/server/controllers/datasource.co b/lib/server/controllers/datasource.co
new file mode 100644 (file)
index 0000000..90c68f3
--- /dev/null
@@ -0,0 +1,74 @@
+fs         = require 'fs'
+Seq        = require 'seq'
+Controller = require '../controller'
+
+YAML_EXT_PAT = /\.ya?ml$/i
+
+
+
+/**
+ * @class Resource controller for graph requests.
+ */
+class DataSourceController extends Controller
+    name    : 'datasources'
+    dataDir : 'data/graphs'
+    
+    mapping :
+        all : 'getAllData'
+    
+    -> super ...
+    
+    /**
+     * Returns a JSON listing of the datasource metadata files.
+     */
+    index : (req, res, next) ->
+        fs.readdir @dataDir, (err, files) ->
+            res.send do
+                files.filter -> /\.(json|ya?ml)$/i.test it
+                     .map    -> "/#{@dataDir}#it".replace YAML_EXT_PAT, '.json'
+    
+    /**
+     * Returns the aggregated JSON content of the datasource metadata files.
+     */
+    allData : (req, res, next) ->
+        data = {}
+        files = []
+        Seq()
+            .seq fs.readdir, @dataDir, Seq
+            .flatten()
+            .filter -> /\.(json|ya?ml)$/.test it
+            .seq ->
+                files := @stack.slice()
+                # console.log 'files:', files
+                @ok files
+            .flatten()
+            .parMap_ (next, f) ~>
+                # console.log "fs.readFile '#CWD/data/#f'"
+                fs.readFile "#{@dataDir}/#f", 'utf8', next
+            .parMap (text, i) ->
+                f = files[i]
+                # console.log "parsing file[#i]: '#f' -> text[#{text.length}]..."
+                k = f.replace YAML_EXT_PAT, '.json'
+                v = data[k] = {}
+                try
+                    if YAML_EXT_PAT.test f
+                        v = data[k] = yaml.load text
+                    else
+                        v = data[k] = JSON.parse text
+                    # console.log "#f ok!", data
+                    @ok v
+                catch err
+                    console.error "[/data/all] catch! #err"
+                    console.error err
+                    console.error that if err.stack
+                    res.send { error:String(err), partial_data:data }
+            .seq -> res.send data
+            .catch (err) ->
+                console.error '[/data/all] catch!'
+                console.error err
+                console.error that if err.stack
+                res.send { error:String(err), partial_data:data }
+
+
+
+module.exports = exports = DataSourceController
diff --git a/lib/server/controllers/graph.co b/lib/server/controllers/graph.co
new file mode 100644 (file)
index 0000000..8017081
--- /dev/null
@@ -0,0 +1,107 @@
+_        = require 'underscore'
+fs       = require 'fs'
+path     = require 'path'
+yaml     = require 'js-yaml'
+
+{existsSync:exists} = path
+{mkdirp, mkdirpAsync} = require '../mkdirp'
+Controller = require '../controller'
+
+
+/**
+ * @class Resource controller for graph requests.
+ */
+class GraphController extends Controller
+    name    : 'graphs'
+    dataDir : 'data/graphs'
+    -> super ...
+    
+    
+    toFile: (id) -> "#{@dataDir}/#id.json"
+    
+    /**
+     * Auto-load :id for related requests.
+     */
+    autoload: (id, cb) ->
+        file = @toFile id
+        parser = JSON.parse
+        
+        yamlFile = file.replace /\.json$/i, '.yaml'
+        if exists yamlFile 
+            file = yamlFile
+            parser = yaml.load
+        
+        err, data <- fs.readFile file, 'utf8'
+        if err
+            console.error "GraphController.autoload(#id, #{typeof cb}) -->\nerr"
+            return cb err
+        try
+            cb null, parser data
+        catch err
+            console.error "GraphController.autoload(#id, #{typeof cb}) -->\nerr"
+            cb err
+    
+    # GET /graphs
+    index: (req, res) ->
+        res.render 'dashboard'
+    
+    # GET /graphs/:graph
+    show: (req, res) ->
+        res.send req.graph
+    
+    # GET /graphs/:graph/edit
+    edit: (req, res) ->
+        res.send req.graph
+    
+    # GET /graphs/new
+    new: (req, res) ->
+        ...
+    
+    # POST /graphs
+    create: (req, res) ->
+        return unless data = @processBody req, res
+        file = @toFile data.id
+        if exists file
+            return res.send { result:"error", message:"Graph already exists!" }
+        else
+            fs.writeFile file, JSON.stringify(data), "utf8", @errorHandler(res, "Error writing graph!")
+    
+    # PUT /graphs/:graph
+    update: (req, res) ->
+        return unless data = @processBody req, res
+        fs.writeFile @toFile(data.id), JSON.stringify(data), "utf8", @errorHandler(res, "Error writing graph!")
+    
+    # DELETE /graphs/:graph
+    destroy: (req, res) ->
+        fs.unlink @toFile(req.param.graph), @errorHandler(res, "Graph does not exist!")
+    
+    
+    ### Helpers
+    
+    processBody: (req, res) ->
+        if not req.body
+            res.send {result:"error", message:"Data required!"}, 501
+            return false
+        
+        data = req.body
+        data.slug or= data.id
+        data.id or= data.slug
+        
+        if not data.slug
+            res.send {result:"error", message:"Slug required!"}, 501
+            return false
+        
+        mkdirp @dataDir if not exists @dataDir
+        return data
+    
+    errorHandler: (res, msg) ->
+        (err) ->
+            if err
+                msg or= err.message or String(err)
+                console.error msg
+                res.send { result:"error", message:msg }, 501
+            else
+                res.send { result:"ok" }
+    
+
+module.exports = exports = GraphController
index cec9568..fcf1090 100755 (executable)
@@ -2,7 +2,6 @@
 
 fs       = require 'fs'
 path     = require 'path'
-{parse}  = require 'url'
 {existsSync:exists} = path
 {exec, spawn} = require 'child_process'
 {mkdirp, mkdirpAsync} = require './mkdirp'
@@ -15,10 +14,10 @@ Seq      = require 'seq'
 yaml     = require 'js-yaml'
 mime     = require 'mime'
 express  = require 'express'
+Resource = require 'express-resource'
 compiler = require 'connect-compiler-extras'
 
 
-
 ### Config
 
 # TODO: read KRAKEN_PORT from ENV
@@ -29,9 +28,11 @@ WWW    = "#CWD/www"
 VAR    = "#CWD/var"
 STATIC = "#CWD/static"
 DIST   = "#CWD/dist"
+DATA   = "#CWD/data"
 
 NODE_ENV  = process.env.NODE_ENV or 'dev'
 LOG_LEVEL = if _.startsWith NODE_ENV, 'dev' then 'INFO' else 'WARN'
+# LOG_LEVEL = 'DEBUG'
 
 
 VERSION = 'dev'
@@ -66,6 +67,7 @@ app.configure ->
     } import require './view-helpers'
     
     app.use express.logger()   if LOG_LEVEL is 'DEBUG'
+    # app.use express.logger()
     
     # Parse URL, fiddle
     app.use require('./reqinfo')({})
@@ -87,6 +89,12 @@ app.configure ->
         options : stylus : { nib:true, include:"#WWW/css" }
         log_level : LOG_LEVEL
     
+    app.use compiler do
+        enabled : 'yaml'
+        src     : DATA
+        dest    : "#VAR/data"
+        log_level : LOG_LEVEL
+    
     # wrap modules in commonjs closure for browser
     app.use compiler do
         enabled : 'commonjs_define'
@@ -113,7 +121,7 @@ app.configure ->
     app.use express.static VAR
     app.use express.static STATIC
     # app.use express.static DIST if exists DIST
-    app.use express.static DIST
+    app.use express.static DIST if NODE_ENV is 'prod'
     
     # Serve directory listings
     # app.use express.directory WWW
@@ -126,58 +134,15 @@ app.configure ->
         showStack      : true
 
 
-/* * * *  Routes  * * * {{{ */
 
-app.get '/', (req, res) ->
-    res.render 'dashboard'
+/* * * *  Routes and Controllers  * * * {{{ */
 
-saveGraph = (req, res, next) ->
-    if not req.body
-        return res.send {result:"error", message:"JSON required!"}, 501
-    
-    data = req.body
-    {id, slug} = data
-    if not slug
-        return res.send {result:"error", message:"slug required!"}, 501
-    mkdirp "#VAR/presets" if not exists "#VAR/presets"
-    
-    id or= slug
-    data.id = id
-    err <- fs.writeFile "#VAR/presets/#id.json", JSON.stringify(data), "utf8"
-    if err
-        res.send { result:"error", message:err.message or String(err) }, 501
-    else
-        res.send { result:"ok" }
+Controller   = require './controller'
+app.controller require './controllers/graph'
+app.controller require './controllers/datasource'
 
-
-app.post '/graph/save', saveGraph
-app.put '/graph/:slug\.json', saveGraph
-
-app.get '/graph/:slug\.json', (req, res, next) ->
-    req.url .= replace /^\/graph\//i, '/presets/'
-    next()
-
-app.get '/preset/:slug', (req, res, next) ->
-    {slug} = req.params
-    if exists("#VAR/presets/#slug.yaml") or exists("#VAR/presets/#slug.json")
-        req.url .= replace /^\/preset\/[^\/?]/i, "/presets/#slug.json"
-        # req.url += '.json'
-    next()
-
-app.get '/graph(/:slug)?/?', (req, res, next) ->
-    {slug} = req.params
-    # console.log '/graph/:slug/?'
-    # console.log '  slug:  ', slug
-    # console.log '  params:', req.params
-    unless _.str.include slug, '.'
-        {pathname, search or ''} = req.info
-        req.url = path.join pathname, "view#search"
-        req.params.action = 'view'
-    next()
-
-app.get '/graph/:slug/:action/?', (req, res) ->
-    {slug, action} = req.params
-    res.render "graph/#action"
+app.get '/', (req, res) ->
+    res.render 'dashboard'
 
 app.get '/:type/:action/?', (req, res, next) ->
     {type, action} = req.params
@@ -186,85 +151,26 @@ app.get '/:type/:action/?', (req, res, next) ->
     else
         next()
 
-
-
-# }}}
-/* * * *  Data Source Oracle  * * * {{{ */
-
-YAML_EXT_PAT = /\.ya?ml$/i
-
-/**
- * Returns a JSON listing of the datasource metadata files.
- */
-app.get '/data/list', (req, res, next) ->
-    fs.readdir "#CWD/data", (err, files) ->
-        res.send do
-            files.filter -> /\.(json|ya?ml)$/i.test it
-                 .map    -> "/data/#it".replace YAML_EXT_PAT, '.json'
-
-/**
- * Returns the aggregated JSON content of the datasource metadata files.
- */
-app.get '/data/all', (req, res, next) ->
-    data = {}
-    files = []
-    Seq()
-        .seq fs.readdir, "#CWD/data", Seq
-        .flatten()
-        .filter -> /\.(json|ya?ml)$/.test it
-        .seq ->
-            files := @stack.slice()
-            # console.log 'files:', files
-            @ok files
-        .flatten()
-        .parMap (f) ->
-            # console.log "fs.readFile '#CWD/data/#f'"
-            fs.readFile "#CWD/data/#f", 'utf8', this
-        .parMap (text, i) ->
-            f = files[i]
-            # console.log "parsing file[#i]: '#f' -> text[#{text.length}]..."
-            k = f.replace YAML_EXT_PAT, '.json'
-            v = data[k] = {}
-            try
-                if YAML_EXT_PAT.test f
-                    v = data[k] = yaml.load text
-                else
-                    v = data[k] = JSON.parse text
-                # console.log "#f ok!", data
-                @ok v
-            catch err
-                console.error "[/data/all] catch! #err"
-                console.error err
-                console.error that if err.stack
-                res.send { error:String(err), partial_data:data }
-        .seq -> res.send data
-        .catch (err) ->
-            console.error '[/data/all] catch!'
-            console.error err
-            console.error that if err.stack
-            res.send { error:String(err), partial_data:data }
-
-# }}}
-
 /**
  * Handle webhook notification to pull from origin.
  */
 app.all '/webhook/post-update', (req, res) ->
-    
+
     # exec the pull async...
-    console.log '[/webhook/post-update] $ git pull origin master'
-    child = exec 'git pull origin master', (err, stdout, stderr) ->
+    cmd = 'git pull origin master'
+    console.log "[/webhook/post-update] $ #cmd"
+    child = exec cmd, (err, stdout, stderr) ->
         res.contentType '.txt'
         console.log '[/webhook/post-update]  ', stdout
         console.log '[/webhook/post-update]  ', stderr
         if err
             console.error '[/webhook/post-update] ERROR!', err
-            res.send "#stdout\n\n#stderr\n\nERROR! #err", 503
+            res.send "$ #cmd\n\n#stdout\n\n#stderr\n\nERROR! #err", 503
         else
-            res.send "#stdout\n\n#stderr", 200
-
+            res.send "$ #cmd\n\n#stdout\n\n#stderr", 200
 
 
+# }}}
 
 exports import {
     CWD, WWW, VAR, STATIC,
index c66ff9e..b7698fb 100644 (file)
@@ -12,6 +12,7 @@ dependencies                    :
     'coco'                      : '>= 0.7.0'
     'mime'                      : '>= 1.2.5'
     'express'                   : '>= 2.5.8'
+    'express-resource'          : '>= 0.2.4'
     'connect-compiler'          : 'https://github.com/dsc/connect-compiler/tarball/master'
     'connect-compiler-extras'   : 'https://github.com/dsc/connect-compiler-extras/tarball/master'
     'jade'                      : '>= 0.20.1'
index 9c41e1a..838f439 100644 (file)
@@ -16,6 +16,7 @@
     "coco": ">= 0.7.0",
     "mime": ">= 1.2.5",
     "express": ">= 2.5.8",
+    "express-resource": ">= 0.2.4",
     "connect-compiler": "https://github.com/dsc/connect-compiler/tarball/master",
     "connect-compiler-extras": "https://github.com/dsc/connect-compiler-extras/tarball/master",
     "jade": ">= 0.20.1",