├── example
├── rest-models
│ ├── public
│ │ ├── client.js
│ │ ├── destroy.png
│ │ ├── index.html
│ │ ├── todos.css
│ │ ├── todos.js
│ │ └── json2.js
│ ├── app.js
│ └── remotes.js
├── socket-io
│ ├── test.txt
│ ├── client.js
│ └── server.js
├── documentation
│ ├── remotes
│ │ ├── index.js
│ │ ├── contract.js
│ │ ├── simple.js
│ │ ├── contract-class.js
│ │ └── simple-class.js
│ └── index.js
├── simple-types.js
├── simple.js
├── root.js
├── remote-fs.js
├── streams.js
├── shared-class.js
└── before-after.js
├── test
├── data
│ └── foo.json
├── helpers
│ ├── test-server.js
│ └── shared-objects-factory.js
├── e2e
│ ├── fixtures
│ │ ├── remotes.js
│ │ └── user.js
│ ├── e2e-server.js
│ └── smoke.test.js
├── streams.js
├── shared-method.test.js
├── type.test.js
├── http-invocation.test.js
├── jsonrpc.test.js
├── shared-class.test.js
├── rest.browser.test.js
└── rest-adapter.test.js
├── .jshintignore
├── .gitignore
├── index.js
├── docs.json
├── LICENSE.md
├── .jshintrc
├── README.md
├── ext
└── meta.js
├── package.json
├── lib
├── dynamic.js
├── socket-io-context.js
├── exports-helper.js
├── socket-io-adapter.js
├── jsonrpc-adapter.js
├── http-invocation.js
├── shared-class.js
├── http-context.js
├── shared-method.js
├── remote-objects.js
└── rest-adapter.js
├── Gruntfile.js
└── CONTRIBUTING.md
/example/rest-models/public/client.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/data/foo.json:
--------------------------------------------------------------------------------
1 | {"bar":"baz"}
2 |
--------------------------------------------------------------------------------
/example/socket-io/test.txt:
--------------------------------------------------------------------------------
1 | hello, world!
--------------------------------------------------------------------------------
/.jshintignore:
--------------------------------------------------------------------------------
1 | example/rest-models/public
2 | node_modules
3 |
--------------------------------------------------------------------------------
/test/helpers/test-server.js:
--------------------------------------------------------------------------------
1 | module.exports = function(callback) {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/example/rest-models/public/destroy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/strong-remoting/master/example/rest-models/public/destroy.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/
3 | *.seed
4 | *.log
5 | *.csv
6 | *.dat
7 | *.out
8 | *.pid
9 | *.swp
10 | *.swo
11 | node_modules/
12 | /coverage/
13 | dist
14 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * remotes ~ public api
3 | */
4 |
5 | module.exports = require('./lib/remote-objects');
6 | module.exports.SharedClass = require('./lib/shared-class');
7 |
--------------------------------------------------------------------------------
/test/e2e/fixtures/remotes.js:
--------------------------------------------------------------------------------
1 | var RemoteObjects = require('../../../');
2 | var remotes = module.exports = RemoteObjects.create();
3 |
4 | remotes.exports.User = require('./user');
5 |
--------------------------------------------------------------------------------
/docs.json:
--------------------------------------------------------------------------------
1 | {
2 | "content": [
3 | {"title": "Remote Objects API", "depth": 2},
4 | "lib/remote-objects.js",
5 | "lib/shared-class.js",
6 | "lib/shared-method.js",
7 | "lib/http-context.js",
8 | "lib/http-invocation.js"
9 | ],
10 | "codeSectionDepth": 3
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | strong-remoting uses a dual license model.
2 |
3 | You may use this library under the terms of the [Artistic 2.0 license][],
4 | or under the terms of the [StrongLoop Subscription Agreement][].
5 |
6 | [Artistic 2.0 license]: http://opensource.org/licenses/Artistic-2.0
7 | [StrongLoop Subscription Agreement]: http://strongloop.com/license
8 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "camelcase" : true,
4 | "eqnull" : true,
5 | "indent": 2,
6 | "undef": true,
7 | "quotmark": "single",
8 | "maxlen": 80,
9 | "trailing": true,
10 | "newcap": true,
11 | "nonew": true,
12 | "undef": true,
13 | "laxcomma" : true,
14 | "globals" : {
15 | "it": true,
16 | "describe": true,
17 | "beforeEach": true,
18 | "afterEach": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/rest-models/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * To run this app do this:
3 | *
4 | * $ npm install express jugglingdb
5 | * $ node app.js
6 | */
7 |
8 | var express = require('express');
9 | var app = express();
10 | app.disable('x-powered-by');
11 | var remotes = require('./remotes');
12 |
13 | app.use(remotes.handler('rest'));
14 | app.use(express.static('public'));
15 |
16 | app.listen(3000);
17 |
--------------------------------------------------------------------------------
/test/e2e/e2e-server.js:
--------------------------------------------------------------------------------
1 | // this server should be started before running tests in the e2e directory
2 | var path = require('path');
3 | var FIXTURES = path.join(__dirname, 'fixtures');
4 | var remotes = require(path.join(FIXTURES, 'remotes'));
5 |
6 | require('http')
7 | .createServer(remotes.handler('rest'))
8 | .listen(3000, function() {
9 | console.log('e2e server listening...');
10 | });
11 |
--------------------------------------------------------------------------------
/example/documentation/remotes/index.js:
--------------------------------------------------------------------------------
1 | var remotes = require('../../../').create();
2 |
3 | /**
4 | * Example API
5 | */
6 | remotes.exports.simple = require('./simple');
7 | remotes.exports.contract = require('./contract');
8 | remotes.exports.SimpleClass = require('./simple-class').SimpleClass;
9 | remotes.exports.ContractClass = require('./contract-class').ContractClass;
10 |
11 | module.exports = remotes;
12 |
--------------------------------------------------------------------------------
/example/socket-io/client.js:
--------------------------------------------------------------------------------
1 | var Remotes = require('../../client/js/client');
2 | var SocketIOAdapter = require('../../client/js/socket-io-adapter');
3 | var remotes = Remotes.connect('http://localhost:3000', SocketIOAdapter);
4 |
5 | remotes.invoke('fs.readFile', {path: 'test.txt'}, function (err, data) {
6 | console.log(data.toString());
7 | });
8 |
9 | remotes.invoke('ee.on', {event: 'foo'}, function (err, data) {
10 | console.log('foo event ran!', data); // logged multiple times
11 | });
--------------------------------------------------------------------------------
/example/simple-types.js:
--------------------------------------------------------------------------------
1 | // create a set of shared classes
2 | var remotes = require('../').create();
3 |
4 | // expose a simple object
5 | var user = remotes.exports.user = {
6 | greet: function (fn) {
7 | fn(null, {msg: 'hello, world!'});
8 | }
9 | };
10 |
11 | // share the greet method
12 | user.greet.shared = true;
13 |
14 | // expose it over http
15 | require('http')
16 | .createServer(remotes.handler('rest'))
17 | .listen(3000);
18 |
19 | /*
20 |
21 | Test the above with curl or a rest client:
22 |
23 | $ node simple.js
24 | $ curl http://localhost:3000/user/greet
25 | {
26 | "msg": "hello, world!"
27 | }
28 |
29 | */
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # strong-remoting
2 |
3 | Objects (and, therefore, data) in Node applications commonly need to be accessible by other Node processes, browsers, and even mobile clients. Strong remoting:
4 | * Makes local functions remotable, exported over adapters
5 | * Supports multiple transports, including custom transports
6 | * Manages serialization to JSON and deserialization from JSON
7 | * Supports multiple client SDKs, including mobile clients
8 |
9 | ## Installation
10 |
11 | ```sh
12 | $ npm install strong-remoting
13 | ```
14 |
15 | ## Documentation
16 |
17 | Please see the [official StrongLoop documentation](http://docs.strongloop.com/display/NODE/Strong+Remoting).
18 |
--------------------------------------------------------------------------------
/test/e2e/smoke.test.js:
--------------------------------------------------------------------------------
1 | var RemoteObjects = require('../../');
2 | var expect = require('chai').expect;
3 | var REMOTE_URL = 'http://localhost:3000';
4 | var remotes = require('./fixtures/remotes');
5 |
6 | remotes.connect(REMOTE_URL, 'rest');
7 |
8 | describe('smoke test', function () {
9 | describe('remote.invoke()', function () {
10 | it('invokes a remote static method', function (done) {
11 | remotes.invoke(
12 | 'User.login',
13 | [{username: 'joe', password: 'secret'}],
14 | function(err, session) {
15 | expect(err).to.not.exist;
16 | expect(session.userId).to.equal(123);
17 | done();
18 | }
19 | );
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/example/documentation/index.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var remotes = require('./remotes');
3 | var meta = require('../../ext/meta');
4 | var swagger = require('../../ext/swagger');
5 | var port = process.argv[2] || 3000;
6 | var handler;
7 | var adapter;
8 |
9 | // The installation order sets which routes are captured by Swagger.
10 | swagger(remotes, {
11 | basePath: 'http://localhost:3000'
12 | });
13 | meta(remotes);
14 |
15 | http
16 | .createServer(remotes.handler('rest'))
17 | .listen(port, function (err) {
18 | if (err) {
19 | console.error('Failed to start server with: %s', err.stack || err.message || err);
20 | process.exit(1);
21 | }
22 |
23 | console.log('Listening on port %s...', port);
24 | });
25 |
--------------------------------------------------------------------------------
/example/documentation/remotes/contract.js:
--------------------------------------------------------------------------------
1 | var helper = require('../../../').extend(module.exports);
2 |
3 | /**
4 | * Returns a secret message.
5 | */
6 | helper.method(getSecret, {
7 | http: { verb: 'GET', path: '/customizedGetSecret' },
8 | returns: { name: 'secret', type: 'string' }
9 | });
10 | function getSecret(callback) {
11 | callback(null, 'shhh!');
12 | }
13 |
14 | /**
15 | * Takes a string and returns an updated string.
16 | */
17 | helper.method(transform, {
18 | http: { verb: 'PUT', path: '/customizedTransform' },
19 | accepts: [{ name: 'str', type: 'string', required: true }],
20 | returns: { name: 'str', type: 'string' }
21 | });
22 | function transform(str, callback) {
23 | callback(null, 'transformed: ' + str);
24 | }
25 |
--------------------------------------------------------------------------------
/example/simple.js:
--------------------------------------------------------------------------------
1 | // create a set of shared classes
2 | var remotes = require('../').create();
3 |
4 | // expose a simple object
5 | var user = remotes.exports.user = {
6 | greet: function (fn) {
7 | fn(null, 'hello, world!');
8 | }
9 | };
10 |
11 | // share the greet method
12 | user.greet.shared = true;
13 | user.greet.returns = {arg: 'msg'};
14 |
15 | // expose it over http
16 | require('http')
17 | .createServer(remotes.handler('rest'))
18 | .listen(3000);
19 |
20 | /*
21 |
22 | Test the above with curl or a rest client:
23 |
24 | $ node simple.js
25 | $ curl http://localhost:3000/user/greet
26 | # responds as an object, with the msg attribute
27 | # set to the result of the function
28 | {
29 | "msg": "hello, world!"
30 | }
31 |
32 | */
--------------------------------------------------------------------------------
/example/root.js:
--------------------------------------------------------------------------------
1 | // create a set of shared classes
2 | var remotes = require('../').create();
3 |
4 | // expose a simple object
5 | var products = remotes.exports.products = {
6 | find: function (fn) {
7 | fn(null, ['tv', 'vcr', 'radio']);
8 | }
9 | };
10 |
11 | // share the find method
12 | products.find.shared = true;
13 | products.find.returns = {arg: 'products', root: true, type: 'array'};
14 |
15 | // expose it over http
16 | require('http')
17 | .createServer(remotes.handler('rest'))
18 | .listen(3000);
19 |
20 | /*
21 |
22 | Test the above with curl or a rest client:
23 |
24 | $ node root.js
25 | $ curl http://localhost:3000/products/find
26 | # responds as an array (instead of an object)
27 | [
28 | "tv",
29 | "vcr",
30 | "radio"
31 | ]
32 |
33 | */
--------------------------------------------------------------------------------
/example/remote-fs.js:
--------------------------------------------------------------------------------
1 | // create a set of shared classes
2 | var remotes = require('../').create();
3 |
4 | // share some fs module code
5 | var fs = remotes.exports.fs = require('fs');
6 |
7 | // specifically the createReadStream function
8 | fs.createReadStream.shared = true;
9 |
10 | // describe the arguments
11 | fs.createReadStream.accepts = {arg: 'path', type: 'string'};
12 |
13 | // describe the stream destination
14 | fs.createReadStream.http = {
15 | // pipe to the response
16 | // for the http transport
17 | pipe: {
18 | dest: 'res'
19 | }
20 | };
21 |
22 | // over rest / http
23 | require('http')
24 | .createServer(remotes.handler('rest'))
25 | .listen(3000);
26 |
27 | /*
28 |
29 | Test the above with curl or a rest client:
30 |
31 | $ node remote-fs.js
32 | $ curl http://localhost:3000/fs/createReadStream?path=simple.js
33 |
34 | */
--------------------------------------------------------------------------------
/example/documentation/remotes/simple.js:
--------------------------------------------------------------------------------
1 | // This helper adds methods to a module that we assume will be added to the remotes.
2 | // TODO(schoon) - Make this _the_ API, not a "helper".
3 | // TODO(schoon) - Document EVERYTHING
4 | var helper = require('../../../').extend(module.exports);
5 |
6 | /**
7 | * Returns a secret message.
8 | */
9 | helper.method(getSecret, {
10 | returns: { name: 'secret', type: 'string' }
11 | });
12 | function getSecret(callback) {
13 | callback(null, 'shhh!');
14 | }
15 |
16 | /**
17 | * Takes a string and returns an updated string.
18 | */
19 | helper.method(transform, {
20 | accepts: [{ name: 'str', type: 'string', required: true, description: 'The value to update' }],
21 | returns: { name: 'str', type: 'string' },
22 | description: 'Takes a string and returns an updated string.'
23 | });
24 | function transform(str, callback) {
25 | callback(null, 'transformed: ' + str);
26 | }
27 |
--------------------------------------------------------------------------------
/example/streams.js:
--------------------------------------------------------------------------------
1 | // faux remote stream
2 | var destination = process.stdout;
3 | var fs = require('fs');
4 | var path = require('path');
5 |
6 | // create a set of shared classes
7 | var remotes = require('../').create();
8 |
9 | // our modules
10 | var fileService = remotes.exports.files = {
11 | upload: function() {
12 | return destination;
13 | },
14 | download: function() {
15 | return fs.createReadStream(path.join(__dirname, 'streams.js'));
16 | }
17 | }
18 |
19 | fileService.upload.http = {
20 | // pipe to the request
21 | // to the result of the function
22 | pipe: {
23 | source: 'req'
24 | }
25 | };
26 | fileService.upload.shared = true;
27 |
28 |
29 | fileService.download.http = {
30 | // pipe to the response
31 | // for the http transport
32 | pipe: {
33 | dest: 'res'
34 | }
35 | };
36 | fileService.download.shared = true;
37 |
38 | // over rest / http
39 | require('http')
40 | .createServer(remotes.handler('rest'))
41 | .listen(3000);
42 |
--------------------------------------------------------------------------------
/ext/meta.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Expose the `Meta` plugin.
3 | */
4 | module.exports = Meta;
5 |
6 | /**
7 | * Module dependencies.
8 | */
9 | var Remoting = require('../');
10 |
11 | /**
12 | * Create a remotable Meta module for plugging into `RemoteObjects`.
13 | */
14 | function Meta(remotes, options) {
15 | // Unfold options.
16 | var name = (options && options.name) || 'meta';
17 |
18 | // We need a temporary REST adapter to discover our available routes.
19 | var adapter = remotes.handler('rest').adapter;
20 | var extension = {};
21 | var helper = Remoting.extend(extension);
22 |
23 | helper.method(routes, { returns: { type: 'object', root: true }});
24 | function routes(callback) {
25 | callback(null, adapter.allRoutes());
26 | }
27 |
28 | helper.method(classes, { returns: { type: 'object', root: true }});
29 | function classes(callback) {
30 | callback(null, remotes.classes());
31 | }
32 |
33 | remotes.exports[name] = extension;
34 | return extension;
35 | }
36 |
--------------------------------------------------------------------------------
/example/socket-io/server.js:
--------------------------------------------------------------------------------
1 | // create a set of shared classes
2 | var remotes = require('../../').create();
3 |
4 | // share some fs module code
5 | var fs = remotes.exports.fs = require('fs');
6 |
7 | // specifically the readFile function
8 | fs.readFile.shared = true;
9 |
10 | // describe the arguments
11 | fs.readFile.accepts = {arg: 'path', type: 'string'};
12 |
13 | // describe the result
14 | fs.readFile.returns = {arg: 'data', type: 'buffer'};
15 |
16 | // event emitter
17 | var EventEmitter = require('events').EventEmitter
18 | var ee = remotes.exports.ee = new EventEmitter();
19 |
20 | // expose the on method
21 | ee.on.shared = true;
22 | ee.on.accepts = {arg: 'event', type: 'string'};
23 | ee.on.returns = {arg: 'data', type: 'object'};
24 |
25 | setInterval(function() {
26 | // emit some data
27 | ee.emit('foo', {some: 'data'});
28 | }, 1000);
29 |
30 | // expose it over http
31 | var server =
32 | require('http')
33 | .createServer()
34 | .listen(3000);
35 |
36 | remotes.handler('socket-io', server);
--------------------------------------------------------------------------------
/example/shared-class.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 |
3 | // define a vanilla JavaScript class
4 | function Dog(name) {
5 | this.name = name;
6 | }
7 |
8 | // add a shared constructor
9 | Dog.sharedCtor = function (name, fn) {
10 | fn(null, new Dog(name));
11 | }
12 |
13 | // define the args for the shared constructor
14 | Dog.sharedCtor.accepts = {arg: 'name', type: 'string', http: {source: 'path'}};
15 |
16 | // change the default routing
17 | Dog.sharedCtor.http = {path: '/:name'};
18 |
19 | // define a regular instance method
20 | Dog.prototype.speak = function (fn) {
21 | fn(null, 'roof! my name is ' + this.name);
22 | }
23 |
24 | // mark it as shared
25 | Dog.prototype.speak.returns = {arg: 'result', type: 'string', root: true};
26 | Dog.prototype.speak.shared = true;
27 |
28 | // create a set of shared classes
29 | var remotes = require('../').create();
30 |
31 | // expose the Dog class
32 | remotes.exports.dog = Dog;
33 |
34 | var app = express();
35 | app.use(remotes.handler('rest'));
36 |
37 | app.listen(3000);
38 |
39 | /*
40 |
41 | Test the above with curl or a rest client:
42 |
43 | $ node shared-class.js
44 | $ curl http://localhost:3000/dog/fido/speak
45 | roof! my name is fido
46 |
47 | */
48 |
--------------------------------------------------------------------------------
/test/e2e/fixtures/user.js:
--------------------------------------------------------------------------------
1 | var debug = require('debug')('test user')
2 |
3 | module.exports = User;
4 |
5 | function User() {
6 | debug('constructed a user: %j', this);
7 | }
8 |
9 | User.sharedCtor = function(id, callback) {
10 | var user = new User();
11 | user.username = 'joe';
12 | callback(null, user);
13 | }
14 | User.sharedCtor.shared = true;
15 | User.sharedCtor.accepts = {arg: 'id', type: 'string'};
16 | User.sharedCtor.http = [
17 | {path: '/:id', verb: 'get'},
18 | {path: '/', verb: 'get'}
19 | ];
20 |
21 | var login = User.login = function(credentials, callback) {
22 | debug('login with credentials: %j', credentials);
23 | setTimeout(function() {
24 | if(!credentials.password) {
25 | return callback(new Error('password required'));
26 | }
27 | callback(null, {userId: 123});
28 | }, 0);
29 | }
30 | login.shared = true;
31 | login.accepts = {arg: 'credentials', type: 'object'};
32 | login.returns = {arg: 'session', type: 'object'};
33 |
34 | var hasUsername = User.prototype.hasUsername = function(username, callback) {
35 | callback(null, username === this.username);
36 | }
37 |
38 | hasUsername.shared = true;
39 | hasUsername.accepts = {arg: 'username', type: 'string'};
40 | hasUsername.returns = {arg: 'hasUsername', type: 'boolean'};
41 |
--------------------------------------------------------------------------------
/example/documentation/remotes/contract-class.js:
--------------------------------------------------------------------------------
1 | // This example shows using the helper for a type in a "definitive" fashion.
2 | var helper = require('../../../').extend(module.exports);
3 | var clshelper;
4 |
5 | /**
6 | * A simple class that contains a name, this time with a custom HTTP contract.
7 | */
8 | clshelper = helper.type(ContractClass, {
9 | accepts: [{ name: 'name', type: 'string', required: true }],
10 | http: { path: '/:name' }
11 | });
12 | function ContractClass(name) {
13 | this.name = name;
14 | }
15 |
16 | /**
17 | * Returns the ContractClass instance's name.
18 | */
19 | clshelper.method(getName, {
20 | returns: { name: 'name', type: 'string' }
21 | });
22 | function getName(callback) {
23 | callback(null, this.name);
24 | }
25 |
26 | /**
27 | * Takes in a name, returning a greeting for that name.
28 | */
29 | clshelper.method(greet, {
30 | accepts: [{ name: 'other', type: 'string', required: true }],
31 | returns: { name: 'greeting', type: 'string' }
32 | });
33 | function greet(other, callback) {
34 | callback(null, 'Hi, ' + other + '!');
35 | }
36 |
37 | /**
38 | * Returns the ContractClass prototype's favorite person's name.
39 | */
40 | helper.method(getFavoritePerson, {
41 | path: 'ContractClass.getFavoritePerson',
42 | returns: { name: 'name', type: 'string' }
43 | });
44 | function getFavoritePerson(callback) {
45 | callback(null, 'You');
46 | }
47 |
--------------------------------------------------------------------------------
/test/streams.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var RemoteObjects = require('../');
3 | var express = require('express');
4 | var request = require('supertest');
5 | var fs = require('fs');
6 |
7 | describe('strong-remoting', function () {
8 | var app, remotes, objects;
9 |
10 | beforeEach(function(){
11 | objects = RemoteObjects.create();
12 | remotes = objects.exports;
13 | app = express();
14 | app.disable('x-powered-by');
15 |
16 | app.use(function (req, res, next) {
17 | objects.handler('rest').apply(objects, arguments);
18 | });
19 | });
20 |
21 | function json(method, url) {
22 | return request(app)[method](url)
23 | .set('Accept', 'application/json')
24 | .set('Content-Type', 'application/json')
25 | .expect(200)
26 | .expect('Content-Type', /json/);
27 | }
28 |
29 | it('should stream the file output', function (done) {
30 | remotes.fs = fs;
31 | fs.createReadStream.shared = true;
32 | fs.createReadStream.accepts = [{arg: 'path', type: 'string'}];
33 | fs.createReadStream.returns = {arg: 'res', type: 'stream'};
34 | fs.createReadStream.http = {
35 | verb: 'get',
36 | // path: '/fs/createReadStream',
37 | pipe: {
38 | dest: 'res'
39 | }
40 | };
41 |
42 | json('get', '/fs/createReadStream?path=' + __dirname + '/data/foo.json')
43 | .expect({bar: 'baz'}, done);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/example/documentation/remotes/simple-class.js:
--------------------------------------------------------------------------------
1 | // This example shows using the helper for a type in a "post-definition" style.
2 | var helper = require('../../../').extend(module.exports);
3 |
4 | /**
5 | * A simple class that contains a name.
6 | */
7 | function SimpleClass(name) {
8 | this.name = name;
9 | }
10 | helper.type(SimpleClass, {
11 | description: 'A simple class example',
12 | accepts: [{ name: 'name', type: 'string', required: true }]
13 | });
14 |
15 | /**
16 | * Returns the SimpleClass instance's name.
17 | */
18 | SimpleClass.prototype.getName = function(callback) {
19 | callback(null, this.name);
20 | };
21 | helper.method(SimpleClass.prototype.getName, {
22 | path: 'SimpleClass.prototype.getName',
23 | description: 'Returns the SimpleClass instance\'s name.',
24 | returns: { name: 'name', type: 'string' }
25 | });
26 |
27 | /**
28 | * Takes in a name, returning a greeting for that name.
29 | */
30 | SimpleClass.prototype.greet = function(other, callback) {
31 | callback(null, 'Hi, ' + other + '!');
32 | };
33 | helper.method(SimpleClass.prototype.greet, {
34 | path: 'SimpleClass.prototype.greet',
35 | description: 'Takes in a name, returning a greeting for that name.',
36 | accepts: [{ name: 'other', type: 'string', required: true }],
37 | returns: { name: 'greeting', type: 'string' }
38 | });
39 |
40 | /**
41 | * Returns the SimpleClass prototype's favorite person's name.
42 | */
43 | SimpleClass.getFavoritePerson = function(callback) {
44 | callback(null, 'You');
45 | };
46 | helper.method(SimpleClass.getFavoritePerson, {
47 | path: 'SimpleClass.getFavoritePerson',
48 | description: 'Returns the SimpleClass prototype\'s favorite person\'s name.',
49 | returns: { name: 'name', type: 'string' }
50 | });
51 |
--------------------------------------------------------------------------------
/test/helpers/shared-objects-factory.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2013 StrongLoop, Inc.
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to deal
5 | * in the Software without restriction, including without limitation the rights
6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | * copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included
11 | * in all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | * THE SOFTWARE.
20 | */
21 |
22 | var extend = require('util')._extend;
23 |
24 | exports.createSharedClass = function createSharedClass(config) {
25 | // create a class that can be remoted
26 | var SharedClass = function(id) {
27 | this.id = id;
28 | };
29 | extend(SharedClass, config);
30 |
31 | SharedClass.shared = true;
32 |
33 | SharedClass.sharedCtor = function(id, cb) {
34 | cb(null, new SharedClass(id));
35 | };
36 |
37 | extend(SharedClass.sharedCtor, {
38 | shared: true,
39 | accepts: [ { arg: 'id', type: 'any', http: { source: 'path' }}],
40 | http: { path: '/:id' },
41 | returns: { root: true }
42 | });
43 |
44 | return SharedClass;
45 | };
46 |
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "strong-remoting",
3 | "description": "StrongLoop Remoting Module",
4 | "keywords": [
5 | "StrongLoop",
6 | "LoopBack",
7 | "Remoting",
8 | "REST"
9 | ],
10 | "version": "2.9.0",
11 | "scripts": {
12 | "test": "mocha"
13 | },
14 | "dependencies": {
15 | "express": "4.x",
16 | "body-parser": "^1.7.0",
17 | "debug": "^2.0.0",
18 | "eventemitter2": "^0.4.14",
19 | "cors": "^2.4.1",
20 | "jayson": "^1.1.1",
21 | "js2xmlparser": "^0.1.3",
22 | "async": "^0.9.0",
23 | "traverse": "^0.6.6",
24 | "request": "^2.42.0",
25 | "browser-request": "^0.3.2",
26 | "qs": "^2.2.3",
27 | "inflection": "^1.4.2",
28 | "xml2js": "^0.4.4"
29 | },
30 | "devDependencies": {
31 | "supertest": "~0.13.0",
32 | "mocha": "~1.21.4",
33 | "socket.io": "~1.1.0",
34 | "chai": "~1.9.1",
35 | "grunt": "~0.4.5",
36 | "grunt-contrib-uglify": "~0.5.1",
37 | "grunt-contrib-jshint": "~0.10.0",
38 | "browserify": "~5.11.1",
39 | "grunt-browserify": "~3.0.1",
40 | "grunt-contrib-watch": "~0.6.1",
41 | "karma-script-launcher": "~0.1.0",
42 | "karma-chrome-launcher": "~0.1.4",
43 | "karma-firefox-launcher": "~0.1.3",
44 | "karma-html2js-preprocessor": "~0.1.0",
45 | "karma-jasmine": "~0.1.5",
46 | "karma-coffee-preprocessor": "~0.2.1",
47 | "requirejs": "~2.1.14",
48 | "karma-requirejs": "~0.2.2",
49 | "karma-phantomjs-launcher": "~0.1.4",
50 | "karma": "~0.12.23",
51 | "grunt-karma": "~0.9.0",
52 | "karma-browserify": "0.2.1",
53 | "karma-mocha": "~0.1.9"
54 | },
55 | "repository": {
56 | "type": "git",
57 | "url": "https://github.com/strongloop/strong-remoting"
58 | },
59 | "browser": {
60 | "express": false,
61 | "body-parser": false,
62 | "request": "browser-request"
63 | },
64 | "license": {
65 | "name": "Dual MIT/StrongLoop",
66 | "url": "https://github.com/strongloop/strong-remoting/blob/master/LICENSE"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/example/rest-models/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Backbone.js Todos
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
24 |
25 |
29 |
30 |
31 |
32 |
33 | Double-click to edit a todo.
34 |
35 |
36 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
59 |
60 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/test/shared-method.test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var extend = require('util')._extend;
3 | var expect = require('chai').expect;
4 | var SharedMethod = require('../lib/shared-method');
5 | var factory = require('./helpers/shared-objects-factory.js');
6 |
7 | describe('SharedMethod', function() {
8 | describe('sharedMethod.isDelegateFor(suspect, [isStatic])', function () {
9 |
10 | // stub function
11 | function myFunction() {};
12 |
13 | it('checks if the given function is going to be invoked', function () {
14 | var mockSharedClass = {};
15 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass);
16 | assert.equal(sharedMethod.isDelegateFor(myFunction), true);
17 | });
18 |
19 | it('checks by name if a function is going to be invoked', function () {
20 | var mockSharedClass = { prototype: { myName: myFunction } };
21 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass);
22 | assert.equal(sharedMethod.isDelegateFor('myName', false), true);
23 | assert.equal(sharedMethod.isDelegateFor('myName', true), false);
24 | assert.equal(sharedMethod.isDelegateFor('myName'), true);
25 | });
26 |
27 | it('checks by name if static function is going to be invoked', function () {
28 | var mockSharedClass = { myName: myFunction };
29 | var options = { isStatic: true };
30 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
31 | assert.equal(sharedMethod.isDelegateFor('myName', true), true);
32 | assert.equal(sharedMethod.isDelegateFor('myName', false), false);
33 | });
34 |
35 | it('checks by alias if static function is going to be invoked', function () {
36 | var mockSharedClass = { myName: myFunction };
37 | var options = { isStatic: true, aliases: ['myAlias'] };
38 | var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
39 | assert.equal(sharedMethod.isDelegateFor('myAlias', true), true);
40 | assert.equal(sharedMethod.isDelegateFor('myAlias', false), false);
41 | });
42 |
43 | it('checks if the given name is a string', function () {
44 | var mockSharedClass = {};
45 | var err;
46 | try {
47 | var sharedMethod = new SharedMethod(myFunction, Number, mockSharedClass);
48 | } catch(e) {
49 | err = e;
50 | }
51 | assert(err);
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/example/before-after.js:
--------------------------------------------------------------------------------
1 | // create a set of shared classes
2 | var remotes = require('../').create();
3 |
4 | // expose a simple object
5 | var user = remotes.exports.user = {
6 | greet: function (fn) {
7 | fn(null, 'hello, world!');
8 | }
9 | };
10 |
11 | // share the greet method
12 | user.greet.shared = true;
13 |
14 | // expose a simple class
15 | function Dog(name) {
16 | this.name = name;
17 | }
18 |
19 | // define a vanilla JavaScript class
20 | function Dog(name) {
21 | this.name = name;
22 | }
23 |
24 | // add a shared constructor
25 | Dog.sharedCtor = function (name, fn) {
26 | fn(null, new Dog(name));
27 | }
28 |
29 | // define the args for the shared constructor
30 | Dog.sharedCtor.accepts = {arg: 'name', type: 'string'};
31 |
32 | // change the default routing
33 | Dog.sharedCtor.http = {path: '/:name'};
34 |
35 | // define a regular instance method
36 | Dog.prototype.speak = function (fn) {
37 | fn(null, 'roof! my name is ' + this.name);
38 | }
39 |
40 | Dog.prototype.speak.shared = true;
41 |
42 | // expose the dog class
43 | remotes.exports.dog = Dog;
44 |
45 | // do something before greet
46 | remotes.before('user.greet', function (ctx, next) {
47 | if((ctx.req.param('password') || '').toString() !== '1234') {
48 | next(new Error('bad password!'));
49 | } else {
50 | next();
51 | }
52 | });
53 |
54 | // do something before any user method
55 | remotes.before('user.*', function (ctx, next) {
56 | console.log('calling a user method');
57 | next();
58 | });
59 |
60 | // do something before a dog instance method
61 | remotes.before('dog.prototype.*', function (ctx, next) {
62 | var dog = this;
63 | console.log('calling a method on', dog.name);
64 | next();
65 | });
66 |
67 | // do something after the dog speak method
68 | // note: you cannot cancel a method after
69 | // it has been called
70 | remotes.after('dog.prototype.speak', function (ctx, next) {
71 | console.log('after speak!');
72 | next();
73 | });
74 |
75 | // do something before all methods
76 | remotes.before('**', function (ctx, next, method) {
77 | console.log('calling', method.name);
78 | next();
79 | });
80 |
81 | // modify all results
82 | remotes.after('**', function (ctx, next) {
83 | ctx.result += '!!!';
84 | next();
85 | });
86 |
87 | // expose it over http
88 | require('http')
89 | .createServer(remotes.handler('rest'))
90 | .listen(3000);
91 |
92 | /*
93 |
94 | Test the above with curl or a rest client:
95 |
96 | $ node before-after.js
97 | $ curl http://localhost:3000/user/greet
98 | $ curl http://localhost:3000/user/greet?password=1234
99 | $ curl http://localhost:3000/dog/fido/speak
100 |
101 | */
--------------------------------------------------------------------------------
/test/type.test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var Dynamic = require('../lib/dynamic');
3 | var RemoteObjects = require('../');
4 |
5 | describe('types', function () {
6 | var remotes;
7 | beforeEach(function() {
8 | remotes = RemoteObjects.create();
9 | });
10 | describe('remotes.defineType(name, fn)', function () {
11 | it('should define a new type converter', function () {
12 | var name = 'MyType';
13 | remotes.defineType(name, function(val, ctx) {
14 | return val;
15 | });
16 | assert(Dynamic.getConverter(name));
17 | });
18 | });
19 |
20 | describe('Dynamic(val, [ctx])', function () {
21 | describe('Dynamic.to(typeName)', function () {
22 | it('should convert the dynamic to the given type', function () {
23 | Dynamic.define('beep', function(str) {
24 | return 'boop';
25 | });
26 | var dyn = new Dynamic('beep');
27 | assert.equal(dyn.to('beep'), 'boop');
28 | });
29 | });
30 | describe('Dynamic.canConvert(typeName)', function () {
31 | it('should only return true when a converter exists', function () {
32 | Dynamic.define('MyType', function() {});
33 | assert(Dynamic.canConvert('MyType'));
34 | assert(!Dynamic.canConvert('FauxType'));
35 | });
36 | });
37 | describe('Built in converters', function(){
38 | it('should convert Boolean values', function() {
39 | shouldConvert(true, true);
40 | shouldConvert(false, false);
41 | shouldConvert(256, true);
42 | shouldConvert(-1, true);
43 | shouldConvert(1, true);
44 | shouldConvert(0, false);
45 | shouldConvert('true', true);
46 | shouldConvert('false', false);
47 | shouldConvert('0', false);
48 | shouldConvert('1', true);
49 | shouldConvert('-1', true);
50 | shouldConvert('256', true);
51 | shouldConvert('null', false);
52 | shouldConvert('undefined', false);
53 | shouldConvert('', false);
54 |
55 | function shouldConvert(val, expected) {
56 | var dyn = new Dynamic(val);
57 | assert.equal(dyn.to('boolean'), expected);
58 | }
59 | });
60 | it('should convert Number values', function() {
61 | shouldConvert('-1', -1);
62 | shouldConvert('0', 0);
63 | shouldConvert('1', 1);
64 | shouldConvert('0.1', 0.1);
65 | shouldConvert(1, 1);
66 | shouldConvert(true, 1);
67 | shouldConvert(false, 0);
68 | shouldConvert({}, 'NaN');
69 | shouldConvert([], 0);
70 |
71 | function shouldConvert(val, expected) {
72 | var dyn = new Dynamic(val);
73 |
74 | if(expected === 'NaN') {
75 | return assert(Number.isNaN(dyn.to('number')));
76 | }
77 |
78 | assert.equal(dyn.to('number'), expected);
79 | }
80 | });
81 | });
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/test/http-invocation.test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var HttpInvocation = require('../lib/http-invocation');
3 | var SharedMethod = require('../lib/shared-method');
4 | var extend = require('util')._extend;
5 | var expect = require('chai').expect;
6 |
7 | describe('HttpInvocation', function() {
8 | describe('namedArgs', function() {
9 |
10 | function expectNamedArgs(accepts, inputArgs, expectedNamedArgs) {
11 | var method = givenSharedStaticMethod({
12 | accepts: accepts
13 | });
14 | var inv = new HttpInvocation(method, null, inputArgs);
15 | expect(inv.namedArgs).to.deep.equal(expectedNamedArgs);
16 | }
17 |
18 | it('should correctly name a single arg', function() {
19 | expectNamedArgs(
20 | [{arg: 'a', type: 'number'}],
21 | [1],
22 | {a: 1}
23 | );
24 | });
25 |
26 | it('should correctly name multiple args', function() {
27 | expectNamedArgs(
28 | [{arg: 'a', type: 'number'}, {arg: 'str', type: 'string'}],
29 | [1, 'foo'],
30 | {a: 1, str: 'foo'}
31 | );
32 | });
33 |
34 | it('should correctly name multiple args when a partial set is provided', function() {
35 | expectNamedArgs(
36 | [{arg: 'a', type: 'number'}, {arg: 'str', type: 'string'}],
37 | [1],
38 | {a: 1}
39 | );
40 | });
41 |
42 | describe('HttpContext.isAcceptable()', function() {
43 | it('should accept an acceptable argument', function() {
44 | var acceptable = HttpInvocation.isAcceptable(2, {
45 | arg: 'foo',
46 | type: 'number'
47 | });
48 | expect(acceptable).to.equal(true);
49 | });
50 |
51 | it('should always accept args when type is any', function() {
52 | var acceptable = HttpInvocation.isAcceptable(2, {
53 | arg: 'bar',
54 | type: 'any'
55 | });
56 | expect(acceptable).to.equal(true);
57 | });
58 |
59 | it('should always accept args when type is complex', function() {
60 | var acceptable = HttpInvocation.isAcceptable({}, {
61 | arg: 'bar',
62 | type: 'MyComplexType'
63 | });
64 | expect(acceptable).to.equal(true);
65 | });
66 |
67 | it('should accept null arg when type is complex', function() {
68 | var acceptable = HttpInvocation.isAcceptable(null, {
69 | arg: 'bar',
70 | type: 'MyComplexType'
71 | });
72 | expect(acceptable).to.equal(true);
73 | });
74 | });
75 | });
76 | });
77 |
78 | function givenSharedStaticMethod(fn, config) {
79 | if (typeof fn === 'object' && config === undefined) {
80 | config = fn;
81 | fn = null;
82 | }
83 | fn = fn || function(cb) { cb(); };
84 |
85 | var testClass = { testMethod: fn };
86 | config = extend({ shared: true }, config);
87 | extend(testClass.testMethod, config);
88 | return SharedMethod.fromFunction(fn, 'testStaticMethodName', null, true);
89 | }
90 |
--------------------------------------------------------------------------------
/lib/dynamic.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Expose `Dynamic`.
3 | */
4 |
5 | module.exports = Dynamic;
6 |
7 | /**
8 | * Module dependencies.
9 | */
10 |
11 | var debug = require('debug')('strong-remoting:dynamic')
12 | , assert = require('assert');
13 |
14 | /**
15 | * Create a dynamic value from the given value.
16 | *
17 | * @param {*} val The value object
18 | * @param {Context} ctx The Remote Context
19 | */
20 |
21 | function Dynamic(val, ctx) {
22 | this.val = val;
23 | this.ctx = ctx;
24 | }
25 |
26 | /*!
27 | * Object containing converter functions.
28 | */
29 |
30 | Dynamic.converters = [];
31 |
32 | /**
33 | * Define a named type conversion. The conversion is used when a
34 | * `SharedMethod` argument defines a type with the given `name`.
35 | *
36 | * ```js
37 | * Dynamic.define('MyType', function(val, ctx) {
38 | * // use the val and ctx objects to return the concrete value
39 | * return new MyType(val);
40 | * });
41 | * ```
42 | *
43 | * @param {String} name The type name
44 | * @param {Function} converter
45 | */
46 |
47 | Dynamic.define = function(name, converter) {
48 | converter.typeName = name;
49 | this.converters.unshift(converter);
50 | }
51 |
52 | /**
53 | * Is the given type supported.
54 | *
55 | * @param {String} type
56 | * @returns {Boolean}
57 | */
58 |
59 | Dynamic.canConvert = function(type) {
60 | return !!this.getConverter(type);
61 | }
62 |
63 | /**
64 | * Get converter by type name.
65 | *
66 | * @param {String} type
67 | * @returns {Function}
68 | */
69 |
70 | Dynamic.getConverter = function(type) {
71 | var converters = this.converters;
72 | var converter;
73 | for(var i = 0; i < converters.length; i++) {
74 | converter = converters[i];
75 | if(converter.typeName === type) {
76 | return converter;
77 | }
78 | }
79 | }
80 |
81 | /**
82 | * Convert the dynamic value to the given type.
83 | *
84 | * @param {String} type
85 | * @returns {*} The concrete value
86 | */
87 |
88 | Dynamic.prototype.to = function(type) {
89 | var converter = this.constructor.getConverter(type);
90 | assert(converter, 'No Type converter defined for ' + type);
91 | return converter(this.val, this.ctx);
92 | }
93 |
94 | /**
95 | * Built in type converters...
96 | */
97 |
98 | Dynamic.define('boolean', function convertBoolean(val) {
99 | switch(typeof val) {
100 | case 'string':
101 | switch(val) {
102 | case 'false':
103 | case 'undefined':
104 | case 'null':
105 | case '0':
106 | case '':
107 | return false;
108 | break;
109 | default:
110 | return true;
111 | break;
112 | }
113 | break;
114 | case 'number':
115 | return val !== 0;
116 | break;
117 | default:
118 | return Boolean(val);
119 | break;
120 | }
121 | });
122 |
123 | Dynamic.define('number', function convertNumber(val) {
124 | if(val === 0) return val;
125 | if(!val) return val;
126 | return Number(val);
127 | });
128 |
--------------------------------------------------------------------------------
/example/rest-models/remotes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Dependencies.
3 | */
4 |
5 | var jugglingdb = require('jugglingdb');
6 |
7 | /**
8 | * Create a set of remote classes and export them.
9 | */
10 |
11 | var remotes = module.exports = require('../../').create();
12 |
13 | /**
14 | * Define some models. Remotely export them.
15 | */
16 |
17 | var Schema = require('jugglingdb').Schema;
18 | var schema = new Schema('memory');
19 |
20 | var Todo = remotes.exports.post = schema.define('Post', {
21 | title: { type: String, length: 255 },
22 | done: { type: Boolean },
23 | date: { type: Date, default: function () { return new Date;} },
24 | changed: { type: Number, default: Date.now }
25 | });
26 |
27 | var User = remotes.exports.user = schema.define('User', {
28 | name: String,
29 | bio: Schema.Text,
30 | approved: Boolean,
31 | joinedAt: Date,
32 | age: Number
33 | });
34 |
35 |
36 | // // setup relationships
37 | // User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});
38 | // creates instance methods:
39 | // user.posts(conds)
40 | // user.posts.build(data) // like new Post({userId: user.id});
41 | // user.posts.create(data) // build and save
42 |
43 | // Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
44 | // creates instance methods:
45 | // post.author(callback) -- getter when called with function
46 | // post.author() -- sync getter when called without params
47 | // post.author(user) -- setter when called with object
48 |
49 | // setup remote attributes
50 | setup(Todo);
51 | setup(User);
52 |
53 | // create some test data
54 | User.create({name: 'joe', age: 20});
55 | User.create({name: 'bob', age: 30});
56 | User.create({name: 'jim', age: 40});
57 | User.create({name: 'jan', age: 50});
58 |
59 | Todo.create({title: 'hello', done: false});
60 | Todo.create({title: 'foo', done: false});
61 | Todo.create({title: 'lorem', done: true});
62 |
63 | // setup custom routes
64 | User.http = {path: '/u'};
65 | Todo.http = {path: '/t'};
66 |
67 | // annotate with remotes settings
68 | function setup(Model) {
69 | Model.sharedCtor = function (id, fn) {
70 | Model.find(id, fn);
71 | }
72 | Model.sharedCtor.shared = true;
73 | Model.sharedCtor.accepts = {arg: 'id', type: 'string'};
74 | Model.sharedCtor.http = [
75 | {path: '/:id', verb: 'get'},
76 | {path: '/', verb: 'get'}
77 | ];
78 |
79 | Model.prototype.save.shared = true;
80 | Model.prototype.save.http = [
81 | {verb: 'post', path: '/'},
82 | {verb: 'put', path: '/'}
83 | ];
84 | Model.all.shared = true;
85 | Model.all.http = [
86 | {verb: 'get', path: '/'}
87 | ];
88 | Model.all.accepts = [
89 | {
90 | arg: 'query',
91 | type: 'object'
92 | // http: function (ctx) {
93 | // var q = ctx.req.url.split('?')[1];
94 | //
95 | // if(q) {
96 | // q = decodeURIComponent(q);
97 | // }
98 | //
99 | // return JSON.parse(q);
100 | // }
101 | }
102 | ];
103 | }
104 |
--------------------------------------------------------------------------------
/lib/socket-io-context.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Expose `SocketIOContext`.
3 | */
4 |
5 | module.exports = SocketIOContext;
6 |
7 | /**
8 | * Module dependencies.
9 | */
10 |
11 | var EventEmitter = require('events').EventEmitter
12 | , debug = require('debug')('strong-remoting:socket-io-context')
13 | , util = require('util')
14 | , inherits = util.inherits
15 | , assert = require('assert');
16 |
17 | /**
18 | * Create a new `SocketIOContext` with the given `options`.
19 | *
20 | * @param {Object} options
21 | * @return {SocketIOContext}
22 | */
23 |
24 | function SocketIOContext(req, ctorArgs, args) {
25 | this.req = req;
26 | this.ctorArgs = ctorArgs;
27 | this.args = args;
28 | }
29 |
30 | /**
31 | * Inherit from `EventEmitter`.
32 | */
33 |
34 | inherits(SocketIOContext, EventEmitter);
35 |
36 | /**
37 | * Get an arg by name using the given options.
38 | *
39 | * @param {String} name
40 | * @param {Object} options **optional**
41 | */
42 |
43 | SocketIOContext.prototype.getArgByName = function (name, options) {
44 | return this.args[name];
45 | }
46 |
47 | /**
48 | * Set an arg by name using the given options.
49 | *
50 | * @param {String} name
51 | * @param {Object} options **optional**
52 | */
53 |
54 | SocketIOContext.prototype.setArgByName = function (name, options) {
55 | throw 'not implemented'
56 | }
57 |
58 | /**
59 | * Set part or all of the result by name using the given options.
60 | *
61 | * @param {String} name
62 | * @param {Object} options **optional**
63 | */
64 |
65 | SocketIOContext.prototype.setResultByName = function (name, options) {
66 |
67 | }
68 |
69 | /**
70 | * Get part or all of the result by name using the given options.
71 | *
72 | * @param {String} name
73 | * @param {Object} options **optional**
74 | */
75 |
76 | SocketIOContext.prototype.getResultByName = function (name, options) {
77 |
78 | }
79 |
80 | /**
81 | * Invoke the given shared method using the provided scope against the current context.
82 | */
83 |
84 | SocketIOContext.prototype.invoke = function (scope, method, fn) {
85 | var args = method.isSharedCtor ? this.ctorArgs : this.args;
86 | var accepts = method.accepts;
87 | var returns = method.returns;
88 | var errors = method.errors;
89 | var scope;
90 | var result;
91 |
92 | // invoke the shared method
93 | method.invoke(scope, args, function (err) {
94 | if(method.name === 'on' && method.ctor instanceof EventEmitter) {
95 | arguments[1] = arguments[0];
96 | err = null;
97 | }
98 |
99 | if(err) {
100 | return fn(err);
101 | }
102 |
103 | var resultArgs = arguments;
104 |
105 | // map the arguments using the returns description
106 | if(returns.length > 1) {
107 | // multiple
108 | result = {};
109 |
110 | returns.forEach(function (o, i) {
111 | // map the name of the arg in the returns desc
112 | // to the same arg in the callback
113 | result[o.name || o.arg] = resultArgs[i + 1];
114 | });
115 | } else {
116 | // single or no result...
117 | result = resultArgs[1];
118 | }
119 |
120 | fn(null, result);
121 | });
122 | }
123 |
--------------------------------------------------------------------------------
/lib/exports-helper.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Expose `ExportsHelper`.
3 | */
4 |
5 | module.exports = ExportsHelper;
6 |
7 | /*!
8 | * Module dependencies.
9 | */
10 | var debug = require('debug')('strong-remoting:exports-helper');
11 |
12 | /*!
13 | * Constants
14 | */
15 | var PASSTHROUGH_OPTIONS = ['http', 'description', 'notes'];
16 |
17 | /**
18 | * @class A wrapper to make manipulating the exports object easier.
19 | *
20 | * @constructor
21 | * Create a new `ExportsHelper` with the given `options`.
22 | */
23 |
24 | function ExportsHelper(obj) {
25 | if (!(this instanceof ExportsHelper)) {
26 | return new ExportsHelper(obj);
27 | }
28 |
29 | this._obj = obj;
30 | }
31 |
32 | /**
33 | * Sets a value at any path within the exports object.
34 | */
35 | ExportsHelper.prototype.setPath = setPath;
36 | function setPath(path, value) {
37 | var self = this;
38 | var obj = self._obj;
39 | var split = path.split('.');
40 | var name = split.pop();
41 |
42 | split.forEach(function (key) {
43 | if (!obj[key]) {
44 | obj[key] = {};
45 | }
46 |
47 | obj = obj[key];
48 | });
49 |
50 | debug('Setting %s to %s', path, value);
51 | obj[name] = value;
52 |
53 | return self;
54 | }
55 |
56 | /**
57 | * Exports a constructor ("type") with the provided options.
58 | */
59 | ExportsHelper.prototype.addType = type;
60 | ExportsHelper.prototype.type = type;
61 | function type(fn, options) {
62 | var self = this;
63 | var path = options.path || options.name || fn.name || null;
64 | var sharedCtor = options.sharedCtor || null;
65 | var accepts = options.accepts || null;
66 |
67 | if (!path) {
68 | // TODO: Error.
69 | return self;
70 | }
71 |
72 | if (!sharedCtor) {
73 | // TODO(schoon) - This shouldn't be thought of (or named) as a "shared
74 | // constructor". Instead, this is the lazy find/create sl-remoting uses when
75 | // a prototype method is called. `getInstance`? `findOrCreate`? `load`?
76 | sharedCtor = function () {
77 | var _args = [].slice.call(arguments);
78 | _args.pop()(null, fn.apply(null, _args));
79 | };
80 | }
81 |
82 | if (!sharedCtor.accepts) {
83 | sharedCtor.accepts = accepts;
84 | }
85 |
86 | // This is required because sharedCtors are called just like any other
87 | // remotable method. However, you always expect the instance and nothing else.
88 | if (!sharedCtor.returns) {
89 | sharedCtor.returns = { type: 'object', root: true };
90 | }
91 |
92 | PASSTHROUGH_OPTIONS.forEach(function (key) {
93 | if (options[key]) {
94 | sharedCtor[key] = options[key];
95 | }
96 | });
97 |
98 | self.setPath(path, fn);
99 | fn.shared = true;
100 | fn.sharedCtor = sharedCtor;
101 |
102 | return new ExportsHelper(fn.prototype);
103 | }
104 |
105 | /**
106 | * Exports a Function with the provided options.
107 | */
108 | ExportsHelper.prototype.addMethod = method;
109 | ExportsHelper.prototype.method = method;
110 | function method(fn, options) {
111 | var self = this;
112 | var path = options.path || options.name || fn.name || null;
113 | var accepts = options.accepts || null;
114 | var returns = options.returns || null;
115 | var errors = options.errors || null;
116 |
117 | if (!path) {
118 | // TODO: Error.
119 | return self;
120 | }
121 |
122 | self.setPath(path, fn);
123 | fn.shared = true;
124 | fn.accepts = accepts;
125 | fn.returns = returns;
126 | fn.errors = errors;
127 |
128 | PASSTHROUGH_OPTIONS.forEach(function (key) {
129 | if (options[key]) {
130 | fn[key] = options[key];
131 | }
132 | });
133 |
134 | return self;
135 | }
136 |
--------------------------------------------------------------------------------
/lib/socket-io-adapter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Expose `SocketIOAdapter`.
3 | */
4 |
5 | module.exports = SocketIOAdapter;
6 |
7 | /**
8 | * Module dependencies.
9 | */
10 |
11 | var EventEmitter = require('events').EventEmitter
12 | , debug = require('debug')('strong-remoting:socket-io-adapter')
13 | , util = require('util')
14 | , inherits = util.inherits
15 | , assert = require('assert')
16 | , express = require('express')
17 | , SocketIOContext = require('./socket-io-context');
18 |
19 | /**
20 | * Create a new `RestAdapter` with the given `options`.
21 | *
22 | * @param {Object} options
23 | * @return {RestAdapter}
24 | */
25 |
26 | function SocketIOAdapter(remotes, server) {
27 | EventEmitter.apply(this, arguments);
28 |
29 | // throw an error if args are not supplied
30 | // assert(typeof options === 'object', 'RestAdapter requires an options object');
31 |
32 | this.remotes = remotes;
33 | this.server = server;
34 | this.Context = SocketIOContext;
35 | }
36 |
37 | /**
38 | * Inherit from `EventEmitter`.
39 | */
40 |
41 | inherits(SocketIOAdapter, EventEmitter);
42 |
43 | /*!
44 | * Simplified APIs
45 | */
46 |
47 | SocketIOAdapter.create = function (remotes) {
48 | // add simplified construction / sugar here
49 | return new SocketIOAdapter(remotes);
50 | }
51 |
52 | SocketIOAdapter.prototype.createHandler = function () {
53 | var adapter = this;
54 | var remotes = this.remotes;
55 | var Context = this.Context;
56 | var classes = this.remotes.classes();
57 | var io = require('socket.io').listen(this.server);
58 |
59 | io.sockets.on('connection', function (socket) {
60 | socket.on('invoke', function (methodString, ctorArgs, args, id) {
61 | var method = remotes.findMethod(methodString);
62 |
63 | if(method) {
64 | // create context NEED ARGS
65 | var ctx = new Context(socket.request, ctorArgs, args);
66 |
67 | adapter.invoke(ctx, method, args, function (err, result) {
68 | socket.emit('result', {
69 | data: result,
70 | id: id,
71 | methodString: methodString,
72 | __types__: method.returns
73 | });
74 | });
75 | } else {
76 | socket.emit('result', {
77 | err: 'method does not exist',
78 | id: id,
79 | methodString: methodString
80 | });
81 | }
82 | });
83 | });
84 | }
85 |
86 | SocketIOAdapter.prototype.invoke = function (ctx, method, args, callback) {
87 | var remotes = this.remotes;
88 |
89 | if(method.isStatic) {
90 | remotes.execHooks('before', method, method.ctor, ctx, function (err) {
91 | if(err) return callback(err);
92 |
93 | // invoke the static method on the actual constructor
94 | ctx.invoke(method.ctor, method, function (err, result) {
95 | if(err) return callback(err);
96 | ctx.result = result;
97 | remotes.execHooks('after', method, method.ctor, ctx, function (err) {
98 | // send the result
99 | callback(err, ctx.result);
100 | });
101 | });
102 | });
103 | } else {
104 | // invoke the shared constructor to get an instance
105 | ctx.invoke(method, method.sharedCtor, function (err, inst) {
106 | if(err) return callback(err);
107 | remotes.execHooks('before', method, inst, ctx, function (err) {
108 | if(err) {
109 | callback(err);
110 | } else {
111 | // invoke the instance method
112 | ctx.invoke(inst, method, function (err, result) {
113 | if(err) return callback(err);
114 |
115 | ctx.result = result;
116 | remotes.execHooks('after', method, inst, ctx, function (err) {
117 | // send the result
118 | callback(err, ctx.result);
119 | });
120 | });
121 | }
122 | });
123 | });
124 | }
125 | }
--------------------------------------------------------------------------------
/test/jsonrpc.test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var RemoteObjects = require('../');
3 | var express = require('express');
4 | var request = require('supertest');
5 | var SharedClass = require('../lib/shared-class');
6 |
7 | describe('strong-remoting-jsonrpc', function () {
8 | var app;
9 | var server;
10 | var objects;
11 | var remotes;
12 |
13 | // setup
14 | beforeEach(function () {
15 | if (server) server.close();
16 | objects = RemoteObjects.create({json: {limit: '1kb'}});
17 | remotes = objects.exports;
18 | app = express();
19 | });
20 |
21 | function jsonrpc(url, method, parameters) {
22 | return request(app)['post'](url)
23 | .set('Accept', 'application/json')
24 | .set('Content-Type', 'application/json')
25 | .send({"jsonrpc": "2.0", "method": method, "params": parameters, "id": 1})
26 | .expect(200)
27 | .expect('Content-Type', /json/);
28 | }
29 |
30 | describe('handlers', function () {
31 | describe('jsonrpc', function () {
32 | beforeEach(function () {
33 | app.use(function (req, res, next) {
34 | // create the handler for each request
35 | objects.handler('jsonrpc').apply(objects, arguments);
36 | });
37 | function greet(msg, fn) {
38 | fn(null, msg);
39 | }
40 |
41 | // Create a shared method directly on the function object
42 | remotes.user = {
43 | greet: greet
44 | };
45 | greet.shared = true;
46 |
47 | // Create a shared method directly on the function object for named parameters tests
48 | function sum(numA,numB,cb){
49 | cb(null,numA+numB);
50 | };
51 | remotes.mathematic={
52 | sum:sum
53 | };
54 | sum.accepts=[
55 | {'arg':'numA','type':'number'},
56 | {'arg':'numB','type':'number'},
57 | ];
58 | sum.returns={
59 | 'arg':'sum',
60 | 'type':'number'
61 | }
62 | sum.shared=true;
63 |
64 |
65 | // Create a shared method using SharedClass/SharedMethod
66 | function Product() {
67 | }
68 |
69 | Product.getPrice = function(cb) {
70 | process.nextTick(function() {
71 | return cb(null, 100);
72 | });
73 | };
74 |
75 | var productClass = new SharedClass('product', Product, {});
76 | productClass.defineMethod('getPrice', {isStatic: true});
77 | objects.addClass(productClass);
78 | });
79 |
80 | it('should support calling object methods', function (done) {
81 | jsonrpc('/user/jsonrpc', 'greet', ['JS'])
82 | .expect({"jsonrpc": "2.0", "id": 1, "result": "JS"}, done);
83 |
84 | });
85 | it('Should successfully call a method with named parameters',function(done){
86 | jsonrpc('/mathematic/jsonrpc', 'sum', {'numB':9,'numA':2})
87 | .expect({"jsonrpc": "2.0", "id": 1, "result": 11}, done);
88 | });
89 | it('should support a remote method using shared method', function (done) {
90 | jsonrpc('/product/jsonrpc', 'getPrice', [])
91 | .expect({"jsonrpc": "2.0", "id": 1, "result": 100}, done);
92 | });
93 |
94 | it('should report error for non-existent methods', function (done) {
95 | jsonrpc('/user/jsonrpc', 'greet1', ['JS'])
96 | .expect({
97 | "jsonrpc": "2.0",
98 | "id": 1,
99 | "error": {
100 | "code": -32601,
101 | "message": "Method not found"
102 | }
103 | }, done);
104 | });
105 |
106 | // The 1kb limit is set by RemoteObjects.create({json: {limit: '1kb'}});
107 | it('should reject json payload larger than 1kb', function (done) {
108 | // Build an object that is larger than 1kb
109 | var name = "";
110 | for (var i = 0; i < 2048; i++) {
111 | name += "11111111111";
112 | }
113 |
114 | jsonrpc('/user/jsonrpc', 'greet', [name])
115 | .expect(413, done);
116 | });
117 | });
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /*global module:false*/
2 | module.exports = function(grunt) {
3 |
4 | // Project configuration.
5 | grunt.initConfig({
6 | // Metadata.
7 | pkg: grunt.file.readJSON('package.json'),
8 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
9 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
10 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
11 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
12 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n',
13 | // Task configuration.
14 | uglify: {
15 | options: {
16 | banner: '<%= banner %>'
17 | },
18 | dist: {
19 | files: {
20 | 'dist/strong-remoting.min.js': ['dist/strong-remoting.js']
21 | }
22 | }
23 | },
24 | jshint: {
25 | options: {
26 | jshintrc: true
27 | },
28 | gruntfile: {
29 | src: 'Gruntfile.js'
30 | },
31 | lib_test: {
32 | src: ['lib/**/*.js', 'test/**/*.js']
33 | }
34 | },
35 | watch: {
36 | gruntfile: {
37 | files: '<%= jshint.gruntfile.src %>',
38 | tasks: ['jshint:gruntfile']
39 | },
40 | lib_test: {
41 | files: '<%= jshint.lib_test.src %>',
42 | tasks: ['jshint:lib_test']
43 | }
44 | },
45 | browserify: {
46 | dist: {
47 | files: {
48 | 'dist/strong-remoting.js': ['index.js'],
49 | },
50 | options: {
51 | ignore: ['nodemailer', 'passport'],
52 | standalone: 'strong-remoting'
53 | }
54 | }
55 | },
56 | karma: {
57 | unit: {
58 | options: {
59 | // base path, that will be used to resolve files and exclude
60 | basePath: '',
61 |
62 | // frameworks to use
63 | frameworks: ['mocha', 'browserify'],
64 |
65 | // list of files / patterns to load in the browser
66 | files: [
67 | 'test/e2e/fixtures/*.js',
68 | 'test/e2e/smoke.test.js'
69 | ],
70 |
71 | // list of files to exclude
72 | exclude: [
73 |
74 | ],
75 |
76 | // test results reporter to use
77 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
78 | reporters: ['dots'],
79 |
80 | // web server port
81 | port: 9876,
82 |
83 | // cli runner port
84 | runnerPort: 9100,
85 |
86 | // enable / disable colors in the output (reporters and logs)
87 | colors: true,
88 |
89 | // level of logging
90 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
91 | logLevel: 'warn',
92 |
93 | // enable / disable watching file and executing tests whenever any file changes
94 | autoWatch: true,
95 |
96 | // Start these browsers, currently available:
97 | // - Chrome
98 | // - ChromeCanary
99 | // - Firefox
100 | // - Opera
101 | // - Safari (only Mac)
102 | // - PhantomJS
103 | // - IE (only Windows)
104 | browsers: [
105 | 'Chrome'
106 | ],
107 |
108 | // If browser does not capture in given timeout [ms], kill it
109 | captureTimeout: 60000,
110 |
111 | // Continuous Integration mode
112 | // if true, it capture browsers, run tests and exit
113 | singleRun: false,
114 |
115 | // Browserify config (all optional)
116 | browserify: {
117 | // extensions: ['.coffee'],
118 | ignore: [
119 | 'superagent',
120 | 'supertest'
121 | ],
122 | // transform: ['coffeeify'],
123 | // debug: true,
124 | // noParse: ['jquery'],
125 | watch: true,
126 | },
127 |
128 | // Add browserify to preprocessors
129 | preprocessors: {
130 | 'test/e2e/**': ['browserify'],
131 | 'lib/*.js': ['browserify']
132 | }
133 | }
134 | }
135 | }
136 |
137 | });
138 |
139 | grunt.registerTask('e2e-server', 'Run the e2e server', function() {
140 | require('test/e2e/e2e-server.js');
141 | });
142 |
143 |
144 | // These plugins provide necessary tasks.
145 | grunt.loadNpmTasks('grunt-browserify');
146 | grunt.loadNpmTasks('grunt-contrib-uglify');
147 | grunt.loadNpmTasks('grunt-contrib-jshint');
148 | grunt.loadNpmTasks('grunt-contrib-watch');
149 | grunt.loadNpmTasks('grunt-karma');
150 |
151 | // Default task.
152 | grunt.registerTask('default', ['browserify']);
153 |
154 | // browser / e2e testing...
155 | grunt.registerTask('e2e', ['e2e-server', 'karma']);
156 |
157 | };
158 |
--------------------------------------------------------------------------------
/example/rest-models/public/todos.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | body {
8 | font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
9 | line-height: 1.4em;
10 | background: #eeeeee;
11 | color: #333333;
12 | width: 520px;
13 | margin: 0 auto;
14 | -webkit-font-smoothing: antialiased;
15 | }
16 |
17 | #todoapp {
18 | background: #fff;
19 | padding: 20px;
20 | margin-bottom: 40px;
21 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
22 | -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
23 | -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
24 | -o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
25 | box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
26 | -webkit-border-radius: 0 0 5px 5px;
27 | -moz-border-radius: 0 0 5px 5px;
28 | -ms-border-radius: 0 0 5px 5px;
29 | -o-border-radius: 0 0 5px 5px;
30 | border-radius: 0 0 5px 5px;
31 | }
32 |
33 | #todoapp h1 {
34 | font-size: 36px;
35 | font-weight: bold;
36 | text-align: center;
37 | padding: 0 0 10px 0;
38 | }
39 |
40 | #todoapp input[type="text"] {
41 | width: 466px;
42 | font-size: 24px;
43 | font-family: inherit;
44 | line-height: 1.4em;
45 | border: 0;
46 | outline: none;
47 | padding: 6px;
48 | border: 1px solid #999999;
49 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
50 | -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
51 | -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
52 | -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
53 | box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
54 | }
55 |
56 | #todoapp input::-webkit-input-placeholder {
57 | font-style: italic;
58 | }
59 |
60 | #main {
61 | display: none;
62 | }
63 |
64 | #todo-list {
65 | margin: 10px 0;
66 | padding: 0;
67 | list-style: none;
68 | }
69 |
70 | #todo-list li {
71 | padding: 18px 20px 18px 0;
72 | position: relative;
73 | font-size: 24px;
74 | border-bottom: 1px solid #cccccc;
75 | }
76 |
77 | #todo-list li:last-child {
78 | border-bottom: none;
79 | }
80 |
81 | #todo-list li.done label {
82 | color: #777777;
83 | text-decoration: line-through;
84 | }
85 |
86 | #todo-list .destroy {
87 | position: absolute;
88 | right: 5px;
89 | top: 20px;
90 | display: none;
91 | cursor: pointer;
92 | width: 20px;
93 | height: 20px;
94 | background: url(destroy.png) no-repeat;
95 | }
96 |
97 | #todo-list li:hover .destroy {
98 | display: block;
99 | }
100 |
101 | #todo-list .destroy:hover {
102 | background-position: 0 -20px;
103 | }
104 |
105 | #todo-list li.editing {
106 | border-bottom: none;
107 | margin-top: -1px;
108 | padding: 0;
109 | }
110 |
111 | #todo-list li.editing:last-child {
112 | margin-bottom: -1px;
113 | }
114 |
115 | #todo-list li.editing .edit {
116 | display: block;
117 | width: 444px;
118 | padding: 13px 15px 14px 20px;
119 | margin: 0;
120 | }
121 |
122 | #todo-list li.editing .view {
123 | display: none;
124 | }
125 |
126 | #todo-list li .view label {
127 | word-break: break-word;
128 | }
129 |
130 | #todo-list li .edit {
131 | display: none;
132 | }
133 |
134 | #todoapp footer {
135 | display: none;
136 | margin: 0 -20px -20px -20px;
137 | overflow: hidden;
138 | color: #555555;
139 | background: #f4fce8;
140 | border-top: 1px solid #ededed;
141 | padding: 0 20px;
142 | line-height: 37px;
143 | -webkit-border-radius: 0 0 5px 5px;
144 | -moz-border-radius: 0 0 5px 5px;
145 | -ms-border-radius: 0 0 5px 5px;
146 | -o-border-radius: 0 0 5px 5px;
147 | border-radius: 0 0 5px 5px;
148 | }
149 |
150 | #clear-completed {
151 | float: right;
152 | line-height: 20px;
153 | text-decoration: none;
154 | background: rgba(0, 0, 0, 0.1);
155 | color: #555555;
156 | font-size: 11px;
157 | margin-top: 8px;
158 | margin-bottom: 8px;
159 | padding: 0 10px 1px;
160 | cursor: pointer;
161 | -webkit-border-radius: 12px;
162 | -moz-border-radius: 12px;
163 | -ms-border-radius: 12px;
164 | -o-border-radius: 12px;
165 | border-radius: 12px;
166 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
167 | -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
168 | -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
169 | -o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
170 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
171 | }
172 |
173 | #clear-completed:hover {
174 | background: rgba(0, 0, 0, 0.15);
175 | -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
176 | -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
177 | -ms-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
178 | -o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
179 | box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
180 | }
181 |
182 | #clear-completed:active {
183 | position: relative;
184 | top: 1px;
185 | }
186 |
187 | #todo-count span {
188 | font-weight: bold;
189 | }
190 |
191 | #instructions {
192 | margin: 10px auto;
193 | color: #777777;
194 | text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
195 | text-align: center;
196 | }
197 |
198 | #instructions a {
199 | color: #336699;
200 | }
201 |
202 | #credits {
203 | margin: 30px auto;
204 | color: #999;
205 | text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
206 | text-align: center;
207 | }
208 |
209 | #credits a {
210 | color: #888;
211 | }
212 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Contributing ###
2 |
3 | Thank you for your interest in `strong-remoting`, an open source project
4 | administered by StrongLoop.
5 |
6 | Contributing to `strong-remoting` is easy. In a few simple steps:
7 |
8 | * Ensure that your effort is aligned with the project's roadmap by
9 | talking to the maintainers, especially if you are going to spend a
10 | lot of time on it.
11 |
12 | * Make something better or fix a bug.
13 |
14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and
15 | [Google Javascript Style Guide][].
16 |
17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/strong-remoting)
18 |
19 | * Submit a pull request through Github.
20 |
21 |
22 | ### Contributor License Agreement ###
23 |
24 | ```
25 | Individual Contributor License Agreement
26 |
27 | By signing this Individual Contributor License Agreement
28 | ("Agreement"), and making a Contribution (as defined below) to
29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and
30 | agree to the following terms and conditions for Your present and
31 | future Contributions submitted to StrongLoop. Except for the license
32 | granted in this Agreement to StrongLoop and recipients of software
33 | distributed by StrongLoop, You reserve all right, title, and interest
34 | in and to Your Contributions.
35 |
36 | 1. Definitions
37 |
38 | "You" or "Your" shall mean the copyright owner or the individual
39 | authorized by the copyright owner that is entering into this
40 | Agreement with StrongLoop.
41 |
42 | "Contribution" shall mean any original work of authorship,
43 | including any modifications or additions to an existing work, that
44 | is intentionally submitted by You to StrongLoop for inclusion in,
45 | or documentation of, any of the products owned or managed by
46 | StrongLoop ("Work"). For purposes of this definition, "submitted"
47 | means any form of electronic, verbal, or written communication
48 | sent to StrongLoop or its representatives, including but not
49 | limited to communication or electronic mailing lists, source code
50 | control systems, and issue tracking systems that are managed by,
51 | or on behalf of, StrongLoop for the purpose of discussing and
52 | improving the Work, but excluding communication that is
53 | conspicuously marked or otherwise designated in writing by You as
54 | "Not a Contribution."
55 |
56 | 2. You Grant a Copyright License to StrongLoop
57 |
58 | Subject to the terms and conditions of this Agreement, You hereby
59 | grant to StrongLoop and recipients of software distributed by
60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge,
61 | royalty-free, irrevocable copyright license to reproduce, prepare
62 | derivative works of, publicly display, publicly perform,
63 | sublicense, and distribute Your Contributions and such derivative
64 | works under any license and without any restrictions.
65 |
66 | 3. You Grant a Patent License to StrongLoop
67 |
68 | Subject to the terms and conditions of this Agreement, You hereby
69 | grant to StrongLoop and to recipients of software distributed by
70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge,
71 | royalty-free, irrevocable (except as stated in this Section)
72 | patent license to make, have made, use, offer to sell, sell,
73 | import, and otherwise transfer the Work under any license and
74 | without any restrictions. The patent license You grant to
75 | StrongLoop under this Section applies only to those patent claims
76 | licensable by You that are necessarily infringed by Your
77 | Contributions(s) alone or by combination of Your Contributions(s)
78 | with the Work to which such Contribution(s) was submitted. If any
79 | entity institutes a patent litigation against You or any other
80 | entity (including a cross-claim or counterclaim in a lawsuit)
81 | alleging that Your Contribution, or the Work to which You have
82 | contributed, constitutes direct or contributory patent
83 | infringement, any patent licenses granted to that entity under
84 | this Agreement for that Contribution or Work shall terminate as
85 | of the date such litigation is filed.
86 |
87 | 4. You Have the Right to Grant Licenses to StrongLoop
88 |
89 | You represent that You are legally entitled to grant the licenses
90 | in this Agreement.
91 |
92 | If Your employer(s) has rights to intellectual property that You
93 | create, You represent that You have received permission to make
94 | the Contributions on behalf of that employer, that Your employer
95 | has waived such rights for Your Contributions, or that Your
96 | employer has executed a separate Corporate Contributor License
97 | Agreement with StrongLoop.
98 |
99 | 5. The Contributions Are Your Original Work
100 |
101 | You represent that each of Your Contributions are Your original
102 | works of authorship (see Section 8 (Submissions on Behalf of
103 | Others) for submission on behalf of others). You represent that to
104 | Your knowledge, no other person claims, or has the right to claim,
105 | any right in any intellectual property right related to Your
106 | Contributions.
107 |
108 | You also represent that You are not legally obligated, whether by
109 | entering into an agreement or otherwise, in any way that conflicts
110 | with the terms of this Agreement.
111 |
112 | You represent that Your Contribution submissions include complete
113 | details of any third-party license or other restriction (including,
114 | but not limited to, related patents and trademarks) of which You
115 | are personally aware and which are associated with any part of
116 | Your Contributions.
117 |
118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions
119 |
120 | You are not expected to provide support for Your Contributions,
121 | except to the extent You desire to provide support. You may provide
122 | support for free, for a fee, or not at all.
123 |
124 | 6. No Warranties or Conditions
125 |
126 | StrongLoop acknowledges that unless required by applicable law or
127 | agreed to in writing, You provide Your Contributions on an "AS IS"
128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES
130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR
131 | FITNESS FOR A PARTICULAR PURPOSE.
132 |
133 | 7. Submission on Behalf of Others
134 |
135 | If You wish to submit work that is not Your original creation, You
136 | may submit it to StrongLoop separately from any Contribution,
137 | identifying the complete details of its source and of any license
138 | or other restriction (including, but not limited to, related
139 | patents, trademarks, and license agreements) of which You are
140 | personally aware, and conspicuously marking the work as
141 | "Submitted on Behalf of a Third-Party: [named here]".
142 |
143 | 8. Agree to Notify of Change of Circumstances
144 |
145 | You agree to notify StrongLoop of any facts or circumstances of
146 | which You become aware that would make these representations
147 | inaccurate in any respect. Email us at callback@strongloop.com.
148 | ```
149 |
150 | [Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml
151 | [Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
152 |
--------------------------------------------------------------------------------
/lib/jsonrpc-adapter.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Expose `JsonRpcAdapter`.
3 | */
4 |
5 | module.exports = JsonRpcAdapter;
6 |
7 | /*!
8 | * Module dependencies.
9 | */
10 |
11 | var EventEmitter = require('events').EventEmitter
12 | , debug = require('debug')('strong-remoting:jsonrpc-adapter')
13 | , util = require('util')
14 | , inherits = util.inherits
15 | , jayson = require('jayson')
16 | , express = require('express')
17 | , bodyParser = require('body-parser')
18 | , cors = require('cors')
19 | , HttpContext = require('./http-context');
20 |
21 | var json = bodyParser.json;
22 | var urlencoded = bodyParser.urlencoded;
23 |
24 | /**
25 | * Create a new `JsonRpcAdapter` with the given `options`.
26 | *
27 | * @param {Object} options
28 | * @return {JsonRpcAdapter}
29 | */
30 |
31 | function JsonRpcAdapter(remotes) {
32 | EventEmitter.call(this);
33 |
34 | this.remotes = remotes;
35 | this.Context = HttpContext;
36 | }
37 |
38 | /**
39 | * Inherit from `EventEmitter`.
40 | */
41 |
42 | inherits(JsonRpcAdapter, EventEmitter);
43 |
44 | /*!
45 | * Simplified APIs
46 | */
47 |
48 | JsonRpcAdapter.create =
49 | JsonRpcAdapter.createJsonRpcAdapter = function (remotes) {
50 | // add simplified construction / sugar here
51 | return new JsonRpcAdapter(remotes);
52 | };
53 |
54 | /**
55 | * Get the path for the given method.
56 | */
57 |
58 | JsonRpcAdapter.prototype.getRoutes = function (obj) {
59 | // build default route
60 | var routes = [
61 | {
62 | verb: 'POST',
63 | path: obj.name ? ('/' + obj.name) : ''
64 | }
65 | ];
66 | return routes;
67 | };
68 |
69 | JsonRpcAdapter.errorHandler = function() {
70 | return function restErrorHandler(err, req, res, next) {
71 | if(typeof err === 'string') {
72 | err = new Error(err);
73 | err.status = err.statusCode = 500;
74 | }
75 |
76 | res.statusCode = err.statusCode || err.status || 500;
77 |
78 | debug('Error in %s %s: %s', req.method, req.url, err.stack);
79 | var data = {
80 | name: err.name,
81 | status: res.statusCode,
82 | message: err.message || 'An unknown error occurred'
83 | };
84 |
85 | for (var prop in err)
86 | data[prop] = err[prop];
87 |
88 | // TODO(bajtos) Remove stack info when running in production
89 | data.stack = err.stack;
90 |
91 | res.send({jsonrpc: "2.0", error: {code: -32000, message: "Server error", data: data},
92 | "id": null});
93 | };
94 | };
95 |
96 | JsonRpcAdapter.prototype.createHandler = function () {
97 |
98 | var root = express.Router();
99 | var classes = this.remotes.classes();
100 |
101 | // Add a handler to tolerate empty json as connect's json middleware throws an error
102 | root.use(function (req, res, next) {
103 | if (req.is('application/json')) {
104 | if (req.get('Content-Length') === '0') { // This doesn't cover the transfer-encoding: chunked
105 | req._body = true; // Mark it as parsed
106 | req.body = {};
107 | }
108 | }
109 | next();
110 | });
111 |
112 | // Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed
113 | debug("remoting options: %j", this.remotes.options);
114 | var jsonOptions = this.remotes.options.json || {strict: false};
115 | var corsOptions = this.remotes.options.cors || {origin: true, credentials: true};
116 |
117 | // Optimize the cors handler
118 | var corsHandler = function(req, res, next) {
119 | var reqUrl = req.protocol + '://' + req.get('host');
120 | if (req.method === 'OPTIONS' || reqUrl !== req.get('origin')) {
121 | cors(corsOptions)(req, res, next);
122 | } else {
123 | next();
124 | }
125 | };
126 |
127 | // Set up CORS first so that it's always enabled even when parsing errors
128 | // happen in urlencoded/json
129 | root.use(corsHandler);
130 | root.use(json(jsonOptions));
131 |
132 | root.use(JsonRpcAdapter.errorHandler());
133 |
134 | classes.forEach(function (sc) {
135 | var server = new jayson.server();
136 | root.post('/' + sc.name + '/jsonrpc', new jayson.server.interfaces.middleware(server, {}));
137 |
138 | var methods = sc.methods();
139 |
140 | methods.forEach(function (method) {
141 | // Wrap the method so that it will keep its own receiver - the shared class
142 | var fn = function () {
143 | var args = arguments;
144 | if (method.isStatic) {
145 | method.getFunction().apply(method.ctor, args);
146 | } else {
147 | method.sharedCtor.invoke(method, function (err, instance) {
148 | method.getFunction().apply(instance, args);
149 | });
150 | }
151 | };
152 |
153 | /*
154 | I had to this because jayson uses fn.toString to map the parameters,
155 | and since you wrap the method, the jsonrpc server cannot read them.
156 | I solved this overwriting the toString method on the wrapped function,
157 | creating this fake string that has the paramenters names in it.
158 | */
159 | if(method.accepts)
160 | {
161 | var argsNames = method.accepts.map(function(item)
162 | {
163 | return item.arg;
164 | });
165 |
166 | fn.toString = function()
167 | {
168 | return 'function (' + argsNames.concat('callback').join(',') + '){}';
169 | };
170 | }
171 |
172 | server.method(method.name, fn);
173 | });
174 |
175 | });
176 |
177 | return root;
178 | };
179 |
180 |
181 | JsonRpcAdapter.prototype.allRoutes = function () {
182 | var routes = [];
183 | var adapter = this;
184 | var classes = this.remotes.classes();
185 | var currentRoot = '';
186 |
187 | classes.forEach(function (sc) {
188 |
189 |
190 | adapter
191 | .getRoutes(sc)
192 | .forEach(function (classRoute) {
193 | currentRoot = classRoute.path;
194 | var methods = sc.methods();
195 |
196 | var functions = [];
197 | methods.forEach(function (method) {
198 | // Use functions to keep track of JS functions to dedupe
199 | if (functions.indexOf(method.fn) === -1) {
200 | functions.push(method.fn);
201 | } else {
202 | return; // Skip duplicate methods such as X.m1 = X.m2 = function() {...}
203 | }
204 | adapter.getRoutes(method).forEach(function (route) {
205 | if (method.isStatic) {
206 | addRoute(route.verb, route.path, method);
207 | } else {
208 | adapter
209 | .getRoutes(method.sharedCtor)
210 | .forEach(function (sharedCtorRoute) {
211 | addRoute(route.verb, sharedCtorRoute.path + route.path, method);
212 | });
213 | }
214 | });
215 | });
216 | });
217 | });
218 |
219 | return routes;
220 |
221 |
222 | function addRoute(verb, path, method) {
223 | if (path === '/' || path === '//') {
224 | path = currentRoot;
225 | } else {
226 | path = currentRoot + path;
227 | }
228 |
229 | if (path[path.length - 1] === '/') {
230 | path = path.substr(0, path.length - 1);
231 | }
232 |
233 | // TODO this could be cleaner
234 | path = path.replace(/\/\//g, '/');
235 |
236 | routes.push({
237 | verb: verb,
238 | path: path,
239 | description: method.description,
240 | notes: method.notes,
241 | method: method.stringName,
242 | accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined,
243 | returns: (method.returns && method.returns.length) ? method.returns : undefined,
244 | errors: (method.errors && method.errors.length) ? method.errors : undefined
245 | });
246 | }
247 | };
248 |
249 |
--------------------------------------------------------------------------------
/lib/http-invocation.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Expose `HttpInvocation`.
3 | */
4 |
5 | module.exports = HttpInvocation;
6 |
7 | /*!
8 | * Module dependencies.
9 | */
10 |
11 | var EventEmitter = require('events').EventEmitter
12 | , debug = require('debug')('strong-remoting:http-invocation')
13 | , util = require('util')
14 | , inherits = util.inherits
15 | , path = require('path')
16 | , assert = require('assert')
17 | , request = require('request')
18 | , Dynamic = require('./dynamic')
19 | , SUPPORTED_TYPES = ['json', 'application/javascript', 'text/javascript']
20 | , qs = require('qs');
21 |
22 |
23 | /*!
24 | * JSON Types
25 | */
26 |
27 | var JSON_TYPES = ['boolean', 'string', 'object', 'number'];
28 |
29 | /**
30 | * Create a new `HttpInvocation`.
31 | * @class
32 | * @param {SharedMethod} method
33 | * @param {Array} [args]
34 | * @param {String} base The base URL
35 | * @property {String} base The base URL
36 | * @property {SharedMethod} method The `SharedMethod` which will be invoked
37 | * @property {Array} args The arguments to be used when invoking the `SharedMethod`
38 | */
39 |
40 | function HttpInvocation(method, ctorArgs, args, base) {
41 | this.base = base;
42 | this.method = method;
43 | this.args = args || [];
44 | this.ctorArgs = ctorArgs || [];
45 | this.isStatic =
46 | (method.hasOwnProperty('isStatic') && method.isStatic) ||
47 | (method.hasOwnProperty('sharedMethod') && method.sharedMethod.isStatic);
48 | var namedArgs = this.namedArgs = {};
49 | var val;
50 | var type;
51 |
52 | if (!this.isStatic) {
53 | method.restClass.ctor.accepts.forEach(function(accept) {
54 | val = ctorArgs.shift();
55 | if(HttpInvocation.isAcceptable(val, accept)) {
56 | namedArgs[accept.arg || accept.name] = val;
57 | }
58 | });
59 | }
60 |
61 | method.accepts.forEach(function(accept) {
62 | val = args.shift();
63 | if(HttpInvocation.isAcceptable(val, accept)) {
64 | namedArgs[accept.arg || accept.name] = val;
65 | }
66 | });
67 | }
68 |
69 | /**
70 | * Inherit from `EventEmitter`.
71 | */
72 |
73 | inherits(HttpInvocation, EventEmitter);
74 |
75 | /**
76 | * Determine if the value matches the given accept definition.
77 | */
78 |
79 | HttpInvocation.isAcceptable = function(val, accept) {
80 | var acceptArray = Array.isArray(accept.type) || accept.type.toLowerCase() === 'array';
81 | var type = acceptArray ? 'array' : accept.type && accept.type.toLowerCase();
82 | var strict = type && type !== 'any';
83 |
84 | if(acceptArray) {
85 | return Array.isArray(val);
86 | }
87 |
88 | if(strict) {
89 | if(JSON_TYPES.indexOf(type) === -1) {
90 | return typeof val === 'object';
91 | }
92 | return (typeof val).toLowerCase() === type;
93 | } else {
94 | return true;
95 | }
96 | }
97 |
98 | HttpInvocation.prototype._processArg = function (req, verb, query, accept) {
99 | var httpFormat = accept.http;
100 | var name = accept.name || accept.arg;
101 | var val = this.getArgByName(name);
102 |
103 | if (httpFormat) {
104 | switch (typeof httpFormat) {
105 | case 'function':
106 | // ignore defined formatter
107 | break;
108 | case 'object':
109 | switch (httpFormat.source) {
110 | case 'body':
111 | req.body = val;
112 | break;
113 | case 'form':
114 | // From the form (body)
115 | req.body = req.body || {};
116 | req.body[name] = val;
117 | break;
118 | case 'query':
119 | // From the query string
120 | if (val !== undefined) {
121 | query = query || {};
122 | query[name] = val;
123 | }
124 | break;
125 | case 'header':
126 | if (val !== undefined) {
127 | req.headers = req.headers || {};
128 | req.headers[name] = val;
129 | }
130 | break;
131 | case 'path':
132 | // From the url path
133 | req.url = req.url.replace(':' + name, val);
134 | break;
135 | }
136 | break;
137 | }
138 | } else if (verb.toLowerCase() === 'get') {
139 | // default to query string for GET
140 | if (val !== undefined) {
141 | query = query || {};
142 | query[name] = val;
143 | }
144 | } else {
145 | // default to storing args on the body for !GET
146 | req.body = req.body || {};
147 | req.body[name] = val;
148 | }
149 |
150 | return query;
151 | };
152 |
153 | /**
154 | * Build args object from the http context's `req` and `res`.
155 | */
156 |
157 | HttpInvocation.prototype.createRequest = function () {
158 | var args = {};
159 | var method = this.method;
160 | var verb = method.getHttpMethod();
161 | var req = {json: true, method: verb || 'GET'};
162 | var accepts = method.accepts;
163 | var ctorAccepts = null;
164 | var returns = method.returns;
165 | var errors = method.errors;
166 | var query;
167 | var i;
168 |
169 | // initial url is the format
170 | req.url = this.base + method.getFullPath();
171 |
172 | // build request args and method options
173 | if (!this.isStatic) {
174 | ctorAccepts = method.restClass.ctor.accepts;
175 | for (i in ctorAccepts) {
176 | query = this._processArg(req, verb, query, ctorAccepts[i]);
177 | }
178 | }
179 |
180 | for (i in accepts) {
181 | query = this._processArg(req, verb, query, accepts[i]);
182 | }
183 |
184 | if(query) {
185 | req.url += '?' + qs.stringify(query);
186 | }
187 |
188 | return req;
189 | };
190 |
191 | /**
192 | * Get an arg value by name using the given options.
193 | *
194 | * @param {String} name
195 | */
196 |
197 | HttpInvocation.prototype.getArgByName = function (name) {
198 | return this.namedArgs[name];
199 | }
200 |
201 | /**
202 | * Start the invocation.
203 | */
204 |
205 | HttpInvocation.prototype.invoke = function (callback) {
206 | var req = this.createRequest();
207 | request(req, function(err, res, body) {
208 | if(err instanceof SyntaxError) {
209 | if(res.status === 204) err = null;
210 | }
211 | if(err) return callback(err);
212 | this.transformResponse(res, body, callback);
213 | }.bind(this));
214 | }
215 |
216 | /**
217 | * Transform the response into callback arguments
218 | * @param {HttpResponse} res
219 | * @param {Function} callback
220 | */
221 |
222 | HttpInvocation.prototype.transformResponse = function(res, body, callback) {
223 | var callbackArgs = [null]; // null => placeholder for err
224 | var method = this.method;
225 | var returns = method.returns;
226 | var errors = method.errors;
227 | var isObject = typeof body === 'object';
228 | var err;
229 | var hasError = res.statusCode >= 400;
230 | var errMsg;
231 | var ctx = {
232 | method: method,
233 | req: res.req,
234 | res: res
235 | };
236 |
237 | if(hasError) {
238 | if(isObject && body.error) {
239 | err = new Error(body.error.message);
240 | err.name = body.error.name;
241 | err.stack = body.error.stack;
242 | err.details = body.error.details;
243 | } else {
244 | err = new Error('Error: ' + res.statusCode);
245 | }
246 |
247 | return callback(err);
248 | }
249 |
250 | // build request args and method options
251 | returns.forEach(function (ret) {
252 | var httpFormat = ret.http;
253 | var name = ret.name || ret.arg;
254 | var val;
255 | var dynamic;
256 | var type = ret.type;
257 |
258 | if(ret.root) {
259 | val = res.body;
260 | } else {
261 | val = res.body[name];
262 | }
263 |
264 | if(Dynamic.canConvert(type)) {
265 | dynamic = new Dynamic(val, ctx);
266 | val = dynamic.to(type);
267 | }
268 |
269 | callbackArgs.push(val);
270 | }.bind(this));
271 |
272 | callback.apply(this, callbackArgs);
273 | }
274 |
--------------------------------------------------------------------------------
/lib/shared-class.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Expose `SharedClass`.
3 | */
4 |
5 | module.exports = SharedClass;
6 |
7 | /**
8 | * Module dependencies.
9 | */
10 |
11 | var debug = require('debug')('strong-remoting:shared-class')
12 | , util = require('util')
13 | , inherits = util.inherits
14 | , inflection = require('inflection')
15 | , SharedMethod = require('./shared-method')
16 | , assert = require('assert');
17 |
18 | /**
19 | * Create a new `SharedClass` with the given `options`.
20 | *
21 | * @class SharedClass
22 | * @param {String} name The `SharedClass` name
23 | * @param {Function} constructor The `constructor` the `SharedClass` represents
24 | * @param {Object} options Additional options.
25 | * @property {Function} ctor The `constructor`
26 | * @property {Object} http The HTTP settings
27 | * @return {SharedClass}
28 | */
29 |
30 | function SharedClass(name, ctor, options) {
31 | options = options || {};
32 | this.name = name || ctor.remoteNamespace;
33 | this.ctor = ctor;
34 | this._methods = [];
35 | this._resolvers = [];
36 | this._disabledMethods = {};
37 | var http = ctor && ctor.http;
38 | var normalize = options.normalizeHttpPath;
39 |
40 | var defaultHttp = {};
41 | defaultHttp.path = '/' + this.name;
42 |
43 | if(Array.isArray(http)) {
44 | // use array as is
45 | this.http = http;
46 | if(http.length === 0) {
47 | http.push(defaultHttp);
48 | }
49 | if (normalize) {
50 | this.http.forEach(function(h) {
51 | h.path = SharedClass.normalizeHttpPath(h.path);
52 | });
53 | }
54 | } else {
55 | // set http.path using the name unless it is defined
56 | // TODO(ritch) move http normalization from adapter.getRoutes() to a
57 | // better place... eg SharedMethod#getRoutes() or RestClass
58 | this.http = util._extend(defaultHttp, http);
59 | if (normalize) this.http.path = SharedClass.normalizeHttpPath(this.http.path);
60 | }
61 |
62 | if (typeof ctor === 'function' && ctor.sharedCtor) {
63 | // TODO(schoon) - Can we fall back to using the ctor as a method directly?
64 | // Without that, all remote methods have to be two levels deep, e.g.
65 | // `/meta/routes`.
66 |
67 | this.sharedCtor = new SharedMethod(ctor.sharedCtor, 'sharedCtor', this);
68 | }
69 | assert(this.name, 'must include a remoteNamespace when creating a SharedClass');
70 | }
71 |
72 | /**
73 | * Normalize http path.
74 | */
75 |
76 | SharedClass.normalizeHttpPath = function(path) {
77 | if (typeof path !== 'string') return;
78 | return path.replace(/[^\/]+/g, function(match) {
79 | if (match.indexOf(':') > -1) return match; // skip placeholders
80 | return inflection.transform(match, ['underscore', 'dasherize']);
81 | });
82 | }
83 |
84 | /**
85 | * Get all shared methods belonging to this shared class.
86 | *
87 | * @returns {SharedMethod[]} An array of shared methods
88 | */
89 |
90 | SharedClass.prototype.methods = function () {
91 | var ctor = this.ctor;
92 | var methods = [];
93 | var sc = this;
94 | var functionIndex = [];
95 |
96 | // static methods
97 | eachRemoteFunctionInObject(ctor, function (fn, name) {
98 | if(functionIndex.indexOf(fn) === -1) {
99 | functionIndex.push(fn);
100 | } else {
101 | sharedMethod = find(methods, fn);
102 | sharedMethod.addAlias(name);
103 | return;
104 | }
105 | methods.push(SharedMethod.fromFunction(fn, name, sc, true));
106 | });
107 |
108 | // instance methods
109 | eachRemoteFunctionInObject(ctor.prototype, function (fn, name) {
110 | if(functionIndex.indexOf(fn) === -1) {
111 | functionIndex.push(fn);
112 | } else {
113 | sharedMethod = find(methods, fn);
114 | sharedMethod.addAlias(name);
115 | return;
116 | }
117 | methods.push(SharedMethod.fromFunction(fn, name, sc));
118 | });
119 |
120 | // resolvers
121 | this._resolvers.forEach(function(resolver) {
122 | resolver.call(this, _define.bind(sc, methods));
123 | });
124 |
125 | methods = methods.concat(this._methods);
126 |
127 | return methods.filter(sc.isMethodEnabled.bind(sc));
128 | }
129 |
130 | SharedClass.prototype.isMethodEnabled = function(sharedMethod) {
131 | if(!sharedMethod.shared) return false;
132 |
133 | var key = this.getKeyFromMethodNameAndTarget(sharedMethod.name, sharedMethod.isStatic);
134 |
135 | if(this._disabledMethods.hasOwnProperty(key)) {
136 | return false;
137 | }
138 |
139 | return true;
140 | }
141 |
142 | /**
143 | * Define a shared method with the given name.
144 | *
145 | * @param {String} name The method name
146 | * @param {Object} [options] Set of options used to create a `SharedMethod`.
147 | * [See the full set of options](#sharedmethod-new-sharedmethodfn-name-sharedclass-options)
148 | */
149 |
150 | SharedClass.prototype.defineMethod = function(name, options, fn) {
151 | return _define.call(this, this._methods, name, options, fn);
152 | }
153 |
154 | function _define(methods, name, options, fn) {
155 | options = options || {};
156 | var sharedMethod = new SharedMethod(fn, name, this, options)
157 | methods.push(sharedMethod);
158 | return sharedMethod;
159 | }
160 |
161 | /**
162 | * Define a shared method resolver for dynamically defining methods.
163 | *
164 | * ```js
165 | * // below is a simple example
166 | * sharedClass.resolve(function(define) {
167 | * define('myMethod', {
168 | * accepts: {arg: 'str', type: 'string'},
169 | * returns: {arg: 'str', type: 'string'}
170 | * errors: [ { code: 404, message: 'Not Found', responseModel: 'Error' } ]
171 | * }, myMethod);
172 | * });
173 | * function myMethod(str, cb) {
174 | * cb(null, str);
175 | * }
176 | * ```
177 | *
178 | * @param {Function} resolver
179 | */
180 |
181 | SharedClass.prototype.resolve = function(resolver) {
182 | this._resolvers.push(resolver);
183 | }
184 |
185 | /**
186 | * Find a sharedMethod with the given name or function object.
187 | *
188 | * @param {String|Function} fn The function or method name
189 | * @param {Boolean} [isStatic] Required if `fn` is a `String`.
190 | * Only find a static method with the given name.
191 | * @returns {SharedMethod}
192 | */
193 |
194 | SharedClass.prototype.find = function(fn, isStatic) {
195 | var methods = this.methods();
196 | return find(methods, fn, isStatic);
197 | }
198 |
199 | /**
200 | * Disable a sharedMethod with the given name or function object.
201 | *
202 | * @param {String} fn The function or method name
203 | * @param {Boolean} isStatic Disable a static or prototype method
204 | */
205 |
206 | SharedClass.prototype.disableMethod = function(fn, isStatic) {
207 | var disableMethods = this._disabledMethods;
208 | var key = this.getKeyFromMethodNameAndTarget(fn, isStatic);
209 | disableMethods[key] = true;
210 | }
211 |
212 | /**
213 | * Get a key for the given method.
214 | *
215 | * @param {String} fn The function or method name
216 | * @param {Boolean} isStatic
217 | */
218 |
219 | SharedClass.prototype.getKeyFromMethodNameAndTarget = function(name, isStatic) {
220 | return (isStatic ? '' : 'prototype.') + name;
221 | }
222 |
223 | function find(methods, fn, isStatic) {
224 | for(var i = 0; i < methods.length; i++) {
225 | var method = methods[i];
226 | if(method.isDelegateFor(fn, isStatic)) return method;
227 | }
228 | return null;
229 | }
230 |
231 | function eachRemoteFunctionInObject(obj, f) {
232 | if(!obj) return;
233 |
234 | for(var key in obj) {
235 | if(key === 'super_') {
236 | // Skip super class
237 | continue;
238 | }
239 | var fn;
240 |
241 | try {
242 | fn = obj[key];
243 | } catch(e) {
244 | }
245 |
246 | // HACK: [rfeng] Do not expose model constructors
247 | // We have the following usage to set other model classes as properties
248 | // User.email = Email;
249 | // User.accessToken = AccessToken;
250 | // Both Email and AccessToken can have shared flag set to true
251 | if(typeof fn === 'function' && fn.shared && !fn.modelName) {
252 | f(fn, key);
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/test/shared-class.test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var extend = require('util')._extend;
3 | var expect = require('chai').expect;
4 | var SharedClass = require('../lib/shared-class');
5 | var factory = require('./helpers/shared-objects-factory.js');
6 | var RemoteObjects = require('../');
7 |
8 | describe('SharedClass', function() {
9 | var SomeClass;
10 | beforeEach(function() { SomeClass = factory.createSharedClass(); });
11 |
12 | describe('constructor', function() {
13 | it('fills http.path from ctor.http', function() {
14 | SomeClass.http = { path: '/foo' };
15 | var sc = new SharedClass('some', SomeClass);
16 | expect(sc.http.path).to.equal('/foo');
17 | });
18 |
19 | it('fills http.path using the name', function() {
20 | var sc = new SharedClass('some', SomeClass);
21 | expect(sc.http.path).to.equal('/some');
22 | });
23 |
24 | it('fills http.path using a normalized path', function() {
25 | var sc = new SharedClass('SomeClass', SomeClass, { normalizeHttpPath: true });
26 | expect(sc.http.path).to.equal('/some-class');
27 | });
28 |
29 | it('does not require a sharedConstructor', function() {
30 | var myClass = {};
31 | myClass.remoteNamespace = 'bar';
32 | myClass.foo = function() {};
33 | myClass.foo.shared = true;
34 |
35 | var sc = new SharedClass(undefined, myClass);
36 | var fns = sc.methods().map(function(m) {return m.name});
37 | expect(fns).to.contain('foo');
38 | expect(sc.http).to.eql({ path: '/bar' });
39 | });
40 | });
41 |
42 | describe('sharedClass.methods()', function() {
43 | it('discovers remote methods', function() {
44 | var sc = new SharedClass('some', SomeClass);
45 | SomeClass.staticMethod = function() {};
46 | SomeClass.staticMethod.shared = true;
47 | SomeClass.prototype.instanceMethod = function() {};
48 | SomeClass.prototype.instanceMethod.shared = true;
49 | var fns = sc.methods().map(function(m) {return m.fn});
50 | expect(fns).to.contain(SomeClass.staticMethod);
51 | expect(fns).to.contain(SomeClass.prototype.instanceMethod);
52 | });
53 | it('only discovers a function once with aliases', function() {
54 | function MyClass() {};
55 | var sc = new SharedClass('some', MyClass);
56 | var fn = function() {};
57 | fn.shared = true;
58 | MyClass.a = fn;
59 | MyClass.b = fn;
60 | MyClass.prototype.a = fn;
61 | MyClass.prototype.b = fn;
62 | var methods = sc.methods();
63 | var fns = methods.map(function(m) {return m.fn});
64 | expect(fns.length).to.equal(1);
65 | expect(methods[0].aliases.sort()).to.eql(['a', 'b']);
66 | });
67 | it('discovers multiple functions correctly', function() {
68 | function MyClass() {};
69 | var sc = new SharedClass('some', MyClass);
70 | MyClass.a = createSharedFn();
71 | MyClass.b = createSharedFn();
72 | MyClass.prototype.a = createSharedFn();
73 | MyClass.prototype.b = createSharedFn();
74 | var fns = sc.methods().map(function(m) {return m.fn});
75 | expect(fns.length).to.equal(4);
76 | expect(fns).to.contain(MyClass.a);
77 | expect(fns).to.contain(MyClass.b);
78 | expect(fns).to.contain(MyClass.prototype.a);
79 | expect(fns).to.contain(MyClass.prototype.b);
80 | function createSharedFn() {
81 | var fn = function() {};
82 | fn.shared = true;
83 | return fn;
84 | }
85 | });
86 | it('should skip properties that are model classes', function() {
87 | var sc = new SharedClass('some', SomeClass);
88 | function MockModel1() {};
89 | MockModel1.modelName = 'M1';
90 | MockModel1.shared = true;
91 | SomeClass.staticMethod = MockModel1;
92 |
93 | function MockModel2() {};
94 | MockModel2.modelName = 'M2';
95 | MockModel2.shared = true;
96 | SomeClass.prototype.instanceMethod = MockModel2;
97 | var fns = sc.methods().map(function(m) {return m.fn});
98 | expect(fns).to.not.contain(SomeClass.staticMethod);
99 | expect(fns).to.not.contain(SomeClass.prototype.instanceMethod);
100 | });
101 | });
102 |
103 | describe('sharedClass.defineMethod(name, options)', function() {
104 | it('defines a remote method', function () {
105 | var sc = new SharedClass('SomeClass', SomeClass);
106 | SomeClass.prototype.myMethod = function() {};
107 | var METHOD_NAME = 'myMethod';
108 | sc.defineMethod(METHOD_NAME, {
109 | prototype: true
110 | });
111 | var methods = sc.methods().map(function(m) {return m.name});
112 | expect(methods).to.contain(METHOD_NAME);
113 | });
114 | it('should allow a shared class to resolve dynamically defined functions',
115 | function (done) {
116 | var MyClass = function() {};
117 | var METHOD_NAME = 'dynFn';
118 | process.nextTick(function() {
119 | MyClass[METHOD_NAME] = function(str, cb) {
120 | cb(null, str);
121 | }
122 | done();
123 | });
124 |
125 | var sharedClass = new SharedClass('MyClass', MyClass);
126 |
127 | sharedClass.defineMethod(METHOD_NAME, {});
128 | var methods = sharedClass.methods().map(function(m) {return m.name});
129 | expect(methods).to.contain(METHOD_NAME);
130 | }
131 | );
132 | });
133 |
134 | describe('sharedClass.resolve(resolver)', function () {
135 | it('should allow sharedMethods to be resolved dynamically', function () {
136 | function MyClass() {};
137 | MyClass.obj = {
138 | dyn: function(cb) {
139 | cb();
140 | }
141 | };
142 | var sharedClass = new SharedClass('MyClass', MyClass);
143 | sharedClass.resolve(function(define) {
144 | define('dyn', {}, MyClass.obj.dyn);
145 | });
146 | var methods = sharedClass.methods().map(function(m) {return m.name});
147 | expect(methods).to.contain('dyn');
148 | });
149 | });
150 |
151 | describe('sharedClass.find()', function () {
152 | var sc;
153 | var sm;
154 | beforeEach(function() {
155 | sc = new SharedClass('SomeClass', SomeClass);
156 | SomeClass.prototype.myMethod = function() {};
157 | var METHOD_NAME = 'myMethod';
158 | sm = sc.defineMethod(METHOD_NAME, {
159 | prototype: true
160 | });
161 | });
162 | it('finds sharedMethod for the given function', function () {
163 | assert(sc.find(SomeClass.prototype.myMethod) === sm);
164 | });
165 | it('find sharedMethod by name', function () {
166 | assert(sc.find('myMethod') === sm);
167 | });
168 | });
169 |
170 |
171 | describe('remotes.addClass(sharedClass)', function() {
172 | it('should make the class available', function () {
173 | var CLASS_NAME = 'SomeClass';
174 | var remotes = RemoteObjects.create();
175 | var sharedClass = new SharedClass(CLASS_NAME, SomeClass);
176 | remotes.addClass(sharedClass);
177 | var classes = remotes.classes().map(function(c) {return c.name});
178 | expect(classes).to.contain(CLASS_NAME);
179 | });
180 | });
181 |
182 | describe('sharedClass.disableMethod(methodName, isStatic)', function () {
183 | var sc;
184 | var sm;
185 | var METHOD_NAME = 'testMethod';
186 | var INST_METHOD_NAME = 'instTestMethod';
187 | var DYN_METHOD_NAME = 'dynMethod';
188 |
189 | beforeEach(function() {
190 | sc = new SharedClass('SomeClass', SomeClass);
191 | sm = sc.defineMethod(METHOD_NAME, {isStatic: true});
192 | sm = sc.defineMethod(INST_METHOD_NAME, {isStatic: false});
193 | sc.resolve(function(define) {
194 | define(DYN_METHOD_NAME, {isStatic: true});
195 | });
196 | });
197 |
198 | it('excludes disabled static methods from the method list', function () {
199 | sc.disableMethod(METHOD_NAME, true);
200 | var methods = sc.methods().map(function(m) {return m.name});
201 | expect(methods).to.not.contain(METHOD_NAME);
202 | });
203 |
204 | it('excludes disabled prototype methods from the method list', function () {
205 | sc.disableMethod(INST_METHOD_NAME, false);
206 | var methods = sc.methods().map(function(m) {return m.name});
207 | expect(methods).to.not.contain(INST_METHOD_NAME);
208 | });
209 |
210 | it('excludes disabled dynamic (resolved) methods from the method list', function () {
211 | sc.disableMethod(DYN_METHOD_NAME, true);
212 | var methods = sc.methods().map(function(m) {return m.name});
213 | expect(methods).to.not.contain(DYN_METHOD_NAME);
214 | })
215 | ; });
216 | });
217 |
--------------------------------------------------------------------------------
/lib/http-context.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Expose `HttpContext`.
3 | */
4 |
5 | module.exports = HttpContext;
6 |
7 | /*!
8 | * Module dependencies.
9 | */
10 |
11 | var EventEmitter = require('events').EventEmitter
12 | , debug = require('debug')('strong-remoting:http-context')
13 | , util = require('util')
14 | , inherits = util.inherits
15 | , assert = require('assert')
16 | , Dynamic = require('./dynamic')
17 | , js2xmlparser = require('js2xmlparser')
18 | , DEFAULT_SUPPORTED_TYPES = [
19 | 'application/json', 'application/javascript', 'application/xml',
20 | 'text/javascript', 'text/xml',
21 | 'json', 'xml',
22 | '*/*'
23 | ];
24 |
25 | /**
26 | * Create a new `HttpContext` with the given `options`.
27 | *
28 | * @param {Object} options
29 | * @return {HttpContext}
30 | * @class
31 | */
32 |
33 | function HttpContext(req, res, method, options) {
34 | this.req = req;
35 | this.res = res;
36 | this.method = method;
37 | this.args = this.buildArgs(method);
38 | this.methodString = method.stringName;
39 | this.options = options || {};
40 | this.supportedTypes = this.options.supportedTypes || DEFAULT_SUPPORTED_TYPES;
41 |
42 | if (this.supportedTypes === DEFAULT_SUPPORTED_TYPES && !this.options.xml) {
43 | // Disable all XML-based types by default
44 | this.supportedTypes = this.supportedTypes.filter(function(type) {
45 | return !/\bxml\b/i.test(type);
46 | });
47 | }
48 | }
49 |
50 | /**
51 | * Inherit from `EventEmitter`.
52 | */
53 |
54 | inherits(HttpContext, EventEmitter);
55 |
56 | /**
57 | * Build args object from the http context's `req` and `res`.
58 | */
59 |
60 | HttpContext.prototype.buildArgs = function (method) {
61 | var args = {};
62 | var ctx = this;
63 | var accepts = method.accepts;
64 | var returns = method.returns;
65 | var errors = method.errors;
66 |
67 | // build arguments from req and method options
68 | accepts.forEach(function (o) {
69 | var httpFormat = o.http;
70 | var name = o.name || o.arg;
71 | var val;
72 |
73 | if(httpFormat) {
74 | switch(typeof httpFormat) {
75 | case 'function':
76 | // the options have defined a formatter
77 | val = httpFormat(this);
78 | break;
79 | case 'object':
80 | switch(httpFormat.source) {
81 | case 'body':
82 | val = this.req.body;
83 | break;
84 | case 'form':
85 | // From the form (body)
86 | val = this.req.body && this.req.body[name];
87 | break;
88 | case 'query':
89 | // From the query string
90 | val = this.req.query[name];
91 | break;
92 | case 'path':
93 | // From the url path
94 | val = this.req.params[name];
95 | break;
96 | case 'header':
97 | val = this.req.get(name);
98 | break;
99 | case 'req':
100 | // Direct access to http req
101 | val = this.req;
102 | break;
103 | case 'res':
104 | // Direct access to http res
105 | val = this.res;
106 | break;
107 | case 'context':
108 | // Direct access to http context
109 | val = this;
110 | break;
111 | }
112 | break;
113 | }
114 | } else {
115 | val = this.getArgByName(name, o);
116 | }
117 |
118 | // cast booleans and numbers
119 | var dynamic;
120 | var otype = (typeof o.type === 'string') && o.type.toLowerCase();
121 |
122 | if(Dynamic.canConvert(otype)) {
123 | dynamic = new Dynamic(val, ctx);
124 | val = dynamic.to(otype);
125 | }
126 |
127 | // set the argument value
128 | args[o.arg] = val;
129 | }.bind(this));
130 |
131 | return args;
132 | }
133 |
134 | /**
135 | * Get an arg by name using the given options.
136 | *
137 | * @param {String} name
138 | * @param {Object} options **optional**
139 | */
140 |
141 | HttpContext.prototype.getArgByName = function (name, options) {
142 | var req = this.req;
143 | var args = req.param('args');
144 |
145 | if(args) {
146 | args = JSON.parse(args);
147 | }
148 |
149 | if(typeof args !== 'object' || !args) {
150 | args = {};
151 | }
152 |
153 | var arg = (args && args[name] !== undefined) ? args[name] :
154 | this.req.param(name) !== undefined ? this.req.param(name) :
155 | this.req.get(name);
156 | // search these in order by name
157 | // req.params
158 | // req.body
159 | // req.query
160 | // req.header
161 |
162 |
163 | // coerce simple types in objects
164 | if(typeof arg === 'object') {
165 | arg = coerceAll(arg);
166 | }
167 |
168 | return arg;
169 | }
170 |
171 | /*!
172 | * Integer test regexp.
173 | */
174 |
175 | var isint = /^[0-9]+$/;
176 |
177 | /*!
178 | * Float test regexp.
179 | */
180 |
181 | var isfloat = /^([0-9]+)?\.[0-9]+$/;
182 |
183 | function coerce(str) {
184 | if(typeof str != 'string') return str;
185 | if ('null' == str) return null;
186 | if ('true' == str) return true;
187 | if ('false' == str) return false;
188 | if (isfloat.test(str)) return parseFloat(str, 10);
189 | if (isint.test(str)) return parseInt(str, 10);
190 | return str;
191 | }
192 |
193 | // coerce every string in the given object / array
194 | function coerceAll(obj) {
195 | var type = Array.isArray(obj) ? 'array' : typeof obj;
196 |
197 | switch(type) {
198 | case 'string':
199 | return coerce(obj);
200 | break;
201 | case 'object':
202 | if(obj) {
203 | Object.keys(obj).forEach(function (key) {
204 | obj[key] = coerceAll(obj[key]);
205 | });
206 | }
207 | break;
208 | case 'array':
209 | obj.map(function (o) {
210 | return coerceAll(o);
211 | });
212 | break;
213 | }
214 |
215 | return obj;
216 | }
217 |
218 | /**
219 | * Invoke the given shared method using the provided scope against the current context.
220 | */
221 |
222 | HttpContext.prototype.invoke = function (scope, method, fn, isCtor) {
223 | var args = this.args;
224 | if(isCtor) {
225 | try {
226 | args = this.buildArgs(method);
227 | } catch(err) {
228 | // JSON.parse() might throw
229 | return fn(err);
230 | }
231 | }
232 | var http = method.http;
233 | var pipe = http && http.pipe;
234 | var pipeDest = pipe && pipe.dest;
235 | var pipeSrc = pipe && pipe.source;
236 |
237 | if(pipeDest) {
238 | // only support response for now
239 | switch(pipeDest) {
240 | case 'res':
241 | // Probably not correct...but passes my test.
242 | this.res.header('Content-Type', 'application/json');
243 | this.res.header('Transfer-Encoding', 'chunked');
244 |
245 | var stream = method.invoke(scope, args, fn);
246 | stream.pipe(this.res);
247 | break;
248 | default:
249 | fn(new Error('unsupported pipe destination'));
250 | break;
251 | }
252 | } else if(pipeSrc) {
253 | // only support request for now
254 | switch(pipeDest) {
255 | case 'req':
256 | this.req.pipe(method.invoke(scope, args, fn));
257 | break;
258 | default:
259 | fn(new Error('unsupported pipe source'));
260 | break;
261 | }
262 | } else {
263 | // simple invoke
264 | method.invoke(scope, args, fn);
265 | }
266 | }
267 |
268 | function toJSON(input) {
269 | if (!input) {
270 | return input;
271 | }
272 | if (typeof input.toJSON === 'function') {
273 | return input.toJSON();
274 | } else if (Array.isArray(input)) {
275 | return input.map(toJSON);
276 | } else {
277 | return input;
278 | }
279 | }
280 |
281 | function toXML(input) {
282 | var xml;
283 | if (input && typeof input.toXML === 'function') {
284 | xml = input.toXML();
285 | } else {
286 | if (input) {
287 | // Trigger toJSON() conversions
288 | input = toJSON(input);
289 | }
290 | if (Array.isArray(input)) {
291 | input = { result: input };
292 | }
293 | xml = js2xmlparser('response', input, {
294 | prettyPrinting: {
295 | indentString: ' '
296 | },
297 | convertMap: {
298 | '[object Date]': function(date) {
299 | return date.toISOString();
300 | }
301 | }
302 | });
303 | }
304 | return xml;
305 | }
306 | /**
307 | * Finish the request and send the correct response.
308 | */
309 |
310 | HttpContext.prototype.done = function () {
311 | // send the result back as
312 | // the requested content type
313 | var data = this.result;
314 | var res = this.res;
315 | var accepts = this.req.accepts(this.supportedTypes);
316 |
317 | if (this.req.query._format) {
318 | accepts = this.req.query._format.toLowerCase();
319 | }
320 | var dataExists = typeof data !== 'undefined';
321 |
322 | if(dataExists) {
323 | switch(accepts) {
324 | case '*/*':
325 | case 'application/json':
326 | case 'json':
327 | res.json(data);
328 | break;
329 | case 'application/javascript':
330 | case 'text/javascript':
331 | res.jsonp(data);
332 | break;
333 | case 'application/xml':
334 | case 'text/xml':
335 | case 'xml':
336 | if (accepts === 'application/xml') {
337 | res.header('Content-Type', 'application/xml');
338 | } else {
339 | res.header('Content-Type', 'text/xml');
340 | }
341 | if (data === null) {
342 | res.header('Content-Length', '7');
343 | res.end('');
344 | } else {
345 | try {
346 | var xml = toXML(data);
347 | res.send(xml);
348 | } catch(e) {
349 | res.send(500, e + '\n' + data);
350 | }
351 | }
352 | break;
353 | default:
354 | // not acceptable
355 | res.send(406);
356 | break;
357 | }
358 | } else {
359 | res.get('Content-Type') || res.header('Content-Type', 'application/json');
360 | res.statusCode = 204;
361 | res.end();
362 | }
363 | }
364 |
--------------------------------------------------------------------------------
/example/rest-models/public/todos.js:
--------------------------------------------------------------------------------
1 | // An example Backbone application contributed by
2 | // [Jérôme Gravel-Niquet](http://jgn.me/). This demo uses a simple
3 | // [LocalStorage adapter](backbone-localstorage.html)
4 | // to persist Backbone models within your browser.
5 |
6 | // Load the application once the DOM is ready, using `jQuery.ready`:
7 | $(function(){
8 |
9 | // Remote REST Contract
10 | var contract = {
11 | routes: {
12 | 'Todo.all': {verb: 'get', path: '/t'},
13 | 'Todo.prototype.save': {verb: 'post', path: '/t/:id'},
14 | 'Todo.prototype.fetch': {verb: 'get', path: '/t/:id'}
15 | }
16 | };
17 |
18 | // Remote Objects
19 | var remoteObjects = RemoteObjects.connect('http://localhost:3000', contract);
20 |
21 | // Todo Model
22 | // ----------
23 |
24 | // Our basic **Todo** model has `title`, `order`, and `done` attributes.
25 | var Todo = Backbone.Model.extend({
26 |
27 | // Default attributes for the todo item.
28 | defaults: function() {
29 | return {
30 | title: "empty todo...",
31 | order: Todos.nextOrder(),
32 | done: false
33 | };
34 | },
35 |
36 | // Toggle the `done` state of this todo item.
37 | toggle: function() {
38 | this.save({done: !this.get("done")});
39 | },
40 |
41 | save: function (data) {
42 | this.set(data);
43 | var remoteObj = remoteObjects.construct('Todo', {data: this.toJSON()});
44 | remoteObj.invoke('save', function () {
45 | // saved
46 | });
47 | }
48 | });
49 |
50 | // Todo Collection
51 | // ---------------
52 |
53 | // The collection of todos is backed a remote server.
54 | var TodoList = Backbone.Collection.extend({
55 |
56 | // Reference to this collection's model.
57 | model: Todo,
58 |
59 | // Filter down the list of all todo items that are finished.
60 | done: function() {
61 | return this.where({done: true});
62 | },
63 |
64 | // Filter down the list to only todo items that are still not finished.
65 | remaining: function() {
66 | return this.without.apply(this, this.done());
67 | },
68 |
69 | // We keep the Todos in sequential order, despite being saved by unordered
70 | // GUID in the database. This generates the next order number for new items.
71 | nextOrder: function() {
72 | if (!this.length) return 1;
73 | return this.last().get('order') + 1;
74 | },
75 |
76 | // Todos are sorted by their original insertion order.
77 | comparator: 'order',
78 |
79 | fetch: function () {
80 | var self = this;
81 |
82 | remoteObjects.invoke('Todo.all', null, null, function (err, data) {
83 | self.reset(data);
84 | });
85 | }
86 | });
87 |
88 | // Create our global collection of **Todos**.
89 | var Todos = new TodoList;
90 |
91 | // Todo Item View
92 | // --------------
93 |
94 | // The DOM element for a todo item...
95 | var TodoView = Backbone.View.extend({
96 |
97 | //... is a list tag.
98 | tagName: "li",
99 |
100 | // Cache the template function for a single item.
101 | template: _.template($('#item-template').html()),
102 |
103 | // The DOM events specific to an item.
104 | events: {
105 | "click .toggle" : "toggleDone",
106 | "dblclick .view" : "edit",
107 | "click a.destroy" : "clear",
108 | "keypress .edit" : "updateOnEnter",
109 | "blur .edit" : "close"
110 | },
111 |
112 | // The TodoView listens for changes to its model, re-rendering. Since there's
113 | // a one-to-one correspondence between a **Todo** and a **TodoView** in this
114 | // app, we set a direct reference on the model for convenience.
115 | initialize: function() {
116 | this.listenTo(this.model, 'change', this.render);
117 | this.listenTo(this.model, 'destroy', this.remove);
118 | },
119 |
120 | // Re-render the titles of the todo item.
121 | render: function() {
122 | this.$el.html(this.template(this.model.toJSON()));
123 | this.$el.toggleClass('done', this.model.get('done'));
124 | this.input = this.$('.edit');
125 | return this;
126 | },
127 |
128 | // Toggle the `"done"` state of the model.
129 | toggleDone: function() {
130 | this.model.toggle();
131 | },
132 |
133 | // Switch this view into `"editing"` mode, displaying the input field.
134 | edit: function() {
135 | this.$el.addClass("editing");
136 | this.input.focus();
137 | },
138 |
139 | // Close the `"editing"` mode, saving changes to the todo.
140 | close: function() {
141 | var value = this.input.val();
142 | if (!value) {
143 | this.clear();
144 | } else {
145 | this.model.save({title: value});
146 | this.$el.removeClass("editing");
147 | }
148 | },
149 |
150 | // If you hit `enter`, we're through editing the item.
151 | updateOnEnter: function(e) {
152 | if (e.keyCode == 13) this.close();
153 | },
154 |
155 | // Remove the item, destroy the model.
156 | clear: function() {
157 | this.model.destroy();
158 | }
159 |
160 | });
161 |
162 | // The Application
163 | // ---------------
164 |
165 | // Our overall **AppView** is the top-level piece of UI.
166 | var AppView = Backbone.View.extend({
167 |
168 | // Instead of generating a new element, bind to the existing skeleton of
169 | // the App already present in the HTML.
170 | el: $("#todoapp"),
171 |
172 | // Our template for the line of statistics at the bottom of the app.
173 | statsTemplate: _.template($('#stats-template').html()),
174 |
175 | // Delegated events for creating new items, and clearing completed ones.
176 | events: {
177 | "keypress #new-todo": "createOnEnter",
178 | "click #clear-completed": "clearCompleted",
179 | "click #toggle-all": "toggleAllComplete"
180 | },
181 |
182 | // At initialization we bind to the relevant events on the `Todos`
183 | // collection, when items are added or changed. Kick things off by
184 | // loading any preexisting todos that might be saved in *localStorage*.
185 | initialize: function() {
186 |
187 | this.input = this.$("#new-todo");
188 | this.allCheckbox = this.$("#toggle-all")[0];
189 |
190 | this.listenTo(Todos, 'add', this.addOne);
191 | this.listenTo(Todos, 'reset', this.addAll);
192 | this.listenTo(Todos, 'all', this.render);
193 |
194 | this.footer = this.$('footer');
195 | this.main = $('#main');
196 |
197 | Todos.fetch();
198 | },
199 |
200 | // Re-rendering the App just means refreshing the statistics -- the rest
201 | // of the app doesn't change.
202 | render: function() {
203 | var done = Todos.done().length;
204 | var remaining = Todos.remaining().length;
205 |
206 | if (Todos.length) {
207 | this.main.show();
208 | this.footer.show();
209 | this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
210 | } else {
211 | this.main.hide();
212 | this.footer.hide();
213 | }
214 |
215 | this.allCheckbox.checked = !remaining;
216 | },
217 |
218 | // Add a single todo item to the list by creating a view for it, and
219 | // appending its element to the ``.
220 | addOne: function(todo) {
221 | var view = new TodoView({model: todo});
222 | this.$("#todo-list").append(view.render().el);
223 | },
224 |
225 | // Add all items in the **Todos** collection at once.
226 | addAll: function() {
227 | Todos.each(this.addOne, this);
228 | },
229 |
230 | // If you hit return in the main input field, create new **Todo** model
231 | createOnEnter: function(e) {
232 | if (e.keyCode != 13) return;
233 | if (!this.input.val()) return;
234 |
235 | Todos.create({title: this.input.val()});
236 | this.input.val('');
237 | },
238 |
239 | // Clear all done todo items, destroying their models.
240 | clearCompleted: function() {
241 | _.invoke(Todos.done(), 'destroy');
242 | return false;
243 | },
244 |
245 | toggleAllComplete: function () {
246 | var done = this.allCheckbox.checked;
247 | Todos.each(function (todo) { todo.save({'done': done}); });
248 | }
249 |
250 | });
251 |
252 | // Finally, we kick things off by creating the **App**.
253 | var App = new AppView;
254 |
255 | });
256 |
257 | function RemoteObjects(url, contract) {
258 | this.url = url;
259 | this.contract = contract;
260 | }
261 |
262 | RemoteObjects.prototype.construct = function (name) {
263 | return new RemoteObject(Array.prototype.slice.call(arguments, 0), this);
264 | }
265 |
266 | RemoteObjects.prototype.invoke = function (methodString, ctorArgs, args, fn) {
267 |
268 | if(typeof ctorArgs === 'function') {
269 | fn = ctorArgs;
270 | ctorArgs = args = undefined;
271 | }
272 |
273 | if(typeof args === 'function') {
274 | fn = args;
275 | args = undefined;
276 | }
277 |
278 |
279 | this.createRequest(methodString, ctorArgs, args, fn);
280 | }
281 |
282 | // REST ADAPTER IMPL.
283 |
284 |
285 |
286 | RemoteObjects.prototype.buildUrl = function (methodString, args) {
287 | var base = this.url
288 | var route = this.contract.routes[methodString];
289 | var path = route.path;
290 | var pathParts = path.split('/');
291 | var finalPathParts = [];
292 | var argString = args ? ('?args=' + encodeURI(JSON.stringify(args))) : '';
293 |
294 | for (var i = 0; i < pathParts.length; i++) {
295 | var part = pathParts[i];
296 | var isKey = part[0] === ':';
297 | var val = isKey && args && args[part.replace(':', '')];
298 |
299 | if(!isKey) {
300 | finalPathParts.push(part);
301 | } else if(val) {
302 | finalPathParts.push(val);
303 | }
304 | }
305 |
306 | // build url
307 | return base + finalPathParts.join('/') + argString;
308 | }
309 |
310 | RemoteObjects.prototype.createRequest = function (methodString, ctorArgs, args, fn) {
311 | var self = this;
312 | var route = this.contract.routes[methodString];
313 |
314 | $.ajax({
315 | type: route.verb,
316 | url: this.buildUrl(methodString, args),
317 | body: ctorArgs,
318 | success: function (data) {
319 | fn(null, data);
320 | },
321 | error: function (data) {
322 | fn(data);
323 | }
324 | });
325 | }
326 |
327 | // END REST ADAPTER IMPL.
328 |
329 | RemoteObjects.connect = function (url, contract) {
330 | return new RemoteObjects(url, contract);
331 | }
332 |
333 | function RemoteObject(args, remotes) {
334 | // remove name
335 | this.name = args.shift();
336 |
337 | // save args
338 | this.ctorArgs = args;
339 |
340 | // all remote objects
341 | this.remotes = remotes;
342 | }
343 |
344 | RemoteObject.prototype.invoke = function (method, args, fn) {
345 | if(typeof args === 'function') {
346 | fn = args;
347 | args = undefined;
348 | }
349 |
350 | this.remotes.invoke(this.name + '.prototype.' + method, this.ctorArgs, args, fn);
351 | }
--------------------------------------------------------------------------------
/test/rest.browser.test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var extend = require('util')._extend;
3 | var inherits = require('util').inherits;
4 | var RemoteObjects = require('../');
5 | var express = require('express');
6 | var request = require('supertest');
7 | var expect = require('chai').expect;
8 | var factory = require('./helpers/shared-objects-factory.js');
9 |
10 | describe('strong-remoting-rest', function(){
11 | var app;
12 | var server;
13 | var objects;
14 | var remotes;
15 | var adapterName = 'rest';
16 |
17 | before(function(done) {
18 | app = express();
19 | app.use(function (req, res, next) {
20 | // create the handler for each request
21 | objects.handler(adapterName).apply(objects, arguments);
22 | });
23 | server = app.listen(done);
24 | });
25 |
26 | // setup
27 | beforeEach(function(){
28 | objects = RemoteObjects.create();
29 | remotes = objects.exports;
30 |
31 | // connect to the app
32 | objects.connect('http://localhost:' + server.address().port, adapterName);
33 | });
34 |
35 | describe('client', function() {
36 | describe('call of constructor method', function(){
37 | it('should work', function(done) {
38 | var method = givenSharedStaticMethod(
39 | function greet(msg, cb) {
40 | cb(null, msg);
41 | },
42 | {
43 | accepts: { arg: 'person', type: 'string' },
44 | returns: { arg: 'msg', type: 'string' }
45 | }
46 | );
47 |
48 | var msg = 'hello';
49 | objects.invoke(method.name, [msg], function(err, resMsg) {
50 | assert.equal(resMsg, msg);
51 | done();
52 | });
53 | });
54 |
55 | it('should allow arguments in the path', function(done) {
56 | var method = givenSharedStaticMethod(
57 | function bar(a, b, cb) {
58 | cb(null, a + b);
59 | },
60 | {
61 | accepts: [
62 | { arg: 'b', type: 'number' },
63 | { arg: 'a', type: 'number', http: {source: 'path' } }
64 | ],
65 | returns: { arg: 'n', type: 'number' },
66 | http: { path: '/:a' }
67 | }
68 | );
69 |
70 | objects.invoke(method.name, [1, 2], function(err, n) {
71 | assert.equal(n, 3);
72 | done();
73 | });
74 | });
75 |
76 | it('should allow arguments in the query', function(done) {
77 | var method = givenSharedStaticMethod(
78 | function bar(a, b, cb) {
79 | cb(null, a + b);
80 | },
81 | {
82 | accepts: [
83 | { arg: 'b', type: 'number' },
84 | { arg: 'a', type: 'number', http: {source: 'query' } }
85 | ],
86 | returns: { arg: 'n', type: 'number' },
87 | http: { path: '/' }
88 | }
89 | );
90 |
91 | objects.invoke(method.name, [1, 2], function(err, n) {
92 | assert.equal(n, 3);
93 | done();
94 | });
95 | });
96 |
97 | it('should allow arguments in the header', function(done) {
98 | var method = givenSharedStaticMethod(
99 | function bar(a, b, cb) {
100 | cb(null, a + b);
101 | },
102 | {
103 | accepts: [
104 | { arg: 'b', type: 'number' },
105 | { arg: 'a', type: 'number', http: {source: 'header' } }
106 | ],
107 | returns: { arg: 'n', type: 'number' },
108 | http: { path: '/' }
109 | }
110 | );
111 |
112 | objects.invoke(method.name, [1, 2], function(err, n) {
113 | assert.equal(n, 3);
114 | done();
115 | });
116 | });
117 |
118 | it('should pass undefined if the argument is not supplied', function (done) {
119 | var called = false;
120 | var method = givenSharedStaticMethod(
121 | function bar(a, cb) {
122 | called = true;
123 | assert(a === undefined, 'a should be undefined');
124 | cb();
125 | },
126 | {
127 | accepts: [
128 | { arg: 'b', type: 'number' }
129 | ]
130 | }
131 | );
132 |
133 | objects.invoke(method.name, [], function(err) {
134 | assert(called);
135 | done();
136 | });
137 | });
138 |
139 | it('should allow arguments in the body', function(done) {
140 | var method = givenSharedStaticMethod(
141 | function bar(a, cb) {
142 | cb(null, a);
143 | },
144 | {
145 | accepts: [
146 | { arg: 'a', type: 'object', http: {source: 'body' } }
147 | ],
148 | returns: { arg: 'data', type: 'object', root: true },
149 | http: { path: '/' }
150 | }
151 | );
152 |
153 | var obj = {
154 | foo: 'bar'
155 | };
156 |
157 | objects.invoke(method.name, [obj], function(err, data) {
158 | expect(obj).to.deep.equal(data);
159 | done();
160 | });
161 | });
162 |
163 | it('should allow arguments in the body with date', function(done) {
164 | var method = givenSharedStaticMethod(
165 | function bar(a, cb) {
166 | cb(null, a);
167 | },
168 | {
169 | accepts: [
170 | { arg: 'a', type: 'object', http: {source: 'body' } }
171 | ],
172 | returns: { arg: 'data', type: 'object', root: true },
173 | http: { path: '/' }
174 | }
175 | );
176 |
177 | var data = {date: {$type: 'date', $data: new Date()}};
178 | objects.invoke(method.name, [data], function(err, resData) {
179 | expect(resData).to.deep.equal({date: data.date.$data.toISOString()});
180 | done();
181 | });
182 | });
183 |
184 | it('should allow arguments in the form', function(done) {
185 | var method = givenSharedStaticMethod(
186 | function bar(a, b, cb) {
187 | cb(null, a + b);
188 | },
189 | {
190 | accepts: [
191 | { arg: 'b', type: 'number', http: {source: 'form' } },
192 | { arg: 'a', type: 'number', http: {source: 'form' } }
193 | ],
194 | returns: { arg: 'n', type: 'number' },
195 | http: { path: '/' }
196 | }
197 | );
198 |
199 | objects.invoke(method.name, [1, 2], function(err, n) {
200 | assert.equal(n, 3);
201 | done();
202 | });
203 | });
204 |
205 | it('should respond with correct args if returns has multiple args', function(done) {
206 | var method = givenSharedStaticMethod(
207 | function(a, b, cb) {
208 | cb(null, a, b);
209 | },
210 | {
211 | accepts: [
212 | { arg: 'a', type: 'number' },
213 | { arg: 'b', type: 'number' }
214 | ],
215 | returns: [
216 | { arg: 'a', type: 'number' },
217 | { arg: 'b', type: 'number' }
218 | ]
219 | }
220 | );
221 |
222 | objects.invoke(method.name, [1, 2], function(err, a, b) {
223 | assert.equal(a, 1);
224 | assert.equal(b, 2);
225 | done();
226 | });
227 | });
228 |
229 | it('should allow and return falsy required arguments of correct type', function(done) {
230 | var method = givenSharedStaticMethod(
231 | function bar(num, str, bool, cb) {
232 | cb(null, num, str, bool);
233 | },
234 | {
235 | accepts: [
236 | { arg: 'num', type: 'number', required: true },
237 | { arg: 'str', type: 'string', required: true },
238 | { arg: 'bool', type: 'boolean', required: true }
239 | ],
240 | returns: [
241 | { arg: 'num', type: 'number' },
242 | { arg: 'str', type: 'string' },
243 | { arg: 'bool', type: 'boolean' }
244 | ],
245 | http: { path: '/' }
246 | }
247 | );
248 |
249 | objects.invoke(method.name, [0, '', false], function(err, a, b, c) {
250 | expect(err).to.not.be.an.instanceof(Error);
251 | assert.equal(a, 0);
252 | assert.equal(b, '');
253 | assert.equal(c, false);
254 | done();
255 | });
256 | });
257 |
258 | it('should reject falsy required arguments of incorrect type', function(done) {
259 | var method = givenSharedStaticMethod(
260 | function bar(num, str, bool, cb) {
261 | cb(null, num, str, bool);
262 | },
263 | {
264 | accepts: [
265 | { arg: 'num', type: 'number', required: true },
266 | { arg: 'str', type: 'string', required: true },
267 | { arg: 'bool', type: 'boolean', required: true }
268 | ],
269 | returns: [
270 | { arg: 'num', type: 'number' },
271 | { arg: 'str', type: 'string' },
272 | { arg: 'bool', type: 'boolean' }
273 | ],
274 | http: { path: '/' }
275 | }
276 | );
277 |
278 | objects.invoke(method.name, ['', false, 0], function(err, a, b, c) {
279 | expect(err).to.be.an.instanceof(Error);
280 | done();
281 | });
282 | });
283 |
284 | describe('uncaught errors', function () {
285 | it('should return 500 if an error object is thrown', function (done) {
286 | var errMsg = 'an error';
287 | var method = givenSharedStaticMethod(
288 | function(a, b, cb) {
289 | throw new Error(errMsg);
290 | }
291 | );
292 |
293 | objects.invoke(method.name, function(err) {
294 | assert(err instanceof Error);
295 | assert.equal(err.message, errMsg);
296 | done();
297 | });
298 | });
299 | });
300 | });
301 | });
302 |
303 | function givenSharedStaticMethod(fn, config) {
304 | if (typeof fn === 'object' && config === undefined) {
305 | config = fn;
306 | fn = null;
307 | }
308 | fn = fn || function(cb) { cb(); };
309 |
310 | remotes.testClass = { testMethod: fn };
311 | config = extend({ shared: true }, config);
312 | extend(remotes.testClass.testMethod, config);
313 | return {
314 | name: 'testClass.testMethod',
315 | url: '/testClass/testMethod',
316 | classUrl: '/testClass'
317 | };
318 | }
319 |
320 | function givenSharedPrototypeMethod(fn, config) {
321 | if (typeof fn === 'object' && config === undefined) {
322 | config = fn;
323 | fn = undefined;
324 | }
325 |
326 | fn = fn || function(cb) { cb(); };
327 | remotes.testClass = factory.createSharedClass();
328 | remotes.testClass.prototype.testMethod = fn;
329 | config = extend({ shared: true }, config);
330 | extend(remotes.testClass.prototype.testMethod, config);
331 | return {
332 | name: 'testClass.prototype.testMethod',
333 | getClassUrlForId: function(id) {
334 | return '/testClass/' + id;
335 | },
336 | getUrlForId: function(id) {
337 | return this.getClassUrlForId(id) + '/testMethod';
338 | },
339 | url: '/testClass/an-id/testMethod'
340 | };
341 | }
342 |
343 | function expectErrorResponseContaining(keyValues, done) {
344 | return function(err, resp) {
345 | if (err) return done(err);
346 | for (var prop in keyValues) {
347 | expect(resp.body.error).to.have.property(prop, keyValues[prop]);
348 | }
349 | done();
350 | }
351 | }
352 |
353 | });
354 |
--------------------------------------------------------------------------------
/test/rest-adapter.test.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var extend = require('util')._extend;
3 | var inherits = require('util').inherits;
4 | var RemoteObjects = require('../');
5 | var RestAdapter = require('../lib/rest-adapter');
6 | var SharedClass = require('../lib/shared-class');
7 | var SharedMethod = require('../lib/shared-method');
8 | var expect = require('chai').expect;
9 | var factory = require('./helpers/shared-objects-factory.js');
10 |
11 | describe('RestAdapter', function() {
12 | var remotes;
13 |
14 | beforeEach(function() {
15 | remotes = RemoteObjects.create();
16 | });
17 |
18 | describe('getClasses()', function() {
19 | it('fills `name`', function() {
20 | remotes.exports.testClass = factory.createSharedClass();
21 | var classes = getRestClasses();
22 | expect(classes[0]).to.have.property('name', 'testClass');
23 | });
24 |
25 | it('fills `routes`', function() {
26 | remotes.exports.testClass = factory.createSharedClass();
27 | remotes.exports.testClass.http = { path: '/test-class', verb: 'any' };
28 |
29 | var classes = getRestClasses();
30 |
31 | expect(classes[0]).to.have.property('routes')
32 | .eql([{ path: '/test-class', verb: 'any' }]);
33 | });
34 |
35 | it('fills `ctor`', function() {
36 | var testClass = remotes.exports.testClass = factory.createSharedClass();
37 | testClass.sharedCtor.http = { path: '/shared-ctor', verb: 'all' };
38 |
39 | var classes = getRestClasses();
40 |
41 | expect(classes[0].ctor).to.have.property('routes')
42 | .eql([{ path: '/shared-ctor', verb: 'all' }]);
43 | });
44 |
45 | it('fills static methods', function() {
46 | var testClass = remotes.exports.testClass = factory.createSharedClass();
47 | testClass.staticMethod = extend(someFunc, { shared: true });
48 |
49 | var methods = getRestClasses()[0].methods;
50 |
51 | expect(methods).to.have.length(1);
52 | expect(methods[0]).to.have.property('name', 'staticMethod');
53 | expect(methods[0]).to.have.property('fullName', 'testClass.staticMethod');
54 | expect(methods[0])
55 | .to.have.deep.property('routes[0].path', '/staticMethod');
56 | });
57 |
58 | it('fills prototype methods', function() {
59 | var testClass = remotes.exports.testClass = factory.createSharedClass();
60 | testClass.prototype.instanceMethod = extend(someFunc, { shared: true });
61 |
62 | var methods = getRestClasses()[0].methods;
63 |
64 | expect(methods).to.have.length(1);
65 | expect(methods[0])
66 | .to.have.property('fullName', 'testClass.prototype.instanceMethod');
67 | expect(methods[0])
68 | // Note: the `/id:` part is coming from testClass.sharedCtor
69 | .to.have.deep.property('routes[0].path', '/:id/instanceMethod');
70 | });
71 |
72 | function getRestClasses() {
73 | return new RestAdapter(remotes).getClasses();
74 | }
75 | });
76 |
77 | describe('path normalization', function() {
78 | it('fills `routes`', function() {
79 | remotes.exports.testClass = factory.createSharedClass();
80 | remotes.exports.testClass.http = { path: '/testClass', verb: 'any' };
81 |
82 | var classes = getRestClasses();
83 |
84 | expect(classes[0]).to.have.property('routes')
85 | .eql([{ path: '/test-class', verb: 'any' }]);
86 | });
87 |
88 | function getRestClasses() {
89 | return new RestAdapter(remotes, { normalizeHttpPath: true }).getClasses();
90 | }
91 | });
92 |
93 | describe('RestClass', function() {
94 | describe('getPath', function() {
95 | it('returns the path of the first route', function() {
96 | var restClass = givenRestClass({ http: [
97 | { path: '/a-path' },
98 | { path: '/another-path' }
99 | ]});
100 | expect(restClass.getPath()).to.equal('/a-path');
101 | });
102 | });
103 |
104 | function givenRestClass(config) {
105 | var ctor = factory.createSharedClass(config);
106 | remotes.testClass = ctor;
107 | var sharedClass = new SharedClass('testClass', ctor);
108 | return new RestAdapter.RestClass(sharedClass);
109 | }
110 | });
111 |
112 | describe('RestMethod', function() {
113 | var anArg = { arg: 'an-arg-name', type: String };
114 |
115 | it('has `accepts`', function() {
116 | var method = givenRestStaticMethod({ accepts: anArg });
117 | expect(method.accepts).to.eql([anArg]);
118 | });
119 |
120 | it('has `returns`', function() {
121 | var method = givenRestStaticMethod({ returns: anArg });
122 | expect(method.returns).to.eql([anArg]);
123 | });
124 |
125 | it('has `errors`', function() {
126 | var method = givenRestStaticMethod({ errors: anArg });
127 | expect(method.errors).to.eql([anArg]);
128 | });
129 |
130 | it('has `description`', function() {
131 | var method = givenRestStaticMethod({ description: 'a-desc' });
132 | expect(method.description).to.equal('a-desc');
133 | });
134 |
135 | it('has `notes`', function() {
136 | var method = givenRestStaticMethod({ notes: 'some-notes' });
137 | expect(method.notes).to.equal('some-notes');
138 | });
139 |
140 | it('has `documented`', function() {
141 | var method = givenRestStaticMethod({ documented: false });
142 | expect(method.documented).to.equal(false);
143 | });
144 |
145 | it('has `documented:true` by default', function() {
146 | var method = givenRestStaticMethod();
147 | expect(method.documented).to.equal(true);
148 | });
149 |
150 | describe('isReturningArray()', function() {
151 | it('returns true when there is single root Array arg', function() {
152 | var method = givenRestStaticMethod({
153 | returns: { root: true, type: Array }
154 | });
155 | expect(method.isReturningArray()).to.equal(true);
156 | });
157 |
158 | it('returns true when there is single root "array" arg', function() {
159 | var method = givenRestStaticMethod({
160 | returns: { root: true, type: Array }
161 | });
162 | expect(method.isReturningArray()).to.equal(true);
163 | });
164 |
165 | it('returns true when there is single root [Model] arg', function() {
166 | var method = givenRestStaticMethod({
167 | returns: { root: true, type: ['string'] }
168 | });
169 | expect(method.isReturningArray()).to.equal(true);
170 | });
171 |
172 | it('returns false otherwise', function() {
173 | var method = givenRestStaticMethod({
174 | returns: { arg: 'result', type: Array }
175 | });
176 | expect(method.isReturningArray()).to.equal(false);
177 | });
178 |
179 | it('handles invalid type', function() {
180 | var method = givenRestStaticMethod({
181 | returns: { root: true }
182 | });
183 | expect(method.isReturningArray()).to.equal(false);
184 | });
185 | });
186 |
187 | describe('acceptsSingleBodyArgument()', function() {
188 | it('returns true when the arg is a single Object from body', function() {
189 | var method = givenRestStaticMethod({
190 | accepts: {
191 | arg: 'data',
192 | type: Object,
193 | http: { source: 'body' }
194 | }
195 | });
196 | expect(method.acceptsSingleBodyArgument()).to.equal(true);
197 | });
198 |
199 | it('returns false otherwise', function() {
200 | var method = givenRestStaticMethod({
201 | accepts: { arg: 'data', type: Object }
202 | });
203 | expect(method.acceptsSingleBodyArgument()).to.equal(false);
204 | });
205 | });
206 |
207 | describe('getHttpMethod', function() {
208 | it('returns POST for `all`', function() {
209 | var method = givenRestStaticMethod({ http: { verb: 'all'} });
210 | expect(method.getHttpMethod()).to.equal('POST');
211 | });
212 |
213 | it('returns DELETE for `del`', function() {
214 | var method = givenRestStaticMethod({ http: { verb: 'del'} });
215 | expect(method.getHttpMethod()).to.equal('DELETE');
216 | });
217 |
218 | it('returns upper-case value otherwise', function() {
219 | var method = givenRestStaticMethod({ http: { verb: 'get'} });
220 | expect(method.getHttpMethod()).to.equal('GET');
221 | });
222 | });
223 |
224 | describe('getPath', function() {
225 | it('returns the path of the first route', function() {
226 | var method = givenRestStaticMethod({ http: [
227 | { path: '/a-path' },
228 | { path: '/another-path' }
229 | ]});
230 | expect(method.getPath()).to.equal('/a-path');
231 | });
232 | });
233 |
234 | describe('getFullPath', function() {
235 | it('returns class path + method path', function() {
236 | var method = givenRestStaticMethod(
237 | { http: { path: '/a-method' } },
238 | { http: { path: '/a-class' } }
239 | );
240 |
241 | expect(method.getFullPath()).to.equal('/a-class/a-method');
242 | });
243 | });
244 |
245 | function givenRestStaticMethod(methodConfig, classConfig) {
246 | var name = 'testMethod';
247 | methodConfig = extend({ shared: true }, methodConfig);
248 | classConfig = extend({ shared: true}, classConfig);
249 | remotes.testClass = extend({}, classConfig);
250 | var fn = remotes.testClass[name] = extend(function(){}, methodConfig);
251 |
252 | var sharedClass = new SharedClass('testClass', remotes.testClass, true);
253 | var restClass = new RestAdapter.RestClass(sharedClass);
254 |
255 | var sharedMethod = new SharedMethod(fn, name, sharedClass, methodConfig);
256 | return new RestAdapter.RestMethod(restClass, sharedMethod);
257 | }
258 | });
259 |
260 | describe('sortRoutes', function() {
261 | it('should sort routes based on verb & path', function() {
262 | var routes = [
263 | {route: {verb: 'get', path: '/'}},
264 | {route: {verb: 'get', path: '/:id'}},
265 | {route: {verb: 'get', path: '/findOne'}},
266 | {route: {verb: 'delete', path: '/'}},
267 | {route: {verb: 'del', path: '/:id'}}
268 | ];
269 |
270 | routes.sort(RestAdapter.sortRoutes);
271 |
272 | expect(routes).to.eql([
273 | {route: {verb: 'get', path: '/findOne'}},
274 | {route: {verb: 'get', path: '/:id'}},
275 | {route: {verb: 'get', path: '/'}},
276 | {route: {verb: 'del', path: '/:id'}},
277 | {route: {verb: 'delete', path: '/'}}
278 | ]);
279 |
280 | });
281 |
282 | it('should sort routes based on path accuracy', function() {
283 | var routes = [
284 | {route: {verb: 'get', path: '/'}},
285 | {route: {verb: 'get', path: '/:id/docs'}},
286 | {route: {verb: 'get', path: '/:id'}},
287 | {route: {verb: 'get', path: '/findOne'}}
288 | ];
289 |
290 | routes.sort(RestAdapter.sortRoutes);
291 |
292 | expect(routes).to.eql([
293 | {route: {verb: 'get', path: '/findOne'}},
294 | {route: {verb: 'get', path: '/:id/docs'}},
295 | {route: {verb: 'get', path: '/:id'}},
296 | {route: {verb: 'get', path: '/'}}
297 | ]);
298 |
299 | });
300 |
301 | it('should sort routes with common parts', function() {
302 | var routes = [
303 | {route: {verb: 'get', path: '/sum'}},
304 | {route: {verb: 'get', path: '/sum/1'}}
305 | ];
306 |
307 | routes.sort(RestAdapter.sortRoutes);
308 |
309 | expect(routes).to.eql([
310 | {route: {verb: 'get', path: '/sum/1'}},
311 | {route: {verb: 'get', path: '/sum'}}
312 | ]);
313 |
314 | });
315 |
316 | it('should sort routes with trailing /', function() {
317 | var routes = [
318 | {route: {verb: 'get', path: '/sum/'}},
319 | {route: {verb: 'get', path: '/sum/1'}}
320 | ];
321 |
322 | routes.sort(RestAdapter.sortRoutes);
323 |
324 | expect(routes).to.eql([
325 | {route: {verb: 'get', path: '/sum/1'}},
326 | {route: {verb: 'get', path: '/sum/'}}
327 | ]);
328 |
329 | });
330 | });
331 | });
332 |
333 | function someFunc() {
334 | }
335 |
--------------------------------------------------------------------------------
/lib/shared-method.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Expose `SharedMethod`.
3 | */
4 |
5 | module.exports = SharedMethod;
6 |
7 | /*!
8 | * Module dependencies.
9 | */
10 |
11 | var debug = require('debug')('strong-remoting:shared-method')
12 | , util = require('util')
13 | , traverse = require('traverse')
14 | , assert = require('assert');
15 |
16 | /**
17 | * Create a new `SharedMethod` with the given `fn`.
18 | *
19 | * @class SharedMethod
20 | * @param {Function} fn The `Function` to be invoked when the method is invoked
21 | * @param {String} name The name of the `SharedMethod`
22 | * @param {SharedClass} sharedClass The `SharedClass` the method will be attached to
23 | * @param {Object|Boolean} options
24 | * @param {Boolean} [options.isStatic] Is the method a static method or a
25 | * a `prototype` method
26 | * @param {Array} [options.aliases] A list of aliases for the
27 | * `sharedMethod.name`
28 | * @param {Array|Object} [options.accepts] An `Array` of argument definitions
29 | * that describe the arguments of the `SharedMethod`.
30 | * @param {Boolean} [options.shared] Default is `true`
31 | * @param {String} [options.accepts.arg] The name of the argument
32 | * @param {String} [options.accepts.http] HTTP mapping for the argument
33 | * @param {String} [options.accepts.http.source] The HTTP source for the
34 | * argument. May be one of the following:
35 | *
36 | * - `req` - the Express `Request` object
37 | * - `req` - the Express `Request` object
38 | * - `body` - the `req.body` value
39 | * - `form` - `req.body[argumentName]`
40 | * - `query` - `req.query[argumentName]`
41 | * - `path` - `req.params[argumentName]`
42 | * - `header` - `req.headers[argumentName]`
43 | * - `context` - the current `HttpContext`
44 | * @param {Object} [options.accepts.rest] The REST mapping / settings for the
45 | * argument.
46 | * @param {Array|Object} [options.returns] An `Array` of argument definitions
47 | * @param {Array|Object} [options.errors] An `Array` of error definitions
48 | * The same options are available as `options.accepts`.
49 | * @property {String} name The method name
50 | * @property {String[]} aliases An array of method aliases
51 | * @property {Array|Object} isStatic
52 | * @property {Array|Object} accepts See `options.accepts`
53 | * @property {Array|Object} returns See `options.returns`
54 | * @property {Array|Object} errors See `options.errors`
55 | * @property {String} description
56 | * @property {String} notes
57 | * @property {String} http
58 | * @property {String} rest
59 | * @property {String} shared
60 | * @property {Boolean} [documented] Default: true. Set to `false` to exclude
61 | * the method from Swagger metadata.
62 | */
63 |
64 | function SharedMethod(fn, name, sc, options) {
65 | if (typeof options === 'boolean') {
66 | options = { isStatic: options };
67 | }
68 |
69 | this.fn = fn;
70 | fn = fn || {};
71 | this.name = name;
72 | assert(typeof name === 'string', 'The method name must be a string');
73 | options = options || {};
74 | this.aliases = options.aliases || [];
75 | var isStatic = this.isStatic = options.isStatic || false;
76 | this.accepts = options.accepts || fn.accepts || [];
77 | this.returns = options.returns || fn.returns || [];
78 | this.errors = options.errors || fn.errors || [];
79 | this.description = options.description || fn.description;
80 | this.notes = options.notes || fn.notes;
81 | this.documented = (options.documented || fn.documented) !== false;
82 | this.http = options.http || fn.http || {};
83 | this.rest = options.rest || fn.rest || {};
84 | this.shared = options.shared;
85 | if(this.shared === undefined) {
86 | this.shared = true;
87 | }
88 | if(fn.shared === false) {
89 | this.shared = false;
90 | }
91 | this.sharedClass = sc;
92 |
93 | if(sc) {
94 | this.ctor = sc.ctor;
95 | this.sharedCtor = sc.sharedCtor;
96 | }
97 | if(name === 'sharedCtor') {
98 | this.isSharedCtor = true;
99 | }
100 |
101 | if(this.accepts && !Array.isArray(this.accepts)) {
102 | this.accepts = [this.accepts];
103 | }
104 | if(this.returns && !Array.isArray(this.returns)) {
105 | this.returns = [this.returns];
106 | }
107 | if(this.errors && !Array.isArray(this.errors)) {
108 | this.errors = [this.errors];
109 | }
110 |
111 | this.stringName = (sc ? sc.name : '') + (isStatic ? '.' : '.prototype.') + name;
112 | }
113 |
114 | /**
115 | * Create a new `SharedMethod` with the given `fn`. The function should include
116 | * all the method options.
117 | *
118 | * @param {Function} fn
119 | * @param {Function} name
120 | * @param {SharedClass} SharedClass
121 | * @param {Boolean} isStatic
122 | */
123 |
124 | SharedMethod.fromFunction = function(fn, name, sharedClass, isStatic) {
125 | return new SharedMethod(fn, name, sharedClass, {
126 | isStatic: isStatic,
127 | accepts: fn.accepts,
128 | returns: fn.returns,
129 | errors: fn.errors,
130 | description: fn.description,
131 | notes: fn.notes,
132 | http: fn.http,
133 | rest: fn.rest
134 | });
135 | }
136 |
137 | /**
138 | * Execute the remote method using the given arg data.
139 | *
140 | * @param {Object} args containing named argument data
141 | * @param {Function} fn callback `fn(err, result)` containing named result data
142 | */
143 |
144 | SharedMethod.prototype.invoke = function (scope, args, fn) {
145 | var accepts = this.accepts;
146 | var returns = this.returns;
147 | var errors = this.errors;
148 | var method = this.getFunction();
149 | var sharedMethod = this;
150 | var formattedArgs = [];
151 | var result;
152 |
153 | // map the given arg data in order they are expected in
154 | if(accepts) {
155 | for(var i = 0; i < accepts.length; i++) {
156 | var desc = accepts[i];
157 | var name = desc.name || desc.arg;
158 | var uarg = SharedMethod.convertArg(desc, args[name]);
159 | var actualType = SharedMethod.getType(uarg);
160 |
161 | // is the arg optional?
162 | // arg was not provided
163 | if(actualType === 'undefined') {
164 | if(desc.required) {
165 | var err = new Error(name + ' is a required arg');
166 | err.statusCode = 400;
167 | return fn(err);
168 | } else {
169 | // Add the argument even if it's undefined to stick with the accepts
170 | formattedArgs.push(undefined);
171 | continue;
172 | }
173 | }
174 |
175 | // convert strings
176 | if(actualType === 'string' && desc.type !== 'any' && actualType !== desc.type) {
177 | switch(desc.type) {
178 | case 'string':
179 | break;
180 | case 'date':
181 | uarg = new Date(uarg);
182 | break;
183 | case 'number':
184 | uarg = Number(uarg);
185 | break;
186 | case 'boolean':
187 | uarg = Boolean(uarg);
188 | break;
189 | // Other types such as 'object', 'array',
190 | // ModelClass, ['string'], or [ModelClass]
191 | default:
192 | try {
193 | uarg = JSON.parse(uarg);
194 | } catch(err) {
195 | debug('- %s - invalid value for argument \'%s\' of type \'%s\': %s',
196 | sharedMethod.name, name, desc.type, uarg);
197 | return fn(err);
198 | }
199 | break;
200 | }
201 | }
202 |
203 | // Add the argument even if it's undefined to stick with the accepts
204 | formattedArgs.push(uarg);
205 | }
206 | }
207 |
208 | // define the callback
209 | function callback(err) {
210 | if(err) {
211 | return fn(err);
212 | }
213 |
214 | result = SharedMethod.toResult(returns, [].slice.call(arguments, 1));
215 |
216 | debug('- %s - result %j', sharedMethod.name, result);
217 |
218 | fn(null, result);
219 | }
220 |
221 | // add in the required callback
222 | formattedArgs.push(callback);
223 |
224 | debug('- %s - invoke with', this.name, formattedArgs);
225 |
226 | // invoke
227 | try {
228 | return method.apply(scope, formattedArgs);
229 | } catch (err) {
230 | debug('error caught during the invocation of %s', this.name);
231 | return fn(err);
232 | }
233 | }
234 |
235 | /**
236 | * Returns an appropriate type based on `val`.
237 | * @param {*} val The value to determine the type for
238 | * @returns {String} The type name
239 | */
240 |
241 | SharedMethod.getType = function (val) {
242 | var type = typeof val;
243 |
244 | switch (type) {
245 | case 'undefined':
246 | case 'boolean':
247 | case 'number':
248 | case 'function':
249 | case 'string':
250 | return type;
251 | case 'object':
252 | // null
253 | if (val === null) {
254 | return 'null';
255 | }
256 |
257 | // buffer
258 | if (Buffer.isBuffer(val)) {
259 | return 'buffer';
260 | }
261 |
262 | // array
263 | if (Array.isArray(val)) {
264 | return 'array';
265 | }
266 |
267 | // date
268 | if (val instanceof Date) {
269 | return 'date';
270 | }
271 |
272 | // object
273 | return 'object';
274 | }
275 | };
276 |
277 | /**
278 | * Returns a reformatted Object valid for consumption as remoting function
279 | * arguments
280 | */
281 |
282 | SharedMethod.convertArg = function(accept, raw) {
283 | if(accept.http && (accept.http.source === 'req'
284 | || accept.http.source === 'res'
285 | || accept.http.source === 'context'
286 | )) {
287 | return raw;
288 | }
289 | if(raw === null || typeof raw !== 'object') {
290 | return raw;
291 | }
292 | var data = traverse(raw).forEach(function(x) {
293 | if(x === null || typeof x !== 'object') {
294 | return x;
295 | }
296 | var result = x;
297 | if(x.$type === 'base64' || x.$type === 'date') {
298 | switch (x.$type) {
299 | case 'base64':
300 | result = new Buffer(x.$data, 'base64');
301 | break;
302 | case 'date':
303 | result = new Date(x.$data);
304 | break;
305 | }
306 | this.update(result);
307 | }
308 | return result;
309 | });
310 | return data;
311 | };
312 |
313 | /**
314 | * Returns a reformatted Object valid for consumption as JSON from an Array of
315 | * results from a remoting function, based on `returns`.
316 | */
317 |
318 | SharedMethod.toResult = function(returns, raw) {
319 | var result = {};
320 |
321 | if (!returns.length) {
322 | return;
323 | }
324 |
325 | returns = returns.filter(function (item, index) {
326 | if (index >= raw.length) {
327 | return false;
328 | }
329 |
330 | if (item.root) {
331 | result = convert(raw[index]);
332 | return false;
333 | }
334 |
335 | return true;
336 | });
337 |
338 | returns.forEach(function (item, index) {
339 | result[item.name || item.arg] = convert(raw[index]);
340 | });
341 |
342 | return result;
343 |
344 | function convert(val) {
345 | switch (SharedMethod.getType(val)) {
346 | case 'date':
347 | return {
348 | $type: 'date',
349 | $data: val.toString()
350 | };
351 | case 'buffer':
352 | return {
353 | $type: 'base64',
354 | $data: val.toString('base64')
355 | };
356 | }
357 |
358 | return val;
359 | }
360 | };
361 |
362 |
363 | /**
364 | * Get the function the `SharedMethod` will `invoke()`.
365 | */
366 |
367 | SharedMethod.prototype.getFunction = function() {
368 | var fn;
369 |
370 | if(!this.ctor) return this.fn;
371 |
372 | if(this.isStatic) {
373 | fn = this.ctor[this.name];
374 | } else {
375 | fn = this.ctor.prototype[this.name];
376 | }
377 |
378 | return fn || this.fn;
379 | }
380 |
381 | /**
382 | * Will this shared method invoke the given `suspect`?
383 | *
384 | * ```js
385 | * // examples
386 | * sharedMethod.isDelegateFor(myClass.myMethod); // pass a function
387 | * sharedMethod.isDelegateFor(myClass.prototype.myInstMethod);
388 | * sharedMethod.isDelegateFor('myMethod', true); // check for a static method by name
389 | * sharedMethod.isDelegateFor('myInstMethod', false); // instance method by name
390 | * ```
391 | *
392 | * @param {String|Function} suspect The name of the suspected function
393 | * or a `Function`.
394 | */
395 |
396 | SharedMethod.prototype.isDelegateFor = function(suspect, isStatic) {
397 | var type = typeof suspect;
398 | isStatic = isStatic || false;
399 |
400 |
401 | if(suspect) {
402 | switch(type) {
403 | case 'function':
404 | return this.getFunction() === suspect;
405 | break;
406 | case 'string':
407 | if(this.isStatic !== isStatic) return false;
408 | return this.name === suspect || this.aliases.indexOf(suspect) !== -1;
409 | break;
410 | }
411 | }
412 |
413 | return false;
414 | }
415 |
416 | /**
417 | * Add an alias
418 | *
419 | * @param {String} alias
420 | */
421 |
422 | SharedMethod.prototype.addAlias = function(alias) {
423 | if(this.aliases.indexOf(alias) === -1) {
424 | this.aliases.push(alias);
425 | }
426 | }
427 |
--------------------------------------------------------------------------------
/lib/remote-objects.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Expose `RemoteObjects`.
3 | */
4 |
5 | module.exports = RemoteObjects;
6 |
7 | /*!
8 | * Module dependencies.
9 | */
10 |
11 | var EventEmitter = require('eventemitter2').EventEmitter2
12 | , debug = require('debug')('strong-remoting:remotes')
13 | , util = require('util')
14 | , inherits = util.inherits
15 | , assert = require('assert')
16 | , Dynamic = require('./dynamic')
17 | , SharedClass = require('./shared-class')
18 | , ExportsHelper = require('./exports-helper');
19 |
20 | // require the rest adapter for browserification
21 | // TODO(ritch) remove this somehow...?
22 | require('./rest-adapter');
23 |
24 | /**
25 | * Create a new `RemoteObjects` with the given `options`.
26 | *
27 | * ```js
28 | * var remoteObjects = require('strong-remoting').create();
29 | * ```
30 | *
31 | * @param {Object} options
32 | * @return {RemoteObjects}
33 | * @class
34 | */
35 |
36 | function RemoteObjects(options) {
37 | EventEmitter.call(this, {wildcard: true});
38 | // Avoid warning: possible EventEmitter memory leak detected
39 | this.setMaxListeners(16);
40 | this.options = options || {};
41 | this.exports = this.options.exports || {};
42 | this._classes = {};
43 | }
44 |
45 | /*!
46 | * Inherit from `EventEmitter`.
47 | */
48 |
49 | inherits(RemoteObjects, EventEmitter);
50 |
51 | /*!
52 | * Simplified APIs
53 | */
54 |
55 | RemoteObjects.create = function (options) {
56 | return new RemoteObjects(options);
57 | }
58 |
59 | RemoteObjects.extend = function (exports) {
60 | return new ExportsHelper(exports);
61 | }
62 |
63 | /**
64 | * Create a handler from the given adapter.
65 | *
66 | * @param {String} name Adapter name
67 | * @param {Object} options Adapter options
68 | * @return {Function}
69 | */
70 |
71 | RemoteObjects.prototype.handler = function (name, options) {
72 | var Adapter = this.adapter(name);
73 | var adapter = new Adapter(this, options);
74 | var handler = adapter.createHandler();
75 |
76 | if(handler) {
77 | // allow adapter reference from handler
78 | handler.adapter = adapter;
79 | }
80 |
81 | return handler;
82 | }
83 |
84 | /**
85 | * Create a connection to a remoting server.
86 | *
87 | * @param {String} url Server root
88 | * @param {String} name Name of the adapter (eg. "rest")
89 | */
90 |
91 | RemoteObjects.prototype.connect = function(url, name) {
92 | var Adapter = this.adapter(name);
93 | var adapter = new Adapter(this);
94 | this.serverAdapter = adapter;
95 | return adapter.connect(url);
96 | }
97 |
98 | /**
99 | * Invoke a method on a remote server using the connected adapter.
100 | *
101 | * @param {String} method The remote method string
102 | * @param {String} [ctorArgs] Constructor arguments (for prototype methods)
103 | * @param {String} [args] Method arguments
104 | * @callback {Function} [callback] callback
105 | * @param {Error} err
106 | * @param {Any} arg...
107 | * @end
108 | */
109 |
110 | RemoteObjects.prototype.invoke = function(method, ctorArgs, args, callback) {
111 | assert(this.serverAdapter, 'Cannot invoke method without an adapter. See RemoteObjects#connect().');
112 | return this.serverAdapter.invoke.apply(this.serverAdapter, arguments, callback);
113 | }
114 |
115 | /**
116 | * Get an adapter by name.
117 | * @param {String} name The adapter name
118 | * @return {Adapter}
119 | */
120 |
121 | RemoteObjects.prototype.adapter = function (name) {
122 | return require('./' + name + '-adapter');
123 | }
124 |
125 | /**
126 | * Get all classes.
127 | */
128 |
129 | RemoteObjects.prototype.classes = function (options) {
130 | options = options || {};
131 | var exports = this.exports;
132 | var result = [];
133 | var sharedClasses = this._classes;
134 |
135 | Object
136 | .keys(exports)
137 | .forEach(function (name) {
138 | result.push(new SharedClass(name, exports[name], options));
139 | });
140 |
141 | Object
142 | .keys(sharedClasses)
143 | .forEach(function (name) {
144 | result.push(sharedClasses[name]);
145 | });
146 |
147 | return result;
148 | }
149 |
150 | /**
151 | * Add a shared class.
152 | *
153 | * @param {SharedClass} sharedClass
154 | */
155 |
156 | RemoteObjects.prototype.addClass = function (sharedClass) {
157 | assert(sharedClass instanceof SharedClass);
158 | this._classes[sharedClass.name] = sharedClass;
159 | }
160 |
161 | /**
162 | * Find a method by its string name.
163 | *
164 | * Example Method Strings:
165 | *
166 | * - `MyClass.prototype.myMethod`
167 | * - `MyClass.staticMethod`
168 | * - `obj.method`
169 | *
170 | * @param {String} methodString
171 | */
172 |
173 | RemoteObjects.prototype.findMethod = function (methodString) {
174 | var methods = this.methods();
175 |
176 | for (var i = 0; i < methods.length; i++) {
177 | if(methods[i].stringName === methodString) return methods[i];
178 | }
179 | }
180 |
181 | /**
182 | * List all methods.
183 | */
184 |
185 | RemoteObjects.prototype.methods = function () {
186 | var methods = [];
187 |
188 | this
189 | .classes()
190 | .forEach(function (sc) {
191 | methods = sc.methods().concat(methods);
192 | });
193 |
194 | return methods;
195 | }
196 |
197 | /**
198 | * Get as JSON.
199 | */
200 |
201 | RemoteObjects.prototype.toJSON = function () {
202 | var result = {};
203 | var methods = this.methods();
204 |
205 | methods.forEach(function (sharedMethod) {
206 | result[sharedMethod.stringName] = {
207 | http: sharedMethod.fn && sharedMethod.fn.http,
208 | accepts: sharedMethod.accepts,
209 | returns: sharedMethod.returns,
210 | errors: sharedMethod.errors
211 | };
212 | });
213 |
214 | return result;
215 | }
216 |
217 | /**
218 | * Execute the given function before the matched method string.
219 | *
220 | * **Examples:**
221 | *
222 | * ```js
223 | * // Do something before our `user.greet` example, earlier.
224 | * remotes.before('user.greet', function (ctx, next) {
225 | * if((ctx.req.param('password') || '').toString() !== '1234') {
226 | * next(new Error('Bad password!'));
227 | * } else {
228 | * next();
229 | * }
230 | * });
231 | *
232 | * // Do something before any `user` method.
233 | * remotes.before('user.*', function (ctx, next) {
234 | * console.log('Calling a user method.');
235 | * next();
236 | * });
237 | *
238 | * // Do something before a `dog` instance method.
239 | * remotes.before('dog.prototype.*', function (ctx, next) {
240 | * var dog = this;
241 | * console.log('Calling a method on "%s".', dog.name);
242 | * next();
243 | * });
244 | * ```
245 | *
246 | * @param {String} methodMatch The glob to match a method string
247 | * @callback {Function} hook
248 | * @param {Context} ctx The adapter specific context
249 | * @param {Function} next Call with an optional error object
250 | * @param {SharedMethod} method The SharedMethod object
251 | */
252 |
253 | RemoteObjects.prototype.before = function (methodMatch, fn) {
254 | this.on('before.' + methodMatch, fn);
255 | }
256 |
257 | /**
258 | * Execute the given `hook` function after the matched method string.
259 | *
260 | * **Examples:**
261 | *
262 | * ```js
263 | * // Do something after the `speak` instance method.
264 | * // NOTE: you cannot cancel a method after it has been called.
265 | * remotes.after('dog.prototype.speak', function (ctx, next) {
266 | * console.log('After speak!');
267 | * next();
268 | * });
269 | *
270 | * // Do something before all methods.
271 | * remotes.before('**', function (ctx, next, method) {
272 | * console.log('Calling:', method.name);
273 | * next();
274 | * });
275 | *
276 | * // Modify all returned values named `result`.
277 | * remotes.after('**', function (ctx, next) {
278 | * ctx.result += '!!!';
279 | * next();
280 | * });
281 | * ```
282 | *
283 | * @param {String} methodMatch The glob to match a method string
284 | * @callback {Function} hook
285 | * @param {Context} ctx The adapter specific context
286 | * @param {Function} next Call with an optional error object
287 | * @param {SharedMethod} method The SharedMethod object
288 | */
289 |
290 | RemoteObjects.prototype.after = function (methodMatch, fn) {
291 | this.on('after.' + methodMatch, fn);
292 | }
293 |
294 | /*!
295 | * Create a middleware style emit that supports wildcards.
296 | */
297 |
298 | RemoteObjects.prototype.execHooks = function(when, method, scope, ctx, next) {
299 | var stack = [];
300 | var ee = this;
301 | var type = when + '.' + method.sharedClass.name + (method.isStatic ? '.' : '.prototype.') + method.name;
302 |
303 | this._events || init.call(this);
304 |
305 | var handler;
306 |
307 | // context
308 | this.objectName = method.sharedClass.name;
309 | this.methodName = method.name;
310 |
311 | if(this.wildcard) {
312 | handler = [];
313 | var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
314 | searchListenerTree.call(this, handler, ns, this.listenerTree, 0);
315 | } else {
316 | handler = this._events[type];
317 | }
318 |
319 | if (typeof handler === 'function') {
320 | this.event = type;
321 |
322 | addToStack(handler);
323 |
324 | return execStack();
325 | } else if (handler) {
326 | var l = arguments.length;
327 | var args = new Array(l - 1);
328 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
329 |
330 | var listeners = handler.slice();
331 | for (var i = 0, l = listeners.length; i < l; i++) {
332 | addToStack(listeners[i]);
333 | }
334 | }
335 |
336 | function addToStack(fn) {
337 | stack.push(fn);
338 | }
339 |
340 | function execStack(err) {
341 | if(err) return next(err);
342 |
343 | var cur = stack.shift();
344 |
345 | if(cur) {
346 | cur.call(scope, ctx, execStack, method);
347 | } else {
348 | next();
349 | }
350 | }
351 |
352 | return execStack();
353 | };
354 |
355 | // from EventEmitter2
356 | function searchListenerTree(handlers, type, tree, i) {
357 | if (!tree) {
358 | return [];
359 | }
360 | var listeners=[], leaf, len, branch, xTree, xxTree, isolatedBranch, endReached,
361 | typeLength = type.length, currentType = type[i], nextType = type[i+1];
362 | if (i === typeLength && tree._listeners) {
363 | //
364 | // If at the end of the event(s) list and the tree has listeners
365 | // invoke those listeners.
366 | //
367 | if (typeof tree._listeners === 'function') {
368 | handlers && handlers.push(tree._listeners);
369 | return [tree];
370 | } else {
371 | for (leaf = 0, len = tree._listeners.length; leaf < len; leaf++) {
372 | handlers && handlers.push(tree._listeners[leaf]);
373 | }
374 | return [tree];
375 | }
376 | }
377 |
378 | if ((currentType === '*' || currentType === '**') || tree[currentType]) {
379 | //
380 | // If the event emitted is '*' at this part
381 | // or there is a concrete match at this patch
382 | //
383 | if (currentType === '*') {
384 | for (branch in tree) {
385 | if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
386 | listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+1));
387 | }
388 | }
389 | return listeners;
390 | } else if(currentType === '**') {
391 | endReached = (i+1 === typeLength || (i+2 === typeLength && nextType === '*'));
392 | if(endReached && tree._listeners) {
393 | // The next element has a _listeners, add it to the handlers.
394 | listeners = listeners.concat(searchListenerTree(handlers, type, tree, typeLength));
395 | }
396 |
397 | for (branch in tree) {
398 | if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
399 | if(branch === '*' || branch === '**') {
400 | if(tree[branch]._listeners && !endReached) {
401 | listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], typeLength));
402 | }
403 | listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
404 | } else if(branch === nextType) {
405 | listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+2));
406 | } else {
407 | // No match on this one, shift into the tree but not in the type array.
408 | listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
409 | }
410 | }
411 | }
412 | return listeners;
413 | }
414 |
415 | listeners = listeners.concat(searchListenerTree(handlers, type, tree[currentType], i+1));
416 | }
417 |
418 | xTree = tree['*'];
419 | if (xTree) {
420 | //
421 | // If the listener tree will allow any match for this part,
422 | // then recursively explore all branches of the tree
423 | //
424 | searchListenerTree(handlers, type, xTree, i+1);
425 | }
426 |
427 | xxTree = tree['**'];
428 | if(xxTree) {
429 | if(i < typeLength) {
430 | if(xxTree._listeners) {
431 | // If we have a listener on a '**', it will catch all, so add its handler.
432 | searchListenerTree(handlers, type, xxTree, typeLength);
433 | }
434 |
435 | // Build arrays of matching next branches and others.
436 | for(branch in xxTree) {
437 | if(branch !== '_listeners' && xxTree.hasOwnProperty(branch)) {
438 | if(branch === nextType) {
439 | // We know the next element will match, so jump twice.
440 | searchListenerTree(handlers, type, xxTree[branch], i+2);
441 | } else if(branch === currentType) {
442 | // Current node matches, move into the tree.
443 | searchListenerTree(handlers, type, xxTree[branch], i+1);
444 | } else {
445 | isolatedBranch = {};
446 | isolatedBranch[branch] = xxTree[branch];
447 | searchListenerTree(handlers, type, { '**': isolatedBranch }, i+1);
448 | }
449 | }
450 | }
451 | } else if(xxTree._listeners) {
452 | // We have reached the end and still on a '**'
453 | searchListenerTree(handlers, type, xxTree, typeLength);
454 | } else if(xxTree['*'] && xxTree['*']._listeners) {
455 | searchListenerTree(handlers, type, xxTree['*'], typeLength);
456 | }
457 | }
458 |
459 | return listeners;
460 | }
461 |
462 | /**
463 | * Invoke the given shared method using the supplied context.
464 | * Execute registered before/after hooks.
465 | * @param ctx
466 | * @param method
467 | * @param cb
468 | */
469 | RemoteObjects.prototype.invokeMethodInContext = function(ctx, method, cb) {
470 | var self = this;
471 |
472 | var scope = this.getScope(ctx, method);
473 |
474 | self.execHooks('before', method, scope, ctx, function(err) {
475 | if (err) return cb(err);
476 |
477 | ctx.invoke(scope, method, function(err, result) {
478 | if (err) return cb(err);
479 | ctx.result = result;
480 | self.execHooks('after', method, scope, ctx, function(err) {
481 | if (err) return cb(err);
482 | cb();
483 | });
484 | });
485 | });
486 | };
487 |
488 | /**
489 | * Determine what scope object to use when invoking the given remote method in
490 | * the given context.
491 | * @private
492 | */
493 |
494 | RemoteObjects.prototype.getScope = function(ctx, method) {
495 | // Static methods are invoked on the constructor (this = constructor fn)
496 | // Prototype methods are invoked on the instance (this = instance)
497 | return ctx.instance || method.ctor;
498 | }
499 |
500 | /**
501 | * Define a named type conversion. The conversion is used when a
502 | * `SharedMethod` argument defines a type with the given `name`.
503 | *
504 | * ```js
505 | * remotes.defineType('MyType', function(val, ctx) {
506 | * // use the val and ctx objects to return the concrete value
507 | * return new MyType(val);
508 | * });
509 | * ```
510 | *
511 | * **Note: the alias `remotes.convert()` is deprecated.**
512 | *
513 | * @param {String} name The type name
514 | * @param {Function} converter
515 | */
516 |
517 | RemoteObjects.defineType =
518 | RemoteObjects.convert =
519 | RemoteObjects.prototype.defineType =
520 | RemoteObjects.prototype.convert = function(name, fn) {
521 | Dynamic.define(name, fn);
522 | }
523 |
--------------------------------------------------------------------------------
/example/rest-models/public/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | http://www.JSON.org/json2.js
3 | 2009-09-29
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, strict: false */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | if (!this.JSON) {
163 | this.JSON = {};
164 | }
165 |
166 | (function () {
167 |
168 | function f(n) {
169 | // Format integers to have at least two digits.
170 | return n < 10 ? '0' + n : n;
171 | }
172 |
173 | if (typeof Date.prototype.toJSON !== 'function') {
174 |
175 | Date.prototype.toJSON = function (key) {
176 |
177 | return isFinite(this.valueOf()) ?
178 | this.getUTCFullYear() + '-' +
179 | f(this.getUTCMonth() + 1) + '-' +
180 | f(this.getUTCDate()) + 'T' +
181 | f(this.getUTCHours()) + ':' +
182 | f(this.getUTCMinutes()) + ':' +
183 | f(this.getUTCSeconds()) + 'Z' : null;
184 | };
185 |
186 | String.prototype.toJSON =
187 | Number.prototype.toJSON =
188 | Boolean.prototype.toJSON = function (key) {
189 | return this.valueOf();
190 | };
191 | }
192 |
193 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
194 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
195 | gap,
196 | indent,
197 | meta = { // table of character substitutions
198 | '\b': '\\b',
199 | '\t': '\\t',
200 | '\n': '\\n',
201 | '\f': '\\f',
202 | '\r': '\\r',
203 | '"' : '\\"',
204 | '\\': '\\\\'
205 | },
206 | rep;
207 |
208 |
209 | function quote(string) {
210 |
211 | // If the string contains no control characters, no quote characters, and no
212 | // backslash characters, then we can safely slap some quotes around it.
213 | // Otherwise we must also replace the offending characters with safe escape
214 | // sequences.
215 |
216 | escapable.lastIndex = 0;
217 | return escapable.test(string) ?
218 | '"' + string.replace(escapable, function (a) {
219 | var c = meta[a];
220 | return typeof c === 'string' ? c :
221 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
222 | }) + '"' :
223 | '"' + string + '"';
224 | }
225 |
226 |
227 | function str(key, holder) {
228 |
229 | // Produce a string from holder[key].
230 |
231 | var i, // The loop counter.
232 | k, // The member key.
233 | v, // The member value.
234 | length,
235 | mind = gap,
236 | partial,
237 | value = holder[key];
238 |
239 | // If the value has a toJSON method, call it to obtain a replacement value.
240 |
241 | if (value && typeof value === 'object' &&
242 | typeof value.toJSON === 'function') {
243 | value = value.toJSON(key);
244 | }
245 |
246 | // If we were called with a replacer function, then call the replacer to
247 | // obtain a replacement value.
248 |
249 | if (typeof rep === 'function') {
250 | value = rep.call(holder, key, value);
251 | }
252 |
253 | // What happens next depends on the value's type.
254 |
255 | switch (typeof value) {
256 | case 'string':
257 | return quote(value);
258 |
259 | case 'number':
260 |
261 | // JSON numbers must be finite. Encode non-finite numbers as null.
262 |
263 | return isFinite(value) ? String(value) : 'null';
264 |
265 | case 'boolean':
266 | case 'null':
267 |
268 | // If the value is a boolean or null, convert it to a string. Note:
269 | // typeof null does not produce 'null'. The case is included here in
270 | // the remote chance that this gets fixed someday.
271 |
272 | return String(value);
273 |
274 | // If the type is 'object', we might be dealing with an object or an array or
275 | // null.
276 |
277 | case 'object':
278 |
279 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
280 | // so watch out for that case.
281 |
282 | if (!value) {
283 | return 'null';
284 | }
285 |
286 | // Make an array to hold the partial results of stringifying this object value.
287 |
288 | gap += indent;
289 | partial = [];
290 |
291 | // Is the value an array?
292 |
293 | if (Object.prototype.toString.apply(value) === '[object Array]') {
294 |
295 | // The value is an array. Stringify every element. Use null as a placeholder
296 | // for non-JSON values.
297 |
298 | length = value.length;
299 | for (i = 0; i < length; i += 1) {
300 | partial[i] = str(i, value) || 'null';
301 | }
302 |
303 | // Join all of the elements together, separated with commas, and wrap them in
304 | // brackets.
305 |
306 | v = partial.length === 0 ? '[]' :
307 | gap ? '[\n' + gap +
308 | partial.join(',\n' + gap) + '\n' +
309 | mind + ']' :
310 | '[' + partial.join(',') + ']';
311 | gap = mind;
312 | return v;
313 | }
314 |
315 | // If the replacer is an array, use it to select the members to be stringified.
316 |
317 | if (rep && typeof rep === 'object') {
318 | length = rep.length;
319 | for (i = 0; i < length; i += 1) {
320 | k = rep[i];
321 | if (typeof k === 'string') {
322 | v = str(k, value);
323 | if (v) {
324 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
325 | }
326 | }
327 | }
328 | } else {
329 |
330 | // Otherwise, iterate through all of the keys in the object.
331 |
332 | for (k in value) {
333 | if (Object.hasOwnProperty.call(value, k)) {
334 | v = str(k, value);
335 | if (v) {
336 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
337 | }
338 | }
339 | }
340 | }
341 |
342 | // Join all of the member texts together, separated with commas,
343 | // and wrap them in braces.
344 |
345 | v = partial.length === 0 ? '{}' :
346 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
347 | mind + '}' : '{' + partial.join(',') + '}';
348 | gap = mind;
349 | return v;
350 | }
351 | }
352 |
353 | // If the JSON object does not yet have a stringify method, give it one.
354 |
355 | if (typeof JSON.stringify !== 'function') {
356 | JSON.stringify = function (value, replacer, space) {
357 |
358 | // The stringify method takes a value and an optional replacer, and an optional
359 | // space parameter, and returns a JSON text. The replacer can be a function
360 | // that can replace values, or an array of strings that will select the keys.
361 | // A default replacer method can be provided. Use of the space parameter can
362 | // produce text that is more easily readable.
363 |
364 | var i;
365 | gap = '';
366 | indent = '';
367 |
368 | // If the space parameter is a number, make an indent string containing that
369 | // many spaces.
370 |
371 | if (typeof space === 'number') {
372 | for (i = 0; i < space; i += 1) {
373 | indent += ' ';
374 | }
375 |
376 | // If the space parameter is a string, it will be used as the indent string.
377 |
378 | } else if (typeof space === 'string') {
379 | indent = space;
380 | }
381 |
382 | // If there is a replacer, it must be a function or an array.
383 | // Otherwise, throw an error.
384 |
385 | rep = replacer;
386 | if (replacer && typeof replacer !== 'function' &&
387 | (typeof replacer !== 'object' ||
388 | typeof replacer.length !== 'number')) {
389 | throw new Error('JSON.stringify');
390 | }
391 |
392 | // Make a fake root object containing our value under the key of ''.
393 | // Return the result of stringifying the value.
394 |
395 | return str('', {'': value});
396 | };
397 | }
398 |
399 |
400 | // If the JSON object does not yet have a parse method, give it one.
401 |
402 | if (typeof JSON.parse !== 'function') {
403 | JSON.parse = function (text, reviver) {
404 |
405 | // The parse method takes a text and an optional reviver function, and returns
406 | // a JavaScript value if the text is a valid JSON text.
407 |
408 | var j;
409 |
410 | function walk(holder, key) {
411 |
412 | // The walk method is used to recursively walk the resulting structure so
413 | // that modifications can be made.
414 |
415 | var k, v, value = holder[key];
416 | if (value && typeof value === 'object') {
417 | for (k in value) {
418 | if (Object.hasOwnProperty.call(value, k)) {
419 | v = walk(value, k);
420 | if (v !== undefined) {
421 | value[k] = v;
422 | } else {
423 | delete value[k];
424 | }
425 | }
426 | }
427 | }
428 | return reviver.call(holder, key, value);
429 | }
430 |
431 |
432 | // Parsing happens in four stages. In the first stage, we replace certain
433 | // Unicode characters with escape sequences. JavaScript handles many characters
434 | // incorrectly, either silently deleting them, or treating them as line endings.
435 |
436 | cx.lastIndex = 0;
437 | if (cx.test(text)) {
438 | text = text.replace(cx, function (a) {
439 | return '\\u' +
440 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
441 | });
442 | }
443 |
444 | // In the second stage, we run the text against regular expressions that look
445 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
446 | // because they can cause invocation, and '=' because it can cause mutation.
447 | // But just to be safe, we want to reject all unexpected forms.
448 |
449 | // We split the second stage into 4 regexp operations in order to work around
450 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
451 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
452 | // replace all simple value tokens with ']' characters. Third, we delete all
453 | // open brackets that follow a colon or comma or that begin the text. Finally,
454 | // we look to see that the remaining characters are only whitespace or ']' or
455 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
456 |
457 | if (/^[\],:{}\s]*$/.
458 | test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
459 | replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
460 | replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
461 |
462 | // In the third stage we use the eval function to compile the text into a
463 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
464 | // in JavaScript: it can begin a block or an object literal. We wrap the text
465 | // in parens to eliminate the ambiguity.
466 |
467 | j = eval('(' + text + ')');
468 |
469 | // In the optional fourth stage, we recursively walk the new structure, passing
470 | // each name/value pair to a reviver function for possible transformation.
471 |
472 | return typeof reviver === 'function' ?
473 | walk({'': j}, '') : j;
474 | }
475 |
476 | // If the text is not JSON parseable, then a SyntaxError is thrown.
477 |
478 | throw new SyntaxError('JSON.parse');
479 | };
480 | }
481 | }());
--------------------------------------------------------------------------------
/lib/rest-adapter.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Expose `RestAdapter`.
3 | */
4 |
5 | module.exports = RestAdapter;
6 |
7 | RestAdapter.RestClass = RestClass;
8 | RestAdapter.RestMethod = RestMethod;
9 |
10 | /*!
11 | * Module dependencies.
12 | */
13 |
14 | var EventEmitter = require('events').EventEmitter
15 | , debug = require('debug')('strong-remoting:rest-adapter')
16 | , util = require('util')
17 | , inherits = util.inherits
18 | , assert = require('assert')
19 | , express = require('express')
20 | , bodyParser = require('body-parser')
21 | , cors = require('cors')
22 | , async = require('async')
23 | , HttpInvocation = require('./http-invocation')
24 | , HttpContext = require('./http-context');
25 |
26 | var json = bodyParser.json;
27 | var urlencoded = bodyParser.urlencoded;
28 | /**
29 | * Create a new `RestAdapter` with the given `options`.
30 | *
31 | * @param {Object} [options] REST options, default to `remotes.options.rest`.
32 | * @return {RestAdapter}
33 | */
34 |
35 | function RestAdapter(remotes, options) {
36 | EventEmitter.call(this);
37 |
38 | this.remotes = remotes;
39 | this.Context = HttpContext;
40 | this.options = options || (remotes.options || {}).rest;
41 | }
42 |
43 | /**
44 | * Inherit from `EventEmitter`.
45 | */
46 |
47 | inherits(RestAdapter, EventEmitter);
48 |
49 | /*!
50 | * Simplified APIs
51 | */
52 |
53 | RestAdapter.create =
54 | RestAdapter.createRestAdapter = function (remotes) {
55 | // add simplified construction / sugar here
56 | return new RestAdapter(remotes);
57 | }
58 |
59 | /**
60 | * Get the path for the given method.
61 | */
62 |
63 | RestAdapter.prototype.getRoutes = getRoutes;
64 | function getRoutes(obj) {
65 | var routes = obj.http;
66 |
67 | if(routes && !Array.isArray(routes)) {
68 | routes = [routes];
69 | }
70 |
71 | // overidden
72 | if(routes) {
73 | // patch missing verbs / routes
74 | routes.forEach(function (r) {
75 | r.verb = String(r.verb || 'all').toLowerCase();
76 | r.path = r.path || ('/' + obj.name);
77 | });
78 | } else {
79 | if(obj.name === 'sharedCtor') {
80 | routes = [{
81 | verb: 'all',
82 | path: '/prototype'
83 | }];
84 | } else {
85 | // build default route
86 | routes = [{
87 | verb: 'all',
88 | path: obj.name ? ('/' + obj.name) : ''
89 | }];
90 | }
91 | }
92 |
93 | return routes;
94 | }
95 |
96 | RestAdapter.prototype.connect = function(url) {
97 | this.connection = url;
98 | }
99 |
100 | RestAdapter.prototype.invoke = function(method, ctorArgs, args, callback) {
101 | assert(this.connection, 'Cannot invoke method without a connection. See RemoteObjects#connect().');
102 | assert(typeof method === 'string', 'method is required when calling invoke()');
103 |
104 | var lastArg = arguments[arguments.length - 1];
105 | callback = typeof lastArg === 'function' ? lastArg : undefined;
106 |
107 | ctorArgs = Array.isArray(ctorArgs) ? ctorArgs : [];
108 | if (!Array.isArray(args)) {
109 | args = ctorArgs;
110 | ctorArgs = [];
111 | }
112 |
113 | var restMethod = this.getRestMethodByName(method);
114 | var invocation = new HttpInvocation(restMethod, ctorArgs, args, this.connection);
115 | invocation.invoke(callback);
116 | }
117 |
118 | RestAdapter.prototype.getRestMethodByName = function(name) {
119 | var classes = this.getClasses();
120 | for(var i = 0; i < classes.length; i++) {
121 | var restClass = classes[i];
122 | for(var j = 0; j < restClass.methods.length; j++) {
123 | var restMethod = restClass.methods[j];
124 | if(restMethod.fullName === name) {
125 | return restMethod;
126 | }
127 | }
128 | }
129 | }
130 |
131 | /*!
132 | * Compare two routes
133 | * @param {Object} r1 The first route {route: {verb: 'get', path: '/:id'}, method: ...}
134 | * @param [Object} r2 The second route route: {verb: 'get', path: '/findOne'}, method: ...}
135 | * @returns {number} 1: r1 comes after 2, -1: r1 comes before r2, 0: equal
136 | */
137 | function sortRoutes(r1, r2) {
138 | var a = r1.route;
139 | var b = r2.route;
140 |
141 | // Normalize the verbs
142 | var verb1 = a.verb.toLowerCase();
143 | var verb2 = b.verb.toLowerCase();
144 |
145 | if (verb1 === 'del') {
146 | verb1 = 'delete';
147 | }
148 | if (verb2 === 'del') {
149 | verb2 = 'delete';
150 | }
151 | // First sort by verb
152 | if (verb1 > verb2) {
153 | return -1;
154 | } else if (verb1 < verb2) {
155 | return 1;
156 | }
157 |
158 | // Sort by path part by part using the / delimiter
159 | // For example '/:id' will become ['', ':id'], '/findOne' will become
160 | // ['', 'findOne']
161 | var p1 = a.path.split('/');
162 | var p2 = b.path.split('/');
163 | var len = Math.min(p1.length, p2.length);
164 |
165 | // Loop through the parts and decide which path should come first
166 | for (var i = 0; i < len; i++) {
167 | // Empty part has lower weight
168 | if (p1[i] === '' && p2[i] !== '') {
169 | return 1;
170 | } else if (p1[i] !== '' && p2[i] === '') {
171 | return -1;
172 | }
173 | // Wildcard has lower weight
174 | if (p1[i][0] === ':' && p2[i][0] !== ':') {
175 | return 1;
176 | } else if (p1[i][0] !== ':' && p2[i][0] === ':') {
177 | return -1;
178 | }
179 | // Now the regular string comparision
180 | if (p1[i] > p2[i]) {
181 | return 1;
182 | } else if (p1[i] < p2[i]) {
183 | return -1;
184 | }
185 | }
186 | // Both paths have the common parts. The longer one should come before the
187 | // shorter one
188 | return p2.length - p1.length;
189 | }
190 |
191 | RestAdapter.sortRoutes = sortRoutes; // For testing
192 |
193 | RestAdapter.prototype.createHandler = function () {
194 | var root = express.Router();
195 | var adapter = this;
196 | var classes = this.getClasses();
197 |
198 | // Add a handler to tolerate empty json as connect's json middleware throws an error
199 | root.use(function(req, res, next) {
200 | if(req.is('application/json')) {
201 | if(req.get('Content-Length') === '0') { // This doesn't cover the transfer-encoding: chunked
202 | req._body = true; // Mark it as parsed
203 | req.body = {};
204 | }
205 | }
206 | next();
207 | });
208 |
209 | // Set strict to be `false` so that anything `JSON.parse()` accepts will be parsed
210 | debug('remoting options: %j', this.remotes.options);
211 | var urlencodedOptions = this.remotes.options.urlencoded || {extended: true};
212 | if (urlencodedOptions.extended === undefined) {
213 | urlencodedOptions.extended = true;
214 | }
215 | var jsonOptions = this.remotes.options.json || {strict: false};
216 | var corsOptions = this.remotes.options.cors || {origin: true, credentials: true};
217 |
218 | // Optimize the cors handler
219 | var corsHandler = function(req, res, next) {
220 | var reqUrl = req.protocol + '://' + req.get('host');
221 | if (req.method === 'OPTIONS' || reqUrl !== req.get('origin')) {
222 | cors(corsOptions)(req, res, next);
223 | } else {
224 | next();
225 | }
226 | };
227 |
228 | // Set up CORS first so that it's always enabled even when parsing errors
229 | // happen in urlencoded/json
230 | root.use(corsHandler);
231 |
232 | root.use(urlencoded(urlencodedOptions));
233 | root.use(json(jsonOptions));
234 |
235 | classes.forEach(function (restClass) {
236 | var router = express.Router();
237 | var className = restClass.sharedClass.name;
238 |
239 | debug('registering REST handler for class %j', className);
240 |
241 | var methods = [];
242 | // Register handlers for all shared methods of this class sharedClass
243 | restClass
244 | .methods
245 | .forEach(function(restMethod) {
246 | var sharedMethod = restMethod.sharedMethod;
247 | debug(' method %s', sharedMethod.stringName);
248 | restMethod.routes.forEach(function(route) {
249 | methods.push({route: route, method: sharedMethod});
250 | });
251 | });
252 |
253 | // Sort all methods based on the route path
254 | methods.sort(sortRoutes);
255 |
256 | methods.forEach(function(m) {
257 | adapter._registerMethodRouteHandlers(router, m.method, m.route);
258 | });
259 |
260 | // Convert requests for unknown methods of this sharedClass into 404.
261 | // Do not allow other middleware to invade our URL space.
262 | router.use(RestAdapter.remoteMethodNotFoundHandler(className));
263 |
264 | // Mount the remoteClass router on all class routes.
265 | restClass
266 | .routes
267 | .forEach(function (route) {
268 | debug(' at %s', route.path);
269 | root.use(route.path, router);
270 | });
271 |
272 | });
273 |
274 | // Convert requests for unknown URLs into 404.
275 | // Do not allow other middleware to invade our URL space.
276 | root.use(RestAdapter.urlNotFoundHandler());
277 |
278 | // Use our own error handler to make sure the error response has
279 | // always the format expected by remoting clients.
280 | root.use(RestAdapter.errorHandler(this.remotes.options.errorHandler));
281 |
282 | return root;
283 | };
284 |
285 | RestAdapter.remoteMethodNotFoundHandler = function(className) {
286 | className = className || '(unknown)';
287 | return function restRemoteMethodNotFound(req, res, next) {
288 | var message = 'Shared class "' + className + '"' +
289 | ' has no method handling ' + req.method + ' ' + req.url;
290 | var error = new Error(message);
291 | error.status = error.statusCode = 404;
292 | next(error);
293 | };
294 | };
295 |
296 | RestAdapter.urlNotFoundHandler = function() {
297 | return function restUrlNotFound(req, res, next) {
298 | var message = 'There is no method to handle ' + req.method + ' ' + req.url;
299 | var error = new Error(message);
300 | error.status = error.statusCode = 404;
301 | next(error);
302 | };
303 | };
304 |
305 | RestAdapter.errorHandler = function(options) {
306 | options = options || {};
307 | return function restErrorHandler(err, req, res, next) {
308 | if (typeof options.handler === 'function') {
309 | try {
310 | options.handler(err, req, res, defaultHandler);
311 | } catch(e) {
312 | defaultHandler(e);
313 | }
314 | } else {
315 | return defaultHandler();
316 | }
317 |
318 | function defaultHandler(handlerError) {
319 | if(handlerError) {
320 | // ensure errors that occurred during
321 | // the handler are reported
322 | err = handlerError;
323 | }
324 | if(typeof err === 'string') {
325 | err = new Error(err);
326 | err.status = err.statusCode = 500;
327 | }
328 |
329 | res.statusCode = err.statusCode || err.status || 500;
330 |
331 | debug('Error in %s %s: %s', req.method, req.url, err.stack);
332 | var data = {
333 | name: err.name,
334 | status: res.statusCode,
335 | message: err.message || 'An unknown error occurred'
336 | };
337 |
338 | for (var prop in err) {
339 | data[prop] = err[prop];
340 | }
341 |
342 | data.stack = err.stack;
343 | if (process.env.NODE_ENV === 'production' || options.disableStackTrace) {
344 | delete data.stack;
345 | }
346 | res.send({ error: data });
347 | }
348 | };
349 | };
350 |
351 | RestAdapter.prototype._registerMethodRouteHandlers = function(router,
352 | sharedMethod,
353 | route) {
354 | var handler = sharedMethod.isStatic ?
355 | this._createStaticMethodHandler(sharedMethod) :
356 | this._createPrototypeMethodHandler(sharedMethod);
357 |
358 | debug(' %s %s %s', route.verb, route.path, handler.name);
359 | var verb = route.verb;
360 | if(verb === 'del') {
361 | // Express 4.x only supports delete
362 | verb = 'delete';
363 | }
364 | router[verb](route.path, handler);
365 | };
366 |
367 | RestAdapter.prototype._createStaticMethodHandler = function(sharedMethod) {
368 | var self = this;
369 | var Context = this.Context;
370 |
371 | return function restStaticMethodHandler(req, res, next) {
372 | var ctx = new Context(req, res, sharedMethod, self.options);
373 | self._invokeMethod(ctx, sharedMethod, next);
374 | };
375 | };
376 |
377 | RestAdapter.prototype._createPrototypeMethodHandler = function(sharedMethod) {
378 | var self = this;
379 | var Context = this.Context;
380 |
381 | return function restPrototypeMethodHandler(req, res, next) {
382 | var ctx = new Context(req, res, sharedMethod, self.options);
383 |
384 | // invoke the shared constructor to get an instance
385 | ctx.invoke(sharedMethod.ctor, sharedMethod.sharedCtor, function(err, inst) {
386 | if (err) return next(err);
387 | ctx.instance = inst;
388 | self._invokeMethod(ctx, sharedMethod, next);
389 | }, true);
390 | };
391 | };
392 |
393 | RestAdapter.prototype._invokeMethod = function(ctx, method, next) {
394 | var remotes = this.remotes;
395 | var steps = [];
396 |
397 | if (method.rest.before) {
398 | steps.push(function invokeRestBefore(cb) {
399 | debug('Invoking rest.before for ' + ctx.methodString);
400 | method.rest.before.call(remotes.getScope(ctx, method), ctx, cb);
401 | });
402 | }
403 |
404 | steps.push(
405 | this.remotes.invokeMethodInContext.bind(this.remotes, ctx, method)
406 | );
407 |
408 | if (method.rest.after) {
409 | steps.push(function invokeRestAfter(cb) {
410 | debug('Invoking rest.after for ' + ctx.methodString);
411 | method.rest.after.call(remotes.getScope(ctx, method), ctx, cb);
412 | });
413 | }
414 |
415 | async.series(
416 | steps,
417 | function(err) {
418 | if (err) return next(err);
419 | ctx.done();
420 | // Do not call next middleware, the request is handled
421 | }
422 | );
423 | };
424 |
425 | RestAdapter.prototype.allRoutes = function () {
426 | var routes = [];
427 | var adapter = this;
428 | var classes = this.remotes.classes(this.options);
429 | var currentRoot = '';
430 |
431 | classes.forEach(function (sc) {
432 | adapter
433 | .getRoutes(sc)
434 | .forEach(function (classRoute) {
435 | currentRoot = classRoute.path;
436 | var methods = sc.methods();
437 |
438 | methods.forEach(function (method) {
439 | adapter.getRoutes(method).forEach(function (route) {
440 | if(method.isStatic) {
441 | addRoute(route.verb, route.path, method);
442 | } else {
443 | adapter
444 | .getRoutes(method.sharedCtor)
445 | .forEach(function (sharedCtorRoute) {
446 | addRoute(route.verb, sharedCtorRoute.path + route.path, method);
447 | });
448 | }
449 | });
450 | });
451 | });
452 | });
453 |
454 | return routes;
455 |
456 |
457 | function addRoute(verb, path, method) {
458 | if(path === '/' || path === '//') {
459 | path = currentRoot;
460 | } else {
461 | path = currentRoot + path;
462 | }
463 |
464 | if(path[path.length - 1] === '/') {
465 | path = path.substr(0, path.length - 1);
466 | }
467 |
468 | // TODO this could be cleaner
469 | path = path.replace(/\/\//g, '/');
470 |
471 | routes.push({
472 | verb: verb,
473 | path: path,
474 | description: method.description,
475 | notes: method.notes,
476 | documented: method.documented,
477 | method: method.stringName,
478 | accepts: (method.accepts && method.accepts.length) ? method.accepts : undefined,
479 | returns: (method.returns && method.returns.length) ? method.returns : undefined,
480 | errors: (method.errors && method.errors.length) ? method.errors : undefined
481 | });
482 | }
483 | }
484 |
485 | RestAdapter.prototype.getClasses = function() {
486 | return this.remotes.classes(this.options).map(function(c) {
487 | return new RestClass(c);
488 | });
489 | };
490 |
491 | function RestClass(sharedClass) {
492 | nonEnumerableConstPropery(this, 'sharedClass', sharedClass);
493 |
494 | this.name = sharedClass.name;
495 | this.routes = getRoutes(sharedClass);
496 |
497 | this.ctor = sharedClass.sharedCtor &&
498 | new RestMethod(this, sharedClass.sharedCtor);
499 |
500 | this.methods = sharedClass.methods()
501 | .filter(function(sm) { return !sm.isSharedCtor; })
502 | .map(function(sm) {
503 | return new RestMethod(this, sm);
504 | }.bind(this));
505 | }
506 |
507 | RestClass.prototype.getPath = function() {
508 | return this.routes[0].path;
509 | };
510 |
511 | function RestMethod(restClass, sharedMethod) {
512 | nonEnumerableConstPropery(this, 'restClass', restClass);
513 | nonEnumerableConstPropery(this, 'sharedMethod', sharedMethod);
514 |
515 | // The full name is ClassName.methodName or ClassName.prototype.methodName
516 | this.fullName = sharedMethod.stringName;
517 | this.name = this.fullName.split('.').slice(1).join('.');
518 |
519 | this.accepts = sharedMethod.accepts;
520 | this.returns = sharedMethod.returns;
521 | this.errors = sharedMethod.errors;
522 | this.description = sharedMethod.description;
523 | this.notes = sharedMethod.notes;
524 | this.documented = sharedMethod.documented;
525 |
526 | var methodRoutes = getRoutes(sharedMethod);
527 | if (sharedMethod.isStatic || !restClass.ctor) {
528 | this.routes = methodRoutes;
529 | } else {
530 | var routes = this.routes = [];
531 | methodRoutes.forEach(function(route) {
532 | restClass.ctor.routes.forEach(function(ctorRoute) {
533 | var fullRoute = util._extend({}, route);
534 | fullRoute.path = joinPaths(ctorRoute.path, route.path);
535 | routes.push(fullRoute);
536 | });
537 | });
538 | }
539 | }
540 |
541 | RestMethod.prototype.isReturningArray = function() {
542 | return this.returns.length == 1 &&
543 | this.returns[0].root &&
544 | getTypeString(this.returns[0].type) === 'array' || false;
545 | };
546 |
547 | RestMethod.prototype.acceptsSingleBodyArgument = function() {
548 | if (this.accepts.length != 1) return false;
549 | var accepts = this.accepts[0];
550 |
551 | return accepts.http &&
552 | accepts.http.source == 'body' &&
553 | getTypeString(accepts.type) == 'object' || false;
554 | };
555 |
556 | RestMethod.prototype.getHttpMethod = function() {
557 | var verb = this.routes[0].verb;
558 | if (verb == 'all') return 'POST';
559 | if (verb == 'del') return 'DELETE';
560 | return verb.toUpperCase();
561 | };
562 |
563 | RestMethod.prototype.getPath = function() {
564 | return this.routes[0].path;
565 | };
566 |
567 | RestMethod.prototype.getFullPath = function() {
568 | return joinPaths(this.restClass.getPath(), this.getPath());
569 | };
570 |
571 | function getTypeString(ctorOrName) {
572 | if (typeof ctorOrName === 'function')
573 | ctorOrName = ctorOrName.name;
574 | if (typeof ctorOrName === 'string') {
575 | return ctorOrName.toLowerCase();
576 | } else if (Array.isArray(ctorOrName)) {
577 | return 'array';
578 | } else {
579 | debug('WARNING: unkown ctorOrName of type %s: %j',
580 | typeof ctorOrName, ctorOrName);
581 | return typeof undefined;
582 | }
583 | }
584 |
585 | function nonEnumerableConstPropery(object, name, value) {
586 | Object.defineProperty(object, name, {
587 | value: value,
588 | enumerable: false,
589 | writable: false,
590 | configurable: false
591 | });
592 | }
593 |
594 | function joinPaths(left, right) {
595 | if (!left) return right;
596 | if (!right || right == '/') return left;
597 |
598 | var glue = left[left.length-1] + right[0];
599 | if (glue == '//')
600 | return left + right.slice(1);
601 | else if (glue[0] == '/' || glue[1] == '/')
602 | return left + right;
603 | else
604 | return left + '/' + right;
605 | }
606 |
--------------------------------------------------------------------------------