Added browser build, including angular version. minified versions available

This commit is contained in:
Spencer Alger
2013-10-29 19:47:00 -07:00
parent 2557202bf8
commit 286a08c8c2
29 changed files with 32934 additions and 518 deletions

View File

@ -3,8 +3,18 @@
module.exports = function (grunt) {
var _ = require('lodash');
var sharedBrowserfyExclusions = [
'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',
pkg: grunt.file.readJSON('package.json'),
meta: {
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
@ -13,6 +23,11 @@ module.exports = function (grunt) {
' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
' Licensed <%= pkg.license %> */\n\n'
},
clean: {
dist: {
src: ['<%= distDir %>']
}
},
mochaTest: {
unit: [
'test/unit/**/*.test.js'
@ -76,6 +91,49 @@ module.exports = function (grunt) {
'scripts/generate/yaml_tests'
]
}
},
browserify: {
generic: {
files: {
'<%= distDir %>/elasticsearch.js': 'src/elasticsearch.js'
},
options: {
standalone: 'true',
ignore: _.union(sharedBrowserfyExclusions, [
'src/lib/connectors/jquery.js',
'src/lib/connectors/angular.js'
])
}
},
angular: {
files: {
'<%= distDir %>/elasticsearch.angular.js': ['src/elasticsearch.angular.js']
},
options: {
standalone: 'true',
ignore: _.union(sharedBrowserfyExclusions, [
'src/lib/connectors/jquery.js',
'src/lib/connectors/xhr.js'
])
}
}
},
uglify: {
dist: {
files: {
'<%= distDir %>/elasticsearch.min.js': '<%= distDir %>/elasticsearch.js',
'<%= distDir %>/elasticsearch.angular.min.js': '<%= distDir %>/elasticsearch.angular.js'
},
options: {
report: 'min',
banner: '<%= meta.banner %>'
},
global_defs: {
process: {
browser: true
}
}
}
}//,
// docular: {
// groups: [
@ -119,8 +177,11 @@ module.exports = function (grunt) {
// load plugins
// grunt.loadNpmTasks('grunt-docular');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
// Default task.
@ -131,6 +192,12 @@ module.exports = function (grunt) {
'mochaTest:yaml_suite'
]);
grunt.registerTask('build', [
'clean:dist',
'browserify',
'uglify:dist'
]);
grunt.task.registerMultiTask('generate', 'used to generate things', function () {
var done = this.async();
var proc = require('child_process').spawn(

15951
dist/elasticsearch.angular.js vendored Normal file

File diff suppressed because it is too large Load Diff

8
dist/elasticsearch.angular.min.js vendored Normal file

File diff suppressed because one or more lines are too long

15948
dist/elasticsearch.js vendored Normal file

File diff suppressed because it is too large Load Diff

8
dist/elasticsearch.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -19,11 +19,14 @@
"expect.js": "~0.2.0",
"async": "~0.2.9",
"optimist": "~0.6.0",
"minimatch": "~0.2.12"
"minimatch": "~0.2.12",
"browserify": "~2.35.0",
"grunt-browserify": "~1.2.9",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-uglify": "~0.2.5"
},
"license": "Apache License",
"dependencies": {
"require-directory": "git://github.com/spenceralger/node-require-directory.git#master",
"cli-color": "~0.2.3",
"lodash": "~2.2.1",
"tar": "~0.1.18"

View File

@ -26,7 +26,25 @@ function transformFile(entry) {
// itterate all of the specs within the file, should only be one
_.each(JSON.parse(entry.data), function (def, name) {
//camelcase the name
name = _.map(name.split('.'), _.camelCase).join('.');
var steps = name.split('.');
function transformParamKeys(note, param, key) {
var cmlKey = _.camelCase(key);
if (cmlKey !== key) {
param.name = key;
if (key.charAt(0) === '_') {
cmlKey = '_' + cmlKey;
}
}
note[cmlKey] = param;
}
def.url.params = _.transform(def.url.params, transformParamKeys, {});
def.url.parts = _.transform(def.url.parts, transformParamKeys, {});
var allParams = _.extend({}, def.url.params, def.url.parts);
var spec = {
name: name,
@ -53,6 +71,7 @@ function transformFile(entry) {
var optionalVars = {};
var requiredVars = {};
var param;
var name;
var target;
var match;
@ -61,19 +80,16 @@ function transformFile(entry) {
}
while (match = urlParamRE.exec(url)) {
param = def.url.parts[match[1]] || {};
name = _.camelCase(match[1]);
param = def.url.parts[name] || {};
target = (param.required || !param.default) ? requiredVars : optionalVars;
target[match[1]] = _.omit(param, 'required');
target[name] = _.omit(param, 'required', 'description', 'name');
}
[requiredVars, optionalVars].forEach(function (vars) {
_.each(vars, function (v, name) {
vars[name] = _.omit(v, 'description');
});
});
return _.omit({
fmt: url.replace(urlParamRE, '<%=$1%>'),
fmt: url.replace(urlParamRE, function (full, match) {
return '<%=' + _.camelCase(match) + '%>';
}),
opt: _.size(optionalVars) ? optionalVars : null,
req: _.size(requiredVars) ? requiredVars : null,
sortOrder: _.size(requiredVars) * -1
@ -87,15 +103,14 @@ function transformFile(entry) {
});
spec.params = _.transform(spec.params, function (note, param, name) {
param.name = name;
// param.name = name;
note[name] = _.pick(param, [
'type', 'default', 'options', 'required'
'type', 'default', 'options', 'required', 'name'
]);
}, {});
// escape method names with "special" keywords
var location = _.map(spec.name.split('.'), _.camelCase)
.join('.prototype.')
var location = spec.name.split('.').join('.prototype.')
.replace(/(^|\.)(delete|default)(\.|$)/g, '[\'$2\']');
var action = {

View File

@ -1,5 +1,5 @@
module.exports = {
'cluster.node_hot_threads': [
'cluster.nodeHotThreads': [
'/_cluster/nodes/hotthreads',
'/_cluster/nodes/hot_threads',
'/_nodes/hot_threads',
@ -7,7 +7,7 @@ module.exports = {
'/_cluster/nodes/{node_id}/hot_threads',
'/_nodes/{node_id}/hot_threads'
],
'cluster.node_info': [
'cluster.nodeInfo': [
'/_cluster/nodes',
'/_nodes/settings',
'/_nodes/os',
@ -29,10 +29,10 @@ module.exports = {
'/_nodes/{node_id}/http',
'/_nodes/{node_id}/plugin'
],
'cluster.node_shutdown': [
'cluster.nodeShutdown': [
'/_cluster/nodes/_shutdown'
],
'cluster.node_stats': [
'cluster.nodeStats': [
'/_cluster/nodes/stats',
'/_nodes/stats/{metric_family}',
'/_nodes/stats/indices/{metric}/{fields}',
@ -43,7 +43,7 @@ module.exports = {
'get': [
'/{index}/{type}/{id}/_source'
],
'indices.delete_mapping': [
'indices.deleteMapping': [
'/{index}/{type}/_mapping'
],
'indices.stats': [

View File

@ -1,3 +1,5 @@
/* jshint maxlen: false */
var ca = require('./client_action');
var errors = require('./errors');

View File

@ -33,8 +33,9 @@ var es = require('../../../src/elasticsearch'),
startingMoment = moment().startOf('day').subtract('days', days),
endingMoment = moment().endOf('day').add('days', days),
clientConfig = {
maxSockets: 1000,
log: {
level: ['info', 'error']
level: 'error'
}
};
@ -47,13 +48,13 @@ if (argv.host) {
var client = new es.Client(clientConfig);
var log = client.config.log;
log.info('Generating', count, 'events across ±', days, 'days');
console.log('Generating', count, 'events across ±', days, 'days');
fillIndecies(function () {
var actions = [],
samples = makeSamples(startingMoment, endingMoment);
async.timesSeries(count, function (i, done) {
async.times(count, function (i, done) {
// random date, plus less random time
var date = moment(samples.randomMsInDayRange())
.utc()
@ -175,7 +176,7 @@ function fillIndecies(cb) {
movingDate.add('day', 1);
}
async.parralel(indexPushActions, function (err, responses) {
async.parallel(indexPushActions, function (err, responses) {
if (err) {
client.config.log.error(err.message = 'Unable to create indicies: ' + err.message);
} else {

View File

@ -44,5 +44,7 @@ async.series([
});
}
], function (err) {
if (err) console.error(err);
if (err) {
console.error(err);
}
});

View File

@ -0,0 +1,44 @@
/**
* Wrapper for the elasticsearch.js client, which will register the client constructor
* as a factory within angular that can be easily injected with Angular's awesome DI.
*
* 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');
/* global angular */
angular.module('elasticsearch.client', [])
.factory('esFactory', ['$http', '$q', function ($http, $q) {
AngularConnector.prototype.$http = $http;
// store the original request function
Transport.prototype._request = Transport.prototype.request;
// 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);
}
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);
};
}]);

File diff suppressed because it is too large Load Diff

View File

@ -149,7 +149,11 @@ function exec(transport, spec, params, cb) {
params.body && (request.body = params.body);
params.ignore && (request.ignore = _.isArray(params.ignore) ? params.ignore : [params.ignore]);
params.timeout && (request.ignore = _.isArray(params.ignore) ? params.ignore : [params.ignore]);
if (params.timeout === void 0) {
request.timeout = 10000;
} else {
request.timeout = params.timeout;
}
// copy over some properties from the spec
spec.bulkBody && (request.bulkBody = true);
@ -227,5 +231,5 @@ function exec(transport, spec, params, cb) {
request.query = query;
transport.request(request, cb);
return transport.request(request, cb);
}

View File

@ -9,9 +9,26 @@ module.exports = ClientConfig;
var url = require('url');
var _ = require('./utils');
var Host = require('./host');
var selectors = _.reKey(_.requireDir(module, './selectors'), _.camelCase);
var connections = _.requireClasses(module, './connections');
var serializers = _.requireClasses(module, './serializers');
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');
}
_.each(connectors, function (conn, name) {
if (typeof conn !== 'function') {
delete connectors[name];
}
});
var serializers = {
Json: require('./serializers/json')
};
var extractHostPartsRE = /\[([^:]+):(\d+)]/;
var hostProtocolRE = /^([a-z]+:)?\/\//;
@ -31,7 +48,7 @@ var defaultConfig = {
protocol: 'http'
}
],
connectionClass: connections.Http,
connectionClass: process.browser ? connectors.Xhr : connectors.Http,
selector: selectors.roundRobin,
sniffOnStart: false,
sniffAfterRequests: null,
@ -59,16 +76,23 @@ var defaultConfig = {
}
};
// 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);
// validate connectionClass
if (typeof this.connectionClass !== 'function') {
if (typeof connections[this.connectionClass] === 'function') {
this.connectionClass = connections[this.connectionClass];
if (typeof connectors[this.connectionClass] === 'function') {
this.connectionClass = connectors[this.connectionClass];
} else {
throw new TypeError('Invalid connectionClass ' + this.connectionClass + '. ' +
'Expected a constructor or one of ' + _.keys(connections).join(', '));
throw new TypeError('Invalid connectionClass "' + this.connectionClass + '". ' +
'Expected a constructor or one of ' + _.keys(connectors).join(', '));
}
}
@ -77,7 +101,7 @@ function ClientConfig(config) {
if (_.has(selectors, this.selector)) {
this.selector = selectors[this.selector];
} else {
throw new TypeError('Invalid Selector ' + this.selector + '. ' +
throw new TypeError('Invalid Selector "' + this.selector + '". ' +
'Expected a function or one of ' + _.keys(selectors).join(', '));
}
}
@ -108,7 +132,7 @@ ClientConfig.prototype.prepareHosts = function (hosts) {
};
/**
* Shutdown the connections, log outputs, and clear timers
* Shutdown the connectionPool, log outputs, and clear timers
*/
ClientConfig.prototype.close = function () {
this.log.close();

View File

@ -1,7 +1,7 @@
module.exports = ConnectionAbstract;
var _ = require('./utils'),
EventEmitter = require('events').EventEmitter;
var _ = require('./utils');
var EventEmitter = require('events').EventEmitter;
/**
* Abstract class used for Connection classes

View File

@ -9,8 +9,7 @@
module.exports = ConnectionPool;
var _ = require('./utils');
var selectors = _.reKey(_.requireDir(module, './selectors'), _.camelCase);
var connectors = _.reKey(_.requireDir(module, './connections'), _.studlyCase);
var selectors = require('./selectors');
var EventEmitter = require('events').EventEmitter;
var errors = require('./errors');
var Host = require('./host');
@ -31,7 +30,7 @@ ConnectionPool.prototype.select = function (cb) {
this.config.selector(this.connections.alive, cb);
} else {
try {
cb(null, this.config.selector(this.connections.alive));
_.nextTick(cb, null, this.config.selector(this.connections.alive));
} catch (e) {
this.config.log.error(e);
cb(e);

View File

@ -1,15 +0,0 @@
/**
* Connection that registers a module with angular, using angular's $http service
* to communicate with ES.
*
* @class connections.Angular
*/
module.exports = AngularConnection;
var _ = require('../utils'),
ConnectionAbstract = require('../connection');
function AngularConnection() {
}
_.inherits(AngularConnection, ConnectionAbstract);

38
src/lib/connectors/angular.js vendored Normal file
View File

@ -0,0 +1,38 @@
/**
* Connection that registers a module with angular, using angular's $http service
* to communicate with ES.
*
* @class connections.Angular
*/
module.exports = AngularConnection;
var _ = require('../utils');
var ConnectionAbstract = require('../connection');
var ConnectionFault = require('../errors').ConnectionFault;
/* global angular */
function AngularConnection(host, config) {
ConnectionAbstract.call(this, host, config);
}
_.inherits(AngularConnection, ConnectionAbstract);
AngularConnection.prototype.request = function (params, cb) {
var timeoutId;
this.$http({
method: params.method,
url: this.host.makeUrl(params),
data: params.body,
cache: false,
timeout: params.timeout !== Infinity ? params.timeout : 0
}).then(function (response) {
cb(null, response.data, response.status);
}, function (err) {
cb(new ConnectionFault(err.message));
});
};
// must be overwritten before this connection can be used
AngularConnection.prototype.$http = null;

View File

@ -9,11 +9,11 @@ module.exports = XhrConnection;
var _ = require('../utils');
var ConnectionAbstract = require('../connection');
var ConnectionError = require('../errors').ConnectionError;
var TimeoutError = require('../errors').TimeoutError;
var ConnectionFault = require('../errors').ConnectionFault;
var TimeoutError = require('../errors').RequestTimeout;
function XhrConnection(config, nodeInfo) {
ConnectionAbstract.call(this, config, nodeInfo);
function XhrConnection(host, config) {
ConnectionAbstract.call(this, host, config);
}
_.inherits(XhrConnection, ConnectionAbstract);
@ -49,17 +49,17 @@ if (!getXhr) {
XhrConnection.prototype.request = function (params, cb) {
var xhr = getXhr();
var timeoutId;
var url = this.host.makeUrl(params);
if (params.auth) {
xhr.open(params.method, params.url, true, params.auth.user, params.auth.pass);
xhr.open(params.method, url, true, params.auth.user, params.auth.pass);
} else {
xhr.open(params.method, params.url, true);
xhr.open(params.method, url, true);
}
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
clearTimeout(timeoutId);
cb(xhr.status ? null : new ConnectionError(), xhr.responseText, xhr.status);
cb(xhr.status ? null : new ConnectionFault(), xhr.responseText, xhr.status);
}
};

View File

@ -49,17 +49,48 @@ Host.prototype = {
port: 9200,
auth: '',
path: '',
query: {}
query: false
};
Host.prototype.toUrl = function (path, query) {
if (query) {
query = '?' + qs.stringify(_.defaults(typeof query === 'string' ? qs.parse(query) : query, this.query));
} else {
query = '';
Host.prototype.makeUrl = function (params) {
params = params || {};
// build the port
var port = '';
if (this.port !== defaultPort[this.protocol]) {
// add an actual port
port = ':' + this.port;
}
return this.protocol + '://' +
this.host + (this.port !== defaultPort[this.protocol] ? ':' + this.port : '') +
'/' + this.path + (path || '') + query;
// 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) !== '/') {
path = '/' + path;
}
// build the query string
var query = '';
if (params.query) {
// if the user passed in a query, merge it with the defaults from the host
query = qs.stringify(
_.defaults(typeof params.query === 'string' ? qs.parse(params.query) : params.query, this.query)
);
} else if (this.query) {
// just stringify the hosts query
query = qs.stringify(this.query);
}
// prepend the ? if there is actually a valid query string
if (query) {
query = '?' + query;
}
return this.protocol + '://' + this.host + port + path + query;
};

View File

@ -1,6 +1,17 @@
var _ = require('./utils'),
url = require('url'),
EventEmitter = require('events').EventEmitter;
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')
};
}
/**
* Log bridge, which is an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
@ -20,7 +31,7 @@ function Log(config) {
this.config = config || {};
var i;
var output = config.log || 2;
var output = _.isPlainObject(config.log) ? config.log : 'warning';
if (_.isString(output) || _.isFinite(output)) {
output = [
@ -170,15 +181,19 @@ Log.prototype.addOutput = function (config) {
var levels = Log.parseLevels(config.levels || config.level || 'warning');
_.defaults(config || {}, {
type: 'stdio',
type: process.browser ? 'Console' : 'Stdio',
});
// force the levels config
delete config.level;
config.levels = levels;
var Logger = require('./loggers/' + config.type);
var Logger = loggers[config.type];
if (Logger) {
return new Logger(config, this);
} else {
throw new Error('Invalid logger type "' + config.type + '". Expected one of ' + _.keys(loggers).join(', '));
}
};
/**

103
src/lib/loggers/console.js Normal file
View File

@ -0,0 +1,103 @@
/**
* Special version of the Stream logger, which logs errors and warnings to stderr and all other
* levels to stdout.
*
* @class Loggers.Console
* @extends LoggerAbstract
* @constructor
* @param {Object} config - The configuration for the Logger
* @param {string} config.level - The highest log level for this logger to output.
* @param {Log} bridge - The object that triggers logging events, which we will record
*/
module.exports = Console;
var LoggerAbstract = require('../logger');
var _ = require('../utils');
function Console(config, bridge) {
LoggerAbstract.call(this, config, bridge);
// config/state
this.color = _.has(config, 'color') ? !!config.color : true;
}
_.inherits(Console, LoggerAbstract);
/**
* Override the LoggerAbstract's setup listeners to do a little extra setup
*
* @param {Array} levels - The levels that we should be listeneing for
*/
Console.prototype.setupListeners = function (levels) {
// since some of our functions are bound a bit differently (to the console)
// create some of the bound properties manually
this.bound.onWarning = this.onWarning;
this.bound.onInfo = this.onInfo;
this.bound.onDebug = this.onDebug;
// call the super method
LoggerAbstract.prototype.setupListeners.call(this, levels);
};
/**
* Handler for the bridges "error" event
*
* @method onError
* @private
* @param {Error} e - The Error object to log
* @return {undefined}
*/
Console.prototype.onError = _.handler(function (e) {
if (console.error && console.trace) {
console.error(e.name === 'Error' ? 'ERROR' : e.name);
console.trace();
} else {
console.log(e.name === 'Error' ? 'ERROR' : e.name, e.stack);
}
});
/**
* Handler for the bridges "warning" event
*
* @method onWarning
* @private
* @param {String} msg - The message to be logged
* @return {undefined}
*/
Console.prototype.onWarning = console[console.warn ? 'warn' : 'log'].bind(console, 'WARNING');
/**
* Handler for the bridges "info" event
*
* @method onInfo
* @private
* @param {String} msg - The message to be logged
* @return {undefined}
*/
Console.prototype.onInfo = console[console.info ? 'info' : 'log'].bind(console, 'INFO');
/**
* Handler for the bridges "debug" event
*
* @method onDebug
* @private
* @param {String} msg - The message to be logged
* @return {undefined}
*/
Console.prototype.onDebug = console[console.debug ? 'debug' : 'log'].bind(console, 'DEBUG');
/**
* Handler for the bridges "trace" event
*
* @method onTrace
* @private
* @return {undefined}
*/
Console.prototype.onTrace = _.handler(function (method, url, body, responseBody, responseStatus) {
var message = 'curl "' + url.replace(/"/g, '\\"') + '" -X' + method.toUpperCase();
if (body) {
message += ' -d "' + body.replace(/"/g, '\\"') + '"';
}
message += '\n<- ' + responseStatus + '\n' + responseBody;
console.log('TRACE', message);
});

View File

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

View File

@ -45,6 +45,7 @@ TransportRequest.prototype._startRequest = function () {
}
params.req = {
timeout: params.timeout,
path: params.path,
query: params.query,
method: params.method,

View File

@ -1,7 +1,5 @@
var path = require('path'),
_ = require('lodash'),
fs = require('fs'),
requireDir = require('require-directory'),
nodeUtils = require('util');
/**
@ -32,37 +30,11 @@ utils.inspect = function (thing, opts) {
*/
utils.joinPath = path.join;
/**
* Require all of the modules in a directory
*
* @method requireDir
* @param {Function} module - The module object which will own the required modules.
* @param {String} path - Path to the directory which will be traversed (can be relative to module)
* @return {Object} - An object with each required files
*/
utils.requireDir = function (module, dirPath) {
if (dirPath && dirPath[0] === '.') {
dirPath = path.join(path.dirname(module.filename), dirPath);
}
return requireDir(module, dirPath);
};
/**
* Requires all of the files in a directory, then transforms the filenames into
* StudlyCase -- one level deep for now.
* @param {Function} module - The module object which will own the required modules.
* @param {String} dirPath - Path to the directory which will be traversed (can be relative to module)
* @return {Object} - An object with each required files, keys will be the StudlyCase version of the filenames.
*/
utils.requireClasses = function (module, dirPath) {
return utils.reKey(utils.requireDir(module, dirPath), utils.studlyCase, false);
};
/**
* Recursively re-key an object, applying "transform" to each key
* @param {Object} obj - The object to re-key
* @param {Function} transform - The transformation function to apply to each key
* @param {Boolean} [recursive=false] - Should this act recursively?
* @param {Boolean} [recursive=true] - Should this act recursively?
* @param {Object} out - used primarily for recursion, allows you to specify the object which new keys will be written to
* @return {Object}
*/
@ -141,6 +113,53 @@ utils.ucfirst = function (word) {
return word[0].toUpperCase() + word.substring(1).toLowerCase();
};
/**
* Base algo for studlyCase and camelCase
* @param {boolean} firstWordCap - Should the first character of the first word be capitalized
* @return {Function}
*/
function adjustWordCase(firstWordCap, otherWordsCap, sep) {
return function (string) {
var inWord = false;
var i = 0;
var words = [];
var word = '';
var code, c, upper, lower;
for (; i < string.length; i++) {
code = string.charCodeAt(i);
c = string.charAt(i);
lower = code >= 97 && code <= 122;
upper = code >= 65 && code <= 90;
if (upper || !lower) {
// new word
if (word.length) {
words.push(word);
}
word = '';
}
if (upper || lower) {
if (lower && word.length) {
word += c;
} else {
if ((!words.length && firstWordCap) || (words.length && otherWordsCap)) {
word = c.toUpperCase();
}
else {
word = c.toLowerCase();
}
}
}
}
if (word.length) {
words.push(word);
}
return words.join(sep);
};
}
/**
* Transform a string into StudlyCase
*
@ -149,29 +168,27 @@ utils.ucfirst = function (word) {
* @param {String} string
* @return {String}
*/
utils.studlyCase = function (string) {
return _.map(string.split(/\b|_/), function (word, i) {
return word.match(/^[a-z]+$/i) ? utils.ucfirst(word) : '';
}).join('');
};
utils.studlyCase = adjustWordCase(true, true, '');
/**
* Transform a string into camelCase
*
* @todo Tests
* @method cameCase
* @method camelCase
* @param {String} string
* @return {String}
*/
utils.camelCase = function (string) {
return _.map(string.split(/\b|_/), function (word, i) {
if (word.match(/^[a-z]+$/i)) {
return i === 0 ? word.toLowerCase() : utils.ucfirst(word);
} else {
return '';
}
}).join('');
};
utils.camelCase = adjustWordCase(false, true, '');
/**
* Transform a string into snakeCase
*
* @todo Tests
* @method snakeCase
* @param {String} string
* @return {String}
*/
utils.snakeCase = adjustWordCase(false, false, '_');
/**
* Lower-case a string, and return an empty string if any is not a string

View File

@ -118,21 +118,43 @@ describe('Utils', 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');
expect(_.camelCase('Neil Patrick.Harris-is_a.dog')).to.eql('neilPatrickHarrisIsADog');
});
it('ignores abreviations', function () {
expect(_.camelCase('JSON_parser')).to.eql('jsonParser');
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');
expect(_.studlyCase('Neil Patrick.Harris-is_a.dog')).to.eql('NeilPatrickHarrisIsADog');
});
it('ignores abreviations', function () {
expect(_.studlyCase('JSON_parser')).to.eql('JsonParser');
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');
});
});