feat(errorHandlingConfig): make the depth for object stringification in errors configurable
Closes #15402 Closes #15433
This commit is contained in:
committed by
Georgios Kalpakas
parent
538f4606ff
commit
a0641ea475
@@ -15,6 +15,9 @@
|
||||
"splice": false,
|
||||
"push": false,
|
||||
"toString": false,
|
||||
"minErrConfig": false,
|
||||
"errorHandlingConfig": false,
|
||||
"isValidObjectMaxDepth": false,
|
||||
"ngMinErr": false,
|
||||
"_angular": false,
|
||||
"angularModule": false,
|
||||
|
||||
+62
-10
@@ -10,6 +10,9 @@
|
||||
splice,
|
||||
push,
|
||||
toString,
|
||||
minErrConfig,
|
||||
errorHandlingConfig,
|
||||
isValidObjectMaxDepth,
|
||||
ngMinErr,
|
||||
angularModule,
|
||||
uid,
|
||||
@@ -125,6 +128,50 @@ var VALIDITY_STATE_PROPERTY = 'validity';
|
||||
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
var minErrConfig = {
|
||||
objectMaxDepth: 5
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name angular.errorHandlingConfig
|
||||
* @module ng
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Configure several aspects of error handling in AngularJS if used as a setter or return the
|
||||
* current configuration if used as a getter. The following options are supported:
|
||||
*
|
||||
* - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages.
|
||||
*
|
||||
* Omitted or undefined options will leave the corresponding configuration values unchanged.
|
||||
*
|
||||
* @param {Object=} config - The configuration object. May only contain the options that need to be
|
||||
* updated. Supported keys:
|
||||
*
|
||||
* * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
|
||||
* non-positive or non-numeric value, removes the max depth limit.
|
||||
* Default: 5
|
||||
*/
|
||||
function errorHandlingConfig(config) {
|
||||
if (isObject(config)) {
|
||||
if (isDefined(config.objectMaxDepth)) {
|
||||
minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
|
||||
}
|
||||
} else {
|
||||
return minErrConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Number} maxDepth
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isValidObjectMaxDepth(maxDepth) {
|
||||
return isNumber(maxDepth) && maxDepth > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name angular.lowercase
|
||||
@@ -847,9 +894,10 @@ function arrayRemove(array, value) {
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
function copy(source, destination) {
|
||||
function copy(source, destination, maxDepth) {
|
||||
var stackSource = [];
|
||||
var stackDest = [];
|
||||
maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN;
|
||||
|
||||
if (destination) {
|
||||
if (isTypedArray(destination) || isArrayBuffer(destination)) {
|
||||
@@ -872,35 +920,39 @@ function copy(source, destination) {
|
||||
|
||||
stackSource.push(source);
|
||||
stackDest.push(destination);
|
||||
return copyRecurse(source, destination);
|
||||
return copyRecurse(source, destination, maxDepth);
|
||||
}
|
||||
|
||||
return copyElement(source);
|
||||
return copyElement(source, maxDepth);
|
||||
|
||||
function copyRecurse(source, destination) {
|
||||
function copyRecurse(source, destination, maxDepth) {
|
||||
maxDepth--;
|
||||
if (maxDepth < 0) {
|
||||
return '...';
|
||||
}
|
||||
var h = destination.$$hashKey;
|
||||
var key;
|
||||
if (isArray(source)) {
|
||||
for (var i = 0, ii = source.length; i < ii; i++) {
|
||||
destination.push(copyElement(source[i]));
|
||||
destination.push(copyElement(source[i], maxDepth));
|
||||
}
|
||||
} else if (isBlankObject(source)) {
|
||||
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
|
||||
for (key in source) {
|
||||
destination[key] = copyElement(source[key]);
|
||||
destination[key] = copyElement(source[key], maxDepth);
|
||||
}
|
||||
} else if (source && typeof source.hasOwnProperty === 'function') {
|
||||
// Slow path, which must rely on hasOwnProperty
|
||||
for (key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
destination[key] = copyElement(source[key]);
|
||||
destination[key] = copyElement(source[key], maxDepth);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Slowest path --- hasOwnProperty can't be called as a method
|
||||
for (key in source) {
|
||||
if (hasOwnProperty.call(source, key)) {
|
||||
destination[key] = copyElement(source[key]);
|
||||
destination[key] = copyElement(source[key], maxDepth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -908,7 +960,7 @@ function copy(source, destination) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
function copyElement(source) {
|
||||
function copyElement(source, maxDepth) {
|
||||
// Simple values
|
||||
if (!isObject(source)) {
|
||||
return source;
|
||||
@@ -937,7 +989,7 @@ function copy(source, destination) {
|
||||
stackDest.push(destination);
|
||||
|
||||
return needsRecurse
|
||||
? copyRecurse(source, destination)
|
||||
? copyRecurse(source, destination, maxDepth)
|
||||
: destination;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ var version = {
|
||||
|
||||
function publishExternalAPI(angular) {
|
||||
extend(angular, {
|
||||
'errorHandlingConfig': errorHandlingConfig,
|
||||
'bootstrap': bootstrap,
|
||||
'copy': copy,
|
||||
'extend': extend,
|
||||
|
||||
+10
-12
@@ -33,20 +33,19 @@
|
||||
function minErr(module, ErrorConstructor) {
|
||||
ErrorConstructor = ErrorConstructor || Error;
|
||||
return function() {
|
||||
var SKIP_INDEXES = 2;
|
||||
|
||||
var templateArgs = arguments,
|
||||
code = templateArgs[0],
|
||||
var code = arguments[0],
|
||||
template = arguments[1],
|
||||
message = '[' + (module ? module + ':' : '') + code + '] ',
|
||||
template = templateArgs[1],
|
||||
templateArgs = sliceArgs(arguments, 2).map(function(arg) {
|
||||
return toDebugString(arg, minErrConfig.objectMaxDepth);
|
||||
}),
|
||||
paramPrefix, i;
|
||||
|
||||
message += template.replace(/\{\d+\}/g, function(match) {
|
||||
var index = +match.slice(1, -1),
|
||||
shiftedIndex = index + SKIP_INDEXES;
|
||||
var index = +match.slice(1, -1);
|
||||
|
||||
if (shiftedIndex < templateArgs.length) {
|
||||
return toDebugString(templateArgs[shiftedIndex]);
|
||||
if (index < templateArgs.length) {
|
||||
return templateArgs[index];
|
||||
}
|
||||
|
||||
return match;
|
||||
@@ -55,9 +54,8 @@ function minErr(module, ErrorConstructor) {
|
||||
message += '\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/' +
|
||||
(module ? module + '/' : '') + code;
|
||||
|
||||
for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
|
||||
message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
|
||||
encodeURIComponent(toDebugString(templateArgs[i]));
|
||||
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
|
||||
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
|
||||
}
|
||||
|
||||
return new ErrorConstructor(message);
|
||||
|
||||
+9
-3
@@ -2,9 +2,15 @@
|
||||
|
||||
/* global toDebugString: true */
|
||||
|
||||
function serializeObject(obj) {
|
||||
function serializeObject(obj, maxDepth) {
|
||||
var seen = [];
|
||||
|
||||
// There is no direct way to stringify object until reaching a specific depth
|
||||
// and a very deep object can cause a performance issue, so we copy the object
|
||||
// based on this specific depth and then stringify it.
|
||||
if (isValidObjectMaxDepth(maxDepth)) {
|
||||
obj = copy(obj, null, maxDepth);
|
||||
}
|
||||
return JSON.stringify(obj, function(key, val) {
|
||||
val = toJsonReplacer(key, val);
|
||||
if (isObject(val)) {
|
||||
@@ -17,13 +23,13 @@ function serializeObject(obj) {
|
||||
});
|
||||
}
|
||||
|
||||
function toDebugString(obj) {
|
||||
function toDebugString(obj, maxDepth) {
|
||||
if (typeof obj === 'function') {
|
||||
return obj.toString().replace(/ \{[\s\S]*$/, '');
|
||||
} else if (isUndefined(obj)) {
|
||||
return 'undefined';
|
||||
} else if (typeof obj !== 'string') {
|
||||
return serializeObject(obj);
|
||||
return serializeObject(obj, maxDepth);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
/* angular.js */
|
||||
"angular": false,
|
||||
"minErrConfig": false,
|
||||
"errorHandlingConfig": false,
|
||||
"msie": false,
|
||||
"jqLite": false,
|
||||
"jQuery": false,
|
||||
@@ -37,6 +39,7 @@
|
||||
"nodeName_": false,
|
||||
"uid": false,
|
||||
"toDebugString": false,
|
||||
"serializeObject": false,
|
||||
|
||||
"lowercase": false,
|
||||
"uppercase": false,
|
||||
|
||||
@@ -602,6 +602,31 @@ describe('angular', function() {
|
||||
expect(copy(new Number(NaN)).valueOf()).toBeNaN();
|
||||
/* eslint-enable */
|
||||
});
|
||||
|
||||
it('should copy source until reaching a given max depth', function() {
|
||||
var source = {a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}};
|
||||
var dest;
|
||||
|
||||
dest = copy(source, {}, 1);
|
||||
expect(dest).toEqual({a1:1, b1:'...', c1:'...', d1:'...'});
|
||||
|
||||
dest = copy(source, {}, 2);
|
||||
expect(dest).toEqual({a1:1, b1:{b2:'...'}, c1:[1,'...'], d1:{d2:1}});
|
||||
|
||||
dest = copy(source, {}, 3);
|
||||
expect(dest).toEqual({a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}});
|
||||
|
||||
dest = copy(source, {}, 4);
|
||||
expect(dest).toEqual({a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}});
|
||||
});
|
||||
|
||||
they('should copy source and ignore max depth when maxDepth = $prop',
|
||||
[NaN, null, undefined, true, false, -1, 0], function(maxDepth) {
|
||||
var source = {a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}};
|
||||
var dest = copy(source, {}, maxDepth);
|
||||
expect(dest).toEqual({a1: 1, b1: {b2: {b3: 1}}, c1: [1, {c2: 1}], d1: {d2: 1}});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('extend', function() {
|
||||
|
||||
@@ -9,6 +9,11 @@ describe('minErr', function() {
|
||||
var emptyTestError = minErr(),
|
||||
testError = minErr('test');
|
||||
|
||||
var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth;
|
||||
afterEach(function() {
|
||||
minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage;
|
||||
});
|
||||
|
||||
it('should return an Error factory', function() {
|
||||
var myError = testError('test', 'Oops');
|
||||
expect(myError instanceof Error).toBe(true);
|
||||
@@ -68,6 +73,40 @@ describe('minErr', function() {
|
||||
expect(myError.message).toMatch(/a is {"b":{"a":"..."}}/);
|
||||
});
|
||||
|
||||
it('should handle arguments that are objects with max depth', function() {
|
||||
var a = {b: {c: {d: {e: {f: {g: 1}}}}}};
|
||||
|
||||
var myError = testError('26', 'a when objectMaxDepth is default=5 is {0}', a);
|
||||
expect(myError.message).toMatch(/a when objectMaxDepth is default=5 is {"b":{"c":{"d":{"e":{"f":"..."}}}}}/);
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
|
||||
|
||||
errorHandlingConfig({objectMaxDepth: 1});
|
||||
myError = testError('26', 'a when objectMaxDepth is set to 1 is {0}', a);
|
||||
expect(myError.message).toMatch(/a when objectMaxDepth is set to 1 is {"b":"..."}/);
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(1);
|
||||
|
||||
errorHandlingConfig({objectMaxDepth: 2});
|
||||
myError = testError('26', 'a when objectMaxDepth is set to 2 is {0}', a);
|
||||
expect(myError.message).toMatch(/a when objectMaxDepth is set to 2 is {"b":{"c":"..."}}/);
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(2);
|
||||
|
||||
errorHandlingConfig({objectMaxDepth: undefined});
|
||||
myError = testError('26', 'a when objectMaxDepth is set to undefined is {0}', a);
|
||||
expect(myError.message).toMatch(/a when objectMaxDepth is set to undefined is {"b":{"c":"..."}}/);
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBe(2);
|
||||
});
|
||||
|
||||
they('should handle arguments that are objects and ignore max depth when objectMaxDepth = $prop',
|
||||
[NaN, null, true, false, -1, 0], function(maxDepth) {
|
||||
var a = {b: {c: {d: 1}}};
|
||||
|
||||
errorHandlingConfig({objectMaxDepth: maxDepth});
|
||||
var myError = testError('26', 'a is {0}', a);
|
||||
expect(myError.message).toMatch(/a is {"b":{"c":{"d":1}}}/);
|
||||
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
|
||||
}
|
||||
);
|
||||
|
||||
it('should preserve interpolation markers when fewer arguments than needed are provided', function() {
|
||||
// this way we can easily see if we are passing fewer args than needed
|
||||
|
||||
|
||||
@@ -12,4 +12,43 @@ describe('toDebugString', function() {
|
||||
expect(toDebugString(a)).toEqual('{"a":"..."}');
|
||||
expect(toDebugString([a,a])).toEqual('[{"a":"..."},"..."]');
|
||||
});
|
||||
|
||||
it('should convert its argument that are objects to string based on maxDepth', function() {
|
||||
var a = {b: {c: {d: 1}}};
|
||||
expect(toDebugString(a, 1)).toEqual('{"b":"..."}');
|
||||
expect(toDebugString(a, 2)).toEqual('{"b":{"c":"..."}}');
|
||||
expect(toDebugString(a, 3)).toEqual('{"b":{"c":{"d":1}}}');
|
||||
});
|
||||
|
||||
they('should convert its argument that object to string and ignore max depth when maxDepth = $prop',
|
||||
[NaN, null, undefined, true, false, -1, 0], function(maxDepth) {
|
||||
var a = {b: {c: {d: 1}}};
|
||||
expect(toDebugString(a, maxDepth)).toEqual('{"b":{"c":{"d":1}}}');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('serializeObject', function() {
|
||||
it('should convert its argument to a string', function() {
|
||||
expect(serializeObject({a:{b:'c'}})).toEqual('{"a":{"b":"c"}}');
|
||||
|
||||
var a = { };
|
||||
a.a = a;
|
||||
expect(serializeObject(a)).toEqual('{"a":"..."}');
|
||||
expect(serializeObject([a,a])).toEqual('[{"a":"..."},"..."]');
|
||||
});
|
||||
|
||||
it('should convert its argument that are objects to string based on maxDepth', function() {
|
||||
var a = {b: {c: {d: 1}}};
|
||||
expect(serializeObject(a, 1)).toEqual('{"b":"..."}');
|
||||
expect(serializeObject(a, 2)).toEqual('{"b":{"c":"..."}}');
|
||||
expect(serializeObject(a, 3)).toEqual('{"b":{"c":{"d":1}}}');
|
||||
});
|
||||
|
||||
they('should convert its argument that object to string and ignore max depth when maxDepth = $prop',
|
||||
[NaN, null, undefined, true, false, -1, 0], function(maxDepth) {
|
||||
var a = {b: {c: {d: 1}}};
|
||||
expect(serializeObject(a, maxDepth)).toEqual('{"b":{"c":{"d":1}}}');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user