Skip to content

Instantly share code, notes, and snippets.

@rosschapman
Created July 15, 2020 19:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rosschapman/452adf98dcca669d6f217652044c9325 to your computer and use it in GitHub Desktop.
Save rosschapman/452adf98dcca669d6f217652044c9325 to your computer and use it in GitHub Desktop.
Validate Org Chart
// 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