Commit 29d5e3ae by James Cropcho

Merge pull request #96 from todvora/plugin-infrastructure

Plugin infrastructure
parents daf0061e 4382a1ec
...@@ -13,13 +13,16 @@ ...@@ -13,13 +13,16 @@
"BinData": false, "BinData": false,
"DBQuery": false, "DBQuery": false,
"printjson": false, "printjson": false,
"load": false,
"module": false,
"query": true, "query": true,
"limit": true, "limit": true,
"maxDepth": true, "maxDepth": true,
"sort": true, "sort": true,
"outputFormat": true, "outputFormat": true,
"persistResults": true "persistResults": true,
"plugins": true
}, },
"latedef": true, "latedef": true,
"singleGroups": true, "singleGroups": true,
......
(14 May 2015) Version 1.5.0: Introduced basic plugin infrastructure, 'onConfig' and 'formatResults' hooks
(14 Oct 2014) Version 1.4.1: @todvora's fix for maxDepth (matches readme), and fixes for incorrect counts while using query/limit (14 Oct 2014) Version 1.4.1: @todvora's fix for maxDepth (matches readme), and fixes for incorrect counts while using query/limit
(13 Oct 2014) Version 1.4.0: @todvora's fix for nested objects (13 Oct 2014) Version 1.4.0: @todvora's fix for nested objects
......
...@@ -32,6 +32,8 @@ public class Variety { ...@@ -32,6 +32,8 @@ public class Variety {
public static final String PARAM_LIMIT = "limit"; public static final String PARAM_LIMIT = "limit";
public static final String PARAM_OUTPUT_FORMAT = "outputFormat"; public static final String PARAM_OUTPUT_FORMAT = "outputFormat";
public static final String PARAM_PERSIST_RESULTS = "persistResults"; public static final String PARAM_PERSIST_RESULTS = "persistResults";
public static final String PARAM_PLUGINS = "plugins";
private final String inputDatabase; private final String inputDatabase;
private final String inputCollection; private final String inputCollection;
...@@ -46,6 +48,7 @@ public class Variety { ...@@ -46,6 +48,7 @@ public class Variety {
private String outputFormat; private String outputFormat;
private boolean quiet; private boolean quiet;
private boolean persistResults; private boolean persistResults;
private String[] plugins;
/** /**
* Create variety wrapper with defined connection do analysed database and collection * Create variety wrapper with defined connection do analysed database and collection
...@@ -141,6 +144,11 @@ public class Variety { ...@@ -141,6 +144,11 @@ public class Variety {
return this; return this;
} }
public Variety withPlugins(String... plugins) {
this.plugins = plugins;
return this;
}
/** /**
* Executes mongo shell with configured variety options and variety.js script in path. * Executes mongo shell with configured variety options and variety.js script in path.
* @return Stdout of variety.js * @return Stdout of variety.js
...@@ -192,6 +200,11 @@ public class Variety { ...@@ -192,6 +200,11 @@ public class Variety {
if(persistResults) { if(persistResults) {
args.add(PARAM_PERSIST_RESULTS + " = " + persistResults); args.add(PARAM_PERSIST_RESULTS + " = " + persistResults);
} }
if(plugins != null && plugins.length > 0) {
args.add(PARAM_PLUGINS + " = \"" + String.join(",", plugins) + "\"");
}
return args.toString(); return args.toString();
} }
......
...@@ -83,14 +83,14 @@ public class ParametersParsingTest { ...@@ -83,14 +83,14 @@ public class ParametersParsingTest {
public void testDefaultOutputFormatParam() throws Exception { public void testDefaultOutputFormatParam() throws Exception {
final ResultsValidator analysis = variety.runDatabaseAnalysis(); // format option not provided final ResultsValidator analysis = variety.runDatabaseAnalysis(); // format option not provided
final Map<String, String> params = getParamsMap(analysis.getStdOut()); final Map<String, String> params = getParamsMap(analysis.getStdOut());
Assert.assertEquals("ascii", params.get(Variety.PARAM_OUTPUT_FORMAT)); Assert.assertEquals("\"ascii\"", params.get(Variety.PARAM_OUTPUT_FORMAT));
} }
@Test @Test
public void testAsciiOutputFormatParam() throws Exception { public void testAsciiOutputFormatParam() throws Exception {
final ResultsValidator analysis = variety.withFormat(Variety.FORMAT_ASCII).runDatabaseAnalysis(); final ResultsValidator analysis = variety.withFormat(Variety.FORMAT_ASCII).runDatabaseAnalysis();
final Map<String, String> params = getParamsMap(analysis.getStdOut()); final Map<String, String> params = getParamsMap(analysis.getStdOut());
Assert.assertEquals("ascii", params.get(Variety.PARAM_OUTPUT_FORMAT)); Assert.assertEquals("\"ascii\"", params.get(Variety.PARAM_OUTPUT_FORMAT));
} }
@Test @Test
......
package com.github.variety.test;
import com.github.variety.Variety;
import com.github.variety.validator.ResultsValidator;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.net.URL;
public class PluginsTest {
private Variety variety;
public static final String EXPECTED_OUTPUT =
"key|types|occurrences|percents\n" +
"_id|ObjectId|5|100\n" +
"name|String|5|100\n" +
"bio|String|3|60\n" +
"birthday|String|2|40\n" +
"pets|Array,String|2|40\n" +
"someBinData|BinData-old|1|20\n" +
"someWeirdLegacyKey|String|1|20";
@Before
public void setUp() throws Exception {
this.variety = new Variety("test", "users");
variety.getSourceCollection().insert(SampleData.getDocuments());
}
@After
public void tearDown() throws Exception {
variety.getVarietyResultsDatabase().dropDatabase();
variety.getSourceCollection().drop();
}
/**
* Validate correct results read from DB
*/
@Test
public void verifyFormatResults() throws Exception {
final String path = getPluginPath("/csvplugin.js");
final ResultsValidator analysis = variety.withQuiet(true).withPlugins(path).runDatabaseAnalysis();
Assert.assertEquals(EXPECTED_OUTPUT, analysis.getStdOut());
}
@Test
public void verifyPluginParamParsing() throws Exception {
final String path = getPluginPath("/csvplugin.js");
final ResultsValidator analysis = variety.withPlugins(path + "|delimiter=;").runDatabaseAnalysis();
Assert.assertTrue(analysis.getStdOut().contains("Using plugins of [ \"" + path + "\" ]"));
}
private String getPluginPath(final String name) {
final URL resource = this.getClass().getResource(name);
return resource.getFile();
}
}
var getCsv = function(varietyResults) {
var delimiter = this.delimiter || '|';
var headers = ['key', 'types', 'occurrences', 'percents'];
var table = [headers.join(delimiter)];
var rows = varietyResults.map(function(key) {
return [key._id.key, key.value.types, key.totalOccurrences, key.percentContaining].join(delimiter);
}, this);
return table.concat(rows).join('\n');
};
var setConfig = function(pluginConfig) {
this.delimiter = pluginConfig.delimiter;
};
module.exports = {
init: setConfig,
formatResults: getCsv
};
...@@ -18,7 +18,7 @@ var log = function(message) { ...@@ -18,7 +18,7 @@ var log = function(message) {
}; };
log('Variety: A MongoDB Schema Analyzer'); log('Variety: A MongoDB Schema Analyzer');
log('Version 1.4.1, released 14 Oct 2014'); log('Version 1.5.0, released 14 May 2015');
var dbs = []; var dbs = [];
var emptyDbs = []; var emptyDbs = [];
...@@ -57,49 +57,69 @@ if (db[collection].count() === 0) { ...@@ -57,49 +57,69 @@ if (db[collection].count() === 0) {
'Possible collection options for database specified: ' + collNames + '.'; 'Possible collection options for database specified: ' + collNames + '.';
} }
var $query = {}; var readConfig = function(configProvider) {
if(typeof query !== 'undefined') { var config = {};
$query = query; var read = function(name, defaultValue) {
query = '_undefined'; var value = typeof configProvider[name] !== 'undefined' ? configProvider[name] : defaultValue;
} config[name] = value;
log('Using query of ' + tojson($query)); log('Using '+name+' of ' + tojson(value));
};
var $limit = db[collection].find($query).count(); read('collection', null);
if(typeof limit !== 'undefined') { read('query', {});
$limit = limit; read('limit', db[config.collection].find(config.query).count());
limit = '_undefined'; read('maxDepth', 99);
} read('sort', {_id: -1});
log('Using limit of ' + $limit); read('outputFormat', 'ascii');
read('persistResults', false);
return config;
};
var $maxDepth = 99; var config = readConfig(this);
if(typeof maxDepth !== 'undefined') {
$maxDepth = maxDepth; var PluginsClass = function(context) {
maxDepth = '_undefined'; var parsePath = function(val) { return val.slice(-3) !== '.js' ? val + '.js' : val;};
} var parseConfig = function(val) {
log('Using maxDepth of ' + $maxDepth); var config = {};
val.split('&').reduce(function(acc, val) {
var parts = val.split('=');
acc[parts[0]] = parts[1];
return acc;
}, config);
return config;
};
var $sort = {_id: -1}; if(typeof context.plugins !== 'undefined') {
if(typeof sort !== 'undefined') { this.plugins = context.plugins.split(',')
$sort = sort; .map(function(path){return path.trim();})
sort = '_undefined'; .map(function(definition){
} var path = parsePath(definition.split('|')[0]);
log('Using sort of ' + tojson($sort)); var config = parseConfig(definition.split('|')[1] || '');
context.module = context.module || {};
load(path);
var plugin = context.module.exports;
plugin.path = path;
if(typeof plugin.init === 'function') {
plugin.init(config);
}
return plugin;
}, this);
} else {
this.plugins = [];
}
var $outputFormat = 'ascii'; this.execute = function(methodName) {
if(typeof outputFormat !== 'undefined') { var args = Array.prototype.slice.call(arguments, 1);
$outputFormat = outputFormat; var applicablePlugins = this.plugins.filter(function(plugin){return typeof plugin[methodName] === 'function';});
outputFormat = '_undefined'; return applicablePlugins.map(function(plugin) {
} return plugin[methodName].apply(plugin, args);
log('Using outputFormat of ' + $outputFormat); });
};
var $persistResults = false; log('Using plugins of ' + tojson(this.plugins.map(function(plugin){return plugin.path;})));
if(typeof persistResults !== 'undefined') { };
$persistResults = persistResults;
persistResults = '_undefined';
}
log('Using persistResults of ' + $persistResults);
log('Using collection of ' + collection); var $plugins = new PluginsClass(this);
$plugins.execute('onConfig', config);
var varietyTypeOf = function(thing) { var varietyTypeOf = function(thing) {
if (typeof thing === 'undefined') { throw 'varietyTypeOf() requires an argument'; } if (typeof thing === 'undefined') { throw 'varietyTypeOf() requires an argument'; }
...@@ -228,7 +248,7 @@ var convertResults = function(interimResults, documentsCount) { ...@@ -228,7 +248,7 @@ var convertResults = function(interimResults, documentsCount) {
// Merge the keys and types of current object into accumulator object // Merge the keys and types of current object into accumulator object
var reduceDocuments = function(accumulator, object) { var reduceDocuments = function(accumulator, object) {
var docResult = analyseDocument(serializeDoc(object, $maxDepth)); var docResult = analyseDocument(serializeDoc(object, config.maxDepth));
mergeDocument(docResult, accumulator); mergeDocument(docResult, accumulator);
return accumulator; return accumulator;
}; };
...@@ -254,13 +274,13 @@ DBQuery.prototype.reduce = function(callback, initialValue) { ...@@ -254,13 +274,13 @@ DBQuery.prototype.reduce = function(callback, initialValue) {
return result; return result;
}; };
var cursor = db[collection].find($query).sort($sort).limit($limit); var cursor = db[config.collection].find(config.query).sort(config.sort).limit(config.limit);
var interimResults = cursor.reduce(reduceDocuments, {}); var interimResults = cursor.reduce(reduceDocuments, {});
var varietyResults = convertResults(interimResults, cursor.size()) var varietyResults = convertResults(interimResults, cursor.size())
.filter(filter) .filter(filter)
.sort(comparator); .sort(comparator);
if($persistResults) { if(config.persistResults) {
var resultsDB = db.getMongo().getDB('varietyResults'); var resultsDB = db.getMongo().getDB('varietyResults');
var resultsCollectionName = collection + 'Keys'; var resultsCollectionName = collection + 'Keys';
...@@ -270,36 +290,36 @@ if($persistResults) { ...@@ -270,36 +290,36 @@ if($persistResults) {
resultsDB[resultsCollectionName].insert(varietyResults); resultsDB[resultsCollectionName].insert(varietyResults);
} }
if($outputFormat === 'json') { var createAsciiTable = function(results) {
printjson(varietyResults); // valid formatted json output, compressed variant is printjsononeline() var headers = ['key', 'types', 'occurrences', 'percents'];
} else { // output nice ascii table with results // return the number of decimal places or 1, if the number is int (1.23=>2, 100=>1, 0.1415=>4)
var table = [['key', 'types', 'occurrences', 'percents'], ['', '', '', '']]; // header + delimiter rows var significantDigits = function(value) {
var res = value.toString().match(/^[0-9]+\.([0-9]+)$/);
// return the number of decimal places or 1, if the number is int (1.23=>2, 100=>1, 0.1415=>4) return res !== null ? res[1].length : 1;
var significantDigits = function(value) { };
var res = value.toString().match(/^[0-9]+\.([0-9]+)$/);
return res !== null ? res[1].length : 1;
};
var maxDigits = varietyResults.map(function(value){return significantDigits(value.percentContaining);}).reduce(function(acc,val){return acc>val?acc:val;}); var maxDigits = varietyResults.map(function(value){return significantDigits(value.percentContaining);}).reduce(function(acc,val){return acc>val?acc:val;});
varietyResults.forEach(function(key) { var rows = results.map(function(row) {
table.push([key._id.key, key.value.types.toString(), key.totalOccurrences.toString(), key.percentContaining.toFixed(maxDigits).toString()]); return [row._id.key, row.value.types, row.totalOccurrences, row.percentContaining.toFixed(maxDigits)];
}); });
var table = [headers, headers.map(function(){return '';})].concat(rows);
var colMaxWidth = function(arr, index) { var colMaxWidth = function(arr, index) {return Math.max.apply(null, arr.map(function(row){return row[index].toString().length;}));};
return Math.max.apply(null, arr.map(function(row){return row[index].toString().length;}));
};
var pad = function(width, string, symbol) { return width <= string.length ? string : pad(width, isNaN(string) ? string + symbol : symbol + string, symbol); }; var pad = function(width, string, symbol) { return width <= string.length ? string : pad(width, isNaN(string) ? string + symbol : symbol + string, symbol); };
table = table.map(function(row, ri){
var output = ''; return '| ' + row.map(function(cell, i) {return pad(colMaxWidth(table, i), cell.toString(), ri === 1 ? '-' : ' ');}).join(' | ') + ' |';
table.forEach(function(row, ri){
output += '| ' + row.map(function(cell, i) {return pad(colMaxWidth(table, i), cell, ri === 1 ? '-' : ' ');}).join(' | ') + ' |\n';
}); });
var lineLength = output.split('\n')[0].length - 2; // length of first (header) line minus two chars for edges var border = '+' + pad(table[0].length - 2, '', '-') + '+';
var border = '+' + pad(lineLength, '', '-') + '+'; return [border].concat(table).concat(border).join('\n');
print(border + '\n' + output + border); };
var pluginsOutput = $plugins.execute('formatResults', varietyResults);
if (pluginsOutput.length > 0) {
pluginsOutput.forEach(function(i){print(i);});
} else if(config.outputFormat === 'json') {
printjson(varietyResults); // valid formatted json output, compressed variant is printjsononeline()
} else {
print(createAsciiTable(varietyResults)); // output nice ascii table with results
} }
}()); // end strict mode }.bind(this)()); // end strict mode
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment