Created
July 15, 2020 19:37
-
-
Save rosschapman/452adf98dcca669d6f217652044c9325 to your computer and use it in GitHub Desktop.
Validate Org Chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is an example of one of our inputs. | |
// This data sructure represents the org structure of a company. | |
// At the top level here we have an _array_ of employees with various properties. | |
// 1. Required keys need to exist | |
// 2. Values need to match the specified type | |
// 3. Extraneous keys are invalid and should error | |
// 4. Return when you encounter the very first error | |
export function validate(data, schema) { | |
try { | |
validateInner(data, schema); | |
} catch (error) { | |
return error; | |
} | |
return true; | |
} | |
export function validateInner(data, schema) { | |
let schemaFields = schema.map((rule) => rule.name); | |
for (let item of data) { | |
let dataFields = Object.keys(item); | |
let hasExtraKeys = hasExtraneousKeys(dataFields, schemaFields); | |
if (hasExtraKeys) throw extraneousFieldsError(); | |
for (let rule of schema) { | |
let field = item[rule.name]; | |
let required = rule.required; | |
if (required && !field) throw missingFieldError(item, rule); | |
if ((!required && field) || required) { | |
if (isArrayType(rule.type)) { | |
if (Array.isArray(field)) { | |
validateInner(field, schema); | |
} else { | |
throw typeError(item, rule); | |
} | |
} else if (rule.type !== typeof field) { | |
throw typeError(item, rule); | |
} | |
} | |
} | |
} | |
return true; | |
} | |
// Helpers | |
function typeError(employee, rule) { | |
let field = employee[rule.name]; | |
return { | |
valid: false, | |
message: `<name:${employee.name},field:${ | |
rule.name | |
}> - TypeError: Expected ${rule.type} but got ${typeof field}`, | |
}; | |
} | |
function missingFieldError(employee, rule) { | |
return { | |
valid: false, | |
message: `Missing field: <name:${employee.name},field:${rule.name}>`, | |
}; | |
} | |
function extraneousFieldsError() { | |
return { valid: false, message: "Extraneous fields" }; | |
} | |
function isArrayType(type) { | |
return /^array:/.test(type); | |
} | |
function hasExtraneousKeys(dataFields, schemaFields) { | |
return dataFields.some((field) => !schemaFields.includes(field)); | |
} | |
// Sample test | |
import { validate } from "./validate-org.js"; | |
const EMPLOYEE_SCHEMA = [ | |
{ name: "id", required: true, type: "number" }, | |
{ name: "name", required: true, type: "string" }, | |
{ name: "title", required: true, type: "string" }, | |
{ name: "age", required: false, type: "number" }, | |
{ name: "salary", required: true, type: "number" }, | |
{ name: "reports", required: false, type: "array:employee" }, | |
{ name: "subordinates", required: false, type: "array:employee" }, | |
]; | |
let ACME_CORP_NESTED = [ | |
{ | |
id: 1, | |
name: "alice", | |
title: "ceo", | |
age: 40, | |
salary: 100, | |
reports: [ | |
{ | |
id: 2, | |
name: "bob", | |
title: "cfo", | |
salary: 10, | |
reports: [ | |
{ | |
id: 3, | |
name: "zorp", | |
title: "controller", | |
salary: 40, | |
}, | |
], | |
subordinates: [ | |
{ | |
id: 11, | |
name: "kate", | |
title: "accountant", | |
salary: 40, | |
}, | |
{ | |
id: 3, | |
name: "zorp", | |
title: "controller", | |
salary: 40, | |
}, | |
], | |
}, | |
], | |
}, | |
]; | |
let ACME_CORP_NESTED_MISSING = [ | |
{ | |
id: 1, | |
name: "alice", | |
title: "ceo", | |
age: 40, | |
salary: 100, | |
reports: [ | |
{ | |
id: 2, | |
name: "bob", | |
title: "cfo", | |
salary: 10, | |
reports: [ | |
{ | |
// id: 3, | |
name: "zorp", | |
title: "controller", | |
salary: 40, | |
}, | |
], | |
subordinates: [ | |
{ | |
id: 11, | |
name: "kate", | |
title: "accountant", | |
salary: 40, | |
}, | |
{ | |
id: 3, | |
name: "zorp", | |
title: "controller", | |
salary: 40, | |
}, | |
], | |
}, | |
], | |
}, | |
]; | |
let ACME_CORP_EXTRA_KEY = [ | |
{ | |
id: 1, | |
name: "alice", | |
title: "ceo", | |
age: 40, | |
salary: 100, | |
reports: [], | |
extra: "", | |
}, | |
]; | |
let ACME_CORP_MISSING_REQUIRED = [ | |
{ | |
// missing id | |
name: "alice", | |
title: "ceo", | |
age: 40, | |
salary: 100, | |
reports: [], | |
}, | |
]; | |
let ACME_CORP_WRONG_TYPE = [ | |
{ | |
id: "1", // <-- id should be number | |
name: "alice", | |
title: "ceo", | |
age: 40, | |
salary: 100, | |
reports: [], | |
}, | |
]; | |
const INVALID_EXTRA = { message: "Extraneous fields", valid: false }; | |
const INVALID_MISSING_REQUIRED = { | |
valid: false, | |
message: "Missing field: <name:alice,field:id>", | |
}; | |
const INVALID_WRONG_TYPE = { | |
message: "<name:alice,field:id> - TypeError: Expected number but got string", | |
valid: false, | |
}; | |
const INVALID_WRONG_TYPE_NESTED = { | |
message: "Missing field: <name:zorp,field:id>", | |
valid: false, | |
}; | |
describe("validate", () => { | |
it("returns true", () => { | |
let actual = validate(ACME_CORP_NESTED, EMPLOYEE_SCHEMA); | |
expect(actual).toBe(true); | |
}); | |
it("returns a validation error msg if there are extra keys", () => { | |
const actual = validate(ACME_CORP_EXTRA_KEY, EMPLOYEE_SCHEMA); | |
expect(actual).toEqual(INVALID_EXTRA); | |
}); | |
it("returns a validation error msg if a required field is missing", () => { | |
const actual = validate(ACME_CORP_MISSING_REQUIRED, EMPLOYEE_SCHEMA); | |
expect(actual).toEqual(INVALID_MISSING_REQUIRED); | |
}); | |
it("returns a validation error msg if a field is the wrong type", () => { | |
const actual = validate(ACME_CORP_WRONG_TYPE, EMPLOYEE_SCHEMA); | |
expect(actual).toEqual(INVALID_WRONG_TYPE); | |
}); | |
it("recurses for nested arrays", () => { | |
const actual = validate(ACME_CORP_NESTED_MISSING, EMPLOYEE_SCHEMA); | |
expect(actual).toEqual(INVALID_WRONG_TYPE_NESTED); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment