linemanjs / lineman

Lineman helps you build fat-client JavaScript apps. It produces happiness by building assets, mocking servers, running specs on every file change
MIT License
1.18k stars 83 forks source link

Feature Request: First Class API Stubs #318

Open davemo opened 9 years ago

davemo commented 9 years ago

I wanted to create this to capture the thoughts while they are fresh in my mind after context on a client project where this would have saved some time:

I think we should formalize a config/stubs directory which has the following properties:

  1. is watched for changes to files within, triggering a restart of express on change
  2. captures syntax errors in watched stub files and catches the exception to avoid killing express
  3. (optional) loosely formalize a blessed JSON stub format (simplest thing is module.exports = {} for now), we don't need to get too complicated here
  4. (optional) auto require stubs based on filename such that they are available within config/server.{js,coffee} either as a local in the scope or on the node global object

The first two would be mostly quality of life improvements to the way I use the existing API stubbing feature, the latter two are things that would be really cool if we could make them not suck in implementation ;)

If anyone is willing to pair with me on this, I'd like to start with the first two which are captured in #268.

jasonkarns commented 9 years ago

I am in favor of this and I recall an old feature request being opened to do something similar. (I believe it was closed with the advice of simply using require from config/server.js)

Should we consider placing the stubs under /spec? I realize that would complicate the glob patterns for including spec files and helpers. But stubs used for testing make more sense (to me) under /spec than /config.

On Wed, Sep 3, 2014 at 9:31 AM, David Mosher notifications@github.com wrote:

I wanted to create this to capture the thoughts while they are fresh in my mind after context on a client project where this would have saved some time:

I think we should formalize a config/stubs directory which has the following properties:

  1. is watched for changes to files within, triggering a restart of express on change
  2. captures syntax errors in watched stub files and catches the exception to avoid killing express
  3. (optional) loosely formalize a blessed JSON stub format (simplest thing is module.exports = {} for now), we don't need to get too complicated here
  4. (optional) auto require stubs based on filename such that they are available within config/server.{js,coffee} either as a local in the scope or on the node global object

The first two would be mostly quality of life improvements to the way I use the existing API stubbing feature, the latter two are things that would be really cool if we could make them not suck in implementation ;)

If anyone is willing to pair with me on this, I'd like to start with the first two which are captured in #268 https://github.com/linemanjs/lineman/issues/268.

— Reply to this email directly or view it on GitHub https://github.com/linemanjs/lineman/issues/318.

searls commented 9 years ago

Since these stubs would be mostly/entirely used for the dev server and not for tests, I don't think it makes sense for them to be stored under spec. Since our testing facility only covers unit tests, I don't want to encourage people to start using fixtures in external files.

searls commented 9 years ago

As I re-read Dave's ticket, it sounds like this is a way to auto-break up the server file into n smaller server files, and not necessarily an approach for stubbings at a particular route.

But I'm pretty sure I'm just not clear. Dave could you put together a gist or small repo of files/folders of how you'd like to use this feature and then explain what they'd do (e.g. "config/server/stubs/foo/bar/baz.json adds a GET /foo/bar/baz route to express and returns baz.json")?

jasonkarns commented 9 years ago

@searls good call on the fixtures

davemo commented 9 years ago

@searls, what you describe in breaking up the config/server.js is part of the larger and more difficult task in 3, and 4 above; here's an idea of what that might look like in code-ish form:

In a simpler scenario, all I really want at this point is:

searls commented 9 years ago

So in your simpler example what does stubs/user.js export? A function that takes an express app and does [THINGS] to it? Or something more constrained?

On Thu, Sep 4, 2014 at 12:32 PM, David Mosher notifications@github.com wrote:

@searls, what you describe in breaking up the config/server.js is part of the larger and more difficult task in 3, and 4 above; here's an idea of what that might look like in code-ish form:

  • user adds the file path config/stubs/users/get.js and config/stubs/users/post.js which contains something like module.exports = { id: 1, email: 'foo@bar.com };
  • lineman detects that and auto wires up GET /api/users and POST /api/users to return respective results
  • the mechanics of convention is up for debate as is whether it's worth going this far In a simpler scenario, all I really want at this point is:
  • user adds config/stubs/user.js
  • lineman adds a watch glob for config/stubs such that when the file changes express restarts

* AND exceptions in require('path/to/a/stub.js);` are caught and handled in the console output to notify users that something in their stub is syntactically incorrect.

Reply to this email directly or view it on GitHub: https://github.com/linemanjs/lineman/issues/318#issuecomment-54505889

davemo commented 9 years ago

stubs/user.js is the same in the simpler example, i just manually manage routing in config/server.js as per normal.

searls commented 9 years ago

I'm still not getting it, then.

searls commented 9 years ago

Could you post an example listing of stubs/user.js and config/server?

davemo commented 9 years ago

config/server

var users = require('./stubs/users.js');

module.exports = {
  drawRoutes: function(app) {
    app.get('/api/users', function(req, res) { res.json(users); });
  }
};

stubs/users.js

module.exports = {
  users: [
    { id:1, name: 'Dave' },
    { id:2, name: 'Justin' }
  ]
}
searls commented 9 years ago

oh, well in that case I think what you're asking for is much simpler and less opinionated than what I thought you were asking for.

I think the default behavior absolutely should explode on errors, though, to the same extent a compilation failure does. If one particular API starts barfing and we don't tell people, they won't know where to look. And there's no good reason for them to start exploding. (If all you're saying is you don't want the process to blow up, I agree)

Error handling generally should probably give feedback in the browser with a lineman error page and not just in the terminal.

jasonkarns commented 9 years ago

Here's a thing that's somewhat related. worth a look: http://webpro.github.io/dyson/

davemo commented 9 years ago

I'm going to post some of the api stubs we're using in a current project for reference.

There are some interesting usage patterns I've discovered working through these;

var _     = require('underscore');

// var drillingData = require('./stubs/execution/drilling_data.js');
// var upToBatData = require('./stubs/uptobat/uptobat_data.js');
// var upToBatGridsData = require('./stubs/uptobat/uptobat_grids_data.js');
// var upToBatFilterData = require('./stubs/uptobat/uptobat_filter_data.js');
// var projectIssuesOptionsData = require('./stubs/issues/project_issues_options_data.js');
// var drillingExecutionGridWidgetOptions = require('./stubs/execution/drilling_grid_widget_options.js');
// var includesCopyButtonJson = require('./stubs/uptobat/includes_copy_button_configuration.js');
var parameters             = require('./stubs/admin/parameters.js');
var notifications          = require('./stubs/admin/notifications.js');
var notificationRecipients = require("./stubs/admin/notification_recipients.js");
var notificationTriggers   = require("./stubs/admin/notification_triggers.js");
var uptobatCounts          = require('./stubs/uptobat/uptobat_counts.js');

module.exports = {
  drawRoutes: function(app) {
    // ADMIN SCREEN STUBS

    // uptobat counts

    app.get('/api/uptobats/counts', function (req, res) {
        console.log("\n *** LINEMAN STUB: GET /api/uptobat/counts");
        _.each(uptobatCounts, function(v, k){
          uptobatCounts[k] = v + 1;
        });
        res.json(uptobatCounts);
    });

    // parameters
    app.get('/api/admin/parameters/:buid', function(req, res) {
        console.log("\n *** LINEMAN STUB: GET /api/admin/parameters/" + req.params.buid);
        res.json(parameters);
    });

    app.post('/api/admin/parameters/:buid', function (req, res) {
        console.log("\n *** LINEMAN STUB: POST /api/admin/parameters/" + req.params.buid);
        _.extend(parameters, req.body.data);
        res.json(parameters);
    });

    var logEvents = [];
    app.post('/api/log', function(req, res) {
        logEvents.push(req.body);
        console.log("\n *** LOG EVENT LINEMAN STUB ***\n", req.body);
        res.json({status: "ok"});
    });

    app.get('/api/log', function(req, res) {
        res.json(logEvents);
    });

    // notifications
    app.post('/api/admin/notifications/:buid', function (req, res) {
        console.log("\n *** LINEMAN STUB: POST /api/admin/notifications/" + req.params.buid);
        res.json(notifications);
    });

    app.get('/api/admin/notifications/:nid', function (req, res) {
        console.log("\n *** LINEMAN STUB: GET /api/notifications//" + req.params.nid);
        var notification = getNotificationById(req.params.nid);
        res.json(notification);
    });

    app.get('/api/admin/notifications/:nid/recipients', function (req, res) {
        console.log("\n *** LINEMAN STUB: GET /api/notifications/" + req.params.nid + "/recipients");
        res.json(notificationRecipients);
    });

    app.get('/api/admin/notifications/:nid/triggers', function (req, res) {
        console.log("\n *** LINEMAN STUB: GET /api/notifications/" + req.params.nid + "/triggers");
        res.json(notificationTriggers);
    });

    app.put('/api/admin/notifications/:nid/recipients/update', function (req, res) {
        console.log("\n *** LINEMAN STUB: ---> NOTIFICATION RECIPIENT SAVED", req.body.form.nid);
        res.json(req.body.form);
    });

    app.post('/api/admin.notifications/:nid', function(req, res) {
        console.log("\n *** LINEMAN STUB: ---> EMAIL TEMPLATE SAVED");
        var notification = getNotificationById(req.params.nid);
        notification.subject = req.body.subject;
        notification.name = req.body.name;
        notification.body = req.body.body;
        notification.triggers = req.body.triggers;
        notification.recipients = req.body.recipients;
        res.json(notification);
    });

    function getNotificationById(id) {
       for (var key in notifications.data) {
            var notification = notifications.data[key];
            if (notification.id == id) {
                return notification;
            }
       }
    }
    // app.put('/api/data/uptobat/:statenamespace/:uptobatId/:buid', function(req, res){
    //   console.log('\n *** UPDATE up to bat row ***');
    //   console.log('\n *** LINEMAN STUB: PUT /api/data/uptobat/:statenamespace/:uptobatId/:buid ***');
    //   var objToUpdate = _.find(upToBatGridsData, function(obj){
    //     return obj.id == req.body.models[0].id;
    //   });

    //   _.extend(objToUpdate, req.body.models[0]);
    //   res.json(objToUpdate);
    // });

    // app.post('/api/data/uptobat/:statenamespace/:uptobatId/:buid', function(req, res){
    //   console.log('\n *** LINEMAN STUB: POST /api/data/uptobat/' + req.params.statenamespace + '/' + req.params.uptobatId + '/' + req.params.buid + ' *** \n');
    //   res.json(upToBatGridsData);
    // });

    // app.put('/api/data/execution.drilling/:buid', function(req, res) {
    //   console.log('\n *** LINEMAN STUB: PUT /api/data/execution.drilling/' + req.params.buid + ' *** \n');
    //   res.json(req.body);
    // });

    // app.get('/api/display/widget/options/execution.drilling/drilling-grid/:buid', function (req, res) {
    //   console.log('\n *** LINEMAN STUB: GET /api/display/widget/options/execution.drilling/drilling-grid/' + req.params.buId + ' *** \n');
    //   res.json(drillingExecutionGridWidgetOptions);
    // });

    // app.post('/api/data/execution.drilling/:buid', function(req, res){
    //   console.log('\n *** LINEMAN STUB: POST /api/data/execution.drilling/' + req.params.buid + ' *** \n');
    //   res.json(drillingData);
    // });

    // app.get('/api/session', function(req, res) {
    //   console.log('\n *** LINEMAN STUB: GET /api/session *** \n');
    //   res.json(session);
    // });

    // app.post('/api/user/profile', function(req, res) {
    //   _.extend(session.user.profile, req.body);
    //   console.log('\n *** LINEMAN STUB: POST /api/userprofile: persisting session.user.profile in memory *** \n');
    //   console.log(session.user.profile);
    //   res.send(200);
    // });
  }
};
BurningDog commented 8 years ago

The most annoying part of working in this way is having to restart lineman when a stubbed file changes; changes to anything within drawRoutes seems to be detected with the watchr setup internally in Lineman, but anything that is required within the file that changes is not monitored.

Agreed. I'm not sure how to configure lineman to watch an additional folder for changes, but I edited node_modules/lineman/tasks/server.coffee and changed the following lines from

  resetRoutesOnServerConfigChange = (app) ->
    watchr grunt.file.expand('config/server.*'), (err, watcher) ->

to

  resetRoutesOnServerConfigChange = (app) ->
    watchr grunt.file.expand('config/server.*', 'config/stubs/**/*'), (err, watcher) ->

This allows for file changes without restarting lineman, but for some reason livereload doesn't trigger. I'd love to know how to put this change into config/application.js