'use strict';
function validateRecords(records) {
if (!Array.isArray(records)) {
return { stage: 'contract', valid: false, recordCount: null,
validRecordCount: 0,
errors: [{ record: null, path: [], code: 'TOP_LEVEL_ARRAY' }] };
}
const fields = ['id', 'title', 'status', 'minutes'];
const firstIds = new Map();
const errors = [];
let validRecordCount = 0;
records.forEach((item, index) => {
const start = errors.length;
const add = (field, code, extra = {}) => errors.push({
record: index + 1, path: field === null ? [index] : [index, field],
code, ...extra
});
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
add(null, 'RECORD_OBJECT');
return;
}
for (const field of fields) {
if (!Object.hasOwn(item, field)) {
add(field, 'REQUIRED');
continue;
}
const value = item[field];
if (field === 'id') {
if (typeof value !== 'string' || !/^[a-z][a-z0-9-]{0,15}$/.test(value)) {
add(field, 'ID_FORMAT');
} else if (firstIds.has(value)) {
add(field, 'DUPLICATE_ID', { firstRecord: firstIds.get(value) });
} else {
firstIds.set(value, index + 1);
}
} else if (field === 'title') {
if (typeof value !== 'string' || value.trim().length === 0) {
add(field, 'TITLE_TEXT');
}
} else if (field === 'status') {
if (value !== 'draft' && value !== 'ready') add(field, 'STATUS_VALUE');
} else if (!Number.isSafeInteger(value) || value < 0 || value > 480) {
add(field, 'MINUTES_RANGE');
}
}
for (const field of Object.keys(item).filter(key => !fields.includes(key)).sort()) {
add(field, 'EXTRA_FIELD');
}
if (errors.length === start) validRecordCount++;
});
return { stage: 'contract', valid: errors.length === 0,
recordCount: records.length, validRecordCount, errors };
}
function validateJson(text) {
if (typeof text !== 'string') throw new TypeError('Expected JSON text');
let parsed;
try {
parsed = JSON.parse(text);
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
return { stage: 'syntax', valid: false, recordCount: null,
validRecordCount: 0,
errors: [{ record: null, path: [], code: 'JSON_SYNTAX' }] };
}
return validateRecords(parsed);
}
// All fixtures are fictional. No import, database or network operation.
const fixtures = [
{
"name": "valid",
"text": "[{\"id\":\"a\",\"title\":\" 演示甲 \",\"status\":\"draft\",\"minutes\":0},{\"id\":\"b\",\"title\":\"演示乙\",\"status\":\"ready\",\"minutes\":480}]"
},
{
"name": "empty",
"text": "[]"
},
{
"name": "non_array",
"text": "{\"id\":\"a\"}"
},
{
"name": "missing_id",
"text": "[{\"title\":\"演示\",\"status\":\"draft\",\"minutes\":10}]"
},
{
"name": "blank_title",
"text": "[{\"id\":\"a\",\"title\":\" \",\"status\":\"draft\",\"minutes\":10}]"
},
{
"name": "string_minutes",
"text": "[{\"id\":\"a\",\"title\":\"演示\",\"status\":\"draft\",\"minutes\":\"10\"}]"
},
{
"name": "invalid_status",
"text": "[{\"id\":\"a\",\"title\":\"演示\",\"status\":\"done\",\"minutes\":10}]"
},
{
"name": "duplicate_id",
"text": "[{\"id\":\"a\",\"title\":\"甲\",\"status\":\"draft\",\"minutes\":10},{\"id\":\"a\",\"title\":\"乙\",\"status\":\"ready\",\"minutes\":20}]"
},
{
"name": "multi_error_invalid_first",
"text": "[{\"id\":\"a\",\"title\":\" \",\"status\":\"done\",\"minutes\":\"5\",\"zeta\":true,\"alpha\":true},{\"id\":\"a\",\"title\":\"乙\",\"status\":\"ready\",\"minutes\":10},null]"
},
{
"name": "malformed_json",
"text": "[{\"id\":\"a\",}]"
},
{
"name": "invalid_id",
"text": "[{\"id\":\" A \",\"title\":\"演示\",\"status\":\"draft\",\"minutes\":10}]"
},
{
"name": "invalid_minutes",
"text": "[{\"id\":\"a\",\"title\":\"甲\",\"status\":\"draft\",\"minutes\":-1},{\"id\":\"b\",\"title\":\"乙\",\"status\":\"draft\",\"minutes\":481},{\"id\":\"c\",\"title\":\"丙\",\"status\":\"draft\",\"minutes\":1.5},{\"id\":\"d\",\"title\":\"丁\",\"status\":\"draft\",\"minutes\":9007199254740992}]"
},
{
"name": "missing_all_fields",
"text": "[{}]"
},
{
"name": "non_object_records",
"text": "[null,[],5]"
},
{
"name": "id_length_16",
"text": "[{\"id\":\"abcdefghijklmnop\",\"title\":\"demo\",\"status\":\"draft\",\"minutes\":1}]"
},
{
"name": "id_length_17",
"text": "[{\"id\":\"abcdefghijklmnopq\",\"title\":\"demo\",\"status\":\"draft\",\"minutes\":1}]"
},
{
"name": "id_trailing_newline",
"text": "[{\"id\":\"a\\n\",\"title\":\"demo\",\"status\":\"draft\",\"minutes\":1}]"
}
];
if (require.main === module) {
for (const { name, text } of fixtures) {
console.log(JSON.stringify({ name, result: validateJson(text) }));
}
}
module.exports = { validateJson, validateRecords, fixtures };
{
"stage": "contract",
"valid": false,
"recordCount": 3,
"validRecordCount": 0,
"errors": [
{
"record": 1,
"path": [
0,
"title"
],
"code": "TITLE_TEXT"
},
{
"record": 1,
"path": [
0,
"status"
],
"code": "STATUS_VALUE"
},
{
"record": 1,
"path": [
0,
"minutes"
],
"code": "MINUTES_RANGE"
},
{
"record": 1,
"path": [
0,
"alpha"
],
"code": "EXTRA_FIELD"
},
{
"record": 1,
"path": [
0,
"zeta"
],
"code": "EXTRA_FIELD"
},
{
"record": 2,
"path": [
1,
"id"
],
"code": "DUPLICATE_ID",
"firstRecord": 1
},
{
"record": 3,
"path": [
2
],
"code": "RECORD_OBJECT"
}
]
}