Skip to content

Commit 54a6ed0

Browse files
committed
validate: flatten Plotly.validate output
- make Plotly.validate return a 1-level deep array of error objects - add 'container' and 'trace' key in each error object to locate the error in data trace or layout. - add 'astr' to error object (for easy plug into restyle and relayout)
1 parent a2f2160 commit 54a6ed0

File tree

2 files changed

+320
-91
lines changed

2 files changed

+320
-91
lines changed

src/plot_api/validate.js

+172-57
Original file line numberDiff line numberDiff line change
@@ -15,55 +15,75 @@ var Plots = require('../plots/plots');
1515
var PlotSchema = require('./plot_schema');
1616

1717
var isPlainObject = Lib.isPlainObject;
18+
var isArray = Array.isArray;
1819

19-
// validation error codes
20-
var code2msgFunc = {
21-
invisible: function(path) {
22-
return 'trace ' + path + ' got defaulted to be not visible';
23-
},
24-
schema: function(path) {
25-
return 'key ' + path.join('.') + ' is not part of the schema';
26-
},
27-
container: function(path) {
28-
return 'key ' + path.join('.') + ' is supposed to be linked to a container';
29-
},
30-
unused: function(path, valIn) {
31-
var prefix = isPlainObject(valIn) ? 'container' : 'key';
32-
33-
return prefix + ' ' + path.join('.') + ' did not get coerced';
34-
},
35-
value: function(path, valIn) {
36-
return 'key ' + path.join('.') + ' is set to an invalid value (' + valIn + ')';
37-
}
38-
};
3920

21+
/**
22+
* Validate a data array and layout object.
23+
*
24+
* @param {array} data
25+
* @param {object} layout
26+
*
27+
* @return {array} array of error objects each containing:
28+
* - {string} code
29+
* error code ('object', 'array', 'schema', 'unused', 'invisible' or 'value')
30+
* - {string} container
31+
* container where the error occurs ('data' or 'layout')
32+
* - {number} trace
33+
* trace index of the 'data' container where the error occurs
34+
* - {array} path
35+
* nested path to the key that causes the error
36+
* - {string} astr
37+
* attribute string variant of 'path' compatible with Plotly.restyle and
38+
* Plotly.relayout.
39+
* - {string} msg
40+
* error message (shown in console in logger config argument is enable)
41+
*/
4042
module.exports = function valiate(data, layout) {
41-
if(!Array.isArray(data)) {
42-
throw new Error('data must be an array');
43+
var schema = PlotSchema.get(),
44+
errorList = [],
45+
gd = {};
46+
47+
var dataIn, layoutIn;
48+
49+
if(isArray(data)) {
50+
gd.data = Lib.extendDeep([], data);
51+
dataIn = data;
52+
}
53+
else {
54+
gd.data = [];
55+
dataIn = [];
56+
errorList.push(format('array', 'data'));
4357
}
4458

45-
if(!isPlainObject(layout)) {
46-
throw new Error('layout must be an object');
59+
if(isPlainObject(layout)) {
60+
gd.layout = Lib.extendDeep({}, layout);
61+
layoutIn = layout;
62+
}
63+
else {
64+
gd.layout = {};
65+
layoutIn = {};
66+
if(arguments.length > 1) {
67+
errorList.push(format('object', 'layout'));
68+
}
4769
}
4870

49-
var gd = {
50-
data: Lib.extendDeep([], data),
51-
layout: Lib.extendDeep({}, layout)
52-
};
53-
Plots.supplyDefaults(gd);
71+
// N.B. dataIn and layoutIn are in general not the same as
72+
// gd.data and gd.layout after supplyDefaults as some attributes
73+
// in gd.data and gd.layout (still) get mutated during this step.
5474

55-
var schema = PlotSchema.get();
75+
Plots.supplyDefaults(gd);
5676

5777
var dataOut = gd._fullData,
58-
len = data.length,
59-
dataList = new Array(len);
78+
len = dataIn.length;
6079

6180
for(var i = 0; i < len; i++) {
62-
var traceIn = data[i];
63-
var traceList = dataList[i] = [];
81+
var traceIn = dataIn[i],
82+
base = ['data', i];
6483

6584
if(!isPlainObject(traceIn)) {
66-
throw new Error('each data trace must be an object');
85+
errorList.push(format('object', base));
86+
continue;
6787
}
6888

6989
var traceOut = dataOut[i],
@@ -78,25 +98,22 @@ module.exports = function valiate(data, layout) {
7898
};
7999

80100
if(traceOut.visible === false && traceIn.visible !== false) {
81-
traceList.push(format('invisible', i));
101+
errorList.push(format('invisible', base));
82102
}
83103

84-
crawl(traceIn, traceOut, traceSchema, traceList);
104+
crawl(traceIn, traceOut, traceSchema, errorList, base);
85105
}
86106

87107
var layoutOut = gd._fullLayout,
88-
layoutSchema = fillLayoutSchema(schema, dataOut),
89-
layoutList = [];
108+
layoutSchema = fillLayoutSchema(schema, dataOut);
90109

91-
crawl(layout, layoutOut, layoutSchema, layoutList);
110+
crawl(layoutIn, layoutOut, layoutSchema, errorList, 'layout');
92111

93-
return {
94-
data: dataList,
95-
layout: layoutList
96-
};
112+
// return undefined if no validation errors were found
113+
return (errorList.length === 0) ? void(0) : errorList;
97114
};
98115

99-
function crawl(objIn, objOut, schema, list, path) {
116+
function crawl(objIn, objOut, schema, list, base, path) {
100117
path = path || [];
101118

102119
var keys = Object.keys(objIn);
@@ -113,28 +130,34 @@ function crawl(objIn, objOut, schema, list, path) {
113130
var nestedSchema = getNestedSchema(schema, k);
114131

115132
if(!isInSchema(schema, k)) {
116-
list.push(format('schema', p));
133+
list.push(format('schema', base, p));
117134
}
118135
else if(isPlainObject(valIn) && isPlainObject(valOut)) {
119-
crawl(valIn, valOut, nestedSchema, list, p);
136+
crawl(valIn, valOut, nestedSchema, list, base, p);
120137
}
121-
else if(!isPlainObject(valIn) && isPlainObject(valOut)) {
122-
list.push(format('container', p, valIn));
123-
}
124-
else if(nestedSchema.items && Array.isArray(valIn)) {
138+
else if(nestedSchema.items && isArray(valIn)) {
125139
var itemName = k.substr(0, k.length - 1);
126140

127141
for(var j = 0; j < valIn.length; j++) {
128-
p[p.length - 1] = k + '[' + j + ']';
142+
var _nestedSchema = nestedSchema.items[itemName],
143+
_p = p.slice();
129144

130-
crawl(valIn[j], valOut[j], nestedSchema.items[itemName], list, p);
145+
_p.push(j);
146+
147+
crawl(valIn[j], valOut[j], _nestedSchema, list, base, _p);
131148
}
132149
}
150+
else if(!isPlainObject(valIn) && isPlainObject(valOut)) {
151+
list.push(format('object', base, p, valIn));
152+
}
153+
else if(!isArray(valIn) && isArray(valOut) && nestedSchema.valType !== 'info_array') {
154+
list.push(format('array', base, p, valIn));
155+
}
133156
else if(!(k in objOut)) {
134-
list.push(format('unused', p, valIn));
157+
list.push(format('unused', base, p, valIn));
135158
}
136159
else if(!Lib.validate(valIn, nestedSchema)) {
137-
list.push(format('value', p, valIn));
160+
list.push(format('value', base, p, valIn));
138161
}
139162
}
140163

@@ -155,11 +178,82 @@ function fillLayoutSchema(schema, dataOut) {
155178
return schema.layout.layoutAttributes;
156179
}
157180

158-
function format(code, path, valIn) {
181+
// validation error codes
182+
var code2msgFunc = {
183+
object: function(base, astr) {
184+
var prefix;
185+
186+
if(base === 'layout' && astr === '') prefix = 'The layout argument';
187+
else if(base[0] === 'data') {
188+
prefix = 'Trace ' + base[1] + ' in the data argument';
189+
}
190+
else prefix = inBase(base) + 'key ' + astr;
191+
192+
return prefix + ' must be linked to an object container';
193+
},
194+
array: function(base, astr) {
195+
var prefix;
196+
197+
if(base === 'data') prefix = 'The data argument';
198+
else prefix = inBase(base) + 'key ' + astr;
199+
200+
return prefix + ' must be linked to an array container';
201+
},
202+
schema: function(base, astr) {
203+
return inBase(base) + 'key ' + astr + ' is not part of the schema';
204+
},
205+
unused: function(base, astr, valIn) {
206+
var target = isPlainObject(valIn) ? 'container' : 'key';
207+
208+
return inBase(base) + target + ' ' + astr + ' did not get coerced';
209+
},
210+
invisible: function(base) {
211+
return 'Trace ' + base[1] + ' got defaulted to be not visible';
212+
},
213+
value: function(base, astr, valIn) {
214+
return [
215+
inBase(base) + 'key ' + astr,
216+
'is set to an invalid value (' + valIn + ')'
217+
].join(' ');
218+
}
219+
};
220+
221+
function inBase(base) {
222+
if(isArray(base)) return 'In data trace ' + base[1] + ', ';
223+
224+
return 'In ' + base + ', ';
225+
}
226+
227+
function format(code, base, path, valIn) {
228+
path = path || '';
229+
230+
var container, trace;
231+
232+
// container is either 'data' or 'layout
233+
// trace is the trace index if 'data', null otherwise
234+
235+
if(isArray(base)) {
236+
container = base[0];
237+
trace = base[1];
238+
}
239+
else {
240+
container = base;
241+
trace = null;
242+
}
243+
244+
var astr = convertPathToAttributeString(path),
245+
msg = code2msgFunc[code](base, astr, valIn);
246+
247+
// log to console if logger config option is enabled
248+
Lib.log(msg);
249+
159250
return {
160251
code: code,
252+
container: container,
253+
trace: trace,
161254
path: path,
162-
msg: code2msgFunc[code](path, valIn)
255+
astr: astr,
256+
msg: msg
163257
};
164258
}
165259

@@ -192,3 +286,24 @@ function splitKey(key) {
192286
id: id
193287
};
194288
}
289+
290+
function convertPathToAttributeString(path) {
291+
if(!isArray(path)) return String(path);
292+
293+
var astr = '';
294+
295+
for(var i = 0; i < path.length; i++) {
296+
var p = path[i];
297+
298+
if(typeof p === 'number') {
299+
astr = astr.substr(0, astr.length - 1) + '[' + p + ']';
300+
}
301+
else {
302+
astr += p;
303+
}
304+
305+
if(i < path.length - 1) astr += '.';
306+
}
307+
308+
return astr;
309+
}

0 commit comments

Comments
 (0)