Skip to content

Commit

Permalink
Merge pull request #19 from punchcard-cms/validation-for-real-for-real
Browse files Browse the repository at this point in the history
Validation for real for real
  • Loading branch information
scottnath committed May 26, 2016
2 parents 225d48b + a6ecfdd commit cc2051e
Show file tree
Hide file tree
Showing 9 changed files with 512 additions and 14 deletions.
3 changes: 3 additions & 0 deletions lib/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
const html = require('./form/html');
const validation = require('./form/validation');

const validate = require('./form/validate');

// const scripts = require('./form/scripts');
// const css = require('./form/css');

Expand All @@ -23,3 +25,4 @@ const form = (type) => {


module.exports = form;
module.exports.validate = validate;
45 changes: 43 additions & 2 deletions lib/form/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,53 @@

const nunjucks = require('nunjucks');

const renderer = (type) => {
/*
* Add Error Messaging to Rendered Inputs
*
* @param {string} html - Rendered HTML
* @param {InputType} type - Input type being rendered
* @param {FormInputValues} - Errors associated with form inputs
*
* @returns {string} - Rendered HTML, with appropriate error messages, and aria labels for invalid inputs
*/
const addError = (html, input, errors) => {
let render = html;
let result = '';

if (errors) {
Object.keys(input.inputs).map(inp => {
const name = `${input.id}--${inp}`;
const find = new RegExp(`/name=['"]\\s*${name}\\s*['"]/`);
const errored = render.replace(find, `name="${name}" aria-invalid="true"`);

if (errors.hasOwnProperty(name)) {
result += `<p class="form--alert" role="alert" for="${input.inputs[inp].id}">${errors[name]}</p>`;

render = errored;
}
});
}

result += html;

return result;
};

/*
* Renders the input in to HTML
*
* @param {InputType} type - Input type being rendered
* @param {FormInputValues} - Errors associated with form inputs
*
* @returns {string} - Rendered HTML form body (not wrapped in <form>)
*/
const renderer = (type, errors) => {
return new Promise((res) => {
let rendered = type.attributes.map(input => {
const inputs = Object.keys(input.inputs).length;
const description = input.hasOwnProperty('description') && input.description !== '';
const context = input.inputs;
const html = nunjucks.renderString(input.html, context);
let render = '';

// Set opening tags based on number of inputs
Expand All @@ -25,7 +66,7 @@ const renderer = (type) => {
}

// Render the form element
render += nunjucks.renderString(input.html, context);
render += addError(html, input, errors);

// Set closing tags based on number of inputs
if (inputs > 1) {
Expand Down
220 changes: 220 additions & 0 deletions lib/form/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
'use strict';

const util = require('../util');

/*
* @typedef FormInputValues
* @type object
*
* @property {string} {{input-plugin-id}}--{{input-plugin-component}}[--{{filed-instance}}] - The value of the input field. The name of the input field is in the given form
*/

/*
* @typedef SplitInputValues
* @type object
*
* @property {string} plugin - The Input Plugin ID (from plugin configuration)
* @property {string} input - The Input Plugin component (from plugin)
* @property {string|undefined} index - The index of the repeatable element ({undefined} if not a repeatable)
* @property {string} value - The value from the field input
*/

/*
* Splits raw form input into object we can use
*
* @param {FormInputValues} raw - The input from a form
*
* @returns {SplitInputValues} - The structured values to test
*/
const split = raw => {
const output = Object.keys(raw).filter(input => {
const boom = input.split('--');
if (boom.length >= 2) {
return true;
}

return false;
});

return output.map(input => {
const boom = input.split('--');
const value = raw[input];

return {
plugin: boom[0],
input: boom[1],
index: boom[2],
value,
};
});
};

/*
* @typedef WorkingInputValues
* @type SplitInputValues
*
* @property {string|boolean} validation - The result of running validation for the {SplitInputValues}. A {string} is an error message, a {boolean} (can only be true) for valid input.
*/

/*
* Determines if there are any errors in validation
*
* @param {WorkingInputValues} working - The validated input values
*
* @returns {true|FormInputValues} - Returns `true` if there are no validation issues, {FormInputValues} with error messages as values if there are
*/
const join = working => {
const glue = {};

working.map(input => {
let key = `${input.plugin}--${input.input}`;

if (input.index) {
key += `--${input.index}`;
}

if (!input.hasOwnProperty('validation')) {
throw new Error(`Validation for '${key}' requires a validation key!`);
}

if (input.validation !== true) {
glue[key] = input.validation;
}
});

if (Object.keys(glue).length !== 0) {
return glue;
}

return true;
};

/*
* @typedef ValidationSettings
* @type object
*
* @property {object} {{input-plugin-id}} - The ID of the input plugin
* @proeprty {object} {{input-plugin-id}}.{{input-plugin-component}} - The settings of the Input Plugin Component from the input type
*/

/*
* Builds settings for use in validation
*
* @param {InputType} type - Input Type being tested
*
* @returns {ValidationSettings} - All validation settings
*/
const buildSettings = type => {
const settings = {};

type.attributes.map(plugin => {
settings[plugin.id] = {};

return Object.keys(plugin.inputs).map(input => {
settings[plugin.id][input] = plugin.inputs[input].settings;
});
});

return settings;
};

/*
* @typedef ValidationSingleValues
* @type object
*
* @property {object} {{input-plugin-id}} - The ID of the input plugin
* @proeprty {string} {{input-plugin-id}}.{{input-plugin-component}} - The value of the input field
*
*/

/*
* @typedef ValidationMultipleValues
* @type object
*
* @property {object} {{input-plugin-id}} - The ID of the input plugin
* @property {object} {{input-plugin-id}}.{{filed-instance}} - The field instance for a repeating field
* @proeprty {string} {{input-plugin-id}}.{{filed-instance}}.{{input-plugin-component}} - The value of the input field
*
*/

/*
* Builds the values for use in validation
*
* @param {SplitInputValues} working - The structured values to test
*
* @returns {object} - An {object} of {ValidationSingleValues} or {ValidationMultipleValues} (depending on the specific input)
*/
const buildValues = working => {
const values = {};

working.map(value => {
// If there isn't an existing key for the input plugin, make one
if (!values.hasOwnProperty(value.plugin)) {
values[value.plugin] = {};
}

// If there are multiple entries, build an object
if (typeof value.index !== 'undefined') {
// If the index doesn't exist, make it
if (!values[value.plugin].hasOwnProperty(value.index)) {
values[value.plugin][value.index] = {};
}

// Set the value of the input to the value in the index
values[value.plugin][value.index][value.input] = value.value;
}
else {
// Set the value of the input to the value
values[value.plugin][value.input] = value.value;
}
});

return values;
};

/*
* Validates the Raw Input
*
* @param {FormInputValues} raw - Raw form input
* @param {InputType} type - Individual input type
*
@returns {true|FormInputValues} - Returns `true` if there are no validation issues, {FormInputValues} with error messages as values if there are
*/
const validate = (raw, type) => {
let working = split(raw);
const allSettings = buildSettings(type);
const values = buildValues(working);

working = working.map(value => {
const result = value;
const plugin = util.singleItem('id', value.plugin, type.attributes);
const input = {};
const settings = {};

// Build Input
input.target = {};
input.target.name = result.input;
input.target.value = result.value;
input.all = values[result.plugin];

if (typeof value.index !== 'undefined') {
input.all = input.all[result.index];
}

// Build Settings
settings.target = allSettings[value.plugin][result.input];
settings.all = allSettings[value.plugin];

// Run the validation function
result.validation = plugin.validation[plugin.inputs[result.input].validation.function](input, settings);

return result;
});

return join(working);
};


module.exports = validate;
module.exports.split = split;
module.exports.join = join;
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test": "npm run nyc",
"lint": "eslint ./lib/**/*.js ./lib/*.js ./index.js",
"ava": "ava | tap-diff",
"ava:watch": "ava --watch | tap-diff",
"nyc": "nyc --all npm run ava",
"start": "node ./",
"coverage": "nyc report --reporter=text-lcov | coveralls"
Expand All @@ -28,26 +29,25 @@
"license": "Apache-2.0",
"dependencies": {
"browserify": "^13.0.1",
"config": "^1.19.0",
"config": "^1.20.4",
"deepmerge": "^0.2.10",
"isomorphic-ensure": "^1.0.1",
"js-yaml": "^3.5.3",
"lodash": "^4.6.1",
"lodash": "^4.13.1",
"node-dir": "^0.1.11",
"node-sass": "^3.4.2",
"nunjucks": "^2.4.0",
"plugabilly": "0.0.0",
"uuid": "^2.0.2"
},
"devDependencies": {
"ava": "^0.12.0",
"ava": "^0.15.1",
"coveralls": "^2.11.9",
"eslint": "^2",
"eslint-config-punchcard": "^1.0.0",
"input-plugin-datetime": "0.0.1",
"input-plugin-email": "0.0.4",
"input-plugin-text": "0.0.5",
"input-plugin-textarea": "0.0.1",
"input-plugin-datetime": "^0.0.1",
"input-plugin-email": "^0.1.0",
"input-plugin-text": "^0.0.5",
"input-plugin-textarea": "^0.0.1",
"nyc": "^6.0.0",
"open-exchange-rates": "^0.3.0",
"raw-loader": "^0.5.1",
Expand Down
3 changes: 2 additions & 1 deletion tests/content-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ test('merged with correct param', t => {
return types([testCT])
.then(result => {
const merged = result[0];
console.log(util.inspect(result, false, null));

// console.log(util.inspect(result, false, null));

t.is(result.length, 1, 'There is one result');

Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/content-types/bar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ attributes:
id: my-textarea
name: My Awesome Text Area
description: I am the Bar Content Type Config textarea description
- type: email
id: my-email
name: Email Address of Awesome
repeatable:
min: 1
max: 3
6 changes: 6 additions & 0 deletions tests/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import form from '../lib/form';
import util from 'util';
import fs from 'fs';

test('All Form Goodies', t => {
t.is(typeof form, 'function', 'Form exports a function');

t.is(typeof form.validate, 'function', 'Submodule `validate` exists and is a function');
});

test('Form Generation', t => {
return types.only('foo').then(result => {
return form(result);
Expand Down
6 changes: 3 additions & 3 deletions tests/input-plugin-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,19 +159,19 @@ test('Input Plugin Tests Requires Test and Plugin', t => {
if (plugin(false, inputPluginSingle) === 'you must include a test runner') {
validated = true;
}
t.ok(validated, 'A test runner must be parameter 1');
t.true(validated, 'A test runner must be parameter 1');

validated = false;
if (plugin(test, false) === 'you must include a plugin to test') {
validated = true;
}
t.ok(validated, 'The plugin must be parameter 2');
t.true(validated, 'The plugin must be parameter 2');

validated = false;
if (plugin(test, 'foo') === 'plugin must be an object') {
validated = true;
}
t.ok(validated, 'Plugin must be an object');
t.true(validated, 'Plugin must be an object');
});

plugin(test, inputPluginSingle);
Expand Down

0 comments on commit cc2051e

Please sign in to comment.