-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
269 lines (245 loc) · 8.04 KB
/
index.js
File metadata and controls
269 lines (245 loc) · 8.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/**
* jsonchatlib - main file
* copyright (c) 2015 openmason.
* MIT Licensed.
*/
var handy = require('handy');
var logger = require('util');
var _ = require('underscore');
// * - UTF-8 compliant
/*
* Syntax:
*
* --> data sent to Server
* <-- data sent to Client
*
* keywords available in v1.0 are: id, ver, cmd, args, src, dst, tags
* - id (optional) - response would carry back id (copied from original msg)
* - ver (optional) - defaults to "1.0"
* - cmd (optional) - default command is 'pub(lish)' / broadcast in current channel
* - args (required) - input arguments (positional or named) and result
* - src (optional) - can it be populated by client-id?
* - dst (optional) - defaults to current channel, could be a channel or list of channels
* - tags (optional) - additional tags from server that needs to be passed
* - error - on response {'code': ERROR_CODE, message: 'error message(optional)'},
* args would carry the result error string
*
* publish/notification:
*
* --> {"args": "Hi, there"} // cmd='pub'
* --> {"cmd": "pub", "args": "Hi, there"}
* --> {"ver": "1.0", "cmd": "pub", "args": "Hi, there"}
*
* error response:
* <-- {"ver": "1.0", "error": {"code":510,"message":"Parse error"}}
* <-- {"error": {"code":510},"args":"Unexpected end of input"}
*
*/
// Keep the similar code to HTTP
// anything starting with 5xx - basic fundamental issue
// anything with 4xx - application specific, soft issues
var ChatErrors = {
INTERNAL_ERROR : { code:500, message: 'Internal error' },
PARSE_ERROR : { code:510, message: 'Parse error' },
INVALID_REQUEST : { code:511, message: 'Invalid request' },
CMD_NOT_FOUND : { code:520, message: 'Command not found' },
INVALID_PARAMS : { code:521, message: 'Invalid params' },
};
var _TAG_ID = 'id';
var _TAG_VER = 'ver';
var _TAG_CMD = 'cmd';
var _TAG_ARGS = 'args';
var _PUB_CMD = 'pub';
var _VAL_VER = '1.0';
// JSON chat library entry point
// 'module' implements all the extensions, by default only 'pub' is supported
var JSONChat = function(module, minimal, debug) {
this.debug = debug || false;
this.minimal = minimal || false;
this.version = _VAL_VER;
// check & load the methods in module
this.commands = module;
if(handy.getType(module)=='string') {
this.commands = require(module);
}
if(this.debug) {
logger.debug('Loaded with commands:'+_.functions(this.commands));
}
};
// main dispatcher for processing json-chat
// requests
JSONChat.prototype.dispatch = function(packet) {
var jsonBody = packet.payload;
var self=this;
self._debug(true, jsonBody);
var id;
var batch = false;
var cmdObj;
// first step is to parse the json
try {
cmdObj = JSON.parse(jsonBody);
} catch(err) {
return self.error(ChatErrors.PARSE_ERROR, id, err.message);
}
var commands = [];
var results = [];
// if cmdObj is array, then its a batch request
if(handy.getType(cmdObj)=='array') {
if(cmdObj.length==0) {
return self.error(ChatErrors.INVALID_REQUEST, id, 'empty list');
}
batch = true;
commands = cmdObj;
} else {
commands = [cmdObj];
}
// exec all commands
_.each(commands, function(cmdObj) {
var result = self._validate(cmdObj);
if(!result) {
if(!_.has(cmdObj, _TAG_CMD) || cmdObj[_TAG_CMD]==_PUB_CMD) {
// - first handle all pub commands
// those are notifications.
//self._debug(true, 'Notification ' + JSON.stringify(cmdObj));
// - just leave them as they are
result = self.result(id, cmdObj[_TAG_ARGS]);
} else {
// invoke the function
try {
var cmdArgs = [];
cmdArgs.push(packet);
cmdArgs.push.apply(cmdArgs, cmdObj[_TAG_ARGS]);
var res = self.commands[cmdObj[_TAG_CMD]].apply(null, cmdArgs);
result = self.result(cmdObj.id, res, cmdObj[_TAG_CMD]);
}
catch(err) {
result = self.error(ChatErrors.INTERNAL_ERROR, cmdObj.id, err);
}
}
}
if(result) results.push(result);
});
// return back the result
if(results.length<=0) return;
if(batch==false) {
return results[0];
}
return results;
};
// return back the result object
JSONChat.prototype.result = function(id, result, cmd) {
var res = {
args : result,
id: id,
cmd: cmd
};
if(!this.minimal) {
res[_TAG_VER] = this.version;
}
this._debug(false, res);
return res;
};
// return back the correct error object
JSONChat.prototype.error = function(err, id, data) {
var errorObj = {
id: id,
error: { code: err.code },
args: data
};
// include error message only if minimal not set
if(!this.minimal) {
errorObj[_TAG_VER] = this.version;
errorObj['error']['message'] = err.message;
}
this._debug(false, errorObj);
return errorObj;
};
// ---- private functions
// validate the command object
// - returns the error object, if any error
JSONChat.prototype._validate = function(cmdObj) {
var self = this;
var id;
// basic checks
if(handy.getType(cmdObj)!='object') {
return self.error(ChatErrors.INVALID_REQUEST, id, 'invalid object');
}
if(_.size(cmdObj)<=0) {
return self.error(ChatErrors.INVALID_REQUEST, id, 'empty object');
}
id = _.has(cmdObj, _TAG_ID)?cmdObj.id:id;
// - check for version
if(_.has(cmdObj, _TAG_VER) && cmdObj[_TAG_VER]!=_VAL_VER) {
return self.error(ChatErrors.INVALID_REQUEST, id, 'unknown version');
}
// - check for id
if(id) {
var idType = handy.getType(cmdObj.id);
if(idType != 'string' && idType != 'number') {
return self.error(ChatErrors.INVALID_REQUEST, cmdObj.id, 'id should be a valid number/string');
}
}
// - check for cmd absence
if(!_.has(cmdObj, _TAG_CMD) || cmdObj[_TAG_CMD]==_PUB_CMD) {
if(!_.has(cmdObj, _TAG_ARGS)) {
return self.error(ChatErrors.INVALID_REQUEST, id, 'args missing');
}
return; // default to cmd='pub'
}
// - check if cmd is present
var fns = _.functions(self.commands);
if(!_.include(fns, cmdObj[_TAG_CMD])) {
return self.error(ChatErrors.CMD_NOT_FOUND, id, cmdObj[_TAG_CMD] + " - unknown method");
}
// - args checks
var params=_getParamNames(self.commands[cmdObj[_TAG_CMD]]) || [];
var packet_index = params.indexOf("packet");
if (packet_index > -1) {
params.splice(packet_index, 1);
}
// - check params length
if(!_.has(cmdObj, _TAG_ARGS) && params && params.length>0) {
return self.error(ChatErrors.INVALID_PARAMS, id, 'params expected:'+params);
}
// - if params are present
// it has to be either array or object
if(_.has(cmdObj, _TAG_ARGS)) {
var ptype = handy.getType(cmdObj[_TAG_ARGS]);
if(ptype!='array' && ptype!='object') {
return self.error(ChatErrors.INVALID_PARAMS, cmdObj.id, 'params should be either array or object');
}
// @todo - not sure if this check needs to be enabled
// sometimes it might be by design that less valus can be passed
// check if array matches the arguments
if(ptype=='array' && cmdObj[_TAG_ARGS].length != params.length) {
return self.error(ChatErrors.INVALID_PARAMS, cmdObj.id, 'total params expected:'+params.length);
}
// check if the object has matching params
if(ptype=='object') {
var requestValues = [];
requestValues.push.apply(requestValues,_.keys(cmdObj[_TAG_ARGS]));
if(!handy.isArrayEqual(params, requestValues)) {
return self.error(ChatErrors.INVALID_PARAMS, cmdObj.id, 'params mismatch:'+params);
}
// lets convert the params to array
// in the order expected
cmdObj[_TAG_ARGS] = _.values(_.pick(cmdObj[_TAG_ARGS], params));
}
}
};
// returns the function parameters
function _getParamNames(func) {
var funStr = func.toString();
return funStr.slice(funStr.indexOf('(')+1, funStr.indexOf(')')).match(/([^\s,]+)/g);
}
// debug request/response statements
JSONChat.prototype._debug = function(toServer, value) {
if(this.debug) {
if(handy.getType(value)!='string') {
value = JSON.stringify(value);
}
logger.debug((toServer?'-->':'<--')+' ' + value);
}
};
module.exports = JSONChat;
// -- EOF