Skip to content

Commit d0980c3

Browse files
committed
Basic validation
1 parent a97b77f commit d0980c3

File tree

4 files changed

+228
-126
lines changed

4 files changed

+228
-126
lines changed

lib/importer/attribute-types.js

Lines changed: 58 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -22,89 +22,80 @@
2222
// Base types without being wrapped by requiredType or optionalType may then
2323
// handled undefined or "" as they see fit.
2424

25-
class AttributeType {
26-
constructor(validator, translator) {
27-
this.v = validator;
28-
this.t = translator;
25+
// An attribute type is a function from an input value to a result.
26+
27+
class AttributeMappingResult {
28+
constructor(value, warnings, errors) {
29+
this.value = value;
30+
this.warnings = warnings || [];
31+
this.errors = errors;
2932
}
3033

31-
get validator() { return this.v; }
32-
get translator() { return this.t; }
34+
get valid() {
35+
return !this.errors;
36+
}
3337

34-
validate(inputValue) { return this.v(inputValue); }
35-
translate(inputValue) { return this.t(inputValue); }
38+
get empty() {
39+
return this.value !== undefined;
40+
}
3641
}
3742

38-
exports.AttributeType = AttributeType;
43+
// These helpers define the three kinds of results:
44+
45+
function emptyMapping(warnings) {
46+
return new AttributeMappingResult(undefined, warnings);
47+
}
48+
49+
function successfulMapping(outputValue, warnings) {
50+
return new AttributeMappingResult(outputValue, warnings);
51+
}
52+
53+
function failedMapping(warnings, errors) {
54+
return new AttributeMappingResult(undefined, warnings, errors);
55+
}
3956

4057
// Create an optional version of an existing type, that allows empty strings or
4158
// undefined values and maps then to "undefined", and converts any validation errors to warnings
4259
exports.optionalType = (baseType) => {
43-
return new AttributeType(
44-
(inputValue) => {
45-
if(inputValue !== undefined && inputValue != "") {
46-
const [warnings, errors] = baseType.validate(inputValue);
60+
return (inputValue) => {
61+
if(inputValue !== undefined && inputValue != "") {
62+
const result = baseType(inputValue);
63+
if(result.valid) {
64+
return result;
65+
} else {
4766
// Convert any errors into warnings, as this is an optional field
48-
return [warnings.concat(errors), []];
67+
return emptyMapping(result.warnings.concat(result.errors));
4968
}
50-
else {
51-
return [[],[]];
52-
}
53-
},
54-
(inputValue) => {
55-
if(inputValue !== undefined && inputValue != "") return baseType.translate(inputValue);
56-
else return undefined;
5769
}
58-
);
59-
}
60-
61-
// Create a required version of an existing type, that
62-
exports.optionalType = (baseType) => {
63-
return new AttributeType(
64-
(inputValue) => {
65-
if(inputValue !== undefined && inputValue != "") {
66-
const [warnings, errors] = baseType.validate(inputValue);
67-
// Convert any errors into warnings, as this is an optional field
68-
return [warnings.concat(errors), []];
69-
}
70-
else {
71-
return [[],[]];
72-
}
73-
},
74-
(inputValue) => {
75-
if(inputValue !== undefined && inputValue != "") return baseType.translate(inputValue);
76-
else return undefined;
70+
else {
71+
return emptyMapping();
7772
}
78-
);
73+
};
7974
}
8075

81-
// The default string type. Does not allow empty strings!
82-
exports.basicStringType = new AttributeType(
83-
(inputValue) => {
84-
if(inputValue === undefined || inputValue == "") {
85-
return [[],["A value must be provided"]];
76+
// Create a required version of an existing type, that reports an undefined or empty-string input value as an error
77+
exports.requiredType = (baseType) => {
78+
return (inputValue) => {
79+
if(inputValue !== undefined && inputValue != "") {
80+
return baseType(inputValue);
8681
}
87-
// Anything else can be converted into a string
88-
return [[],[]];
89-
},
90-
(inputValue) => {
91-
return String(inputValue);
92-
}
93-
);
94-
95-
exports.basicNumberType = new AttributeType(
96-
(inputValue) => {
97-
if(inputValue === undefined || inputValue == "") {
98-
return [[],["A value must be provided"]];
99-
}
100-
101-
if(parseFloat(inputValue) === NaN) {
102-
return [[],[inputValue + " is not a valid number"]];
82+
else {
83+
return failedMapping([], ["A value must be provided"]);
10384
}
85+
};
86+
}
10487

105-
return [[],[]];
106-
},
107-
(inputValue) => {
108-
return parseFloat(inputValue);
88+
// The default string type
89+
exports.basicStringType = (inputValue) => {
90+
return successfulMapping(String(inputValue));
91+
};
92+
93+
// The default numeric type
94+
exports.basicNumberType = (inputValue) => {
95+
const result = parseFloat(inputValue);
96+
if(isNaN(result)) {
97+
return failedMapping([], ["This is not a valid number"]);
98+
} else {
99+
return successfulMapping(result);
109100
}
110-
);
101+
};
Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,80 @@
11
const attributeTypes = require('./attribute-types');
22

3-
function testType(t, invalidInputs, fixtures) {
4-
for([input,warnings,errors] of invalidInputs) {
5-
expect(t.validate(input)).toMatchObject([warnings, errors]);
6-
}
7-
8-
for ([input, output] of fixtures) {
9-
expect(t.validate(input)).toMatchObject([[],[]]);
10-
expect(t.translate(input)).toEqual(output);
11-
}
12-
};
13-
14-
test('basic string', () => {
15-
const invalidInputs = [
16-
[undefined, [], ["A value must be provided"]],
17-
["", [], ["A value must be provided"]]
18-
];
19-
20-
const fixtures = [
21-
["foo", "foo"],
22-
[123, "123"],
23-
];
24-
25-
testType(attributeTypes.basicStringType, invalidInputs, fixtures);
3+
test('required basic string', () => {
4+
// Also tests the workings of the requiredType system
5+
const t = attributeTypes.requiredType(attributeTypes.basicStringType);
6+
7+
let r = t(undefined);
8+
expect(r.valid).toBeFalsy();
9+
expect(r.warnings).toMatchObject([]);
10+
expect(r.errors).toMatchObject(["A value must be provided"]);
11+
12+
r = t("");
13+
expect(r.valid).toBeFalsy();
14+
expect(r.warnings).toMatchObject([]);
15+
expect(r.errors).toMatchObject(["A value must be provided"]);
16+
17+
r = t("foo");
18+
expect(r.valid).toBeTruthy();
19+
expect(r.value).toEqual("foo");
20+
expect(r.warnings).toMatchObject([]);
21+
22+
r = t(123);
23+
expect(r.valid).toBeTruthy();
24+
expect(r.value).toEqual("123");
25+
expect(r.warnings).toMatchObject([]);
2626
});
2727

2828
test('optional basic string', () => {
29-
const invalidInputs = [];
29+
// Also tests the workings of the optionalType system
30+
const t = attributeTypes.optionalType(attributeTypes.basicStringType);
31+
32+
let r = t(undefined);
33+
expect(r.valid).toBeTruthy();
34+
expect(r.warnings).toMatchObject([]);
35+
expect(r.value).toEqual(undefined);
36+
37+
r = t("");
38+
expect(r.valid).toBeTruthy();
39+
expect(r.warnings).toMatchObject([]);
40+
expect(r.value).toEqual(undefined);
41+
42+
r = t("foo");
43+
expect(r.valid).toBeTruthy();
44+
expect(r.value).toEqual("foo");
45+
expect(r.warnings).toMatchObject([]);
46+
47+
r = t(123);
48+
expect(r.valid).toBeTruthy();
49+
expect(r.value).toEqual("123");
50+
expect(r.warnings).toMatchObject([]);
51+
});
52+
53+
test('required basic number', () => {
54+
const t = attributeTypes.requiredType(attributeTypes.basicNumberType);
3055

31-
const fixtures = [
32-
["foo", "foo"],
33-
[123, "123"],
34-
["", undefined],
35-
[undefined, undefined]
36-
];
56+
r = t("foo");
57+
expect(r.valid).toBeFalsy();
58+
expect(r.warnings).toMatchObject([]);
59+
expect(r.errors).toMatchObject(["This is not a valid number"]);
3760

38-
testType(attributeTypes.optionalType(attributeTypes.basicStringType), invalidInputs, fixtures);
61+
r = t(123);
62+
expect(r.valid).toBeTruthy();
63+
expect(r.value).toEqual(123);
64+
expect(r.warnings).toMatchObject([]);
3965
});
4066

41-
test('basic number', () => {
42-
const invalidInputs = [
43-
[undefined, [], ["A value must be provided"]],
44-
["", [], ["A value must be provided"]],
45-
["foo", [], ["foo is not a valid number"]]
46-
];
67+
test('optional basic number', () => {
68+
// Also tests that optionalType converts errors into warnings
69+
const t = attributeTypes.optionalType(attributeTypes.basicNumberType);
4770

48-
const fixtures = [
49-
[123, 123],
50-
["123", "123"],
51-
];
71+
r = t("foo");
72+
expect(r.valid).toBeTruthy();
73+
expect(r.value).toBeUndefined();
74+
expect(r.warnings).toMatchObject(["This is not a valid number"]);
5275

53-
testType(attributeTypes.basicNumberType, invalidInputs, fixtures);
76+
r = t(123);
77+
expect(r.valid).toBeTruthy();
78+
expect(r.value).toEqual(123);
79+
expect(r.warnings).toMatchObject([]);
5480
});

lib/importer/backend.js

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ function validateMapping(range, mapping) {
459459
// Output types are optional, we fall back to basicStringType if not specified
460460
if(mapping.attributeTypes) {
461461
for(const [attribute, attrType] of Object.entries(mapping.attributeTypes)) {
462-
assert(attrType instanceof AttributeType);
462+
assert(attrType instanceof Function);
463463
}
464464
}
465465
}
@@ -499,13 +499,11 @@ exports.SessionPerformMappingJob = (sid, range, mapping) => {
499499

500500
// For now, attribute mappings are just integer column offsets
501501
const inputColumn = range.start.column + m;
502-
if (inputColumn >= row.length) {
503-
// If a row is missing values at the end, this may be
504-
// represented as a "short" row array.
505-
record[attr] = undefined;
506-
} else {
507-
const cell = row[range.start.column + m];
508-
if(cell.v) {
502+
// If a row is missing values at the end, this may be
503+
// represented as a "short" row array.
504+
if (inputColumn < row.length) {
505+
const cell = row[inputColumn];
506+
if(cell && cell.v) {
509507
record[attr] = mapCellValue(cell);
510508
foundSomeValues = true;
511509
}
@@ -521,21 +519,27 @@ exports.SessionPerformMappingJob = (sid, range, mapping) => {
521519
const [attr, inputVal] = element;
522520
const attrType = attrTypes[attr] || attributeTypes.basicStringType;
523521

524-
const [warnings, errors] = attrType.validate(inputVal);
525-
warnings.forEach((text) => {
522+
const result = attrType(inputVal);
523+
524+
result.warnings.forEach((text) => {
526525
rowWarnings.push([attr,text]);
527526
});
528-
errors.forEach((text) => {
529-
rowErrors.push([attr,text]);
530-
});
531527

532-
if(errors.length == 0) {
533-
mappedRecord[attr] = attrType.translate(inputVal);
528+
if(result.valid) {
529+
// Succeeded, but maybe an empty result
530+
if(result.value !== undefined) {
531+
mappedRecord[attr] = result.value;
532+
}
533+
} else {
534+
// Failed
535+
result.errors.forEach((text) => {
536+
rowErrors.push([attr,text]);
537+
});
534538
}
535539
});
536540

537541
if (rowErrors.length == 0) {
538-
records.push(record);
542+
records.push(mappedRecord);
539543
}
540544
}
541545

0 commit comments

Comments
 (0)