save point durring huge unorganized refactor

This commit is contained in:
Spencer Alger
2013-11-22 16:48:30 -07:00
parent 5bb70fbe58
commit 97ba084795
80 changed files with 46126 additions and 2410 deletions

4
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist
npm-debug.log
node_modules
scripts/scratch*
test/integration/yaml_suite/log
@ -6,4 +7,5 @@ test/integration/yaml_suite/log
## generated files
scripts/last_rest_spec_update.sha
test/browser_integration/yaml_tests.js
test/**/test-output-*.xml
test-output-*.xml
coverage.html

View File

@ -3,17 +3,6 @@
module.exports = function (grunt) {
var _ = require('lodash');
var sharedBrowserfyExclusions = [
'when',
'src/lib/connectors/http.js',
'src/lib/loggers/file.js',
'src/lib/loggers/stdio.js',
'src/lib/loggers/stream.js',
'src/lib/loggers/stream.js'
];
// Project configuration.
grunt.initConfig({
distDir: 'dist',
@ -31,20 +20,6 @@ module.exports = function (grunt) {
src: ['<%= distDir %>']
}
},
mochaTest: {
unit: 'test/unit/**/*.test.js',
yaml_suite: {
src: 'test/integration/yaml_suite/index.js',
options: {
reporter: require('./test/integration/yaml_suite/reporter')
}
},
options: {
require: 'should',
reporter: 'dot',
timeout: 11e3
}
},
jshint: {
source: {
src: [
@ -58,42 +33,6 @@ module.exports = function (grunt) {
}
}
},
watch: {
source: {
files: [
'src/**/*',
'test/**/*',
'Gruntfile.js'
],
tasks: [
'jshint:source'
]
},
options: {
interupt: true
}
},
run: {
generate_js_api: {
args: [
'scripts/generate/js_api'
]
},
generate_yaml_tests: {
args: [
'scripts/generate/yaml_tests'
]
},
integration_server: {
args: [
'test/browser_integration/server.js'
],
options: {
wait: false,
ready: /server listening/
}
}
},
browserify: {
client: {
files: {
@ -101,43 +40,34 @@ module.exports = function (grunt) {
},
options: {
standalone: 'elasticsearch',
ignore: _.union(sharedBrowserfyExclusions, [
ignore: [
'src/lib/connectors/jquery.js',
'src/lib/connectors/angular.js'
])
]
}
},
angular_client: {
angular: {
files: {
'<%= distDir %>/elasticsearch.angular.js': ['src/elasticsearch.angular.js']
},
options: {
standalone: 'elasticsearch',
ignore: _.union(sharedBrowserfyExclusions, [
ignore: [
'src/lib/connectors/jquery.js',
'src/lib/connectors/xhr.js'
])
}
},
yaml_suite: {
files: {
'test/browser_integration/yaml_tests.js': ['test/integration/yaml_suite/index.js']
},
options: {
external: [
'optimist'
'src/lib/connectors/xhr.js',
'when'
]
}
}
},
concat: {
dist_banners: {
},
jquery: {
files: {
'<%= distDir %>/elasticsearch.js': ['<%= distDir %>/elasticsearch.js'],
'<%= distDir %>/elasticsearch.angular.js': ['<%= distDir %>/elasticsearch.angular.js']
'<%= distDir %>/elasticsearch.jquery.js': ['src/elasticsearch.jquery.js']
},
options: {
banner: '<%= meta.banner %>'
ignore: [
'src/lib/connectors/angular.js',
'src/lib/connectors/xhr.js',
'when'
]
}
}
},
@ -145,145 +75,41 @@ module.exports = function (grunt) {
dist: {
files: {
'<%= distDir %>/elasticsearch.min.js': '<%= distDir %>/elasticsearch.js',
'<%= distDir %>/elasticsearch.angular.min.js': '<%= distDir %>/elasticsearch.angular.js'
'<%= distDir %>/elasticsearch.angular.min.js': '<%= distDir %>/elasticsearch.angular.js',
'<%= distDir %>/elasticsearch.jquery.min.js': '<%= distDir %>/elasticsearch.jquery.js'
}
}
},
concat: {
dist_banners: {
files: {
'<%= distDir %>/elasticsearch.angular.js': '<%= distDir %>/elasticsearch.angular.js',
'<%= distDir %>/elasticsearch.angular.min.js': '<%= distDir %>/elasticsearch.angular.min.js',
'<%= distDir %>/elasticsearch.jquery.js': '<%= distDir %>/elasticsearch.jquery.js',
'<%= distDir %>/elasticsearch.jquery.min.js': '<%= distDir %>/elasticsearch.jquery.min.js',
'<%= distDir %>/elasticsearch.js': '<%= distDir %>/elasticsearch.js',
'<%= distDir %>/elasticsearch.min.js': '<%= distDir %>/elasticsearch.min.js'
},
options: {
report: 'min',
banner: '<%= meta.banner %>'
},
global_defs: {
process: {
browser: true
}
}
}
}
});
// load plugins
grunt.loadNpmTasks('grunt-run');
grunt.loadNpmTasks('grunt-open');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
// Default task.
// Default task runs the build process.
grunt.registerTask('default', [
'generate',
'test',
'build'
'clean:dist',
'browserify',
'uglify:dist',
'concat:dist_banners'
]);
// generates the parts of the yaml test suite and api.
grunt.registerTask('generate', [
'run:generate_yaml_tests',
'run:generate_js_api'
]);
// runs the tests, must be run after generate
grunt.registerTask('test', function () {
grunt.task.requires('generate');
grunt.task.run([
'jshint',
'mochaTest:unit',
'mochaTest:yaml_suite'
]);
});
// runs the build process.
grunt.registerTask('build', function () {
grunt.task.requires('generate');
grunt.task.run([
'clean:dist',
'browserify',
'uglify:dist',
'concat:dist_banners'
]);
});
var browsers = {
safari: {
darwin: 'Safari'
},
chrome: {
darwin: 'Google Chrome',
win32: 'Google Chrome',
executable: 'google-chrome'
},
chromium: {
executable: 'chromium-browser',
},
firefox: {
darwin: 'Firefox',
win32: 'Firefox',
executable: 'firefox'
},
opera: {
darwin: 'Opera',
win32: 'Opera',
executable: 'opera'
}
};
// creates browser_tests:{{browser}} tasks, for the browsers listed directly above
Object.keys(browsers).forEach(function (browser) {
var appName = browsers[browser][process.platform];
// on other platforms, open expects app to be the name of the executale...
if (!appName && process.platform !== 'darwin' && process.platform !== 'win32') {
appName = browsers[browser].executable;
}
if (!appName) {
// this browser doesn't run on this arch
return;
}
grunt.config.set('__open_browser_tests.' + browser, {
appName: appName
});
grunt.registerTask('browser_tests:' + browser, [
'generate',
'build',
'run:integration_server',
'__open_browser_tests:' + browser
]);
});
/**
* USE browser_tests:{{browser}} to run this task
*
* Change the port/host that the client connects to with the ES_HOST and ES_PORT environment variables
*
* You must always run the build task first, to ensure that the lastest API and yaml tests are available.
* This is run in the default and browser_tests:{{browser}} tests.
*/
grunt.registerMultiTask('__open_browser_tests', function () {
var host = grunt.option('host') || 'localhost';
var port = grunt.option('port') || 9200;
var taskData = this.data;
grunt.task.requires([
'generate',
'build',
'run:integration_server'
]);
grunt.config.set('open.yaml_suite_' + this.target, {
path: 'http://localhost:8888?es_hostname=' + encodeURIComponent(host) +
'&es_port=' + encodeURIComponent(port) +
'&browser=' + encodeURIComponent(this.target),
app: taskData.appName
});
grunt.task.run([
'open:yaml_suite_' + this.target,
'wait:integration_server'
]);
});
};

0
LICENSE.md Normal file
View File

197
README.md
View File

@ -60,200 +60,9 @@ bower install elasticsearch-jquery
```
## Docs
- [Configuration](#configuration)
- [Configuration](docs/configuration.md)
- [Examples](docs/examples.md)
- [API](docs/api.md)
- [Replacing Core Components](docs/replacing_core_components.md)
- [Errors](docs/errors.md)
- [Setting Up Logging](docs/setting_up_logging.md)
- [FAQ](#faq)
## Configuration
The `Client` constructor accepts a single object as it's argument, and the following keys can be used to configure that client instance:
```js
var elasticsearch = require('elasticsearch');
var es = new elasticsearch.Client({
...
});
```
### hosts
Type: `String`, `String[]` or `Object[]`
Default:
```js
hosts: [
{
host: 'localhost', port: '9200', protocol: 'http'
}
]
```
Specify the list of hosts that this client will connect to. If sniffing is enabled, or you call sniff, this list will be used as seeds for discovery of the rest of the cluster.
### log
Type: `String`, `String[]`, `Object`, `Object[]`, or `Constructor`
Default:
```js
log: {
type: 'stdio',
levels: ['error', 'warning']
}
```
Unless a constructor is specified, this sets the output settings for the bundled logger. See [setting up logging](docs/setting_up_logging.md) for more information.
### connectionClass
Type: `String`, `Constructor`
Default:
- Node: `'http'`
- Browser: `'xhr'`
- Angular Build: `'angular'`
- jQuery Build: `'jquery'`
Defines the class that will be used to create connections. If you are looking to implement a protocol besides HTTP you will probably start by writing a Connection class and specifying it here.
### selector
Type: `String`, `Function`
Default: `'roundRobin'`
Options:
- `'roundRobin'`
- `'random'`
This function will be used to select a connection from the ConnectionPool. It should received a single argument, the list of "active" connections, and return the connection to use. Use this selector to implement special logic for your client such as preferring nodes in a certain rack or data-center.
To make this function asynchronous, accept a second argument which will be the callback to use. The callback should be called Node-style, with a possible error like `cb(err, selectedConnection)`.
### sniffOnStart
Type: `Boolean`
Default: `false`
Should the client attempt to detect the rest of the cluster when it is first instantiated?
### sniffAfterRequests
Type: `Number` or `false`
Default: `false`
After `n` requests, perform a sniff operation and ensure our list of nodes is up to date.
### sniffOnConnectionFail
Type: `Boolean`
Default: `false`
Should the client immediately sniff for a more current list of nodes when a connection dies? (see [node death](#node-death))
### maxRetries
Type: `Number`
Defailt: `3`
How many times should the client try to connect to other nodes before returning a [ConnectionFault](docs/error.md#connectionfault) error. (see [node death](#node-death))
### timeout
Type: `Number`
Default: 10000
How many milliseconds can the connection take before the request is aborted and retried. (TODO: timeout errors shouldn't cause a retry).
### deadTimeout
Type: `Number`
Default: 30000
How many milliseconds should a dead connection/node sit and wait before it is ping-ed? (see [node death](#node-death))
### maxSockets
Type: `Number`
Default: 10
How many sockets should a connection keep to it's corresponding Elasticsearch node? These sockets are currently kept alive ***forever*** (not like nodes current "keep alive" sockets).
### nodesToHostCallback
Type: `Function`
Default: simple, not much going on [here](src/lib/client_config.js#L65).
This function will receive a list of nodes received during a sniff. The list of nodes should be transformed into an array of objects which will each be used to create [Host](src/lib/host.js) objects. (TODO: allow this function to be async).
## API
To maintain consistency across all the low-level clients ([PHP](https://github.com/elasticsearch/elasticsearch-php), [Python](https://github.com/elasticsearch/elasticsearch-ph), [Ruby](https://github.com/elasticsearch/elasticsearch-ruby), [Perl](https://github.com/elasticsearch/elasticsearch-perl)) all API methods accept an object with parameters and a callback. If you don't pass the callback, the functions will return a promise.
For full details on the API, check out [api.md](docs/api.md).
### Examples
#### create the client
```js
var es = new elasticsearch.Client({
hosts: [
'localhost:9200'
],
log: 'trace',
sniffOnStart: true
});
```
#### call an endpoint
```js
es.cluster.nodeInfo({
clear: true,
jvm: true,
os: ture
}, function (err, resp, status) {
// do your thing
})
```
#### skip the callback to get a promise back
```js
es.search({
q: 'pants'
}).then(function (resp) {
// use resp.body and resp.status
}, function (err) {
// freak out!
})
```
#### abort a request
```js
var req = es.search({
q: 'robots'
}, function (err, body, status) {
clearTimeout(timeout);
// do something
});
var timeout = setTimeout(function () {
req.abort();
}, 200);
```
#### or just use the timeout param
```js
es.search({
q: '*',
timeout: 200
}).then(function (resp) {
// Iterate all the hits
})
```
## FAQ
### dead nodes
Q: When is a connection/node considered dead?
A: A connection is considered dead when a request to it does not complete properly. If the server responds with any status, even 500, it is not considered dead.
- [Customize Logging](docs/customize_logging.md)

117
docs/configuration.md Normal file
View File

@ -0,0 +1,117 @@
The `Client` constructor accepts a single object as it's argument, and the following keys can be used to configure that client instance:
```js
var elasticsearch = require('elasticsearch');
var es = new elasticsearch.Client({
...
});
```
### hosts
Type: `String`, `String[]` or `Object[]`
Default:
```js
hosts: [
{
host: 'localhost', port: '9200', protocol: 'http'
}
]
```
Specify the list of hosts that this client will connect to. If sniffing is enabled, or you call sniff, this list will be used as seeds for discovery of the rest of the cluster.
### log
Type: `String`, `String[]`, `Object`, `Object[]`, or `Constructor`
Default:
```js
log: {
type: 'stdio',
levels: ['error', 'warning']
}
```
Unless a constructor is specified, this sets the output settings for the bundled logger. See [setting up logging](docs/setting_up_logging.md) for more information.
### connectionClass
Type: `String`, `Constructor`
Default:
- Node: `'http'`
- Browser: `'xhr'`
- Angular Build: `'angular'`
- jQuery Build: `'jquery'`
Defines the class that will be used to create connections. If you are looking to implement a protocol besides HTTP you will probably start by writing a Connection class and specifying it here.
### selector
Type: `String`, `Function`
Default: `'roundRobin'`
Options:
- `'roundRobin'`
- `'random'`
This function will be used to select a connection from the ConnectionPool. It should received a single argument, the list of "active" connections, and return the connection to use. Use this selector to implement special logic for your client such as preferring nodes in a certain rack or data-center.
To make this function asynchronous, accept a second argument which will be the callback to use. The callback should be called Node-style, with a possible error like `cb(err, selectedConnection)`.
### sniffOnStart
Type: `Boolean`
Default: `false`
Should the client attempt to detect the rest of the cluster when it is first instantiated?
### sniffAfterRequests
Type: `Number` or `false`
Default: `false`
After `n` requests, perform a sniff operation and ensure our list of nodes is up to date.
### sniffOnConnectionFail
Type: `Boolean`
Default: `false`
Should the client immediately sniff for a more current list of nodes when a connection dies? (see [node death](#node-death))
### maxRetries
Type: `Number`
Defailt: `3`
How many times should the client try to connect to other nodes before returning a [ConnectionFault](docs/error.md#connectionfault) error. (see [node death](#node-death))
### timeout
Type: `Number`
Default: 10000
How many milliseconds can the connection take before the request is aborted and retried. (TODO: timeout errors shouldn't cause a retry).
### deadTimeout
Type: `Number`
Default: 30000
How many milliseconds should a dead connection/node sit and wait before it is ping-ed? (see [node death](#node-death))
### maxSockets
Type: `Number`
Default: 10
How many sockets should a connection keep to it's corresponding Elasticsearch node? These sockets are currently kept alive ***forever*** (not like nodes current "keep alive" sockets).
### nodesToHostCallback
Type: `Function`
Default: simple, not much going on [here](src/lib/client_config.js#L65).
This function will receive a list of nodes received during a sniff. The list of nodes should be transformed into an array of objects which will each be used to create [Host](src/lib/host.js) objects. (TODO: allow this function to be async).

View File

@ -1,3 +1,3 @@
# Setting Up Logging
# Customize Logging
TODO: what are loggers, how to use bunyan/winston

52
docs/examples.md Normal file
View File

@ -0,0 +1,52 @@
### Examples
#### create the client
```js
var es = new elasticsearch.Client({
hosts: [
'localhost:9200'
],
log: 'trace',
sniffOnStart: true
});
```
#### call an endpoint
```js
es.cluster.nodeInfo({
clear: true,
jvm: true,
os: ture
}, function (err, resp, status) {
// do your thing
})
```
#### skip the callback to get a promise back
```js
es.search({
q: 'pants'
}).then(function (resp) {
// use resp.body and resp.status
}, function (err) {
// freak out!
})
```
#### abort a request
```js
var req = es.search({
q: 'robots'
}, function (err, body, status) {
clearTimeout(timeout);
// do something
});
var timeout = setTimeout(function () {
req.abort();
}, 200);
```
#### or just use the timeout param
```js
es.search({
q: '*',
timeout: 200
}).then(function (resp) {
// Iterate all the hits
})
```

View File

@ -6,6 +6,10 @@
"name": "elasticsearch-js",
"homepage": "https://github.com/elasticsearch/elasticsearch-js",
"version": "0.0.1",
"browser": {
"./src/lib/connectors/index.js": "./src/lib/connectors/browser_index.js",
"./src/lib/loggers/index.js": "./src/lib/loggers/browser_index.js"
},
"devDependencies": {
"tar": "~0.1.18",
"mocha": "~1.14.0",
@ -25,19 +29,34 @@
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-concat": "~0.3.0",
"grunt-open": "~0.2.2",
"grunt-run": "~0.1.0",
"xmlbuilder": "~0.4.3",
"grunt-contrib-watch": "~0.5.3"
"grunt-contrib-watch": "~0.5.3",
"coveralls": "~2.3.0",
"mocha-lcov-reporter": "0.0.1",
"blanket": "~1.1.5",
"sinon": "~1.7.3",
"nock": "~0.23.0",
"open": "0.0.4",
"testling": "https://github.com/spenceralger/testling/tarball/master"
},
"license": "Apache License",
"dependencies": {
"when": "~2.6.0",
"lodash": "~2.3.0",
"agentkeepalive": "~0.1",
"chalk": "~0.3.0"
"chalk": "~0.3.0",
"inherits": "~2.0.1"
},
"scripts": {
"test": "grunt generate test"
"test": "node scripts/run_tests.js",
"build": "grunt",
"generate": "node scripts/generate/js_api && node scripts/generate/yaml_tests",
"blanket": {
"pattern": "src"
}
},
"testling": {
"harness": "mocha",
"files": "test/unit/test_!(http_connection|stdio_logger).js"
}
}

View File

@ -95,9 +95,13 @@ function transformFile(entry) {
});
});
spec.urls = _.map(_.sortBy(urls, 'sortOrder'), function (url) {
return _.omit(url, 'sortOrder');
});
if (urls.length > 1) {
spec.urls = _.map(_.sortBy(urls, 'sortOrder'), function (url) {
return _.omit(url, 'sortOrder');
});
} else {
spec.url = urls[0];
}
spec.params = _.transform(spec.params, function (note, param, name) {
// param.name = name;
@ -106,6 +110,10 @@ function transformFile(entry) {
]);
}, {});
if (_.size(spec.params) === 0) {
delete spec.params;
}
// escape method names with "special" keywords
var location = spec.name.split('.').join('.prototype.')
.replace(/(^|\.)(delete|default)(\.|$)/g, '[\'$2\']');
@ -114,6 +122,7 @@ function transformFile(entry) {
spec: _.pick(spec, [
'methods',
'params',
'url',
'urls',
'needBody',
'bulkBody',
@ -126,6 +135,61 @@ function transformFile(entry) {
allParams: allParams
};
function hasMethod(/* ...methods */) {
for (var i = 0; i < arguments.length; i++) {
if (~action.spec.methods.indexOf(arguments[i])) {
continue;
} else {
return false;
}
}
return true;
}
function methodsAre(/* ...methods */) {
return hasMethod.apply(null, arguments) && arguments.length === action.spec.methods.length;
}
var method;
if (action.spec.methods.length === 1) {
method = action.spec.methods[0];
} else {
// we need to define what the default method(s) will be
if (hasMethod('DELETE', 'POST')) {
method = 'POST';
}
else if (methodsAre('DELETE')) {
method = 'DELETE';
}
else if (methodsAre('POST', 'PUT')) {
method = 'POST';
}
else if (methodsAre('GET', 'POST')) {
method = 'POST';
}
else if (methodsAre('GET', 'HEAD')) {
if (action.spec.castExists) {
method = 'HEAD';
} else {
method = 'GET';
}
}
}
if (method) {
if (method !== 'GET') {
action.spec.method = method;
}
delete action.spec.methods;
} else {
throw new Error('unable to pick a method for ' + JSON.stringify(action, null, ' '));
}
if (action.name === 'create') {
action.proxy = 'index';
action.transformBody = 'params.op_type = \'create\';';
}
if (actions.push(action) === specCount && doneParsing) {
module.exports.emit('ready', action);
}

View File

@ -17,12 +17,21 @@ function download() {
}
}));
// seperate the proxy actions
var groups = _.groupBy(actions, function (action) {
return action.proxy ? 'proxies' : 'normal';
});
clean(outputPath);
console.log('writing', actions.length, 'api actions to', outputPath);
fs.writeFileSync(outputPath, templates.apiFile({
actions: actions,
actions: groups.normal,
proxies: groups.proxies,
namespaces: _.unique(namespaces.sort(), true)
}));
fs.writeFileSync(docOutputPath, templates.apiDocs({
actions: actions
}));
@ -30,7 +39,7 @@ function download() {
}
restSpecUpdated(function (err, updated) {
if (process.env.FORCE_GEN || err || updated) {
if (process.env.FORCE_GEN || process.env.npm_config_force || err || updated) {
download();
}
});

View File

@ -1,73 +0,0 @@
var _ = require('<%= path2lib %>utils');
var errors = require('<%= path2lib %>errors');<%
if (_.keys(enumOptions).length) {
%>
<% _.each(enumOptions, function(options, name) {
%>
var <%= name %>Options = <%= stringify(options) %>;<%
});
}
%>
/**
* Perform an elasticsearch [<%= name %>](<%= docUrl %>) request
*
* @for Client
* @method <%= name %>
* @param {Object} params - An object with parameters used to carry out this action<% _.each(params, function(param, paramName) { %>
* @param {<%= paramType(param.type) %>} <%= paramWithDefault('params.' + paramName, param.default) %><% if (param.description) { %> - <%= param.description %><% } %><%
})
%>
*/
function do<%= _.studlyCase(name) %>(params, cb) {
if (typeof params === 'function') {
cb = params;
params = {};
} else {
params = params || {};
cb = typeof cb === 'function' ? cb : _.noop;
}
var request = {
<%= writeRequestObjectBody(4, name, body, methods) %>
};
var parts = {};
var query = {};
var responseOpts = {};
<%
if (methods.length > 1) { %>
// figure out the method
if (params.method = _.toUpperString(params.method)) {
if (<%= _.map(methods, function (method) { return 'params.method === ' + stringify(method) }).join(' || ') %>) {
request.method = params.method;
} else {
throw new TypeError('Invalid method: should be one of <%= methods.join(', ') %>');
}
} else {<%
if (_.contains(methods, 'GET')) {
var nonGet = _.find(methods, function (m) {return m !== 'GET'; });%>
request.method = params.body ? <%= stringify(nonGet) %> : 'GET';<%
} else {%>
request.method = <%= stringify(methods[0]) %>;<%
}%>
}<%
}
%>
// find the paths's params
<%= writeParams(2, urlParts, 'parts.') %>
// build the path
<%= writeUrls(2, urls, urlParts, params) %>
// build the query string
<%= writeParams(2, params, 'query.') %>
request.path = request.path + _.makeQueryString(query);
<%= returnStatement(2, name) %>
}
module.exports = do<%= _.studlyCase(name) %>;

View File

@ -12,16 +12,16 @@ _.each(actions, function (action) {
var className = _.studlyCase(namespace) + 'NS';
%>
api.<%= namespace %> = function <%= className %>(client) {
if (this instanceof <%= className %>) {
this.client = client;
} else {
return new <%= className %>(client);
}
api.<%= namespace %> = function <%= className %>(transport) {
this.transport = transport;
};<%
}
%>
}%>
<%= partials.client_action(action) %><%
}); %>
});
_.each(proxies, function (action) {%>
<%= partials.client_action_proxy(action) %><%
});
%>

View File

@ -0,0 +1,19 @@
/**
* Perform a [<%= name %>](<%= docUrl %>) request
*
* @param {Object} params - An object with parameters used to carry out this action<%
_.each(allParams, function(param, paramName) { %>
* @param {<%= paramType(param.type) %>} <%= paramWithDefault('params.' + paramName, param.default) %><%
if (param.description) {
%> - <%= param.description %><%
}
%><% }) %>
*/
api<%= (location[0] === '[' ? '' : '.') + location %> = ca.proxy(<%= 'api' + (proxy[0] === '[' ? '' : '.') + proxy %><%
if (typeof transformBody === 'string') { %>, {
transform: function (params) {
<%= indent(transformBody, 4) %>
}
}<%
}
%>);

View File

@ -42,6 +42,13 @@ var templateGlobals = {
_: _,
indent: function (block, spaces) {
var indent = _.repeat(' ', spaces);
return block.split('\n').map(function (line) {
return indent + line;
}).join('\n');
},
paramType: function (type) {
switch (type && type.toLowerCase ? type.toLowerCase() : 'any') {
case 'time':

View File

@ -34,7 +34,8 @@ var es = require('../../../src/elasticsearch'),
endingMoment = moment().endOf('day').add('days', days),
clientConfig = {
log: {
level: 'error'
level: 'trace',
type: 'stdio'
}
};
@ -95,7 +96,7 @@ fillIndecies(function () {
actions.push(event);
if (actions.length === 3000 || i === count - 1) {
client.config.log.info('writing', actions.length / 2, 'documents');
console.info('writing', actions.length / 2, 'documents');
client.bulk({
body: actions
}, done);
@ -176,10 +177,11 @@ function fillIndecies(cb) {
async.parallel(indexPushActions, function (err, responses) {
if (err) {
client.config.log.error(err.message = 'Unable to create indicies: ' + err.message);
console.error(err.message = 'Unable to create indicies: ' + err.message);
console.error(err.stack);
} else {
_.each(_.groupBy(responses), function (list, did) {
client.config.log.info(list.length, 'indicies', did);
console.info(list.length, 'indicies', did);
});
cb();
}

View File

@ -32,7 +32,7 @@ function download() {
restSpecUpdated(function (err, updated) {
if (process.env.FORCE_GEN || err || updated) {
if (process.env.FORCE_GEN || process.env.npm_config_force || err || updated) {
download();
}
});

View File

@ -1,5 +1,11 @@
var https = require('https');
var lastCommitUrl = 'https://api.github.com/repos/elasticsearch/elasticsearch-rest-api-spec/commits/HEAD';
var request = {
hostname: 'api.github.com',
path: '/repos/elasticsearch/elasticsearch-rest-api-spec/commits/HEAD',
headers: {
'User-Agent': 'spenceralger'
}
};
var fs = require('fs');
var lastRestSpecUpdateFile = __dirname + '/last_rest_spec_update.sha';
@ -10,7 +16,7 @@ if (fs.existsSync(lastRestSpecUpdateFile)) {
lastRestSpecUpdate = fs.readFileSync(lastRestSpecUpdateFile, 'utf8');
}
var req = https.get(lastCommitUrl, function (incoming) {
var req = https.get(request, function (incoming) {
if (incoming.statusCode !== 200) {
req.abort();
console.error('request for last commit failed', incoming.statusCode, incoming.headers);

View File

@ -0,0 +1,146 @@
var server = require('./server');
var child_process = require('child_process');
var _ = require('lodash');
var open = require('open');
var fs = require('fs');
var path = require('path');
var async = require('async');
var yamlTestSourceFile = path.join(__dirname, '../../test/integration/yaml_suite/index.js');
var yamlTestBundleFile = path.join(__dirname, '../../test/browser_integration/yaml_tests.js');
var clientEntryFile = path.join(__dirname, '../../src/elasticsearch.js');
var browsers = _.transform({
safari: {
darwin: 'Safari'
},
chrome: {
darwin: 'Google Chrome',
win32: 'Google Chrome',
executable: 'google-chrome'
},
chromium: {
executable: 'chromium-browser',
},
firefox: {
darwin: 'Firefox',
win32: 'Firefox',
executable: 'firefox'
},
opera: {
darwin: 'Opera',
win32: 'Opera',
executable: 'opera'
}
}, function (browsers, config, name) {
if (config[process.platform]) {
browsers[name] = config[process.platform];
return;
}
if (process.platform !== 'darwin' && process.platform !== 'win32' && config.executable) {
browsers[name] = config.executable;
return;
}
}, {});
var argv = require('optimist')
.default('browser', 'chrome')
.default('force_gen', false)
.boolean('force_gen')
.alias('f', 'force_gen')
.default('host', 'localhost')
.default('port', 9200)
.argv;
var browserAppName;
async.series([
function (done) {
if (browsers.hasOwnProperty(argv.browser)) {
browserAppName = browsers[argv.browser];
done();
} else {
done('--browser must be set to one of ' + _.keys(browsers).join(', ') + ' on this platform');
}
},
function (done) {
fs.exists('dist', function (yes) {
if (!argv.force_gen && yes) {
done();
return;
}
console.log('generating client with "grunt build"');
child_process.spawn('grunt', ['build'], {
stdio: 'inherit'
}).on('close', function (status) {
done(status && 'grunt closed with a status code of ' + status + '. aborting.');
});
});
},
function (done) {
fs.exists(yamlTestBundleFile, function (yes) {
if (!argv.force_gen && yes) {
done();
return;
}
console.log('generating browser\'s yaml_tests.js bundle');
var b = require('browserify')();
b.add(yamlTestSourceFile);
var bundle = b.bundle({
external: [
'optimist'
],
ignore: [
'test/integration/yaml_suite/reporter',
clientEntryFile
]
});
var file = fs.createWriteStream(yamlTestBundleFile, {
flags: 'w',
encoding: 'utf8',
mode: 0666
});
bundle.pipe(file);
file.once('error', function (err) {
done(err);
});
bundle.once('error', function (err) {
done(err);
});
bundle.once('end', function () {
done();
});
});
}
], function (err) {
if (err) {
console.error(err);
process.exit(1);
} else {
server.listen(0, function () {
var port = server.address().port;
console.log('server listening on port', port);
open('http://localhost:' + port + '?es_hostname=' + encodeURIComponent(argv.host) +
'&es_port=' + encodeURIComponent(argv.port) +
'&browser=' + encodeURIComponent(argv.browser), browserAppName);
});
server.on('tests done', function (success) {
console.log('test completed', success ? 'successfully' : 'but failed');
process.exit(success ? 0 : 1);
});
}
});

View File

@ -2,22 +2,43 @@ var http = require('http');
var url = require('url');
var path = require('path');
var fs = require('fs');
var crypto = require('crypto');
var _ = require('lodash');
var util = require('util');
var chalk = require('chalk');
var moment = require('moment');
var makeJUnitXml = require('../make_j_unit_xml');
chalk.enabled = true;
var browserify = require('browserify');
var port = process.argv[2] || 8888;
var middleware = [];
Error.stackTraceLimit = Infinity;
var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
var chars = 'abcdefghijklmnopqrstuvwxyz';
var server = http.createServer(function (req, resp) {
var parsedUrl = url.parse(req.url, true);
req.uri = parsedUrl.pathname;
req.query = parsedUrl.query;
req.filename = path.join(__dirname, '../../test/browser_integration/', req.uri);
var end = resp.end;
resp.end = function () {
console.log(chalk[this.statusCode < 300 ? 'green' : 'red'](this.statusCode), req.uri);
end.apply(resp, arguments);
};
var middleIndex = -1;
function next() {
middleIndex++;
if (middleIndex < middleware.length) {
middleware[middleIndex](req, resp, next);
} else {
resp.writeHead(500);
resp.end('500 Bad Gateway\n');
}
}
next();
});
function rand(length) {
var str = '';
while (str.length < length) {
@ -26,31 +47,10 @@ function rand(length) {
return str;
}
function sendBundle(req, resp, files, opts, extend) {
resp.setHeader('Content-Type', 'application/javascript');
resp.writeHead(200);
var b = browserify(files);
if (typeof extend === 'function') {
extend(b);
}
var out = b.bundle(opts);
out.on('data', function (chunk) {
resp.write(chunk);
});
out.on('end', function () {
resp.end();
});
}
function collectTestResults(req, resp) {
var body = '';
var browser = req.query.browser;
var logFilename = path.join(__dirname, '../test-output-' + browser + '.xml');
var logFilename = path.join(__dirname, '../../test-output-' + browser + '.xml');
req.on('data', function (chunk) {
body += chunk;
@ -79,40 +79,15 @@ function collectTestResults(req, resp) {
if (err) {
console.log('unable to save test-output to', err.message);
console.trace();
process.exit(1);
server.emit('tests done', false);
} else {
console.log('test output written to', logFilename);
process.exit(testDetails.stats.failures ? 1 : 0);
server.emit('tests done', !testDetails.stats.failures);
}
});
});
}
var server = http.createServer(function (req, resp) {
var parsedUrl = url.parse(req.url, true);
req.uri = parsedUrl.pathname;
req.query = parsedUrl.query;
req.filename = path.join(__dirname, req.uri);
var end = resp.end;
resp.end = function () {
console.log(chalk[this.statusCode < 300 ? 'green' : 'red'](this.statusCode), req.uri);
end.apply(resp, arguments);
};
var middleIndex = -1;
function next() {
middleIndex++;
if (middleIndex < middleware.length) {
middleware[middleIndex](req, resp, next);
} else {
resp.writeHead(500);
resp.end('500 Bad Gateway\n');
}
}
next();
});
middleware.push(function (req, resp, next) {
// resolve filenames
@ -145,14 +120,14 @@ middleware.push(function (req, resp, next) {
resp.end();
} else {
if (stats.isDirectory()) {
req.filename = path.join(req.filename, './index.html');
req.filename = path.join(req.filename, '../../test/browser_integration/index.html');
}
next();
}
});
});
middleware.push(function (req, resp, next) {
middleware.push(function (req, resp) {
// static files
var reader = fs.createReadStream(req.filename);
var data = '';
@ -201,7 +176,7 @@ middleware.push(function (req, resp, next) {
es_hostname: 'localhost',
es_port: 9200,
browser: 'unknown',
ts: rand(5)
ts: 'no'//rand(5)
})));
} else {
resp.end(data);
@ -211,6 +186,5 @@ middleware.push(function (req, resp, next) {
}
});
server.listen(parseInt(port, 10), function () {
console.log('server listening on port', server.address().port);
});
module.exports = server;

81
scripts/run_tests.js Normal file
View File

@ -0,0 +1,81 @@
var async = require('async');
var cp = require('child_process');
var chalk = require('chalk');
var argv = require('optimist')
.default({
'check-upstream': false,
'in-node': true,
'in-browser': true,
'not-in-node': false,
'not-in-browser': false,
'unit': true,
'integration': true
})
.alias({
u: 'unit',
i: 'integration',
b: 'in-browser',
n: 'in-node',
})
.parse(JSON.parse(process.env.npm_config_argv).original);
if (argv['not-in-browser']) {
argv.b = argv['in-browser'] = false;
}
if (argv['not-in-node']) {
argv.n = argv['in-node'] = false;
}
var commands = [];
if (argv['check-upstream']) {
commands.push(['node', 'scripts/generate/yaml_tests/index.js']);
}
if (argv.unit) {
if (argv['in-node']) {
commands.push(['mocha', 'test/unit/test_*.js', '--require=should']);
}
if (argv['in-browser']) {
commands.push(['testling', '.']);
}
}
if (argv.integration) {
if (argv['in-node']) {
commands.push(['mocha', 'test/integration/yaml_suite/index.js', '-b', '--require=should']);
}
if (argv['in-browser']) {
commands.push(['node', 'scripts/run_browser_integration_suite/index.js']);
}
}
if (commands.length) {
async.forEachSeries(commands, function (args, done) {
var command = args.shift();
console.log(chalk.gray('\n\n' + '# ' + command + ' ' + args.join(' ')));
var proc = cp.spawn(command, args, {
stdio: 'inherit'
});
proc.on('error', function (err) {
proc.removeAllListeners();
done(err);
});
proc.on('exit', function (status) {
proc.removeAllListeners();
done(status ? new Error(command + ' exited with status ' + status) : void 0);
});
}, function (err) {
if (err) {
console.error(err.message);
process.exit(1);
} else {
process.exit(0);
}
});
} else {
console.log('Arguments resulted in no tests to run.');
console.log('Try combining test types with environments');
}

View File

@ -1,28 +0,0 @@
var http = require('http');
var server = http.createServer(function (req, resp) {
var closed, count = 0;
resp.on('close', function () {
closed = true;
console.log('response was closed');
});
process.removeAllListeners();
var interval = setInterval(function () {
if (count > 99 || resp.closed || closed) {
clearInterval(interval);
console.log('done writing', resp.socket.bytesWritten, 'bytes');
resp.end();
} else {
process.stdout.write('->');
resp.write('line of data, more to come... slowly!');
count++;
}
}, 100);
});
server.listen(7500, function () {
console.log('server listening at', server.address());
});

View File

@ -5,40 +5,27 @@
* It will also instruct the client to use Angular's $http service for it's ajax requests
*/
var AngularConnector = require('./lib/connectors/angular');
var Transport = require('./lib/transport');
var Client = require('./lib/client');
process.angular_build = true;
/* global angular */
angular.module('elasticsearch.client', [])
.factory('esFactory', ['$http', '$q', function ($http, $q) {
AngularConnector.prototype.$http = $http;
AngularConnector.prototype.$http = $http;
// store the original request function
Transport.prototype._request = Transport.prototype.request;
var factory = function (config) {
config = config || {};
config.connectionClass = AngularConnector;
config.createDefer = function () {
return $q.defer();
};
return new Client(config);
};
// overwrite the request function to return a promise
// and support the callback
Transport.prototype.request = function (params, cb) {
var deferred = $q.defer();
this._request(params, function (err, body, status) {
if (typeof cb === 'function') {
cb(err, body, status);
}
factory.errors = require('./lib/errors');
factory.ConnectionPool = require('./lib/connection_pool');
factory.Transport = require('./lib/transport');
if (err) {
deferred.reject(err);
} else {
deferred.resolve({ body: body, status: status });
}
});
return deferred.promise;
};
return function (config) {
config = config || {};
config.connectionClass = AngularConnector;
return new Client(config);
};
}]);
}]);

View File

@ -0,0 +1,4 @@
process.jquery_build = true;
/* global jQuery */
jQuery.es = require('./elasticsearch');

View File

@ -1,5 +1,8 @@
var es = {
Client: require('./lib/client')
Client: require('./lib/client'),
errors: require('./lib/errors'),
ConnectionPool: require('./lib/connection_pool'),
Transport: require('./lib/transport')
};
module.exports = es;
module.exports = es;

File diff suppressed because it is too large Load Diff

View File

@ -22,33 +22,20 @@
*
* @class Client
* @constructor
* @param {Object} [config={}] - Configuration for the transport
* @param {Object} [config.transport] - Transport settings passed to {{#crossLink "Transport"}}Transport Constructor{{/crossLink}}
* @param {String|Array<String>} [config.log] - Log output settings {{#crossLink "Log"}}Log Constructor{{/crossLink}}
* @param {Object} [config.trace=false] - Create a log output to stdio that only tracks trace logs
*/
module.exports = Client;
var _ = require('./utils');
var ClientConfig = require('./client_config');
var api = require('./api.js');
var ca = require('./client_action');
var Transport = require('./transport');
function Client(config) {
this.client = this;
this.transport = new Transport(config);
// setup the config.. this config will be passed EVERYWHERE so for good measure it is locked down
Object.defineProperty(this, 'config', {
configurable: false,
enumerable: false,
writable: false,
value: !config || _.isPlainObject(config) ? new ClientConfig(config) : config,
});
this.config.client = this;
// instansiate the api's namespaces
// instantiate the api's namespaces
for (var i = 0; i < this._namespaces.length; i++) {
this[this._namespaces[i]] = new this[this._namespaces[i]](this);
this[this._namespaces[i]] = new this[this._namespaces[i]](this.transport);
}
}
@ -60,19 +47,14 @@ Client.prototype = api;
* @param {Object} params - Currently just a placeholder, no params used at this time
* @param {Function} cb - callback
*/
Client.prototype.ping = function (params, cb) {
if (typeof params === 'function') {
cb = params;
params = {};
}
this.config.transport.request({
method: 'HEAD',
path: '/',
timeout: 100,
}, cb);
};
Client.prototype.ping = ca({
method: 'HEAD',
url: {
fmt: '/'
},
timeout: 100
});
Client.prototype.close = function () {
this.config.close();
this.transport.close();
};

View File

@ -2,21 +2,50 @@
* Constructs a function that can be called to make a request to ES
* @type {[type]}
*/
module.exports = function ClientAction(spec, client) {
return function (params, cb) {
return exec((client || this.client).config.transport, spec, params, cb);
};
};
module.exports = ClientAction;
var _ = require('./utils');
var when = require('when');
function ClientAction(spec) {
if (!_.isPlainObject(spec.params)) {
spec.params = {};
}
if (!spec.method) {
spec.method = 'GET';
}
return function (params, cb) {
if (typeof params === 'function') {
cb = params;
params = {};
} else {
params = params || {};
cb = typeof cb === 'function' ? cb : null;
}
try {
return exec(this.transport, spec, params, cb);
} catch (e) {
if (typeof cb === 'function') {
_.nextTick(cb, e);
} else {
return when.reject(e);
}
}
};
}
var castType = {
enum: function (param, val, name) {
if (_.contains(param.options, val)) {
return val;
} else {
throw new TypeError('Invalid ' + name + ': expected one of ' + param.options.join(','));
/* jshint eqeqeq: false */
for (var i = 0; i < param.options.length; i++) {
if (param.options[i] == val) {
return param.options[i];
}
}
throw new TypeError('Invalid ' + name + ': expected one of ' + param.options.join(','));
},
duration: function (param, val, name) {
if (_.isNumeric(val) || _.isInterval(val)) {
@ -30,17 +59,16 @@ var castType = {
},
list: function (param, val, name) {
switch (typeof val) {
case 'number':
case 'string':
return val;
case 'object':
if (_.isArray(val)) {
return val.join(',');
} else {
throw new TypeError('Invalid ' + name + ': expected be a comma seperated list, array, or boolean.');
}
break;
/* falls through */
default:
return !!val;
throw new TypeError('Invalid ' + name + ': expected be a comma seperated list, array, number or string.');
}
},
boolean: function (param, val) {
@ -55,18 +83,25 @@ var castType = {
}
},
string: function (param, val, name) {
if (typeof val !== 'object' && val) {
switch (typeof val) {
case 'number':
case 'string':
return '' + val;
} else {
default:
throw new TypeError('Invalid ' + name + ': expected a string.');
}
},
time: function (param, val, name) {
if (typeof val === 'string' || _.isNumeric(val)) {
if (typeof val === 'string') {
return val;
} else if (val instanceof Date) {
return val.getTime();
} else {
}
else if (_.isNumeric(val)) {
return '' + val;
}
else if (val instanceof Date) {
return '' + val.getTime();
}
else {
throw new TypeError('Invalid ' + name + ': expected some sort of time.');
}
}
@ -88,8 +123,12 @@ function resolveUrl(url, params) {
// missing a required param
return false;
} else {
// copy param vals into vars
vars[key] = params[key];
// cast of copy required param
if (castType[url.req[key].type]) {
vars[key] = castType[url.req[key].type](url.req[key], params[key], key);
} else {
vars[key] = params[key];
}
}
}
}
@ -127,60 +166,27 @@ function resolveUrl(url, params) {
}, {}));
}
function exec(transport, spec, params, cb) {
if (typeof params === 'function') {
cb = params;
params = {};
} else {
params = params || {};
cb = typeof cb === 'function' ? cb : _.noop;
}
// export so that we can test this
ClientAction.resolveUrl = resolveUrl;
var request = {};
function exec(transport, spec, params, cb) {
var request = {
method: spec.method,
timeout: spec.timeout || 10000
};
var query = {};
var i;
// verify that we have the body if needed
if (spec.needsBody && !params.body) {
return _.nextTick(cb, new TypeError('A request body is required.'));
throw new TypeError('A request body is required.');
}
if (params.timeout === void 0) {
request.timeout = 10000;
} else {
request.timeout = params.timeout;
}
// copy over some properties from the spec
params.body && (request.body = params.body);
params.ignore && (request.ignore = _.isArray(params.ignore) ? params.ignore : [params.ignore]);
// control params
spec.bulkBody && (request.bulkBody = true);
spec.castExists && (request.castExists = true);
if (spec.methods.length === 1) {
request.method = spec.methods[0];
} else {
// if set, uppercase the user's choice, other wise returns ""
request.method = _.toUpperString(params.method);
if (request.method) {
// use the one specified as long as it's a valid option
if (!_.contains(spec.methods, request.method)) {
return _.nextTick(cb, new TypeError('Invalid method: should be one of ' + spec.methods.join(', ')));
}
} else {
// pick a method
if (request.body) {
// first method that isn't "GET"
request.method = spec.methodWithBody || (
spec.methodWithBody = _.find(spec.methods, function (m) { return m !== 'GET'; })
);
} else {
// just use the first option
request.method = spec.methods[0];
}
}
}
// pick the url
if (spec.url) {
// only one url option
request.path = resolveUrl(spec.url, params);
@ -194,12 +200,9 @@ function exec(transport, spec, params, cb) {
if (!request.path) {
// there must have been some mimimun requirements that were not met
return _.nextTick(
cb,
new TypeError(
'Unable to build a path with those params. Supply at least ' +
_.keys(spec.urls[spec.urls.length - 1].req).join(', ')
)
throw new TypeError(
'Unable to build a path with those params. Supply at least ' +
_.keys(spec.urls[spec.urls.length - 1].req).join(', ')
);
}
@ -207,24 +210,56 @@ function exec(transport, spec, params, cb) {
if (!spec.paramKeys) {
// build a key list on demand
spec.paramKeys = _.keys(spec.params);
}
var key, param, name;
for (i = 0; i < spec.paramKeys.length; i++) {
key = spec.paramKeys[i];
param = spec.params[key];
// param keys don't always match the param name, in those cases it's stored in the param def as "name"
name = param.name || key;
try {
if (params[key] != null) {
query[name] = castType[param.type] ? castType[param.type](param, params[key], key) : params[key];
if (param['default'] && query[name] === param['default']) {
delete query[name];
}
} else if (param.required) {
throw new TypeError('Missing required parameter ' + key);
spec.requireParamKeys = _.transform(spec.params, function (req, param, key) {
if (param.required) {
req.push(key);
}
} catch (e) {
return _.nextTick(cb, e);
}, []);
}
var key, paramSpec;
for (key in params) {
if (params.hasOwnProperty(key) && params[key] != null) {
switch (key) {
case 'body':
request.body = params.body;
break;
case 'ignore':
request.ignore = _.isArray(params.ignore) ? params.ignore : [params.ignore];
break;
case 'timeout':
request.timeout = params.timeout;
break;
case 'method':
request.method = _.toUpperString(params.method);
break;
default:
paramSpec = spec.params[key];
if (paramSpec) {
// param keys don't always match the param name, in those cases it's stored in the param def as "name"
paramSpec.name = paramSpec.name || key;
if (params[key] != null) {
if (castType[paramSpec.type]) {
query[paramSpec.name] = castType[paramSpec.type](paramSpec, params[key], key);
} else {
query[paramSpec.name] = params[key];
}
if (paramSpec['default'] && query[paramSpec.name] === paramSpec['default']) {
delete query[paramSpec.name];
}
}
} else {
query[key] = params[key];
}
}
}
}
for (i = 0; i < spec.requireParamKeys.length; i ++) {
if (!query.hasOwnProperty(spec.requireParamKeys[i])) {
throw new TypeError('Missing required parameter ' + spec.requireParamKeys[i]);
}
}
@ -232,3 +267,23 @@ function exec(transport, spec, params, cb) {
return transport.request(request, cb);
}
ClientAction.proxy = function (fn, spec) {
return function (params, cb) {
if (typeof params === 'function') {
cb = params;
params = {};
} else {
params = params || {};
cb = typeof cb === 'function' ? cb : null;
}
if (spec.transform) {
spec.transform(params);
}
return fn.call(this, params, cb);
};
};

View File

@ -1,146 +0,0 @@
/**
* Manages the configuration of the client.
*
* @class ClientConfig
* @type {Function}
*/
module.exports = ClientConfig;
var _ = require('./utils');
var Host = require('./host');
var selectors = require('./selectors');
var connectors = {};
if (process.browser) {
connectors.Xhr = require('./connectors/xhr');
connectors.Angular = require('./connectors/angular');
connectors.jQuery = require('./connectors/jquery');
} else {
connectors.Http = require('./connectors/http');
}
// remove connectors that have been excluded in the build
_.each(connectors, function (conn, name) {
if (typeof conn !== 'function') {
delete connectors[name];
}
});
var serializers = {
Json: require('./serializers/json')
};
var extractHostPartsRE = /\[([^:]+):(\d+)\]/;
var defaultClasses = {
log: require('./log'),
serializer: serializers.Json,
connectionPool: require('./connection_pool'),
transport: require('./transport'),
};
var defaultConfig = {
loggers: [
{
level: 'warning'
}
],
hosts: [
{
host: 'localhost',
port: 9200,
protocol: 'http'
}
],
connectionClass: process.browser ? connectors.Xhr : connectors.Http,
selector: selectors.roundRobin,
sniffOnStart: false,
sniffAfterRequests: null,
sniffOnConnectionFail: false,
maxRetries: 3,
timeout: 10000,
deadTimeout: 60000,
maxSockets: 10,
// transforms the response from /_cluster/nodes
nodesToHostCallback: function (nodes) {
var hosts = [];
_.each(nodes, function (node, id) {
var hostnameMatches = extractHostPartsRE.exec(node.http_address);
hosts.push({
host: hostnameMatches[1],
port: hostnameMatches[2],
_meta: {
id: id,
name: node.name,
hostname: node.hostname,
version: node.version
}
});
});
return hosts;
}
};
// remove connector classes that were not included in the build
connectors = _.transform(connectors, function (note, connector, name) {
if (connector) {
note[name] = connector;
}
}, {});
function ClientConfig(config) {
_.extend(this, defaultConfig, config);
if (this.log) {
// treat log as an alias for loggers in the config.
this.loggers = this.log;
delete this.log;
}
// validate connectionClass
if (typeof this.connectionClass === 'string') {
this.connectionClass = connectors[_.studlyCase(this.connectionClass)];
}
if (typeof this.connectionClass !== 'function') {
throw new TypeError('Invalid connectionClass "' + this.connectionClass + '". ' +
'Expected a constructor or one of ' + _.keys(connectors).join(', '));
}
// validate selector
if (typeof this.selector === 'string') {
this.selector = selectors[_.camelCase(this.selector)];
}
if (typeof this.selector !== 'function') {
throw new TypeError('Invalid Selector "' + this.selector + '". ' +
'Expected a function or one of ' + _.keys(selectors).join(', '));
}
_.each(defaultClasses, function (DefaultClass, prop) {
this[prop] = typeof this[prop] === 'function' ? new this[prop](this) : new DefaultClass(this);
}, this);
// populate the connection pool
this.connectionPool.setNodes(this.prepareHosts(this.hosts));
// nodes are completely managed by the connection pool, remove traces of the config
// value to prevent confusion
delete this.hosts;
}
ClientConfig.prototype.prepareHosts = function (hosts) {
if (!_.isArray(hosts)) {
hosts = [hosts];
}
return _.map(hosts, function (host) {
return new Host(host);
});
};
/**
* Shutdown the connectionPool, log outputs, and clear timers
*/
ClientConfig.prototype.close = function () {
this.log.close();
this.connectionPool.close();
};

View File

@ -9,13 +9,19 @@ var EventEmitter = require('events').EventEmitter;
* @constructor
*/
function ConnectionAbstract(host, config) {
config = _.defaults(config || {}, {
deadTimeout: 30000
});
EventEmitter.call(this);
this.config = config;
this.host = host;
this.deadTimeout = config.deadTimeout;
this.requestCount = 0;
if (!this.host) {
throw new Error('Missing host config');
if (!host) {
throw new TypeError('Missing host');
} else if (host.makeUrl) {
this.host = host;
} else {
throw new TypeError('Invalid host');
}
_.makeBoundMethods(this);
@ -36,12 +42,11 @@ ConnectionAbstract.prototype.request = function () {
throw new Error('Connection#request must be overwritten by the Connector');
};
ConnectionAbstract.prototype.ping = function (params, cb) {
if (typeof params === 'function') {
cb = params;
} else if (typeof cb !== 'function') {
ConnectionAbstract.prototype.ping = function (cb) {
if (typeof cb !== 'function') {
throw new TypeError('Callback must be a function');
}
return this.request({
path: '/',
method: 'HEAD',
@ -54,16 +59,20 @@ ConnectionAbstract.prototype.setStatus = function (status) {
this.status = status;
if (status === 'dead' || status === 'closed') {
if (this.__deadTimeout) {
clearTimeout(this.__deadTimeout);
}
if (status === 'dead') {
this.__deadTimeout = setTimeout(this.bound.resuscitate, this.config.deadTimeout);
}
if (this._deadTimeoutId) {
clearTimeout(this._deadTimeoutId);
this._deadTimeoutId = null;
}
this.emit('status changed', status, origStatus, this);
if (status === 'dead') {
this._deadTimeoutId = setTimeout(this.bound.resuscitate, this.deadTimeout);
}
this.emit('status set', status, origStatus, this);
if (status === 'closed') {
this.removeAllListeners();
}
};
ConnectionAbstract.prototype.resuscitate = _.scheduled(function () {
@ -74,7 +83,7 @@ ConnectionAbstract.prototype.resuscitate = _.scheduled(function () {
if (!err) {
self.setStatus('alive');
} else {
self.emit('dead');
self.setStatus('dead');
}
});
}

View File

@ -3,33 +3,66 @@
* before providing them to the application
*
* @class ConnectionPool
* @param {Client} client - The client this pool belongs to
* @constructor
* @param {Object} config - The config object passed to the transport.
*/
module.exports = ConnectionPool;
var _ = require('./utils');
var Host = require('./host');
var Log = require('./log');
function ConnectionPool(config) {
_.makeBoundMethods(this);
this.config = config;
this.log = config.log;
if (!this.log) {
this.log = new Log();
}
// get the selector config var
this.selector = _.funcEnum(config, 'selector', ConnectionPool.selectors, ConnectionPool.defaultSelectors);
// get the connection class
this.Connection = _.funcEnum(config, 'connectionClass', ConnectionPool.connectionClasses,
ConnectionPool.defaultConnectionClass);
// a map of connections to their "id" property, used when sniffing
this.index = {};
this.connections = {
alive: [],
dead: []
};
}
// selector options
ConnectionPool.selectors = require('./selectors');
ConnectionPool.defaultSelectors = 'round_robin';
// get the connection options
ConnectionPool.connectionClasses = require('./connectors');
ConnectionPool.defaultConnectionClass = ConnectionPool.connectionClasses._default;
delete ConnectionPool.connectionClasses._default;
/**
* Selects a connection from the list using the this.selector
* Features:
* - detects if the selector is async or not
* - sync selectors should still return asynchronously
* - catches errors in sync selectors
* - automatically selects the first dead connection when there no living connections
*
* @param {Function} cb [description]
* @return {[type]} [description]
*/
ConnectionPool.prototype.select = function (cb) {
if (this.connections.alive.length) {
if (this.config.selector.length > 1) {
this.config.selector(this.connections.alive, cb);
if (this.selector.length > 1) {
this.selector(this.connections.alive, cb);
} else {
try {
_.nextTick(cb, null, this.config.selector(this.connections.alive));
_.nextTick(cb, null, this.selector(this.connections.alive));
} catch (e) {
this.config.log.error(e);
cb(e);
}
}
@ -38,7 +71,7 @@ ConnectionPool.prototype.select = function (cb) {
}
};
ConnectionPool.prototype.onStatusChanged = _.handler(function (status, oldStatus, connection) {
ConnectionPool.prototype.onStatusSet = _.handler(function (status, oldStatus, connection) {
var from, to, index;
if (oldStatus === status) {
@ -48,8 +81,6 @@ ConnectionPool.prototype.onStatusChanged = _.handler(function (status, oldStatus
} else {
return true;
}
} else {
this.config.log.info('connection id:', connection.id, 'is', status);
}
switch (status) {
@ -86,46 +117,56 @@ ConnectionPool.prototype.onStatusChanged = _.handler(function (status, oldStatus
});
ConnectionPool.prototype.addConnection = function (connection) {
if (!connection.id) {
connection.id = connection.host.toString();
}
if (!this.index[connection.id]) {
this.log.info('Adding connection to', connection.id);
this.index[connection.id] = connection;
connection.on('status changed', this.bound.onStatusChanged);
connection.on('status set', this.bound.onStatusSet);
connection.setStatus('alive');
}
};
ConnectionPool.prototype.removeConnection = function (connection) {
if (!connection.id) {
connection.id = connection.host.toString();
}
if (this.index[connection.id]) {
delete this.index[connection.id];
connection.setStatus('closed');
connection.removeListener('status changed', this.bound.onStatusChanged);
connection.removeListener('status set', this.bound.onStatusSet);
}
};
ConnectionPool.prototype.setNodes = function (nodeConfigs) {
ConnectionPool.prototype.setHosts = function (hosts) {
var connection;
var i;
var id;
var node;
var host;
var toRemove = _.clone(this.index);
for (i = 0; i < nodeConfigs.length; i++) {
node = nodeConfigs[i];
if (node instanceof Host) {
id = node.toString();
if (this.index[id]) {
delete toRemove[id];
} else {
connection = new this.config.connectionClass(node, this.config);
connection.id = id;
this.addConnection(connection);
}
for (i = 0; i < hosts.length; i++) {
host = hosts[i];
id = host.toString();
if (this.index[id]) {
delete toRemove[id];
} else {
connection = new this.Connection(host);
connection.id = id;
this.addConnection(connection);
}
}
_.each(toRemove, this.removeConnection, this);
var removeIds = _.keys(toRemove);
for (i = 0; i < removeIds.length; i++) {
this.removeConnection(this.index[removeIds[i]]);
}
};
ConnectionPool.prototype.close = function () {
this.setNodes([]);
this.setHosts([]);
};
ConnectionPool.prototype.empty = ConnectionPool.prototype.close;

View File

@ -0,0 +1,24 @@
var opts = {
xhr: require('./xhr'),
jquery: require('./jquery'),
angular: require('./angular')
};
var _ = require('../utils');
// remove modules that have been ignored by browserify
_.each(opts, function (conn, name) {
if (typeof conn !== 'function') {
delete opts[name];
}
});
// custom __default specification
if (opts.xhr) {
opts._default = 'xhr';
} else if (opts.angular) {
opts._default = 'angular';
} else {
opts._default = 'jquery';
}
module.exports = opts;

View File

@ -12,56 +12,64 @@ var handles = {
http: require('http'),
https: require('https')
};
var Log = require('../log');
var _ = require('../utils');
var errors = require('../errors');
var qs = require('querystring');
var KeepAliveAgent = require('agentkeepalive/lib/agent');
var KeepAliveAgent = require('agentkeepalive');
var ConnectionAbstract = require('../connection');
/**
* Connector used to talk to an elasticsearch node via HTTP
*
* @param {Host} host - The host object representing the elasticsearch node we will be talking to
* @param {Object} [config] - Configuration options (extends the configuration options for ConnectionAbstract)
* @param {Number} [config.maxSockets=10] - the maximum number of sockets that will be opened to this node
* @param {Number} [config.maxFreeSockets=10] - this maximum number of sockets that can sit idle to this node
* @param {Number} [config.maxKeepAliveTime=300000] - an idle timeout for the connections to this node. If your
* maxSockets is much higher than your average concurrent usage, this timeout will cause sockets to close which
* can be interpreted as "bad" behavior for clients.
*/
function HttpConnector(host, config) {
config = _.defaults(config || {}, {
maxSockets: 10,
maxFreeSockets: 10,
maxKeepAliveTime: 3e5 // 5 minutes
});
ConnectionAbstract.call(this, host, config);
this.hand = handles[this.host.protocol];
this.agent = new KeepAliveAgent({
maxSockets: 1,
maxKeepAliveRequests: 0, // max requests per keepalive socket, default is 0, no limit.
maxKeepAliveTime: 30000 // keepalive for 30 seconds
keepAlive: true,
maxSockets: config.maxSockets,
maxFreeSockets: config.maxFreeSockets || this.hand.Agent.defaultMaxSockets,
keepAliveMsecs: config.keepAliveMsecs
});
this.on('closed', this.bound.onClosed);
this.on('alive', this.bound.onAlive);
this.log = config.log;
if (!_.isObject(this.log)) {
this.log = new Log();
}
}
_.inherits(HttpConnector, ConnectionAbstract);
HttpConnector.prototype.onClosed = _.handler(function () {
this.agent.destroy();
this.removeAllListeners();
});
HttpConnector.prototype.onAlive = _.handler(function () {
// only set the agents max agents config once the connection is verified to be alive
this.agent.maxSockets = this.config.maxSockets;
});
HttpConnector.prototype.makeReqParams = function (params) {
params = params || {};
var host = this.host;
var reqParams = {
method: params.method,
protocol: this.host.protocol + ':',
auth: this.host.auth,
hostname: this.host.host,
port: this.host.port,
pathname: this.host.path + params.path,
headers: this.host.headers,
method: params.method || 'GET',
protocol: host.protocol + ':',
auth: host.auth,
hostname: host.host,
port: host.port,
path: (host.path || '') + (params.path || ''),
headers: host.headers,
agent: this.agent
};
var query = this.host.query ? this.host.query : null;
if (typeof query === 'string') {
query = qs.parse(query);
}
if (params.query) {
query = _.defaults({},
typeof params.query === 'string' ? qs.parse(params.query) : params.query,
@ -70,10 +78,7 @@ HttpConnector.prototype.makeReqParams = function (params) {
}
if (query) {
reqParams.query = query;
reqParams.path = reqParams.pathname + '?' + qs.stringify(query);
} else {
reqParams.path = reqParams.pathname;
reqParams.path = reqParams.path + '?' + qs.stringify(query);
}
return reqParams;
@ -86,7 +91,7 @@ HttpConnector.prototype.request = function (params, cb) {
var response;
var status = 0;
var timeout = params.timeout || this.config.timeout;
var log = this.config.log;
var log = this.log;
var reqParams = this.makeReqParams(params);

View File

@ -0,0 +1,4 @@
module.exports = {
http: require('./http'),
_default: 'http'
};

View File

@ -1,5 +1,5 @@
var _ = require('./utils'),
errors = module.exports;
var _ = require('./utils');
var errors = module.exports;
function ErrorAbstract(msg, constructor) {
this.message = msg;
@ -11,6 +11,7 @@ function ErrorAbstract(msg, constructor) {
Error.captureStackTrace(this, constructor);
}
}
errors._Abstract = ErrorAbstract;
_.inherits(ErrorAbstract, Error);
/**

View File

@ -1,5 +1,5 @@
/**
* Class to wrap URLS, formatting them and maintaining their seperate details
* Class to wrap URLS, formatting them and maintaining their separate details
* @type {[type]}
*/
module.exports = Host;
@ -16,42 +16,76 @@ var defaultPort = {
https: 443
};
var urlParseFields = [
'protocol', 'hostname', 'pathname', 'port', 'auth', 'query'
];
var simplify = ['host', 'path'];
function Host(config) {
if (this instanceof Host) {
if (typeof config === 'string') {
return Host.fromString(config);
} else {
_.extend(this, config || {});
config = config || {};
// defaults
this.protocol = 'http';
this.host = 'localhost';
this.port = 9200;
this.auth = null;
this.query = null;
if (typeof config === 'string') {
if (!startsWithProtocolRE.test(config)) {
config = 'http://' + config;
}
config = _.pick(url.parse(config, false, true), urlParseFields);
}
if (_.isObject(config)) {
// move hostname/portname to host/port semi-intelligently.
_.each(simplify, function (to) {
var from = to + 'name';
if (config[from] && config[to]) {
if (config[to].indexOf(config[from]) === 0) {
config[to] = config[from];
}
} else if (config[from]) {
config[to] = config[from];
}
delete config[from];
});
} else {
return new Host(config);
config = {};
}
_.assign(this, config);
// make sure the query string is parsed
if (this.query === null) {
// majority case
this.query = {};
} else if (!_.isPlainObject(this.query)) {
this.query = qs.parse(this.query);
}
// make sure that the port is a number
if (typeof this.port !== 'number') {
this.port = parseInt(this.port, 10);
if (isNaN(this.port)) {
this.port = 9200;
}
}
// make sure the path starts with a leading slash
// and that empty paths convert to '/'
if (!this.path || this.path.charAt(0) !== '/') {
this.path = '/' + (this.path || '');
}
// strip trailing ':' on the protocol (when config comes from url.parse)
if (this.protocol.substr(-1) === ':') {
this.protocol = this.protocol.substring(0, this.protocol.length - 1);
}
}
Host.fromString = function (urlString) {
if (!startsWithProtocolRE.test(urlString)) {
urlString = 'http://' + urlString;
}
var u = url.parse(urlString, true, true);
return new Host({
protocol: u.protocol ? u.protocol.substring(0, u.protocol.length - 1) : 'http',
host: u.hostname || 'localhost',
port: u.port || 9200,
auth: u.auth || '',
path: u.pathname,
query: u.query,
});
};
Host.prototype = {
protocol: 'http',
host: 'localhost',
port: 9200,
auth: '',
path: '',
query: false
};
Host.prototype.makeUrl = function (params) {
params = params || {};
// build the port
@ -62,17 +96,10 @@ Host.prototype.makeUrl = function (params) {
}
// build the path
var path = '';
// add the path prefix if set
if (this.path) {
path += this.path;
}
// then the path from the params
if (params.path) {
path += params.path;
}
// if we still have a path, and it doesn't start with '/' add it.
if (path && path.charAt(0) !== '/') {
var path = '' + (this.path || '') + (params.path || '');
// if path doesn't start with '/' add it.
if (path.charAt(0) !== '/') {
path = '/' + path;
}
@ -90,3 +117,7 @@ Host.prototype.makeUrl = function (params) {
return this.protocol + '://' + this.host + port + path + (query ? '?' + query : '');
};
Host.prototype.toString = function () {
return this.makeUrl();
};

View File

@ -1,20 +1,6 @@
var _ = require('./utils');
var url = require('url');
var EventEmitter = require('events').EventEmitter;
if (process.browser) {
var loggers = {
Console: require('./loggers/console')
};
} else {
var loggers = {
File: require('./loggers/file'),
Stream: require('./loggers/file'),
Stdio: require('./loggers/stdio'),
Tracer: require('./loggers/tracer')
};
}
/**
* Log bridge, which is an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
@ -31,40 +17,42 @@ if (process.browser) {
* @param {string} output.type - The name of the logger to use for this output
*/
function Log(config) {
this.config = config || {};
config = config || {};
var i;
var output = config.loggers ? config.loggers : 'warning';
var outputs;
if (_.isString(output) || _.isFinite(output)) {
output = [
{
level: output
}
];
} else if (_.isPlainObject(output)) {
output = [output];
} else if (_.isArray(output)) {
for (i = 0; i < output.length; i++) {
if (_.isString(output[i])) {
output[i] = {
level: output[i]
};
}
if (config.log) {
if (_.isArrayOfStrings(config.log)) {
outputs = [{
levels: config.log
}];
} else {
outputs = _.createArray(config.log, function (val) {
if (_.isPlainObject(val)) {
return val;
}
if (typeof val === 'string') {
return {
level: val
};
}
});
}
if (!outputs) {
throw new TypeError('Invalid logging output config. Expected either a log level, array of log levels, ' +
'a logger config object, or an array of logger config objects.');
}
for (i = 0; i < outputs.length; i++) {
this.addOutput(outputs[i]);
}
}
if (!_.isArrayOfPlainObjects(output)) {
throw new TypeError('Invalid Logging output config');
}
for (i = 0; i < output.length; i++) {
this.addOutput(output[i]);
}
}
_.inherits(Log, EventEmitter);
Log.loggers = require('./loggers');
Log.prototype.close = function () {
this.emit('closing');
@ -149,11 +137,24 @@ Log.levels = [
* @return {Array} -
*/
Log.parseLevels = function (input) {
if (_.isString(input)) {
return Log.levels.slice(0, _.indexOf(Log.levels, input) + 1);
}
else if (_.isArray(input)) {
return _.intersection(input, Log.levels);
switch (typeof input) {
case 'string':
var i = _.indexOf(Log.levels, input);
if (i >= 0) {
return Log.levels.slice(0, i + 1);
}
/* fall through */
case 'object':
if (_.isArray(input)) {
var valid = _.intersection(input, Log.levels);
if (valid.length === input.length) {
return valid;
}
}
/* fall through */
default:
throw new TypeError('invalid logging level ' + input + '. Expected zero or more of these options: ' +
Log.levels.join(', '));
}
};
@ -190,22 +191,14 @@ Log.join = function (arrayish) {
* @return {Logger}
*/
Log.prototype.addOutput = function (config) {
var levels = Log.parseLevels(config.levels || config.level || 'warning');
config = config || {};
_.defaults(config || {}, {
type: process.browser ? 'Console' : 'Stdio',
});
// force the levels config
// force "levels" key
config.levels = Log.parseLevels(config.levels || config.level || 'warning');
delete config.level;
config.levels = levels;
var Logger = loggers[_.studlyCase(config.type)];
if (Logger) {
return new Logger(config, this);
} else {
throw new Error('Invalid logger type "' + config.type + '". Expected one of ' + _.keys(loggers).join(', '));
}
var Logger = _.funcEnum(config, 'type', Log.loggers, process.browser ? 'console' : 'stdio');
return new Logger(this, config);
};
/**

View File

@ -2,18 +2,17 @@ var _ = require('./utils');
/**
* Abstract class providing common functionality to loggers
* @param {[type]} log [description]
* @param {[type]} config [description]
* @param {[type]} bridge [description]
*/
function LoggerAbstract(config, bridge) {
this.bridge = bridge;
function LoggerAbstract(log, config) {
this.log = log;
this.listeningLevels = [];
_.makeBoundMethods(this);
// when the bridge closes, remove our event listeners
this.bridge.on('closing', this.bound.cleanUpListeners);
// when the log closes, remove our event listeners
this.log.on('closing', this.bound.cleanUpListeners);
this.setupListeners(config.levels);
}
@ -68,7 +67,7 @@ LoggerAbstract.prototype.setupListeners = function (levels) {
_.each(this.listeningLevels, function (level) {
var fnName = 'on' + _.ucfirst(level);
if (this.bound[fnName]) {
this.bridge.on(level, this.bound[fnName]);
this.log.on(level, this.bound[fnName]);
} else {
throw new Error(fnName + ' is not a function');
}
@ -84,12 +83,12 @@ LoggerAbstract.prototype.setupListeners = function (levels) {
*/
LoggerAbstract.prototype.cleanUpListeners = _.handler(function () {
_.each(this.listeningLevels, function (level) {
this.bridge.removeListener(level, this.bound['on' + _.ucfirst(level)]);
this.log.removeListener(level, this.bound['on' + _.ucfirst(level)]);
}, this);
});
/**
* Handler for the bridges "error" event
* Handler for the logs "error" event
*
* @method onError
* @private
@ -101,7 +100,7 @@ LoggerAbstract.prototype.onError = _.handler(function (e) {
});
/**
* Handler for the bridges "warning" event
* Handler for the logs "warning" event
*
* @method onWarning
* @private
@ -113,7 +112,7 @@ LoggerAbstract.prototype.onWarning = _.handler(function (msg) {
});
/**
* Handler for the bridges "info" event
* Handler for the logs "info" event
*
* @method onInfo
* @private
@ -125,7 +124,7 @@ LoggerAbstract.prototype.onInfo = _.handler(function (msg) {
});
/**
* Handler for the bridges "debug" event
* Handler for the logs "debug" event
*
* @method onDebug
* @private
@ -137,7 +136,7 @@ LoggerAbstract.prototype.onDebug = _.handler(function (msg) {
});
/**
* Handler for the bridges "trace" event
* Handler for the logs "trace" event
*
* @method onTrace
* @private

View File

@ -0,0 +1,3 @@
module.exports = {
console: require('./console')
};

View File

@ -15,9 +15,9 @@ module.exports = Console;
var LoggerAbstract = require('../logger');
var _ = require('../utils');
function Console(config, bridge) {
function Console(log, config) {
// call my super
LoggerAbstract.call(this, config, bridge);
LoggerAbstract.call(this, log, config);
// config/state
this.color = _.has(config, 'color') ? !!config.color : true;

View File

@ -15,16 +15,20 @@ var StreamLogger = require('./stream');
var _ = require('../utils');
var fs = require('fs');
function File(config, bridge) {
// setup the stream before calling the super
function File(log, config) {
config = config || {};
// we should probably through an error if they don't support a path
this.path = config.path || 'elasticsearch.log';
// yahoo!
config.stream = fs.createWriteStream(this.path, {
flags: 'a',
encoding: 'utf8'
});
// call my super
StreamLogger.call(this, config, bridge);
StreamLogger.call(this, log, config);
}
_.inherits(File, StreamLogger);

6
src/lib/loggers/index.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
file: require('./file'),
stream: require('./file'),
stdio: require('./stdio'),
tracer: require('./tracer')
};

View File

@ -40,12 +40,12 @@ var defaultColors = {
}
};
function Stdio(config, bridge) {
function Stdio(log, config) {
// call my super
LoggerAbstract.call(this, config, bridge);
LoggerAbstract.call(this, log, config);
// config/state
this.color = Boolean(_.has(config, 'color') ? config.color : chalk.supportsColor);
this.color = !!(_.has(config, 'color') ? config.color : chalk.supportsColor);
this.colors = _.defaults(config.colors || {}, defaultColors);
}

View File

@ -15,11 +15,11 @@ module.exports = Stream;
var LoggerAbstract = require('../logger');
var _ = require('../utils');
function Stream(config, bridge) {
function Stream(log, config) {
// call my super
LoggerAbstract.call(this, config, bridge);
LoggerAbstract.call(this, log, config);
if (config.stream.write && config.stream.end) {
if (config.stream && config.stream.write && config.stream.end) {
this.stream = config.stream;
} else {
throw new TypeError('Invalid stream, use an instance of stream.Writeable');

View File

@ -15,9 +15,9 @@ module.exports = Tracer;
var FileLogger = require('./file');
var _ = require('../utils');
function Tracer(config, bridge) {
function Tracer(log, config) {
// call my super
FileLogger.call(this, config, bridge);
FileLogger.call(this, log, config);
}
_.inherits(Tracer, FileLogger);

View File

@ -1,5 +1,11 @@
module.exports = RandomSelect;
function RandomSelect(connections) {
/**
* Selects a connection randomly
*
* @module selectors
* @type {Function}
* @param {Array} connection - The list of connections to choose from
* @return {Connection} - The selected connection
*/
module.exports = function RandomSelector(connections) {
return connections[Math.floor(Math.random() * connections.length)];
}
};

View File

@ -1,13 +1,13 @@
/**
* Selects a connection the simplest way possible, Round Robin
*
* @class selector.roundRobin
* @constructor
* @module selectors
* @type {Function}
* @param {Array} connections - The list of connections that this selector needs to choose from
* @return {Connection} - The selected connection
*/
module.exports = RoundRobinSelect;
function RoundRobinSelect(connections) {
connections.unshift(connections.pop());
return connections[0];
}
module.exports = function (connections) {
var connection = connections[0];
connections.push(connections.shift());
return connection;
};

View File

@ -6,13 +6,11 @@ module.exports = Json;
var _ = require('../utils');
function Json(client) {
this.client = client;
}
function Json() {}
Json.prototype.serialize = function (val, replacer, spaces) {
if (val == null) {
return null;
return;
}
else if (typeof val === 'string') {
return val;
@ -26,8 +24,7 @@ Json.prototype.unserialize = function (str) {
try {
return JSON.parse(str);
} catch (e) {
this.client.log.error(new Error('unable to parse', str));
return null;
return;
}
} else {
return str;

View File

@ -6,12 +6,66 @@ module.exports = Transport;
var _ = require('./utils');
var errors = require('./errors');
var Host = require('./host');
var Log = require('./log');
var when = require('when');
function Transport(config) {
this.config = config;
config = config || {};
var LogClass;
// setup the log
switch (typeof config.log) {
case 'function':
LogClass = config.log;
break;
case 'undefined':
config.log = 'warning';
/* fall through */
default:
LogClass = Log;
}
config.log = this.log = new LogClass(config);
// overwrite the createDefer method if a new implementation is provided
if (typeof config.createDefer === 'function') {
this.createDefer = config.createDefer;
}
// setup the connection pool
var ConnectionPool = _.funcEnum(config, 'connectionPool', Transport.connectionPools, 'main');
this.connectionPool = new ConnectionPool(config);
if (config.hosts) {
var hosts = _.createArray(config.hosts, function (val) {
if (_.isPlainObject(val) || _.isString(val)) {
return val;
}
});
if (!hosts) {
throw new Error('Invalid hosts config. Expected a URL, an array of urls, a host config object, or an array of ' +
'host config objects.');
}
this.connectionPool.setHosts(_.map(hosts, function (conf) {
return new Host(conf);
}));
}
// setup the serializer
var Serializer = _.funcEnum(config, 'serializer', Transport.serializers, 'json');
this.serializer = new Serializer(config);
}
Transport.connectionPools = {
main: require('./connection_pool')
};
Transport.serializers = {
json: require('./serializers/json')
};
/**
* Perform a request with the client's transport
*
@ -27,15 +81,13 @@ function Transport(config) {
*/
Transport.prototype.request = function (params, cb) {
var log = this.config.log;
var serializer = this.config.serializer;
var connectionPool = this.config.connectionPool;
var remainingRetries = this.config.maxRetries;
var self = this;
var remainingRetries = this.maxRetries;
var connection; // set in sendReqWithConnection
var connectionReq; // an object with an abort method, set in sendReqWithConnection
var request; // the object returned to the user, might be a deferred
log.debug('starting request', params);
self.log.debug('starting request', params);
if (params.body && params.method === 'GET') {
_.nextTick(respond, new TypeError('Body can not be sent with method "GET"'));
@ -44,7 +96,7 @@ Transport.prototype.request = function (params, cb) {
// serialize the body
if (params.body) {
params.body = serializer[params.bulkBody ? 'bulkBody' : 'serialize'](params.body);
params.body = self.serializer[params.bulkBody ? 'bulkBody' : 'serialize'](params.body);
}
params.req = {
@ -55,7 +107,7 @@ Transport.prototype.request = function (params, cb) {
body: params.body,
};
connectionPool.select(sendReqWithConnection);
self.connectionPool.select(sendReqWithConnection);
function abortRequest() {
remainingRetries = 0;
@ -69,7 +121,7 @@ Transport.prototype.request = function (params, cb) {
connection = _connection;
connectionReq = connection.request(params.req, checkRespForFailure);
} else {
log.warning('No living connections');
self.log.warning('No living connections');
respond(new errors.NoConnections());
}
}
@ -77,10 +129,10 @@ Transport.prototype.request = function (params, cb) {
function checkRespForFailure(err, body, status) {
if (err && remainingRetries) {
remainingRetries--;
log.error(err.message, '-- retrying');
connectionPool.select(sendReqWithConnection);
self.log.error(err.message, '-- retrying');
self.connectionPool.select(sendReqWithConnection);
} else {
log.info('Request complete');
self.log.info('Request complete');
respond(err, body, status);
}
}
@ -89,21 +141,15 @@ Transport.prototype.request = function (params, cb) {
var parsedBody;
if (!err && body) {
parsedBody = serializer.unserialize(body);
parsedBody = self.serializer.unserialize(body);
if (parsedBody == null) {
err = new errors.Serialization();
}
}
if (!err) {
// get ignore and ensure that it's an array
var ignore = params.ignore;
if (ignore && !_.isArray(ignore)) {
ignore = [ignore];
}
if ((status < 200 || status >= 300)
&& (!ignore || !_.contains(ignore, status))
&& (!params.ignore || !_.contains(params.ignore, status))
) {
if (errors[status]) {
err = new errors[status](parsedBody && parsedBody.error);
@ -140,7 +186,7 @@ Transport.prototype.request = function (params, cb) {
abort: abortRequest
};
} else {
var defer = when.defer();
var defer = this.createDefer();
defer.promise.abort = abortRequest;
request = defer.promise;
}
@ -148,6 +194,10 @@ Transport.prototype.request = function (params, cb) {
return request;
};
Transport.prototype.createDefer = function () {
return when.defer();
};
/**
* Ask an ES node for a list of all the nodes, add/remove nodes from the connection
* pool as appropriate
@ -155,7 +205,7 @@ Transport.prototype.request = function (params, cb) {
* @param {Function} cb - Function to call back once complete
*/
Transport.prototype.sniff = function (cb) {
var config = this.config;
var self = this;
// make cb a function if it isn't
cb = typeof cb === 'function' ? cb : _.noop;
@ -165,9 +215,16 @@ Transport.prototype.sniff = function (cb) {
method: 'GET'
}, function (err, resp) {
if (!err && resp && resp.nodes) {
var nodes = config.nodesToHostCallback(resp.nodes);
config.connectionPool.setNodes(nodes);
var hosts = _.map(self.nodesToHostCallback(resp.nodes), function (hostConfig) {
return new Host(hostConfig);
});
this.connectionPool.setHosts(hosts);
}
cb(err, resp);
});
};
Transport.prototype.close = function () {
this.log.close();
this.connectionPool.close();
};

View File

@ -13,15 +13,6 @@ var nodeUtils = require('util');
var utils = _.extend({}, _, nodeUtils);
_ = utils;
utils.inspect = function (thing, opts) {
return nodeUtils.inspect(thing, _.defaults(opts || {}, {
showHidden: true,
depth: null,
color: true
}));
};
/**
* Link to [path.join](http://nodejs.org/api/path.html#path_path_join_path1_path2)
*
@ -46,7 +37,7 @@ utils.reKey = function (obj, transform, recursive) {
var out = {};
_.each(obj, function (prop, name) {
if (recursive && typeof prop === 'object') {
if (recursive && _.isPlainObject(prop)) {
out[transform(name)] = utils.reKey(prop, transform, recursive);
} else {
out[transform(name)] = prop;
@ -104,7 +95,6 @@ utils.deepMerge = function (to, from) {
/**
* Capitalize the first letter of a word
*
* @todo Tests
* @method ucfirst
* @param {string} word - The word to transform
* @return {string}
@ -166,7 +156,6 @@ function adjustWordCase(firstWordCap, otherWordsCap, sep) {
/**
* Transform a string into StudlyCase
*
* @todo Tests
* @method studlyCase
* @param {String} string
* @return {String}
@ -176,7 +165,6 @@ utils.studlyCase = adjustWordCase(true, true, '');
/**
* Transform a string into camelCase
*
* @todo Tests
* @method camelCase
* @param {String} string
* @return {String}
@ -186,7 +174,6 @@ utils.camelCase = adjustWordCase(false, true, '');
/**
* Transform a string into snakeCase
*
* @todo Tests
* @method snakeCase
* @param {String} string
* @return {String}
@ -196,7 +183,6 @@ utils.snakeCase = adjustWordCase(false, false, '_');
/**
* Lower-case a string, and return an empty string if any is not a string
*
* @todo Tests
* @param any {*} - Something or nothing
* @returns {string}
*/
@ -214,7 +200,6 @@ utils.toLowerString = function (any) {
/**
* Upper-case the string, return an empty string if any is not a string
*
* @todo Tests
* @param any {*} - Something or nothing
* @returns {string}
*/
@ -232,13 +217,12 @@ utils.toUpperString = function (any) {
/**
* Test if a value is "numeric" meaning that it can be transformed into something besides NaN
*
* @todo Tests
* @method isNumeric
* @param {*} val
* @return {Boolean}
*/
utils.isNumeric = function (val) {
return !isNaN(val === null ? NaN : val * 1);
return typeof val !== 'object' && val - parseFloat(val) >= 0;
};
// regexp to test for intervals
@ -247,7 +231,6 @@ var intervalRE = /^(\d+(?:\.\d+)?)([Mwdhmsy])$/;
/**
* Test if a string represents an interval (eg. 1m, 2Y)
*
* @todo Test
* @method isInterval
* @param {String} val
* @return {Boolean}
@ -259,7 +242,6 @@ utils.isInterval = function (val) {
/**
* Repeat a string n times
*
* @todo Test
* @todo TestPerformance
* @method repeat
* @param {String} what - The string to repeat
@ -271,27 +253,18 @@ utils.repeat = function (what, times) {
};
/**
* Override node's util.inherits function to also supply a callSuper function on the child class that can be called
* with the instance and the arguments passed to the child's constructor. This should only be called from within the
* constructor of the child class and should be removed from the code once the constructor is "done".
* Override node's util.inherits function, providing a browser safe version thanks to lodash
*
* @param constructor {Function} - the constructor that should subClass superConstructor
* @param superConstructor {Function} - The parent constructor
*/
utils.inherits = function (constructor, superConstructor) {
nodeUtils.inherits(constructor, superConstructor);
constructor.callSuper = function (inst, args) {
if (args) {
if (_.isArguments(args)) {
utils.applyArgs(superConstructor, inst, args);
} else {
utils.applyArgs(superConstructor, inst, arguments, 1);
}
} else {
superConstructor.call(inst);
}
};
};
if (process.browser) {
utils.inherits = require('inherits');
}
/**
*
*/
/**
* Remove leading/trailing spaces from a string
@ -412,15 +385,59 @@ _.makeBoundMethods = function (obj, methods) {
_.noop = function () {};
// _.getStackTrace = function (callee) {
// var e = {};
// if (typeof Error.captureStackTrace === 'function') {
// Error.captureStackTrace(e, callee || _.getStackTrace);
// } else {
// e.stack = (new Error()).stack;
// console.log(e.stack);
// }
// return '\n' + e.stack.split('\n').slice(1).join('\n');
// };
/**
* Implements the standard "string or constructor" check that I was copy/pasting everywhere
* @param {String|Function} val - the value that the user passed in
* @param {Object} opts - a map of the options
* @return {Function|undefined} - If a valid option was specified, then the constructor is returned
*/
_.funcEnum = function (config, name, opts, def) {
var val = config[name];
switch (typeof val) {
case 'undefined':
return opts[def];
case 'function':
return val;
case 'string':
if (opts[val]) {
return opts[val];
}
/* falls through */
default:
throw new TypeError('Invalid ' + name + ' "' + val + '", expected a function or one of ' +
_.keys(opts).join(', '));
}
};
/**
* Accepts any object and attempts to convert it into an array. If the object passed in is not
* an array it will be wrapped in one. Then the transform/map function will be called for each element
* and create a new array that is returned. If the map function fails to return something, the loop is
* halted and false is returned instead of an array.
*
* @param {*} input - The value to convert
* @param {Function} transform - A function called for each element of the resulting array
* @return {Array|false} - an array on success, or false on failure.
*/
_.createArray = function (input, transform) {
transform = typeof transform === 'function' ? transform : _.identity;
var output = [];
var item;
var i;
if (!_.isArray(input)) {
input = [input];
}
for (i = 0; i < input.length; i++) {
item = transform(input[i]);
if (item === void 0) {
return false;
} else {
output.push(item);
}
}
return output;
};
module.exports = utils;

View File

@ -34,8 +34,12 @@
var stats = this.stats;
var rootSuite = {
$el: $('<ul id="mocha-report"></ul>'),
name: 'root',
start: Date.now(),
results: [],
suites: []
suites: [],
stdout: '',
stderr: '',
};
@ -135,7 +139,7 @@
runner.on('end', function () {
var testResults = {
stats: stats,
suites: $.map(rootSuite.suites, function removeElements(suite) {
suites: $.map([rootSuite], function removeElements(suite) {
var s = {
name: suite.name,
start: suite.start,

View File

@ -12,6 +12,7 @@
<script type="text/javascript" src="expect.js?_c=<%= ts %>"></script>
<script type="text/javascript" src="mocha.js?_c=<%= ts %>"></script>
<script type="text/javascript" src="jquery.js?_c=<%= ts %>"></script>
<script type="text/javascript" src="client.js?_c=<%= ts %>"></script>
<!-- ESJS Reporter - sets itself as the reporter -->
<link rel="stylesheet" href="esjs_reporter.css?_c=<%= ts %>"/>
@ -24,18 +25,21 @@
var BROWSER_NAME = <%= JSON.stringify(browser) %>;
</script>
<!-- test -->
<script>mocha.setup('bdd')</script>
<script type="text/javascript" src="client.js?_c=<%= ts %>"></script>
<!-- mocha setup -->
<script>
mocha.setup('bdd')
mocha.checkLeaks();
mocha.globals(['console', 'elasticsearch']);
mocha.slow(1000);
mocha.timeout(11000);
mocha.bail();
</script>
<!-- include tests -->
<script type="text/javascript" src="yaml_tests.js?_c=<%= ts %>"></script>
<!-- begin -->
<script>
mocha.checkLeaks();
mocha.slow(1000);
mocha.timeout(11000);
mocha.globals(['console', 'elasticsearch']);
mocha.bail();
/PhantomJS/i.test(navigator.userAgent) || mocha.run();
</script>
</body>

File diff suppressed because one or more lines are too long

View File

@ -37,8 +37,12 @@ module.exports = {
} else if (externalExists === void 0) {
doCreateClient(function () {
client.ping(function (err) {
externalExists = !err;
create(cb);
if (err instanceof es.errors.ConnectionFault) {
externalExists = !err;
create(done);
} else {
done(err);
}
});
});
} else {

View File

@ -10,7 +10,7 @@ var Base = require('mocha/lib/reporters/base');
var _ = require('lodash');
var chalk = require('chalk');
var clientManager = require('./client_manager');
var makeJUnitXml = require('../../make_j_unit_xml');
var makeJUnitXml = require('../../../scripts/make_j_unit_xml');
var fs = require('fs');
var path = require('path');

View File

@ -116,7 +116,7 @@ function YamlDoc(doc, file) {
// check that it's a function
expect(method).to.be.a('function');
if (typeof action.args === 'object') {
if (_.isPlainObject(action.args)) {
action.name += ' ' + _.keys(action.args).join(', ');
} else if (action.args) {
action.name += ' ' + action.args;

View File

@ -1,80 +0,0 @@
/**
* A mock elasticsearch server used to test network libraries quickly and easily.
*
* It is actually a server created using node's http.createServer method and
* listens on a random port by default. The server emits an "online" event once
* it is listening and ready for requests.
*
* @type {}
* @class EsServer
* @param {Object} config - An object containing config options
*/
module.exports = EsServer;
var _ = require('../../src/lib/utils');
var url = require('url');
var http = require('http');
var EventEmitter = require('events').EventEmitter;
var pingResp = JSON.stringify({
'ok': true,
'status': 200,
'name': 'Commando',
'version': {
'number': '0.90.5',
'build_hash': 'c8714e8e0620b62638f660f6144831792b9dedee',
'build_timestamp': '2013-09-17T12:50:20Z',
'build_snapshot': false,
'lucene_version': '4.4'
},
'tagline': 'You Know, for Search'
});
function EsServer() {
var self = this;
var server = http.createServer();
server.on('request', function (request, response) {
request.parsedUrl = url.parse(request.url, true);
var routes = self.routes[request.method.toLowerCase()];
var route = routes && routes[request.parsedUrl.pathname];
if (route) {
route.call(self, request, response);
} else {
response.writeHead(400);
response.end('No handler found for uri [/] and method [' + request.method + ']');
}
});
self.shutdown = function (cb) {
server.close(function () {
self.emit('offline');
cb();
});
};
self.routes = {
get: {
'/' : function (request, response) {
response.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': pingResp.length
});
response.end(pingResp, 'utf8');
}
},
post: {},
put: {},
delete: {},
head: {}
};
process.nextTick(function () {
server.listen(0, function () {
self.emit('online', server.address().port);
});
});
}
_.inherits(EsServer, EventEmitter);

View File

@ -1,16 +0,0 @@
var es = require('../../src/elasticsearch'),
api = require('../../src/lib/api'),
expect = require('expect.js');
describe('Client instances creation', function () {
var client;
beforeEach(function () {
client = new es.Client();
});
it('inherits the api', function () {
client.bulk.should.eql(api.bulk);
client.cluster.nodeStats.should.eql(api.cluster.prototype.nodeStats);
});
});

View File

@ -1,16 +0,0 @@
var es = require('../../src/elasticsearch');
describe('Connection Pool', function () {
var client, pool;
beforeEach(function () {
client = new es.Client();
pool = client.config.connectionPool;
});
describe('default settings', function () {
it('by default has one living connection and no dead connections', function () {
pool.connections.alive.should.have.lengthOf(1);
pool.connections.dead.should.have.lengthOf(0);
});
});
});

View File

@ -1,80 +0,0 @@
var EsServer = require('../mocks/es_server');
var _ = require('../../src/lib/utils');
var http = require('http');
describe('EsServer Mock', function () {
it('should emit an online event when ready, passing it\'s port number', function (done) {
var server = new EsServer();
server.on('online', function (port) {
port.should.have.type('number');
server.shutdown(done);
});
});
describe('when it\'s online', function () {
var server;
var port;
function makeRequest(opts, respCb) {
opts = _.defaults(opts || {}, {
host: 'localhost',
port: port
});
var response = null;
var req = http.request(opts, function (incomming) {
response = '';
incomming.on('data', function (chunk) {
response += chunk;
});
incomming.on('end', function () {
if (incomming.headers['content-type'] === 'application/json') {
try {
respCb(null, JSON.parse(response), incomming.statusCode);
} catch (e) {
respCb(e, response, incomming.statusCode);
}
} else {
respCb(null, response, incomming.statusCode);
}
});
});
req.on('error', respCb);
req.end();
}
before(function (done) {
server = new EsServer();
server.on('online', function (_port) {
port = _port;
done();
});
});
after(function (done) {
server.shutdown(done);
});
it('should respond with json to a ping', function (done) {
makeRequest({
path: '/'
}, function (err, resp, status) {
if (err) {
done(err);
} else {
status.should.be.exactly(200);
resp.version.number.should.match(/^\d+\.\d+\.\d+/);
done();
}
});
});
});
});

View File

@ -1,39 +0,0 @@
describe('Http Connector', function () {
var Host = require('../../src/lib/host');
var HttpConnection = require('../../src/lib/connectors/http');
var host = new Host('http://someesserver.com:9205/prefix');
var con;
describe('#makeReqParams', function () {
before(function () {
con = new HttpConnection(host, {});
});
it('creates the request params property', function () {
var reqParams = con.makeReqParams({
method: 'GET',
path: '/_cluster/nodes/stats',
query: {
jvm: true
}
});
reqParams.should.include({
method: 'GET',
protocol: 'http:',
auth: '',
hostname: 'someesserver.com',
port: '9205',
path: '/prefix/_cluster/nodes/stats?jvm=true'
});
Object.keys(reqParams).should.not.include([
'host', 'pathname', 'query'
]);
});
});
});

View File

@ -1,70 +0,0 @@
var Log = require('../../src/lib/log');
exports['Log::parseLevels'] = {
'parses a string': function (test) {
test.deepEqual(Log.parseLevels('warning'), ['error', 'warning']);
test.done();
},
'filters an array': function (test) {
test.deepEqual(Log.parseLevels(['trace', 'not a level']), ['trace']);
test.done();
},
'returns nothing as a defauls': function (test) {
test.ok(!Log.parseLevels());
test.done();
}
};
exports['Log::join'] = {
'joins strings': function (test) {
test.equal(Log.join(['one', 'two']), 'one two');
test.done();
},
'flattens nested arrays': function (test) {
test.equal(Log.join(['one', ['three', 'four']]), 'one three,four');
test.done();
},
'flattens arguments': function (test) {
(function() {
test.equal(Log.join(arguments), 'one two');
}('one', 'two'));
test.done();
}
};
/**
* Empty log bridge (no outputs)
* @type {Log}
*/
var log;
exports['Log instance with no outputs'] = {
setUp: function (done) {
log = new Log([]);
done();
},
tearDown: function (done) {
done();
},
'should not emit any events': function (test) {
log.emit = function () {
test.ok(false, 'Emit should not have been called');
};
log.error('Error Message');
test.done();
}
};

View File

@ -1,67 +0,0 @@
var es = require('../../src/elasticsearch'),
Stdio = require('../../src/lib/loggers/stdio'),
_ = require('../../src/lib/utils'),
expect = require('expect.js');
describe('Stdio Logger listening to levels warning and error', function () {
var client, log, logger;
before(function () {
client = new es.Client({
log: []
});
log = client.config.log;
});
beforeEach(function () {
if (logger) {
logger.cleanUpListeners();
}
// new logger in warning mode
logger = new Stdio({
levels: ['error', 'warning']
}, log);
});
it('logs error messages', function (done) {
logger.write = function (to, label, colorize, what) {
label.should.eql('ERROR');
what.should.have.type('string');
what.should.match(/^Error: Test Error Message/);
done();
};
log.error('Test Error Message');
});
it('logs warning messages', function (done) {
logger.write = function (to, label, colorize, what) {
expect(label).to.be('WARNING');
expect(what).to.be('Test Warning Message');
done();
};
log.warning('Test Warning', 'Message');
});
it('does not log info messages', function () {
if (log.info('Info')) {
throw new Error('There shouldn\'t be listeners for info logs');
}
});
it('does not log debug messages', function () {
if (log.debug('Debug')) {
throw new Error('There shouldn\'t be listeners for debug logs');
}
});
it('does not log trace messages', function () {
if (log.trace('curl "http://localhost:9200" -d "{ \"query\": ... }"')) {
throw new Error('There shouldn\'t be listeners for trace logs');
}
});
});

35
test/unit/test_client.js Normal file
View File

@ -0,0 +1,35 @@
var es = require('../../src/elasticsearch');
var api = require('../../src/lib/api');
describe('Client instances creation', function () {
var client;
beforeEach(function () {
if (client) {
client.close();
}
client = new es.Client();
});
it('inherits the api', function () {
client.bulk.should.eql(api.bulk);
client.cluster.nodeStats.should.eql(api.cluster.prototype.nodeStats);
});
it('closing the client causes it\'s transport to be closed', function () {
var called = false;
client.transport.close = function () {
called = true;
};
client.close();
called.should.be.exactly(true);
});
it('creates a warning level logger by default', function () {
client.transport.log.listenerCount('error').should.eql(1);
client.transport.log.listenerCount('warning').should.eql(1);
client.transport.log.listenerCount('info').should.eql(0);
client.transport.log.listenerCount('debug').should.eql(0);
client.transport.log.listenerCount('trace').should.eql(0);
});
});

View File

@ -0,0 +1,816 @@
var ca = require('../../src/lib/client_action');
var should = require('should');
var _ = require('lodash');
var when = require('when');
/**
* Creates a simple mock of the client, whose "transport" has a request
* function that just calls back with the parameters it received
*
* @return {Object}
*/
function mockClient() {
return {
transport: {
request: function (params, cb) {
if (typeof cb === 'function') {
process.nextTick(function () {
cb(void 0, params);
});
} else {
return when.resolve(params);
}
}
}
};
}
/**
* Creates a client action, ensuring that is has some default url specs, and binds it to
* a mock client.
*
* @param {Object} spec - the spec for the client action
* @return {Function} - the client action
*/
function makeClientAction(spec) {
spec = spec || {};
if (!spec.urls && !spec.url) {
spec.url = {
fmt: '/'
};
}
return _.bind(ca(spec), mockClient());
}
/**
* Calls ca.proxy and binds it to a mock client
* @param {Function} fn - the function to proxy
* @param {Object} spec - The spec for the proxy
* @return {Function} - the clientActionProxy
*/
function makeClientActionProxy(fn, spec) {
return _.bind(ca.proxy(fn, spec || {}), mockClient());
}
describe('Client Action runner', function () {
var action;
describe('argument juggling', function () {
it('creates an empty param set when no params are sent', function (done) {
action = makeClientAction();
// note: the first arg is the callback
action(function (err, params) {
params.query.should.eql({});
done();
});
});
});
describe('clientAction::proxy', function () {
it('proxies to the passed function', function () {
var action = makeClientActionProxy(function (params, cb) {
throw new Error('proxy function called');
});
(function () {
action({}, function () {});
}).should.throw('proxy function called');
});
it('provides the proper context', function (done) {
var client;
var action = makeClientActionProxy(function (params, cb) {
client = this;
process.nextTick(function () {
cb(void 0, params);
});
});
action({}, function (err, params) {
client.transport.request.should.be.type('function');
done();
});
});
it('handles passing just the callback', function () {
var action = makeClientActionProxy(function (params, cb) {
should(_.isObject(params)).be.ok;
cb.should.be.type('function');
});
action(function () {});
});
it('supports a param transformation function', function () {
var action = makeClientActionProxy(function (params, cb) {
params.should.have.property('transformed');
}, {
transform: function (params) {
params.transformed = true;
}
});
action(function () {});
});
it('returns the proxied function\'s return value', function () {
var football = {};
var action = makeClientActionProxy(function (params, cb) {
return football;
});
action().should.be.exactly(football);
});
});
describe('param casting', function () {
describe('duration', function () {
beforeEach(function () {
action = makeClientAction({
params: {
one: {
type: 'duration'
},
two: {
type: 'duration'
},
three: {
type: 'duration'
},
four: {
type: 'duration'
}
}
});
});
it('accepts a number, string, or interval', function (done) {
action({
one: 1500,
two: '500',
three: '15m'
}, function (err, params) {
if (err) { throw err; }
params.query.one.should.eql(1500);
params.query.two.should.eql('500');
params.query.three.should.eql('15m');
done();
});
});
it('rejects date values', function (done) {
action({
one: new Date()
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects array', function (done) {
action({
one: ['one'],
two: [ 1304 ]
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects object', function (done) {
action({
one: { but: 'duration' }
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
describe('list', function () {
beforeEach(function () {
action = makeClientAction({
params: {
one: { type: 'list' },
two: { type: 'list' },
three: { type: 'list' }
}
});
});
it('accepts a string, number, or array', function (done) {
action({
one: 'some,strings',
two: 1430,
three: ['some', 'strings'],
}, function (err, params) {
if (err) { throw err; }
params.query.should.eql({
one: 'some,strings',
two: 1430,
three: 'some,strings'
});
done();
});
});
it('it rejects regexp', function (done) {
action({
one: /regexp!/g
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('it rejects objects', function (done) {
action({
one: {
pasta: 'sauce'
}
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
describe('enum', function () {
beforeEach(function () {
action = makeClientAction({
params: {
one: { type: 'enum', options: ['opt', 'other opt', '150'] }
}
});
});
it('accepts any value in the list', function (done) {
action({
one: 'opt'
}, function (err, params) {
if (err) { throw err; }
params.query.one.should.eql('opt');
done();
});
});
it('accepts any value kind of in the list', function (done) {
action({
one: 150
}, function (err, params) {
if (err) { throw err; }
params.query.one.should.be.exactly('150');
done();
});
});
it('it rejects things not in the list', function (done) {
action({
one: 'not an opt'
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
describe('boolean', function () {
beforeEach(function () {
action = makeClientAction({
params: {
one: { type: 'boolean' },
two: { type: 'boolean' },
three: { type: 'boolean' },
four: { type: 'boolean' },
five: { type: 'boolean' },
six: { type: 'boolean' },
}
});
});
it('casts "off", "no", and other falsey things to false', function (done) {
action({
one: 'off',
two: 'no',
three: false,
four: ''
}, function (err, params) {
if (err) { throw err; }
should(params.query.one).be.exactly(false);
should(params.query.two).be.exactly(false);
should(params.query.three).be.exactly(false);
should(params.query.four).be.exactly(false);
done();
});
});
it('cast most everything else to true', function (done) {
action({
one: 'yes',
two: 'ok',
three: true,
four: 1,
five: new Date(),
six: {}
}, function (err, params) {
if (err) { throw err; }
should(params.query.one).be.exactly(true);
should(params.query.two).be.exactly(true);
should(params.query.three).be.exactly(true);
should(params.query.four).be.exactly(true);
should(params.query.five).be.exactly(true);
should(params.query.six).be.exactly(true);
done();
});
});
});
describe('number', function () {
beforeEach(function () {
action = makeClientAction({
params: {
one: { type: 'number' },
two: { type: 'number' },
three: { type: 'number' },
four: { type: 'number' },
five: { type: 'number' },
six: { type: 'number' },
}
});
});
it('casts integers properly', function (done) {
action({
one: '42',
two: '-69',
three: 15,
four: -100,
five: '0xFF',
six: 0xFFF
}, function (err, params) {
if (err) { throw err; }
params.query.one.should.equal(42);
params.query.two.should.equal(-69);
params.query.three.should.equal(15);
params.query.four.should.equal(-100);
params.query.five.should.equal(255);
params.query.six.should.equal(4095);
done();
});
});
it('casts floats properly', function (done) {
action({
one: '-1.6',
two: '4.536',
three: -2.6,
four: 3.1415,
five: 8e5,
six: '123e-2',
}, function (err, params) {
if (err) { throw err; }
params.query.one.should.equal(-1.6);
params.query.two.should.equal(4.536);
params.query.three.should.equal(-2.6);
params.query.four.should.equal(3.1415);
params.query.five.should.equal(800000);
params.query.six.should.equal(1.23);
done();
});
});
it('rejects dates', function (done) {
action({
one: new Date()
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects objects', function (done) {
action({
one: {}
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects arrays', function (done) {
action({
one: []
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects regexp', function (done) {
action({
one: /pasta/g
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
describe('string', function () {
beforeEach(function () {
action = makeClientAction({
params: {
one: { type: 'string' },
two: { type: 'string' },
three: { type: 'string' },
four: { type: 'string' },
five: { type: 'string' },
six: { type: 'string' },
}
});
});
it('accepts numbers and strings', function (done) {
action({
one: '42',
two: '-69',
three: 15,
four: -100,
five: '0xFF',
six: 0xFFF
}, function (err, params) {
if (err) { throw err; }
params.query.one.should.equal('42');
params.query.two.should.equal('-69');
params.query.three.should.equal('15');
params.query.four.should.equal('-100');
params.query.five.should.equal('0xFF');
params.query.six.should.equal('4095');
done();
});
});
it('rejects dates', function (done) {
action({
one: new Date()
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects objects', function (done) {
action({
one: {}
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects arrays', function (done) {
action({
one: []
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects regexp', function (done) {
action({
one: /pasta/g
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
describe('time', function () {
beforeEach(function () {
action = makeClientAction({
params: {
one: { type: 'time' },
two: { type: 'time' },
three: { type: 'time' },
four: { type: 'time' },
five: { type: 'time' },
six: { type: 'time' },
}
});
});
it('accepts numbers, strings, and dates', function (done) {
var now = new Date();
action({
one: '42',
two: '-69',
three: 15,
four: now,
five: new Date(999, 2399, 152433)
}, function (err, params) {
if (err) { throw err; }
params.query.one.should.equal('42');
params.query.two.should.equal('-69');
params.query.three.should.equal('15');
params.query.four.should.equal('' + now.getTime());
params.query.five.should.equal('-11162941200000');
done();
});
});
it('rejects objects', function (done) {
action({
one: {}
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects arrays', function (done) {
action({
one: []
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('rejects regexp', function (done) {
action({
one: /pasta/g
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
});
describe('passing of control params from spec', function () {
it('passes bulkBody', function (done) {
var action = makeClientAction({
bulkBody: true
});
action({}, function (err, params) {
params.bulkBody.should.be.exactly(true);
done();
});
});
it('passes castExists', function (done) {
var action = makeClientAction({
castExists: true
});
action({}, function (err, params) {
params.castExists.should.be.exactly(true);
done();
});
});
});
describe('body handling', function () {
var action = makeClientAction({
needsBody: true
});
it('passed the body when it is set', function (done) {
var body = '{"JSON":"PLEASE"}';
action({ body: body }, function (err, params) {
params.body.should.be.exactly(body);
done();
});
});
it('errors when the body is not set but required', function (done) {
action().then(function () {
done(new Error('Error should have been raised'));
}, function (err) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
describe('passing of http method', function () {
it('uppercases and passed the default method', function (done) {
var action = makeClientAction({
method: 'POST'
});
action({method: 'get'}, function (err, params) {
params.method.should.be.exactly('GET');
done();
});
});
it('uppercases and passed the default method', function (done) {
var action = makeClientAction({
method: 'POST'
});
action({}, function (err, params) {
params.method.should.be.exactly('POST');
done();
});
});
});
describe('passing of ignore param', function () {
it('passes ignore as an array', function (done) {
var action = makeClientAction({});
action({ ignore: 404 }, function (err, params) {
params.ignore.should.eql([404]);
done();
});
});
});
describe('passing of timeout', function () {
it('passes the timeout', function (done) {
var action = makeClientAction({
timeout: 100
});
action({}, function (err, params) {
params.timeout.should.be.exactly(100);
done();
});
});
it('passes the provided value for timeout', function (done) {
var action = makeClientAction({
timeout: 100
});
action({ timeout: 3000 }, function (err, params) {
params.timeout.should.be.exactly(3000);
done();
});
});
it('uses 10000 as the default timeout', function (done) {
var action = makeClientAction({});
action({}, function (err, params) {
params.timeout.should.be.exactly(10000);
done();
});
});
});
describe('url resolver', function () {
var action = makeClientAction({
urls: [
{
fmt: '/<%=index%>/<%=type%>/<%=id%>/<%=thing%>',
req: {
index: {
type: 'list'
},
id: {
type: 'any'
}
},
opt: {
type: {
type: 'list',
default: '_all'
},
thing: {
type: 'any',
default: ''
}
}
}
]
});
// match a url to the parameters passed in.
it('rejects a url if it required params that are not present', function (done) {
action({
type: ['type1', 'type2']
}, function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
it('uses the default value for optional params', function (done) {
action({
index: 'index1',
id: '1'
}, function (err, params) {
if (err) { throw err; }
params.path.should.be.exactly('/index1/_all/1/');
done();
});
});
it('casts both optional and required args', function (done) {
action({
index: ['index1', 'index2'],
id: '123',
type: ['_all', '-pizza'],
thing: 'poo'
}, function (err, params) {
if (err) { throw err; }
params.path.should.be.exactly('/index1%2Cindex2/_all%2C-pizza/123/poo');
done();
});
});
});
describe('param collection', function () {
var action = makeClientAction({
params: {
a: { type: 'list', required: true },
b: { type: 'duration', default: '15m' },
q: { type: 'any' }
}
});
it('collects all of the params into params.query', function (done) {
action({
a: 'pizza',
b: '1M'
},
function (err, params) {
if (err) { throw err; }
params.query.should.eql({
a: 'pizza',
b: '1M'
});
done();
});
});
it('includes extra params', function (done) {
action({
a: 'pizza',
b: '3w',
c: 'popular',
},
function (err, params) {
if (err) { throw err; }
params.query.should.eql({
a: 'pizza',
b: '3w',
c: 'popular'
});
done();
});
});
it('excludes default values', function (done) {
action({
a: 'pizza',
b: '15m',
},
function (err, params) {
if (err) { throw err; }
params.query.should.eql({
a: 'pizza'
});
done();
});
});
it('does not include non-query param keys', function (done) {
action({
a: 'pizza',
b: '3w',
q: 'beep',
body: '{ "mmm": "json" }',
timeout: 1000,
method: 'head',
ignore: 201
},
function (err, params) {
if (err) { throw err; }
params.query.should.eql({
a: 'pizza',
b: '3w',
q: 'beep'
});
done();
});
});
it('enforces required params', function (done) {
action({
b: '3w'
},
function (err, params) {
err.should.be.an.instanceOf(TypeError);
done();
});
});
});
});

View File

@ -0,0 +1,154 @@
var ConnectionAbstract = require('../../src/lib/connection');
var Host = require('../../src/lib/host');
var sinon = require('sinon');
var _ = require('lodash');
describe('Connection Abstract', function () {
var host = new Host('localhost:9200');
it('constructs with defaults for deadTimeout, requestCount, host, and bound', function () {
var conn = new ConnectionAbstract(host);
conn.deadTimeout.should.eql(30000);
conn.requestCount.should.eql(0);
conn.host.should.be.exactly(host);
conn.bound.should.have.properties('resuscitate');
});
it('requires a valid host', function () {
(function () {
new ConnectionAbstract();
}).should.throw(TypeError);
(function () {
new ConnectionAbstract({});
}).should.throw(TypeError);
});
it('required that the request method is overridden', function () {
(function () {
var conn = new ConnectionAbstract(host);
conn.request();
}).should.throw(/overwrit/);
});
describe('#ping', function () {
it('requires a callback', function () {
(function () {
(new ConnectionAbstract(host)).ping();
}).should.throw(TypeError);
});
it('calls it\'s own request method', function () {
var conn = new ConnectionAbstract(host);
var football = {};
conn.request = function () {
return football;
};
conn.ping(function () {}).should.be.exactly(football);
});
});
describe('#setStatus', function () {
it('emits the "status set" event with `new`, `old` & `conn` args', function () {
var conn = new ConnectionAbstract(host);
var emitted = false;
conn.emit = function (eventName) {
emitted = {
name: eventName,
args: Array.prototype.slice.call(arguments, 1)
};
};
conn.setStatus('closed');
emitted.name.should.eql('status set');
emitted.args.should.eql(['closed', null, conn]);
});
it('stores the status in this.status', function () {
var conn = new ConnectionAbstract(host);
conn.setStatus('closed');
conn.status.should.eql('closed');
});
it('sets a timeout when set to dead, and removed when alive', function () {
var clock = sinon.useFakeTimers('setTimeout', 'clearTimeout');
var conn = new ConnectionAbstract(host);
var start = _.size(clock.timeouts);
conn.setStatus('dead');
_.size(clock.timeouts).should.be.eql(start + 1);
conn.setStatus('alive');
_.size(clock.timeouts).should.eql(start);
clock.restore();
});
});
describe('#resuscitate', function () {
it('should not ping the connection unless it is still dead', function () {
var conn = new ConnectionAbstract(host);
conn.setStatus('alive');
conn.ping = function () {
throw new Error('ping should not have been called');
};
conn.resuscitate();
});
it('should ping the connection after the deadTimeout, and set the status to "alive" on pong', function (done) {
var conn = new ConnectionAbstract(host);
var clock = sinon.useFakeTimers('setTimeout', 'clearTimeout');
// schedules the resuscitate
conn.setStatus('dead');
// override the ping method to just callback without an error
conn.ping = function (cb) {
process.nextTick(function () {
cb();
});
};
// will be called after the ping calls back
conn.setStatus = function (status) {
status.should.eql('alive');
clock.restore();
done();
};
// fast forward the clock
clock.tick(conn.deadTimeout);
});
it('should ping the connection after the deadTimeout, and set the status to "dead" on error', function (done) {
var conn = new ConnectionAbstract(host);
var clock = sinon.useFakeTimers('setTimeout', 'clearTimeout');
// schedules the resuscitate
conn.setStatus('dead');
// override the ping method to just callback without an error
conn.ping = function (cb) {
process.nextTick(function () {
cb(new Error('server still down'));
});
};
// will be called after the ping calls back
conn.setStatus = function (status) {
status.should.eql('dead');
clock.restore();
done();
};
// fast forward the clock
clock.tick(conn.deadTimeout);
});
});
});

View File

@ -0,0 +1,237 @@
var ConnectionPool = require('../../src/lib/connection_pool');
var Host = require('../../src/lib/host');
var ConnectionAbstract = require('../../src/lib/connection');
var _ = require('lodash');
var EventEmitter = require('events').EventEmitter;
var should = require('should');
function listenerCount(emitter, event) {
if (EventEmitter.listenerCount) {
// newer node
return EventEmitter.listenerCount(emitter, event);
} else {
// older node
return emitter.listeners(event).length;
}
}
describe('Connection Pool', function () {
describe('Adding/Removing/Syncing Connections', function () {
var pool, host, connection, host2, connection2;
beforeEach(function () {
pool = new ConnectionPool({});
host = new Host({
port: 999
});
connection = new ConnectionAbstract(host);
host2 = new Host({
port: 2222
});
connection2 = new ConnectionAbstract(host2);
});
it('#addConnection only adds the connection if it doesn\'t already exist', function () {
_.keys(pool.index).length.should.eql(0);
pool.addConnection(connection);
_.keys(pool.index).should.eql([host.toString()]);
pool.connections.alive.should.eql([connection]);
pool.connections.dead.should.eql([]);
});
describe('#removeConnection', function () {
it('removes the connection if it exist', function () {
pool.addConnection(connection);
pool.removeConnection(connection2);
pool.connections.alive.should.eql([connection]);
pool.connections.dead.should.eql([]);
_.keys(pool.index).length.should.eql(1);
});
it('closes the connection when it removes it', function () {
pool.addConnection(connection);
connection.status.should.eql('alive');
listenerCount(connection, 'status set').should.eql(1);
pool.removeConnection(connection);
connection.status.should.eql('closed');
listenerCount(connection, 'status set').should.eql(0);
});
});
it('#setHosts syncs the list of Hosts with the connections in the index', function () {
// there should now be two connections
pool.setHosts([host, host2]);
pool.connections.alive.length.should.eql(2);
pool.connections.dead.length.should.eql(0);
// get the new connections
connection = pool.index[host.toString()];
connection2 = pool.index[host2.toString()];
// should remove the second connection
pool.setHosts([host]);
pool.connections.alive.should.eql([connection]);
pool.connections.dead.length.should.eql(0);
// should skip the first, but create a new for the second
pool.setHosts([host, host2]);
pool.connections.alive.length.should.eql(2);
pool.connections.dead.length.should.eql(0);
// a new connection should have been created
pool.index[host2.toString()].should.not.be.exactly(connection2);
});
});
describe('Connection selection', function () {
var pool, host, host2;
beforeEach(function () {
pool = new ConnectionPool({});
host = new Host('localhost:9200');
host2 = new Host('localhost:9201');
pool.setHosts([
host,
host2
]);
});
it('detects if the selector is async', function (done) {
pool.selector = function (list, cb) {
cb.should.have.type('function');
cb();
};
pool.select(function (err) {
if (err) { throw err; }
done();
});
});
it('detects if the selector is not async', function (done) {
pool.selector = function (list) {
arguments.should.have.length(1);
};
pool.select(function (err) {
if (err) { throw err; }
done();
});
});
it('sync selectors should still return async', function (done) {
pool.selector = function (list) {
return list[0];
};
var selected = null;
pool.select(function (err, selection) {
if (err) { throw err; }
selection.host.should.be.exactly(host);
selected = selection;
done();
});
should(selected).be.exactly(null);
});
it('should catch errors in sync selectors', function (done) {
pool.selector = function (list) {
return JSON.notAMethod();
};
pool.select(function (err, selection) {
should(err).Error;
done();
});
});
it('should automatically select the first dead connection when there no living connections', function (done) {
pool.connections.alive = [];
pool.connections.dead = [1, 2, 3];
pool.select(function (err, selection) {
selection.should.be.exactly(1);
done();
});
});
});
describe('Connection state management', function () {
var pool, host, host2, connection, connection2;
beforeEach(function () {
pool = new ConnectionPool({});
host = new Host('localhost:9200');
host2 = new Host('localhost:9201');
pool.setHosts([
host,
host2
]);
connection = pool.index[host2.toString()];
connection2 = pool.index[host2.toString()];
pool.connections.alive.should.have.length(2);
pool.connections.dead.should.have.length(0);
});
it('moves an alive connection to dead', function () {
connection.setStatus('dead');
pool.connections.alive.should.have.length(1);
pool.connections.dead.should.have.length(1);
});
it('moves a dead connection to the end of the dead list when it re-dies', function () {
connection.setStatus('dead');
connection2.setStatus('dead');
// connection is at the front of the line
pool.connections.dead[0].should.be.exactly(connection);
// it re-dies
connection.setStatus('dead');
// connection2 is now at the front of the list
pool.connections.dead[0].should.be.exactly(connection2);
});
it('moves a does nothing when a connection is re-alive', function () {
var last = pool.connections.alive[pool.connections.alive.length - 1];
var first = pool.connections.alive[0];
last.should.not.be.exactly(first);
// first re-alives
first.setStatus('alive');
pool.connections.alive[0].should.be.exactly(first);
pool.connections.alive[pool.connections.alive.length - 1].should.be.exactly(last);
// last re-alives
last.setStatus('alive');
pool.connections.alive[0].should.be.exactly(first);
pool.connections.alive[pool.connections.alive.length - 1].should.be.exactly(last);
});
it('removes all its connection when it closes, causing them to be closed', function () {
pool.close();
pool.connections.alive.should.have.length(0);
pool.connections.dead.should.have.length(0);
connection.status.should.eql('closed');
connection2.status.should.eql('closed');
});
});
});

25
test/unit/test_errors.js Normal file
View File

@ -0,0 +1,25 @@
var errors = require('../../src/lib/errors');
var _ = require('lodash');
_.each(errors, function (CustomError, name) {
if (name.charAt(0) !== '_') {
describe(name, function () {
it('extend the ErrorAbstract and Error classes', function () {
var err = new CustomError();
err.message.length.should.be.above(7);
err.should.be.an.instanceOf(Error).and.an.instanceOf(errors._Abstract);
});
});
}
});
describe('Error Abstract', function () {
it('provides a stack property in the browser', function () {
var isBrowser = process.browser;
process.browser = true;
var err = new errors._Abstract();
process.browser = isBrowser;
err.stack.should.be.exactly('');
});
});

134
test/unit/test_host.js Normal file
View File

@ -0,0 +1,134 @@
var Host = require('../../src/lib/host');
var _ = require('lodash');
var url = require('url');
describe('Host class', function () {
describe('construction', function () {
it('properly sets the defaults', function () {
var host = new Host();
host.should.eql({
protocol: 'http',
host: 'localhost',
port: 9200,
path: '/',
auth: null,
query: {}
});
});
it('accepts a string for query', function () {
var host = new Host({ query: 'beep=boop'});
host.query.should.eql({
beep: 'boop'
});
});
it('accepts other generic params', function () {
var headers = { 'X-Special-Routing-Header': 'pie' };
var host = new Host({ headers: headers });
host.headers.should.be.exactly(headers);
});
it('accepts a string for the entire url', function () {
var host = new Host('john:dude@pizza.com:420/pizza/cheese?shrooms=true');
host.should.eql({
protocol: 'http',
host: 'pizza.com',
port: 420,
path: '/pizza/cheese',
auth: 'john:dude',
query: {
shrooms: 'true'
}
});
});
describe('based on the output from url.parse', function () {
it('might cause weird things to happen', function () {
var parsedUrl = url.parse('pizza.com:888');
// I imagine most people don't expect
parsedUrl.should.include({
protocol: 'pizza.com:',
host: '888',
});
var host = new Host(parsedUrl);
host.protocol.should.eql('pizza.com');
host.host.should.eql('888');
});
it('will cause extra properties', function () {
var host = new Host(url.parse('https://joe:diner@pizza.com:888/path?query=yes'));
host.should.include({
protocol: 'https',
host: 'pizza.com',
port: 888,
path: '/path',
auth: 'joe:diner',
query: {
query: 'yes'
}
});
_.keys(host).should.include('slashes', 'hash', 'href', 'search');
});
});
it('ignores anything that\'s not a string or object-y', function () {
var host = new Host(1234);
host.should.eql({
protocol: 'http',
host: 'localhost',
port: 9200,
path: '/',
auth: null,
query: {}
});
});
});
describe('#makeUrl', function () {
it('merges parameters', function () {
var host = new Host({
path: '/prefix',
query: {
user_id: 123
}
});
host.makeUrl({
path: '/this and that',
query: {
param: 1
}
}).should.eql('http://localhost:9200/prefix/this and that?param=1&user_id=123');
});
it('ensures that path starts with a forward-slash', function () {
var host = new Host();
host.path = 'prefix';
host.makeUrl({ path: '/this and that'})
.should.eql('http://localhost:9200/prefix/this and that');
});
it('does not try to prevent double forward-slashes', function () {
var host = new Host({ path: 'prefix/' });
host.makeUrl({ path: '/this and that'})
.should.eql('http://localhost:9200/prefix//this and that');
});
});
describe('#toString', function () {
it('just calls makeUrl with no parameters', function () {
});
});
});

View File

@ -0,0 +1,58 @@
describe('Http Connector', function () {
var Host = require('../../src/lib/host');
var HttpConnection = require('../../src/lib/connectors/http');
describe('#makeReqParams', function () {
it('properly reads the host object', function () {
var host = new Host('john:dude@pizza.com:9200/pizza/cheese?shrooms=true');
var con = new HttpConnection(host, {});
var reqParams = con.makeReqParams();
reqParams.should.eql({
method: 'GET',
protocol: 'http:',
auth: 'john:dude',
hostname: 'pizza.com',
port: 9200,
path: '/pizza/cheese?shrooms=true',
headers: host.headers,
agent: con.agent
});
});
it('accepts merges a query object with the hosts\'', function () {
var con = new HttpConnection(new Host({
query: {
user_id: 123
}
}));
var reqParams = con.makeReqParams({
query: {
jvm: 'yes'
}
});
reqParams.should.include({
path: '/?jvm=yes&user_id=123'
});
});
// it('works with an empty query', function () {
// var reqParams = con.makeReqParams();
// reqParams.should.include({
// method: 'GET',
// path: '/'
// });
// Object.keys(reqParams).should.not.include([
// 'host', 'pathname', 'query'
// ]);
// });
});
});

259
test/unit/test_log.js Normal file
View File

@ -0,0 +1,259 @@
var Log = require('../../src/lib/log');
var _ = require('lodash');
describe('Log class', function () {
describe('::parseLevels', function () {
it('accepts a string and returns it and the other levels below it', function () {
Log.parseLevels('trace').should.eql([
'error',
'warning',
'info',
'debug',
'trace'
]);
});
it('accepts and validates an array of levels', function () {
Log.parseLevels(['warning', 'info']).should.eql(['warning', 'info']);
});
it('throws an error when an invalid string is supplied', function () {
(function () {
Log.parseLevels('INVALID');
}).should.throw(/invalid logging level/);
});
it('throws an error when an invalid string is supplied in side an array', function () {
(function () {
Log.parseLevels(['error', 'INVALID']);
}).should.throw(/invalid logging level/);
});
});
describe('#addOutput', function () {
var log;
Log.loggers.stub = function (log, config) {
this.config = config;
};
beforeEach(function () {
log = new Log();
});
it('returns the newly created logger', function () {
log.addOutput({ type: 'stub' }).should.be.an.instanceOf(Log.loggers.stub);
});
it('Accepts a config object with `level: "{{level}}"`', function () {
var logger = log.addOutput({
type: 'stub',
level: 'warning'
});
logger.config.should.include({
levels: [
'error', 'warning'
]
});
});
it('Accepts a config object with `level: ["{{level}}"]`', function () {
var logger = log.addOutput({
type: 'stub',
level: ['warning']
});
logger.config.should.include({
levels: [
'warning'
]
});
});
it('Accepts a config object with `levels: "{{level}}"`', function () {
var logger = log.addOutput({
type: 'stub',
levels: 'warning'
});
logger.config.should.include({
levels: [
'error', 'warning'
]
});
});
it('Accepts a config object with `levels: ["{{level}}"]`', function () {
var logger = log.addOutput({
type: 'stub',
level: ['warning']
});
logger.config.should.include({
levels: [
'warning'
]
});
});
});
describe('#join', function () {
it('joins strings together with spaces', function () {
Log.join(['foo', 'bar']).should.eql('foo bar');
});
it('stringifies objects', function () {
Log.join([{ foo: 'bar' }]).should.eql('{ foo: \'bar\' }\n');
});
});
describe('instance without any outputs', function () {
var log;
beforeEach(function () {
log = new Log();
});
it('should not emit any events', function () {
log.emit = function () {
throw new Error('Emit should not be called');
};
log.error();
log.info();
log.warning();
log.debug();
log.trace();
});
});
describe('instance without one output listening to all events', function () {
var log, call;
beforeEach(function () {
call = void 0;
log = new Log({
log: [
{
type: function (log, config) {
log.on('error', _.noop);
log.on('warning', _.noop);
log.on('info', _.noop);
log.on('debug', _.noop);
log.on('trace', _.noop);
}
}
]
});
log.emit = function (eventName) {
call = {
event : eventName,
args: Array.prototype.slice.call(arguments, 1)
};
};
});
it('should emit an "error" event with an Error object arg', function () {
var err = new Error('error');
log.error(err);
call.event.should.eql('error');
call.args[0].should.be.exactly(err);
call = void 0;
log.error('error');
call.event.should.eql('error');
call.args[0].should.be.an.instanceOf(Error);
call.args[0].message.should.eql('error');
});
it('should emit a "warning" event with a single message arg for #warning calls', function () {
log.warning('shit!');
call.event.should.eql('warning');
call.args.should.have.length(1);
call.args[0].should.eql('shit!');
});
it('should emit a "info" event with a single message arg for #info calls', function () {
log.info('look out!');
call.event.should.eql('info');
call.args.should.have.length(1);
call.args[0].should.eql('look out!');
});
it('should emit a "debug" event with a single message arg for #debug calls', function () {
log.debug('here');
call.event.should.eql('debug');
call.args.should.have.length(1);
call.args[0].should.eql('here');
});
it('should emit a trace event for trace events, with message and curlCall args', function () {
log.trace('GET', 'http://localhost:9200/_cluster/nodes', '', '', 200);
call.event.should.eql('trace');
call.args.should.have.length(2);
call.args[0].should.match(/^<- 200/);
call.args[1].should.match(/^curl /);
});
});
describe('constructor', function () {
it('looks for output config options at config.log', function () {
var log = new Log({ log: { type: process.browser ? 'console' : 'stdio', level: 'error' } });
log.listenerCount('error').should.eql(1);
log.listenerCount('warning').should.eql(0);
log.listenerCount('info').should.eql(0);
log.listenerCount('debug').should.eql(0);
log.listenerCount('trace').should.eql(0);
});
it('accepts a string and treat it as a log level', function () {
var log = new Log({ log: 'error' });
log.listenerCount('error').should.eql(1);
log.listenerCount('warning').should.eql(0);
log.listenerCount('info').should.eql(0);
log.listenerCount('debug').should.eql(0);
log.listenerCount('trace').should.eql(0);
});
it('accepts an array of strings and treat it as a log level config', function () {
var log = new Log({ log: ['error', 'trace'] });
log.listenerCount('error').should.eql(1);
log.listenerCount('warning').should.eql(0);
log.listenerCount('info').should.eql(0);
log.listenerCount('debug').should.eql(0);
log.listenerCount('trace').should.eql(1);
});
it('accepts an array of output config objects', function () {
var log = new Log({ log: [{ level: 'error' }, { level: 'trace'}] });
log.listenerCount('error').should.eql(2);
log.listenerCount('warning').should.eql(1);
log.listenerCount('info').should.eql(1);
log.listenerCount('debug').should.eql(1);
log.listenerCount('trace').should.eql(1);
});
it('rejects numbers and other truthy data-types', function () {
(function () {
var log = new Log({ log: 1515 });
}).should.throw(/invalid logging output config/i);
(function () {
var log = new Log({ log: /regexp/ });
}).should.throw(/invalid logging output config/i);
(function () {
var log = new Log({ log: new Date() });
}).should.throw(/invalid logging output config/i);
(function () {
var log = new Log({ log: [1515] });
}).should.throw(/invalid logging output config/i);
(function () {
var log = new Log({ log: [/regexp/] });
}).should.throw(/invalid logging output config/i);
(function () {
var log = new Log({ log: [new Date()] });
}).should.throw(/invalid logging output config/i);
});
});
});

View File

@ -0,0 +1,19 @@
var randomSelector = require('../../src/lib/selectors/random');
var _ = require('lodash');
describe('Random Selector', function () {
it('chooses a selection by random', function () {
var log = { a: 0, b: 0, c: 0 };
var choices = _.keys(log);
_.times(1000, function () {
var choice = randomSelector(choices);
log[choice]++;
});
_.filter(log, function (count) {
return count < 200 || count > 400;
}).should.have.length(0);
});
});

View File

@ -0,0 +1,16 @@
var selector = require('../../src/lib/selectors/round_robin');
var _ = require('lodash');
describe('Round Robin Selector', function () {
it('chooses options in order', function () {
var options = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
var expected = _.clone(options);
var selections = [];
_.times(options.length, function () {
selections.push(selector(options));
});
selections.should.eql(expected);
});
});

View File

@ -0,0 +1,71 @@
var es = require('../../src/elasticsearch');
var Log = require('../../src/lib/log');
var StdioLogger = require('../../src/lib/loggers/stdio');
var _ = require('../../src/lib/utils');
var expect = require('expect.js');
var EventEmitter = require('events').EventEmitter;
describe('Stdio Logger', function () {
var log, logger;
// pulled from chalk's stripColor function.
var hasColorRE = /\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/;
function listenerCount(emitter, event) {
if (EventEmitter.listenerCount) {
return EventEmitter.listenerCount(emitter, event);
} else {
return emitter.listeners(event).length;
}
}
describe('pays attention to the level setting', function () {
beforeEach(function () {
log = new Log();
log.emit = function (name/*, ...args */) {
log._emission = {
name: name,
args: Array.prototype.slice(arguments, 1)
};
};
// new logger in warning mode
logger = new StdioLogger(log, {
levels: Log.parseLevels('trace')
});
});
afterEach(function () {
log.close();
});
it('listenes for all the events', function () {
listenerCount(log, 'error').should.eql(1);
listenerCount(log, 'warning').should.eql(1);
listenerCount(log, 'info').should.eql(1);
listenerCount(log, 'debug').should.eql(1);
listenerCount(log, 'trace').should.eql(1);
});
it('emits events because something is listening', function () {
log.error(new Error('error message'));
log._emission.name.should.eql('error');
log.warning('warning');
log._emission.name.should.eql('warning');
log.info('info');
log._emission.name.should.eql('info');
log.debug('debug');
log._emission.name.should.eql('debug');
log.trace('GET', {}, '', '', 200);
log._emission.name.should.eql('trace');
});
});
});

294
test/unit/test_utils.js Normal file
View File

@ -0,0 +1,294 @@
var _ = require('../../src/lib/utils');
var should = require('should');
describe('Utils', function () {
describe('Additional Type Checkers', function () {
_.forEach({
Object: {
is: [[], /regexp/]
},
PlainObject: {
is: [{}, {}]
},
String: {
is: ['steamy', 'poop'],
not: {}
},
Array: {
is: [['im'], ['usefull']],
},
Finite: {
is: [11123, 666],
not: Infinity
},
Function: {
is: [console.error, console.log],
},
RegExp: {
is: [/.*/, new RegExp('a')],
}
},
function (thing, name) {
describe('#isArrayOf' + name, function (test) {
it('likes arrays of ' + name, function () {
should(_['isArrayOf' + name + 's'](thing.is)).be.ok;
});
it('dislikes when there is even one non ' + name, function () {
// notice a string in the array
thing.is.push(thing.not || ' not ');
should(_['isArrayOf' + name + 's'](thing.is)).not.be.ok;
});
});
});
describe('#isNumeric', function () {
it('likes integer literals', function () {
should(_.isNumeric('-10')).be.ok;
should(_.isNumeric('0')).be.ok;
should(_.isNumeric('5')).be.ok;
should(_.isNumeric(-16)).be.ok;
should(_.isNumeric(0)).be.ok;
should(_.isNumeric(32)).be.ok;
should(_.isNumeric('040')).be.ok;
should(_.isNumeric(0144)).be.ok;
should(_.isNumeric('0xFF')).be.ok;
should(_.isNumeric(0xFFF)).be.ok;
});
it('likes float literals', function () {
should(_.isNumeric('-1.6')).be.ok;
should(_.isNumeric('4.536')).be.ok;
should(_.isNumeric(-2.6)).be.ok;
should(_.isNumeric(3.1415)).be.ok;
should(_.isNumeric(8e5)).be.ok;
should(_.isNumeric('123e-2')).be.ok;
});
it('dislikes non-numeric stuff', function () {
should(_.isNumeric('')).not.be.ok;
should(_.isNumeric(' ')).not.be.ok;
should(_.isNumeric('\t\t')).not.be.ok;
should(_.isNumeric('abcdefghijklm1234567890')).not.be.ok;
should(_.isNumeric('xabcdefx')).not.be.ok;
should(_.isNumeric(true)).not.be.ok;
should(_.isNumeric(false)).not.be.ok;
should(_.isNumeric('bcfed5.2')).not.be.ok;
should(_.isNumeric('7.2acdgs')).not.be.ok;
should(_.isNumeric(undefined)).not.be.ok;
should(_.isNumeric(null)).not.be.ok;
should(_.isNumeric(NaN)).not.be.ok;
should(_.isNumeric(Infinity)).not.be.ok;
should(_.isNumeric(Number.POSITIVE_INFINITY)).not.be.ok;
should(_.isNumeric(Number.NEGATIVE_INFINITY)).not.be.ok;
should(_.isNumeric(new Date(2009, 1, 1))).not.be.ok;
should(_.isNumeric([])).not.be.ok;
should(_.isNumeric([1, 2, 3, 4])).not.be.ok;
should(_.isNumeric({})).not.be.ok;
should(_.isNumeric(function () {})).not.be.ok;
});
});
describe('#isInterval', function () {
_.forEach({
M: 'months',
w: 'weeks',
d: 'days',
h: 'hours',
m: 'minutes',
s: 'seconds',
y: 'years'
},
function (name, unit) {
it('likes ' + name, function () {
should(_.isInterval('1' + unit)).be.ok;
});
it('likes decimal ' + name, function () {
should(_.isInterval('1.5' + unit)).be.ok;
});
});
it('dislikes more than one unit', function () {
should(_.isInterval('1my')).not.be.ok;
});
it('dislikes spaces', function () {
should(_.isInterval('1 m')).not.be.ok;
});
});
});
describe('String Transformers', function () {
describe('#camelCase', function () {
it('find spaces, underscores, and other natural word breaks', function () {
_.camelCase('Neil Patrick.Harris-is_a.dog').should.eql('neilPatrickHarrisIsADog');
});
it('ignores abreviations', function () {
_.camelCase('Json_parser').should.eql('jsonParser');
});
it('handles trailing _', function () {
_.camelCase('_thing_one_').should.eql('thingOne');
});
});
describe('#studlyCase', function () {
it('find spaces, underscores, and other natural word breaks', function () {
_.studlyCase('Neil Patrick.Harris-is_a.dog').should.eql('NeilPatrickHarrisIsADog');
});
it('ignores abreviations', function () {
_.studlyCase('Json_parser').should.eql('JsonParser');
});
it('handles trailing _', function () {
_.studlyCase('_thing_one_').should.eql('ThingOne');
});
});
describe('#snakeCase', function () {
it('find spaces, underscores, and other natural word breaks', function () {
_.snakeCase('Neil Patrick.Harris-is_a.dog').should.eql('neil_patrick_harris_is_a_dog');
});
it('ignores abreviations', function () {
_.snakeCase('Json_parser').should.eql('json_parser');
});
it('handles trailing _', function () {
_.snakeCase('_thing_one_').should.eql('thing_one');
});
});
describe('#toLowerString', function () {
it('transforms normal strings', function () {
_.toLowerString('PASTA').should.eql('pasta');
});
it('ignores long form empty vals (null, false, undef)', function () {
_.toLowerString(null).should.eql('');
_.toLowerString(false).should.eql('');
_.toLowerString(void 0).should.eql('');
});
it('uses the objects own toString', function () {
_.toLowerString(['A', 'B']).should.eql('a,b');
});
it('sorta kinda works on objects', function () {
_.toLowerString({a: 'thing'}).should.eql('[object object]');
});
});
describe('#toUpperString', function () {
it('transforms normal strings', function () {
_.toUpperString('PASTA').should.eql('PASTA');
});
it('ignores long form empty vals (null, false, undef)', function () {
_.toUpperString(null).should.eql('');
_.toUpperString(false).should.eql('');
_.toUpperString(void 0).should.eql('');
});
it('uses the objects own toString', function () {
_.toUpperString(['A', 'B']).should.eql('A,B');
});
it('sorta kinda works on objects', function () {
_.toUpperString({a: 'thing'}).should.eql('[OBJECT OBJECT]');
});
});
describe('#repeat', function () {
it('repeats strings', function () {
_.repeat(' ', 5).should.eql(' ');
_.repeat('foobar', 2).should.eql('foobarfoobar');
});
});
describe('#ucfirst', function () {
it('only capitalized the first letter, lowercases everything else', function () {
_.ucfirst('ALGER').should.eql('Alger');
});
});
});
describe('#deepMerge', function () {
it('returns the same object that was passed', function () {
var obj = {
foo: 'bar'
};
_.deepMerge(obj, { bar: 'baz' }).should.eql(obj);
});
it('concats arrays', function () {
var obj = {
foo: ['bax', 'boz']
};
_.deepMerge(obj, { foo: ['boop'] });
obj.foo.should.have.a.lengthOf(3);
});
it('wont merge values of different types', function () {
var obj = {
foo: ['stop', 'foo', 'stahp']
};
_.deepMerge(obj, { foo: 'string' });
obj.foo.should.have.a.lengthOf(3);
});
it('works recursively', function () {
var obj = {
foo: 'bar',
bax: {
foo: ['bax', 'boz']
}
};
_.deepMerge(obj, { bax: { foo: ['poo'] }});
obj.bax.foo.should.have.a.lengthOf(3);
});
});
describe('#createArray', function () {
it('accepts an array of things and simply returns a copy of it', function () {
var inp = [{ a: 1 }, 'pizza'];
var out = _.createArray(inp);
out.should.eql(inp);
out.should.not.be.exactly(inp);
});
it('accepts a primitive value and calls the the transform function', function (done) {
var out = _.createArray('str', function (val) {
val.should.be.exactly('str');
done();
});
});
it('wraps any non-array in an array', function () {
_.createArray({}).should.eql([{}]);
_.createArray('').should.eql(['']);
_.createArray(123).should.eql([123]);
_.createArray(/abc/).should.eql([/abc/]);
_.createArray(false).should.eql([false]);
});
it('returns false when the transform function returns undefined', function () {
_.createArray(['str', 1], function (val) {
if (_.isString(val)) {
return {
val: val
};
}
}).should.be.exactly(false);
});
});
});

View File

@ -1,220 +0,0 @@
var _ = require('../../src/lib/utils')
, expect = require('expect.js');
describe('Utils', function () {
describe('Additional Type Checkers', function () {
_.forEach({
Object: {
is: [[], console.log]
},
PlainObject: {
is: [{}, {}]
},
String: {
is: ['steamy', 'poop'],
not: {}
},
Array: {
is: [['im'], ['usefull']],
},
Finite: {
is: [11123, 666],
not: Infinity
},
Function: {
is: [console.error, console.log],
},
RegExp: {
is: [/.*/, new RegExp('a')],
}
},
function (thing, name) {
describe('#isArrayOf' + name, function (test) {
it('likes arrays of ' + name, function () {
expect(_['isArrayOf' + name + 's'](thing.is)).to.be.true;
});
it('dislikes when there is even one non ' + name, function () {
// notice a string in the array
thing.is.push(thing.not || ' not ');
expect(_['isArrayOf' + name + 's'](thing.is)).to.be.false;
});
});
});
describe('#isNumeric', function () {
it('likes Infinity', function () {
expect(_.isNumeric(Infinity)).to.be.true;
});
it('likes strings', function () {
expect(_.isNumeric('100')).to.be.true;
});
it('likes integers', function () {
expect(_.isNumeric(100)).to.be.true;
});
it('likes floats', function () {
expect(_.isNumeric(100.1)).to.be.true;
});
it('likes exponentials', function () {
expect(_.isNumeric(100e1)).to.be.true;
});
it('likes hexidecimals', function () {
expect(_.isNumeric(0x100)).to.be.true;
});
it('likes imaginary numbers', function () {
expect(_.isNumeric('yeah right')).to.be.false;
});
it('dislikes strings with words', function () {
expect(_.isNumeric('100heat')).to.be.false;
});
it('dislikes strings with words even if they are seperate', function () {
expect(_.isNumeric('100 pasta')).to.be.false;
});
it('dislikes null', function () {
expect(_.isNumeric(null)).to.be.false;
});
});
describe('#isInterval', function () {
_.forEach({
M: 'months',
w: 'weeks',
d: 'days',
h: 'hours',
m: 'minutes',
s: 'seconds',
y: 'years'
},
function (name, unit) {
it('likes ' + name, function () { expect(_.isInterval('1' + unit)).to.be.true; });
it('likes decimal ' + name, function () { expect(_.isInterval('1.5' + unit)).to.be.true; });
});
it('dislikes more than one unit', function () {
expect(_.isInterval('1my')).to.be.false;
});
it('dislikes spaces', function () {
expect(_.isInterval('1 m')).to.be.false;
});
});
});
describe('String Transformers', function () {
describe('#camelCase', function () {
it('find spaces, underscores, and other natural word breaks', function () {
expect(_.camelCase('Neil Patrick.Harris-is_a.dog')).to.eql('neilPatrickHarrisIsADog');
});
it('ignores abreviations', function () {
expect(_.camelCase('Json_parser')).to.eql('jsonParser');
});
it('handles trailing _', function () {
expect(_.camelCase('_thing_one_')).to.eql('thingOne');
});
});
describe('#studlyCase', function () {
it('find spaces, underscores, and other natural word breaks', function () {
expect(_.studlyCase('Neil Patrick.Harris-is_a.dog')).to.eql('NeilPatrickHarrisIsADog');
});
it('ignores abreviations', function () {
expect(_.studlyCase('Json_parser')).to.eql('JsonParser');
});
it('handles trailing _', function () {
expect(_.studlyCase('_thing_one_')).to.eql('ThingOne');
});
});
describe('#snakeCase', function () {
it('find spaces, underscores, and other natural word breaks', function () {
expect(_.snakeCase('Neil Patrick.Harris-is_a.dog')).to.eql('neil_patrick_harris_is_a_dog');
});
it('ignores abreviations', function () {
expect(_.snakeCase('Json_parser')).to.eql('json_parser');
});
it('handles trailing _', function () {
expect(_.snakeCase('_thing_one_')).to.eql('thing_one');
});
});
describe('#toLowerString', function () {
it('transforms normal strings', function () {
expect(_.toLowerString('PASTA')).to.eql('pasta');
});
it('ignores long form empty vals (null, false, undef)', function () {
expect(_.toLowerString(null)).to.eql('');
expect(_.toLowerString(false)).to.eql('');
expect(_.toLowerString(void 0)).to.eql('');
});
it('uses the objects own toString', function () {
expect(_.toLowerString(['A', 'B'])).to.eql('a,b');
});
it('sorta kinda works on objects', function () {
expect(_.toLowerString({a: 'thing'})).to.eql('[object object]');
});
});
});
describe('#deepMerge', function () {
it('returns the same object that was passed', function () {
var obj = {
foo: 'bar'
};
expect(_.deepMerge(obj, { bar: 'baz' })).to.eql(obj);
});
it('concats arrays', function () {
var obj = {
foo: ['bax', 'boz']
};
_.deepMerge(obj, { foo: ['boop'] });
expect(obj.foo).to.have.length(3);
});
it('wont merge values of different types', function () {
var obj = {
foo: ['stop', 'foo', 'stahp']
};
_.deepMerge(obj, { foo: 'string' });
expect(obj.foo).to.have.length(3);
});
it('works recursively', function () {
var obj = {
foo: 'bar',
bax: {
foo: ['bax', 'boz']
}
};
_.deepMerge(obj, { bax: { foo: ['poo'] }});
expect(obj.bax.foo).to.have.length(3);
});
});
});