Allow fact template to flatten or keep key/value structure

This commit is contained in:
Joe Fiorini 2015-06-15 16:23:17 -04:00
parent ad0f267435
commit 61807efb9b
7 changed files with 313 additions and 121 deletions

View File

@ -0,0 +1,16 @@
export default
[ 'moment',
function(moment) {
return function(seconds, formatStr) {
if (!formatStr) {
formatStr = 'll LT';
}
var millis = seconds * 1000;
return moment(millis).format(formatStr);
};
}
];

View File

@ -0,0 +1,9 @@
import formatEpoch from './format-epoch.filter';
import moment from 'tower/shared/moment/main';
export default
angular.module('formatEpoch',
[ moment.name
])
.filter('formatEpoch', formatEpoch);

View File

@ -9,26 +9,39 @@ import compareFlatFacts from './compare-facts/flat';
import FactTemplate from './compare-facts/fact-template';
export function compareFacts(module, facts) {
var renderOptions = _.merge({}, module);
// If the module has a template or includes a list of keys to display,
// then perform a flat comparison, otherwise assume nested
//
if (module.factTemplate || module.nameKey) {
if (renderOptions.factTemplate || renderOptions.nameKey) {
// For flat structures we compare left-to-right, then right-to-left to
// make sure we get a good comparison between both hosts
var compare = _.partialRight(compareFlatFacts,
module.nameKey,
module.compareKey,
new FactTemplate(module.factTemplate));
var leftToRight = compare(facts[0], facts[1]);
var rightToLeft = compare(facts[1], facts[0]);
if (_.isPlainObject(renderOptions.factTemplate)) {
renderOptions.factTemplate =
_.mapValues(renderOptions.factTemplate, function(template) {
if (typeof template === 'string' || typeof template === 'function') {
return new FactTemplate(template);
} else {
return template;
}
});
} else {
renderOptions.factTemplate = new FactTemplate(renderOptions.factTemplate);
}
var leftToRight = compareFlatFacts(facts[0], facts[1], renderOptions);
var rightToLeft = compareFlatFacts(facts[1], facts[0], renderOptions);
return _(leftToRight)
.concat(rightToLeft)
.unique('displayKeyPath')
.thru(function(result) {
return { factData: result,
isNestedDisplay: _.isUndefined(module.factTemplate)
isNestedDisplay: _.isPlainObject(renderOptions.factTemplate)
};
})
.value();

View File

@ -0,0 +1,33 @@
import stringFilters from 'tower/shared/string-filters/main';
import formatEpoch from 'tower/shared/format-epoch/main';
var $injector = angular.injector(['ng', stringFilters.name, formatEpoch.name]);
var $interpolate = $injector.get('$interpolate');
function FactTemplate(templateString) {
this.templateString = templateString;
}
function loadFactTemplate(factTemplate, fact) {
if (_.isFunction(factTemplate)) {
return factTemplate(fact);
} else {
return factTemplate;
}
}
FactTemplate.prototype.render = function(factData) {
if (_.isUndefined(factData) || _.isEmpty(factData)) {
return 'absent';
}
var template = loadFactTemplate(this.templateString, factData);
return $interpolate(template)(factData);
};
FactTemplate.prototype.hasTemplate = function() {
return !_.isUndefined(this.templateString);
};
export default FactTemplate;

View File

@ -27,7 +27,6 @@ export default
var nameKey = renderOptions.nameKey;
var compareKeys = renderOptions.compareKey;
var keyNameMap = renderOptions.keyNameMap;
var valueFormatter = renderOptions.valueFormatter;
var factTemplate = renderOptions.factTemplate;
@ -35,8 +34,6 @@ export default
var searcher = {};
searcher[nameKey] = basisFact[nameKey];
var slottedValues, basisValue, comparatorValue;
var matchingFact = _.where(comparatorFacts.facts, searcher);
var diffs;
@ -50,53 +47,75 @@ export default
var comparisonResults =
_.reduce(compareKeys, function(result, compareKey) {
if (_.isEmpty(matchingFact)) {
comparatorValue = 'absent';
} else {
comparatorValue = matchingFact[0][compareKey];
}
var comparatorFact = matchingFact[0] || {};
var isNestedDisplay = false;
var slottedValues = slotFactValues(basisFacts.position,
basisFact[compareKey],
comparatorValue);
comparatorFact[compareKey]);
if (_.isUndefined(slottedValues.left) && _.isUndefined(slottedValues.right)) {
return result;
}
var template = factTemplate;
if (_.isObject(template) && template.hasOwnProperty(compareKey)) {
template = template[compareKey];
// 'true' means render the key without formatting
if (template === true) {
template =
{ render: function(fact) { return fact[compareKey]; }
};
}
isNestedDisplay = true;
} else if (typeof template.hasTemplate === 'function' && !template.hasTemplate()) {
template =
{ render: function(fact) { return fact[compareKey]; }
};
isNestedDisplay = true;
} else if (typeof factTemplate.render === 'function') {
template = factTemplate;
} else if (!template.hasOwnProperty(compareKey)) {
return result;
}
if (basisFacts.position === 'left') {
slottedValues.left = template.render(basisFact);
slottedValues.right = template.render(comparatorFact);
} else {
slottedValues.left = template.render(comparatorFact);
slottedValues.right = template.render(basisFact);
}
if (slottedValues.left !== slottedValues.right) {
slottedValues.isDivergent = true;
} else {
slottedValues.isDivergent = false;
}
result[compareKey] = slottedValues;
if (isNestedDisplay) {
result[compareKey] = slottedValues;
} else {
result = slottedValues;
}
return result;
}, {});
var hasDiffs = _.any(comparisonResults, { isDivergent: true });
var hasDiffs =
_.any(comparisonResults, { isDivergent: true }) ||
comparisonResults.isDivergent === true;
if (hasDiffs && factTemplate.hasTemplate()) {
if (hasDiffs && typeof factTemplate.render === 'function') {
basisValue = factTemplate.render(basisFact);
if (_.isEmpty(matchingFact)) {
comparatorValue = 'absent';
} else {
comparatorValue = factTemplate.render(matchingFact[0]);
}
if (!_.isEmpty(comparisonResults)) {
slottedValues = slotFactValues(basisFact.position, basisValue, comparatorValue);
diffs =
{ keyName: basisFact[nameKey],
value1: slottedValues.left,
value2: slottedValues.right
};
}
diffs =
{ keyName: basisFact[nameKey],
value1: comparisonResults.left,
value2: comparisonResults.right
};
} else if (hasDiffs) {
@ -111,9 +130,7 @@ export default
return { keyName: keyName,
value1: slottedValues.left,
value1IsAbsent: slottedValues.left === 'absent',
value2: slottedValues.right,
value2IsAbsent: slottedValues.right === 'absent',
isDivergent: slottedValues.isDivergent
};
}).compact()

View File

@ -13,8 +13,16 @@ var moduleConfig =
},
'files':
{ compareKey: ['size', 'mode', 'md5', 'mtime', 'gid', 'uid'],
keyNameMap:
{ 'uid': 'ownership'
},
factTemplate:
{ 'uid': 'user id: {{uid}}, group id: {{gid}}',
'mode': true,
'md5': true,
'mtime': '{{mtime|formatEpoch}}'
},
nameKey: 'path',
displayKeys: ['size', 'mode', 'mtime', 'uid', 'gid', 'md5'],
sortKey: 3
},
'ansible':

View File

@ -3,14 +3,31 @@ import compareFacts from 'tower/system-tracking/compare-facts/flat';
/* jshint node: true */
/* globals -expect, -_ */
var chai = require('chai');
var _ = require('lodash');
var chaiThings = require('chai-things');
var _, expect;
chai.use(chaiThings);
// This makes this test runnable in node OR karma. The sheer
// number of times I had to run this test made the karma
// workflow just too dang slow for me. Maybe this can
// be a pattern going forward? Not sure...
//
(function(global) {
var chai = global.chai || require('chai');
global.expect = chai.expect;
global._ = _;
if (typeof window === 'undefined') {
var chaiThings = global.chaiThings || require('chai-things');
chai.use(chaiThings);
}
_ = global._ || require('lodash');
expect = global.expect || chai.expect;
global.expect = expect;
global._ = _;
})(typeof window === 'undefined' ? global : window);
describe('CompareFacts.Flat', function() {
@ -92,16 +109,12 @@ describe('CompareFacts.Flat', function() {
facts:
[{ keyName: 'value',
value1: 'bar',
value1IsAbsent: false,
value2: 'baz',
value2IsAbsent: false,
isDivergent: true
},
{ keyName: 'extra',
value1: 'doo',
value1IsAbsent: false,
value2: 'doo',
value2IsAbsent: false,
isDivergent: false
}]
}]);
@ -130,16 +143,12 @@ describe('CompareFacts.Flat', function() {
facts:
[{ keyName: 'value',
value1: 'bar',
value1IsAbsent: false,
value2: 'baz',
value2IsAbsent: false,
isDivergent: true
},
{ keyName: 'extra',
value1: 'doo',
value1IsAbsent: false,
value2: 'doo',
value2IsAbsent: false,
isDivergent: false
}]
}]);
@ -172,48 +181,131 @@ describe('CompareFacts.Flat', function() {
expect(result[0].facts).to.include.something.that.deep.equals(
{ keyName: 'blah',
value1: 'doo',
value1IsAbsent: false,
value2: 'doo',
value2IsAbsent: false,
isDivergent: false
});
});
// it('allows formatting values with valueFormat parameter', function() {
// var valueFormat =
// function(key, values) {
// if (key === 'extra') {
// return 'formatted';
// }
// }
it('allows flattening values with factTemplate parameter', function() {
var factTemplate =
{ hasTemplate:
function() {
return true;
},
render: function(fact) {
return 'value: ' + fact.value;
}
};
// var result = compareFacts(
// { position: 'left',
// facts:
// [{ 'name': 'foo',
// 'value': 'bar',
// 'extra': 'doo'
// }]
// },
// { position: 'right',
// facts:
// [{ 'name': 'foo',
// 'value': 'baz',
// 'extra': 'doo'
// }]
// }, 'name', ['value', 'extra', 'blah'], keyNameMap, defaultTemplate, );
var result = compareFacts(
{ position: 'left',
facts:
[{ 'name': 'foo',
'value': 'bar',
'extra': 'doo'
}]
},
{ position: 'right',
facts:
[{ 'name': 'foo',
'value': 'baz',
'extra': 'doo'
}]
}, options({ compareKey: ['value'],
factTemplate: factTemplate
}));
// expect(result[0].facts).to.include.something.that.deep.equals(
// { keyName: 'extra',
// value1: 'formatted',
// value1IsAbsent: false,
// value2: 'formatted',
// value2IsAbsent: false,
// isDivergent: false
// });
expect(result[0].facts).to.deep.equal(
{ keyName: 'foo',
value1: 'value: bar',
value2: 'value: baz'
});
});
// });
it('allows formatting values with factTemplate parameter', function() {
var values = ['value1', 'value2'];
var factTemplate =
{ 'value':
{ hasTemplate: function() {
return true;
},
render: function() {
return values.shift();
}
},
'extra': true
};
var result = compareFacts(
{ position: 'left',
facts:
[{ 'name': 'foo',
'value': 'bar',
'extra': 'doo'
}]
},
{ position: 'right',
facts:
[{ 'name': 'foo',
'value': 'baz',
'extra': 'doo'
}]
}, options({ compareKey: ['value'],
factTemplate: factTemplate
}));
expect(result[0].facts).to.include.something.that.deep.equals(
{ keyName: 'value',
value1: 'value1',
value2: 'value2',
isDivergent: true
},
{ keyName: 'extra',
value1: 'doo',
value2: 'doo',
isDivergent: false
});
});
it('compares values using the formatted values, not the raw ones', function() {
var values = ['value1', 'value2'];
var factTemplate =
{ 'extra':
{ render: function() {
return values.shift();
}
}
};
var result = compareFacts(
{ position: 'left',
facts:
[{ 'name': 'foo',
'value': 'bar',
'extra': 'doo'
}]
},
{ position: 'right',
facts:
[{ 'name': 'foo',
'value': 'bar',
'extra': 'doo'
}]
}, options({ factTemplate: factTemplate,
compareKey: ['extra']
}));
expect(result.length).to.be.greaterThan(0);
expect(result[0].facts).to.include.something.that.deep.equals(
{ keyName: 'extra',
value1: 'value1',
value2: 'value2',
isDivergent: true
});
});
});
@ -230,7 +322,7 @@ describe('CompareFacts.Flat', function() {
return facts;
}
it('uses "absent" for the missing value', function() {
it('keeps missing values as undefined', function() {
var facts = factData([{ 'name': 'foo',
'value': 'bar'
@ -246,14 +338,45 @@ describe('CompareFacts.Flat', function() {
facts:
[{ keyName: 'value',
value1: 'bar',
value1IsAbsent: false,
value2: 'absent',
value2IsAbsent: true,
value2: undefined,
isDivergent: true
}]
}]);
});
it('still keeps missing values as undefined when using a template', function() {
var factTemplate =
{ hasTemplate:
function() {
return true;
},
render:
function(fact) {
return fact.value;
}
};
var facts = factData([{ 'name': 'foo',
'value': 'bar'
}]);
var result = compareFacts(facts[0], facts[1],
options({ compareKey: ['value'],
factTemplate: factTemplate
}));
expect(result).to.deep.equal(
[{ displayKeyPath: 'foo',
nestingLevel: 0,
facts:
{ keyName: 'foo',
value1: 'bar',
value2: undefined
}
}]);
});
it('includes given compare keys from basisFacts', function() {
var facts = factData([{ 'name': 'foo',
'value': 'bar',
@ -270,45 +393,18 @@ describe('CompareFacts.Flat', function() {
facts:
[{ keyName: 'value',
value1: 'bar',
value1IsAbsent: false,
value2: 'absent',
value2IsAbsent: true,
value2: undefined,
isDivergent: true
},
{ keyName: 'extra',
value1: 'doo',
value1IsAbsent: false,
value2: 'absent',
value2IsAbsent: true,
value2: undefined,
isDivergent: true
}]
}]);
});
context('with factTemplate', function() {
it('does not attempt to render the absent fact', function() {
var facts = factData([{ 'name': 'foo'
}]);
var renderCallCount = 0;
var factTemplate =
{ render: function() {
renderCallCount++;
},
hasTemplate: function() { return true; },
template: ""
};
compareFacts(facts[0], facts[1],
options({ compareKey: ['value'],
factTemplate: factTemplate
}));
expect(renderCallCount).to.equal(1);
});
});
});
context('with factTemplate', function() {