From a509e53a3e3216acb0e78a1c4abe35c7dade8914 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 22 Jun 2016 15:13:09 -0400 Subject: [PATCH] Liberate VFormat This will likely undergo some refactoring. * src/validate/VFormat.js: Added. * test/validate/VFormatTest.js: Added. --- src/validate/VFormat.js | 136 +++++++++++++++++++++++++++++++++++ test/validate/VFormatTest.js | 115 +++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 src/validate/VFormat.js create mode 100644 test/validate/VFormatTest.js diff --git a/src/validate/VFormat.js b/src/validate/VFormat.js new file mode 100644 index 0000000..1b8842f --- /dev/null +++ b/src/validate/VFormat.js @@ -0,0 +1,136 @@ +/** + * Validator-formatter + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +var Class = require( 'easejs' ).Class; + + +/** + * Data validation and formatting + */ +module.exports = Class( 'VFormat', +{ + /** + * Pattern definition + * @type {Array.} + */ + 'private _dfn': [], + + /** + * Data retrieval format + * @type {Array} + */ + 'private _retdfn': [], + + + /** + * Initialize with a pattern definition and return format definition + * + * The pattern definition should be an array of arrays with two elements: + * the pattern to match against and its replacement. The patterns must be + * regular expressions and the replacements may be either strings or + * functions. + * + * The return formatter is an optional single array consisting of a pattern + * and a replacement. + * + * @param {Array.} dfn pattern definition + * @param {Array=} retdfn return format definition + */ + __construct: function( dfn, retdfn ) + { + this._dfn = dfn; + this._retdfn = retdfn; + }, + + + /** + * Format the given data or fail if no match is found + * + * If the given data matches a pattern, it will be formatted with respect to + * the first matched pattern. Otherwise, an error will be thrown indicating + * a validation failure. + * + * @param {string} data data to parse + * + * @return {string} formatted string, if a match is found + */ + 'public parse': function( data ) + { + var dfn = this._dfn; + + // cast all data to a string + data = ''+( data ); + + var match; + for ( var i = 0, len = this._dfn.length; i < len; i += 2 ) + { + if ( match = dfn[ i ].test( data ) ) + { + var replace = dfn[ i + 1 ]; + + // minor optimization that may help on very large sets in + // poorly performing browsers; do not perform replacement if it + // it would result in an identical string + try + { + return ( ( replace === '$&' ) + ? data + : data.replace( dfn[ i ], dfn[ i + 1 ] ) + ); + } + // throwing an exception within the replacement function is + // equivalent to saying "no match" and allows for more + // complicated logic that would otherwise overcomplicate a + // regex; fall through to continue to the next matcher + catch ( e ) {} + } + } + + throw Error( 'No match for data: ' + data ); + }, + + + /** + * Retrieve data that may require formatting for display + * + * Return formatting is optional. No formatting will be done if no pattern + * was given when the instance was constructed. + * + * To ensure consistency and correctness, *any data returned by this method + * must be reversible* --- that is, parse( retrieve( data ) ) should not + * throw an exception. + * + * @param {string} data data to format for display + * + * @return {string} data formatted for display + */ + 'public retrieve': function( data ) + { + if ( !( this._retdfn ) ) + { + return data; + } + + return ( ''+data ).replace( this._retdfn[ 0 ], this._retdfn[ 1 ] ); + } +} ); + diff --git a/test/validate/VFormatTest.js b/test/validate/VFormatTest.js new file mode 100644 index 0000000..160822e --- /dev/null +++ b/test/validate/VFormatTest.js @@ -0,0 +1,115 @@ +/** + * Test validator-formatter + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var liza = require( '../../' ), + Sut = liza.validate.VFormat, + expect = require( 'chai' ).expect, + assert = require( 'assert' ), + dfn = [ + /^kitten/, '$&s', + /^[a-z]+/, 'Foo', + /^[0-9]([0-9]+)/, '$1', + ]; + + +describe( 'VFormat', function() +{ + describe( '#parse', function() + { + it( 'formats string according to given definition', function() + { + var sut = createSut( dfn ); + + // this first test also ensures that the very first match + // in dfn takes precedence + [ + [ 'kitten', 'kittens' ], + [ 'abcd', 'Foo' ], + [ '0123', '123' ], + ].forEach( function( test ) + { + assert.equal( sut.parse( test[ 0 ] ), test[ 1 ] ); + } ); + } ); + + + // validation error + it( 'throws an exception if no match is found', function() + { + assert.throws( function() + { + // cannot possibly match anything + createSut( [] ).parse( 'foo' ); + }, Error ); + } ); + + + /** + * To support complex logic that may be difficult to express + * (or not worth expressing due to verbosity required with JS's + * regex impl.), we permit throwing an exception in a + * replacement function to result in the equivalent of "no + * match". + */ + it( 'yields no match given exception during replacement', function() + { + var val = 'bar', + sut = createSut( [ + /^fo/, function() { throw Error( 'ignore me' ); }, + /^foo/, val, + ] ); + + assert.equal( val, sut.parse( 'foo' ), + "Should ignore matches that throw exceptions" + ); + } ); + } ); + + + describe( '#retrieve', function() + { + it( 'retrieval does not format data by default', function() + { + var str = 'foo'; + assert.equal( createSut( [] ).retrieve( str ), str ); + } ); + + + it( 'formats return data according to given definition', function() + { + // the dfn is technically not required, but for the sake + // of a "proper" demonstration, it will be included + var dfn = [ [ /-/, '' ] ], + retdfn = [ /[a-z]/g, '$&-' ]; + + assert.equal( + createSut( dfn, retdfn ).retrieve( 'foo' ), + 'f-o-o-' + ); + } ); + } ); +} ); + + +function createSut( dfn, retdfn ) +{ + return Sut( dfn, retdfn ); +}