diff --git a/README.md b/README.md index 7d4c61f..8132638 100644 --- a/README.md +++ b/README.md @@ -152,12 +152,6 @@ date.1 // composite typecaster Formal::typecast('composite', [$typecaster1, $typecaster2, ..]); -// fields typecaster -Formal::typecast('fields', ['field1' => $typecaster1, 'field2' => $typecaster2, ..]); - -// default value typecaster -Formal::typecast('default', $defaultValue); - // boolean typecaster Formal::typecast('bool'); @@ -201,83 +195,77 @@ Formal::typecast(function($val, $key, $formalInstance) { // optional validator, only if value is not missing Formal::validate('optional', $requiredValidator); -// required validator, fails if value is missing +// required validator, fails if value is missing or null Formal::validate('required'); -// fields validator -Formal::validate('fields', ['field1' => $validator1, 'field2' => $validator2, ..], $errMsg = null); - // is numeric validator -Formal::validate('numeric', $args = null, $errMsg = null); +Formal::validate('numeric'); // is object validator -Formal::validate('object', $args = null, $errMsg = null); +Formal::validate('object'); // is array validator -Formal::validate('array', $args = null, $errMsg = null); - -// is file validator (PHP only) -Formal::validate('file', $args = null, $errMsg = null); +Formal::validate('array'; -// mime-type validator (PHP only) -Formal::validate('mimetype', ['type1', 'type2', ..], $errMsg = null); +// is file validator +Formal::validate('file'); // is empty validator -Formal::validate('empty', $args = null, $errMsg = null); +Formal::validate('empty'); // max items validator -Formal::validate('maxcount', $maxCount, $errMsg = null); +Formal::validate('maxitems', $maxCount); // min items validator -Formal::validate('mincount', $minCount, $errMsg = null); +Formal::validate('minitems', $minCount); // max chars validator -Formal::validate('maxlen', $maxLen, $errMsg = null); +Formal::validate('maxchars', $maxLen); // min chars validator -Formal::validate('minlen', $minLen, $errMsg = null); +Formal::validate('minchars', $minLen); -// max file size validator (PHP only) -Formal::validate('maxsize', $maxSize, $errMsg = null); +// max file size validator +Formal::validate('maxsize', $maxSize); -// min file size validator (PHP only) -Formal::validate('minsize', $minSize, $errMsg = null); +// min file size validator +Formal::validate('minsize', $minSize); // equals validator -Formal::validate('eq', $otherValueOrField, $errMsg = null); +Formal::validate('eq', $otherValueOrField); // not equals validator -Formal::validate('neq', $otherValueOrField, $errMsg = null); +Formal::validate('neq', $otherValueOrField); // greater than validator -Formal::validate('gt', $otherValueOrField, $errMsg = null); +Formal::validate('gt', $otherValueOrField); // greater than or equal validator -Formal::validate('gte', $otherValueOrField, $errMsg = null); +Formal::validate('gte', $otherValueOrField); // less than validator -Formal::validate('lt', $otherValueOrField, $errMsg = null); +Formal::validate('lt', $otherValueOrField); // less than or equal validator -Formal::validate('lte', $otherValueOrField, $errMsg = null); +Formal::validate('lte', $otherValueOrField); // between values (included) validator -Formal::validate('between', [$minValueOrField, $maxValueOrField], $errMsg = null); +Formal::validate('between', [$minValueOrField, $maxValueOrField]); // in array of values validator -Formal::validate('in', [$val1, $val2, ..], $errMsg = null); +Formal::validate('in', [$val1, $val2, ..]); // not in array of values validator -Formal::validate('not_in', [$val1, $val2, ..], $errMsg = null); +Formal::validate('not_in', [$val1, $val2, ..]); // match pattern validator -Formal::validate('match', $pattern, $errMsg = null); +Formal::validate('match', $pattern); // match valid email pattern validator -Formal::validate('email', $args = null, $errMsg = null); +Formal::validate('email'); // match valid url pattern validator -Formal::validate('url', $args = null, $errMsg = null); +Formal::validate('url'); // not validator $validator->_not_(); diff --git a/src/js/Formal.js b/src/js/Formal.js index 8f171a6..a9b3b6d 100644 --- a/src/js/Formal.js +++ b/src/js/Formal.js @@ -23,7 +23,10 @@ var HAS = Object.prototype.hasOwnProperty, toString = Object.prototype.toString, ESC_RE = /[.*+?^${}()|[\]\\]/g, EMAIL_RE = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, - URL_RE = new RegExp('^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$','i'); + URL_RE = new RegExp('^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$','i'), + isNode = ("undefined" !== typeof global) && ("[object global]" === toString.call(global)), + isBrowser = ("undefined" !== typeof window) && ("[object Window]" === toString.call(window)) +; function is_numeric(x) { @@ -41,6 +44,38 @@ function is_object(x) { return ('[object Object]' === toString.call(x)) && ('function' === typeof x.constructor) && ('Object' === x.constructor.name); } +async function is_file(x) +{ + if (isNode) + { + return await new Promise(function(resolve) { + require('fs').lstat(String(x), function(err, stats) { + resolve(err || !stats ? false : stats.isFile()); + }); + }); + } + else if (isBrowser) + { + return ('File' in window) && (x instanceof File); + } + return false; +} +async function filesize(x) +{ + if (isNode) + { + return await new Promise(function(resolve) { + require('fs').lstat(String(x), function(err, stats) { + resolve(err || !stats ? false : stats.size); + }); + }); + } + else if (isBrowser) + { + return ('File' in window) && (x instanceof File) ? x.size : false; + } + return false; +} function is_callable(x) { return 'function' === typeof(x); @@ -286,16 +321,15 @@ class FormalType } async t_composite(v, k, m) { - var types = this.inp, l = types.length, i = 0; - while (i < l) + var types = array(this.inp), l = types.length, i = 0; + for (i=0; i= this.inp; if (!valid) throw new FormalException(!empty(this.msg) ? this.msg.replace('{key}', k).replace('{args}', this.inp) : "\""+k+"\" must have at least "+this.inp+" items!"); return valid; } - v_maxlen(v, k, m, missingValue) { + v_maxchars(v, k, m, missingValue) { var valid = v.length <= this.inp; if (!valid) throw new FormalException(!empty(this.msg) ? this.msg.replace('{key}', k).replace('{args}', this.inp) : "\""+k+"\" must have at most "+this.inp+" characters!"); return valid; } - v_minlen(v, k, m, missingValue) { + v_minchars(v, k, m, missingValue) { var valid = v.length >= this.inp; if (!valid) throw new FormalException(!empty(this.msg) ? this.msg.replace('{key}', k).replace('{args}', this.inp) : "\""+k+"\" must have at least "+this.inp+" characters!"); - return $valid; + return valid; + } + + async v_maxsize(v, k, m, missingValue) { + var fs = false, valid = false; + fs = await filesize(String(v)); + valid = false === fs ? false : fs <= this.inp; + if (!valid) throw new FormalException(!empty(this.msg) ? this.msg.replace('{key}', k).replace('{args}', this.inp) : "\""+k+"\" must have at most "+this.inp+" bytes!"); + return valid; + } + + async v_minsize(v, k, m, missingValue) { + var fs = false, valid = false; + fs = await filesize(String(v)); + valid = false === fs ? false : fs >= this.inp; + if (!valid) throw new FormalException(!empty(this.msg) ? this.msg.replace('{key}', k).replace('{args}', this.inp) : "\""+k+"\" must have at least "+this.inp+" bytes!"); + return valid; } v_eq(v, k, m, missingValue) { @@ -671,7 +727,7 @@ class FormalValidator } valid = -1 === array(val).indexOf(v); if (!valid) throw new FormalException(!empty(this.msg) ? this.msg.replace('{key}', k).replace('{args}', valm) : "\""+k+"\" must not be one of "+valm+"!"); - return $valid; + return valid; } v_match(v, k, m, missingValue) { @@ -901,7 +957,7 @@ class Formal if (is_array(k)) { k.forEach(function(kk) { - o[kk] = clone(defaults); + o[kk] = clone(defaults); // clone }); } else @@ -914,7 +970,7 @@ class Formal } async doMergeDefaults(data, defaults, WILDCARD = '*', SEPARATOR = '.') { - if (is_array(data) || is_object(data)) + if ((is_array(data) || is_object(data)) && (is_array(defaults) || is_object(defaults))) { var keys, key, def, k, kk, n, o, doMerge, i, ok; for (key in defaults) @@ -972,16 +1028,20 @@ class Formal } else if (is_null(data[key]) || (is_string(data[key]) && !data[key].trim().length)) { - data[key] = def; + data[key] = clone(def); // clone } } else { - data[key] = def; + data[key] = clone(def); // clone } } } } + else if (is_null(data[key]) || (is_string(data) && !data.trim().length)) + { + data = clone(defaults); // clone + } return data; } diff --git a/src/php/Formal.php b/src/php/Formal.php index 47f889e..7b64bfa 100644 --- a/src/php/Formal.php +++ b/src/php/Formal.php @@ -253,18 +253,15 @@ public function exec($v, $k = null, $m = null) public function t_composite($v, $k, $m) { - $types = $this->inp; - $l = count($types); - $i = 0; - while ($i < $l) + $types = (array)$this->inp; + for ($i=0,$l=count($types); $i<$l; ++$i) { $v = $types[$i]->exec($v, $k, $m); - ++$i; } return $v; } - public function t_fields($v, $k, $m) + /*public function t_fields($v, $k, $m) { if (!is_array($v)) return $v; $SEPARATOR = $m->option('SEPARATOR'); @@ -283,7 +280,7 @@ public function t_default($v, $k, $m) $v = $defaultValue; } return $v; - } + }*/ public function t_bool($v, $k, $m) { @@ -351,6 +348,11 @@ class FormalValidator public $inp = null; public $msg = null; + public static function strlen($s) + { + return function_exists('mb_strlen') ? mb_strlen((string)$s, 'UTF-8') : strlen((string)$s); + } + public static function _($validator, $args = null, $msg = null) { return new static($validator, $args, $msg); @@ -400,7 +402,7 @@ public function exec($v, $k = null, $m = null, $missingValue = false) $valid = true; if (is_callable($this->func)) { - $valid = call_user_func($this->func, $v, $k, $m, $missingValue); + $valid = (bool)call_user_func($this->func, $v, $k, $m, $missingValue); } return $valid; } @@ -463,7 +465,7 @@ public function v_required($v, $k, $m, $missingValue) return $valid; } - public function v_fields($v, $k, $m, $missingValue) + /*public function v_fields($v, $k, $m, $missingValue) { if (!is_array($v)) return false; $SEPARATOR = $m->option('SEPARATOR'); @@ -481,7 +483,7 @@ public function v_fields($v, $k, $m, $missingValue) } } return true; - } + }*/ public function v_numeric($v, $k, $m, $missingValue) { @@ -506,63 +508,68 @@ public function v_array($v, $k, $m, $missingValue) public function v_file($v, $k, $m, $missingValue) { - $valid = is_file($v); + $valid = is_file((string)$v); if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, ''), $this->msg) : "\"$k\" must be a file!"); return $valid; } - public function v_mimetype($v, $k, $m, $missingValue) - { - $valid = in_array(mime_content_type($v), (array)$this->inp); - if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, implode(',', (array)$this->inp)), $this->msg) : "\"$k\" mime-type must be one of [".implode(',', (array)$this->inp)."]!"); - return $valid; - } - public function v_empty($v, $k, $m, $missingValue) { - $valid = $missingValue || is_null($v) || (is_array($v) ? !count($v) : (is_string($v) && strlen(trim($v)) && is_file($v) ? !filesize($v) : !strlen(trim((string)$v)))); + $valid = $missingValue || is_null($v) || (is_array($v) ? !count($v) : !strlen(trim((string)$v))); if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, ''), $this->msg) : "\"$k\" must be empty!"); return $valid; } - public function v_maxcount($v, $k, $m, $missingValue) + public function v_maxitems($v, $k, $m, $missingValue) { $valid = count($v) <= $this->inp; if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, $this->inp), $this->msg) : "\"$k\" must have at most {$this->inp} items!"); return $valid; } - public function v_mincount($v, $k, $m, $missingValue) + public function v_minitems($v, $k, $m, $missingValue) { $valid = count($v) >= $this->inp; if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, $this->inp), $this->msg) : "\"$k\" must have at least {$this->inp} items!"); return $valid; } - public function v_maxlen($v, $k, $m, $missingValue) + public function v_maxchars($v, $k, $m, $missingValue) { - $valid = strlen($v) <= $this->inp; + $valid = static::strlen($v) <= $this->inp; if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, $this->inp), $this->msg) : "\"$k\" must have at most {$this->inp} characters!"); return $valid; } - public function v_minlen($v, $k, $m, $missingValue) + public function v_minchars($v, $k, $m, $missingValue) { - $valid = strlen($v) >= $this->inp; + $valid = static::strlen($v) >= $this->inp; if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, $this->inp), $this->msg) : "\"$k\" must have at least {$this->inp} characters!"); return $valid; } public function v_maxsize($v, $k, $m, $missingValue) { - $valid = filesize($v) <= $this->inp; + $fs = false; + try { + $fs = @filesize((string)$v); + } catch (Exception $e) { + $fs = false; + } + $valid = false === $fs ? false : $fs <= $this->inp; if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, $this->inp), $this->msg) : "\"$k\" must have at most {$this->inp} bytes!"); return $valid; } public function v_minsize($v, $k, $m, $missingValue) { - $valid = filesize($v) >= $this->inp; + $fs = false; + try { + $fs = @filesize((string)$v); + } catch (Exception $e) { + $fs = false; + } + $valid = false === $fs ? false : $fs >= $this->inp; if (!$valid) throw new FormalException(!empty($this->msg) ? str_replace(array('{key}', '{args}'), array($k, $this->inp), $this->msg) : "\"$k\" must have at least {$this->inp} bytes!"); return $valid; } @@ -944,7 +951,7 @@ private function doMergeKeys($keys, $def) { foreach ($k as $kk) { - $o[$kk] = $defaults; + $o[$kk] = $defaults; // clone } } else @@ -958,7 +965,7 @@ private function doMergeKeys($keys, $def) private function doMergeDefaults($data, $defaults, $WILDCARD = '*', $SEPARATOR = '.') { - if (is_array($data)) + if (is_array($data) && is_array($defaults)) { foreach ($defaults as $key => $def) { @@ -1013,16 +1020,20 @@ private function doMergeDefaults($data, $defaults, $WILDCARD = '*', $SEPARATOR = } elseif (is_null($data[$key]) || (is_string($data[$key]) && !strlen(trim($data[$key])))) { - $data[$key] = $def; + $data[$key] = $def; // clone } } else { - $data[$key] = $def; + $data[$key] = $def; // clone } } } } + elseif (is_null($data) || (is_string($data) && !strlen(trim($data)))) + { + $data = $defaults; // clone + } return $data; } diff --git a/src/python/Formal.py b/src/python/Formal.py new file mode 100644 index 0000000..cb892c1 --- /dev/null +++ b/src/python/Formal.py @@ -0,0 +1,852 @@ +## +# Formal +# validate nested (form) data with built-in and custom rules for PHP, JavaScript, Python +# +# @version 1.0.0 +# https://github.com/foo123/Formal +# +## + +import math, re, functools, os.path + +EMAIL_RE = re.compile(r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$') + +URL_RE = re.compile('^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$', re.I) + +def is_numeric(x): + return not math.isnan(int(x)) + +def is_string(x): + return isinstance(x, str) + +def is_array(x): + return isinstance(x, (list, tuple)) + +def is_object(x): + return isinstance(x, dict) + +def is_file(x): + return os.path.isfile(str(x)) + +def is_callable(x): + return callable(x) + +def method_exists(o, m): + return hasattr(o, m) and callable(getattr(o, m)) + +def array(a): + return a if isinstance(a, list) else (list(a) if isinstance(a, tuple) else [a]) + +def array_keys(o): + if isinstance(o, (list, tuple)): return list(map(str, range(0, len(o)))) + if isinstance(o, dict): return list(o.keys()) + return [] + +def array_values(o): + if isinstance(o, list): return o[:] + if isinstance(o, tuple): return list(o) + if isinstance(o, dict): return list(o.values()) + return [] + +def array_key_exists(k, o): + return int(k) < len(o) if isinstance(o, (list, tuple)) else (str(k) in o if isinstance(o, dict) else False) + +def key_value(k, o): + return o[int(k)] if isinstance(o, (list, tuple)) else (o[str(k)] if isinstance(o, dict) else None) + +def set_key_value(k, v, o): + if isinstance(o, (list, tuple)): + k = int(k) + l = len(o) + if k >= l: o.extend([None for i in range(k+1-l)]) + o[k] = v + elif isinstance(o, dict): + o[str(k)] = v + return o + +def empty(x): + return (x is None) or (x is False) or (0 == x) or ('' == x) or (isinstance(x, (dict, list, tuple)) and not len(x)) + +def is_null(x): + return x is None + +def esc_re(s): + return re.escape(str(s)) + +def by_length_desc(a, b): + return len(b)-len(a) + +def get_alternate_pattern(alts): + alts.sort(key=functools.cmp_to_key(by_length_desc)) + return '|'.join(map(esc_re, alts)) + +def clone(o): + if isinstance(o, (list, tuple)): + return [clone(x) for x in o] + + elif isinstance(o, dict): + oo = {} + for k in o: oo[k] = clone(o[k]) + return oo + + else: + return o + +class FormalException(Exception): + pass + +class FormalField: + def __init__(self, field): + self.field = str(field) + +class FormalDateTime: + def __init__(self, format, locale = None): + if not locale: locale = { + 'day_short' : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + 'day' : ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], + 'month_short' : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + 'month' : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + 'meridian' : {'am' : 'am', 'pm' : 'pm', 'AM' : 'AM', 'PM' : 'PM'}, + 'timezone_short' : ['UTC'], + 'timezone' : ['UTC'], + 'ordinal' : {'ord' : {'1' : 'st', '2' : 'nd', '3' : 'rd'}, 'nth' : 'th'}, + } + + # (php) date formats + # http://php.net/manual/en/function.date.php + D = { + # Day -- + # Day of month w/leading 0; 01..31 + 'd': '(31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|09|08|07|06|05|04|03|02|01)' + # Shorthand day name; Mon...Sun + ,'D': '(' + get_alternate_pattern(locale['day_short']) + ')' + # Day of month; 1..31 + ,'j': '(31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1)' + # Full day name; Monday...Sunday + ,'l': '(' + get_alternate_pattern(locale['day']) + ')' + # ISO-8601 day of week; 1[Mon]..7[Sun] + ,'N': '([1-7])' + # Ordinal suffix for day of month; st, nd, rd, th + ,'S': '' # added below + # Day of week; 0[Sun]..6[Sat] + ,'w': '([0-6])' + # Day of year; 0..365 + ,'z': '([1-3]?[0-9]{1,2})' + + # Week -- + # ISO-8601 week number + ,'W': '([0-5]?[0-9])' + + # Month -- + # Full month name; January...December + ,'F': '(' + get_alternate_pattern(locale['month']) + ')' + # Month w/leading 0; 01...12 + ,'m': '(12|11|10|09|08|07|06|05|04|03|02|01)' + # Shorthand month name; Jan...Dec + ,'M': '(' + get_alternate_pattern(locale['month_short']) + ')' + # Month; 1...12 + ,'n': '(12|11|10|9|8|7|6|5|4|3|2|1)' + # Days in month; 28...31 + ,'t': '(31|30|29|28)' + + # Year -- + # Is leap year?; 0 or 1 + ,'L': '([01])' + # ISO-8601 year + ,'o': '(\\d{2,4})' + # Full year; e.g. 1980...2010 + ,'Y': '([12][0-9]{3})' + # Last two digits of year; 00...99 + ,'y': '([0-9]{2})' + + # Time -- + # am or pm + ,'a': '(' + get_alternate_pattern([ + locale['meridian']['am'], + locale['meridian']['pm'] + ]) + ')' + # AM or PM + ,'A': '(' + get_alternate_pattern([ + locale['meridian']['AM'], + locale['meridian']['PM'] + ]) + ')' + # Swatch Internet time; 000..999 + ,'B': '([0-9]{3})' + # 12-Hours; 1..12 + ,'g': '(12|11|10|9|8|7|6|5|4|3|2|1)' + # 24-Hours; 0..23 + ,'G': '(23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1|0)' + # 12-Hours w/leading 0; 01..12 + ,'h': '(12|11|10|09|08|07|06|05|04|03|02|01)' + # 24-Hours w/leading 0; 00..23 + ,'H': '(23|22|21|20|19|18|17|16|15|14|13|12|11|10|09|08|07|06|05|04|03|02|01|00)' + # Minutes w/leading 0; 00..59 + ,'i': '([0-5][0-9])' + # Seconds w/leading 0; 00..59 + ,'s': '([0-5][0-9])' + # Microseconds; 000000-999000 + ,'u': '([0-9]{6})' + + # Timezone -- + # Timezone identifier; e.g. Atlantic/Azores, ... + ,'e': '(' + get_alternate_pattern(locale['timezone']) + ')' + # DST observed?; 0 or 1 + ,'I': '([01])' + # Difference to GMT in hour format; e.g. +0200 + ,'O': '([+-][0-9]{4})' + # Difference to GMT w/colon; e.g. +02:00 + ,'P': '([+-][0-9]{2}:[0-9]{2})' + # Timezone abbreviation; e.g. EST, MDT, ... + ,'T': '(' + get_alternate_pattern(locale['timezone_short']) + ')' + # Timezone offset in seconds (-43200...50400) + ,'Z': '(-?[0-9]{5})' + + # Full Date/Time -- + # Seconds since UNIX epoch + ,'U': '([0-9]{1,8})' + # ISO-8601 date. Y-m-d\\TH:i:sP + ,'c': '' # added below + # RFC 2822 D, d M Y H:i:s O + ,'r': '' # added below + } + # Ordinal suffix for day of month; st, nd, rd, th + lords = array_values(locale['ordinal']['ord']) + lords.append(locale['ordinal']['nth']) + D['S'] = '(' + get_alternate_pattern(lords) + ')' + # ISO-8601 date. Y-m-d\\TH:i:sP + D['c'] = D['Y']+'-'+D['m']+'-'+D['d']+'\\\\'+D['T']+D['H']+':'+D['i']+':'+D['s']+D['P'] + # RFC 2822 D, d M Y H:i:s O + D['r'] = D['D']+',\\s'+D['d']+'\\s'+D['M']+'\\s'+D['Y']+'\\s'+D['H']+':'+D['i']+':'+D['s']+'\\s'+D['O'] + + format = str(format); + rex = '' + for i in range(len(format)): + f = format[i] + rex += D[ f ] if f in D else esc_re(f) + + + self.format = format + self.pattern = re.compile('^' + rex + '$') + + def getFormat(self): + return self.format + + def getPattern(self): + return self.pattern + + def __str__(self): + return str(self.pattern.pattern) + + +class FormalType: + def __init__(self, type, args = None): + if isinstance(type, FormalType): + self.func = type.func + self.inp = type.inp + else: + method = 't_' + str(type).strip().lower() if is_string(type) else None + self.func = method if method and method_exists(self, method) else (type if is_callable(type) else None) + self.inp = args + + def exec(self, v, k = None, m = None): + if is_string(self.func): + v = getattr(self, self.func)(v, k, m) + elif is_callable(self.func): + v = self.func(v, k, m) + return v + + def t_composite(self, v, k, m): + types = array(self.inp) + for i in range(len(types)): + v = types[i].exec(v, k, m) + return v + + #def t_fields(self, v, k, m): + # SEPARATOR = m.option('SEPARATOR') + # if is_object(v): + # for field in self.inp: + # type = self.inp[field] + # v[field] = type.exec(v[field] if field in v else None, field if empty(k) else k+SEPARATOR+field, m) + # elif is_array(v): + # for field in self.inp: + # type = self.inp[field] + # v[int(field)] = type.exec(v[int(field)] if field in array_keys(v) else None, field if empty(k) else k+SEPARATOR+field, m) + # return v + # + #def t_default(self, v, k, m): + # defaultValue = self.inp + # if is_null(v) or (is_string(v) and not len(v.strip())): + # v = defaultValue + # return v + + def t_bool(self, v, k, m): + # handle string representation of booleans as well + if is_string(v) and len(v): + vs = v.lower() + return 'true' == vs or 'on' == vs or '1' == vs + return bool(v) + + def t_int(self, v, k, m): + return int(v) + + def t_float(self, v, k, m): + return float(v) + + def t_str(self, v, k, m): + return str(v) + + def t_min(self, v, k, m): + min = self.inp + return min if v < min else v + + def t_max(self, v, k, m): + max = self.inp + return max if v > max else v + + def t_clamp(self, v, k, m): + min = self.inp[0] + max = self.inp[1] + return min if v < min else (max if v > max else v) + + def t_trim(self, v, k, m): + return str(v).strip() + + def t_lower(self, v, k, m): + return str(v).lower() + + def t_upper(self, v, k, m): + return str(v).upper() + +class FormalValidator: + def __init__(self, validator, args = None, msg = None): + if isinstance(validator, FormalValidator): + self.func = validator.func + self.inp = validator.inp + self.msg = validator.msg if empty(msg) else msg + else: + method = 'v_' + str(validator).strip().lower() if is_string(validator) else None + self.func = method if method and method_exists(self, method) else (validator if is_callable(validator) else None) + self.inp = args + self.msg = msg + + def _and_(self, validator): + return FormalValidator('and', (self, validator)) + + def _or_(self, validator): + return FormalValidator('or', (self, validator)) + + def _not_(self): + return FormalValidator('not', self) + + def exec(self, v, k = None, m = None, missingValue = False): + valid = True + if is_string(self.func): + valid = bool(getattr(self, self.func)(v, k, m, missingValue)) + elif is_callable(self.func): + valid = bool(self.func(v, k, m, missingValue)) + return valid + + def v_and(self, v, k, m, missingValue): + valid = self.inp[0].exec(v, k, m, missingValue) and self.inp[1].exec(v, k, m, missingValue) + return valid + + def v_or(self, v, k, m, missingValue): + msg1 = None + msg2 = None + valid1 = False + valid2 = False + try: + valid1 = self.inp[0].exec(v, k, m, missingValue) + except FormalException as e: + valid1 = False + msg1 = str(e) + + if not valid1: + try: + valid2 = self.inp[1].exec(v, k, m, missingValue) + except FormalException as e: + valid2 = False + msg2 = str(e) + + valid = valid1 or valid2 + if not valid and (not empty(msg1) or not empty(msg2)): raise FormalException(msg2 if empty(msg1) else msg1) + return valid + + def v_not(self, v, k, m, missingValue): + try: + valid = not self.inp.exec(v, k, m, missingValue) + except FormalException as e: + valid = True + return valid + + def v_optional(self, v, k, m, missingValue): + valid = True + if not missingValue: + valid = self.inp.exec(v, k, m, False) + return valid + + def v_required(self, v, k, m, missingValue): + valid = not missingValue and not is_null(v) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" is required!") + return valid + + #def v_fields(self, v, k, m, missingValue): + # if not is_object(v) and not is_array(v): return False + # SEPARATOR = m.option('SEPARATOR') + # for field in self.inp: + # validator = self.inp[field] + # if is_object(v): + # if not field in v: + # if not validator.exec(None, field if empty(k) else k+SEPARATOR+field, m, True): + # return False + # else: + # if not validator.exec(v[field], field if empty(k) else k+SEPARATOR+field, m, missingValue) + # return False + # elif is_array(v): + # if not field in array_keys(v): + # if not validator.exec(None, field if empty(k) else k+SEPARATOR+field, m, True): + # return False + # else: + # if not validator.exec(v[int(field)], field if empty(k) else k+SEPARATOR+field, m, missingValue) + # return False + # return True + + def v_numeric(self, v, k, m, missingValue): + valid = is_numeric(v) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" must be numeric value!") + return valid + + def v_object(self, v, k, m, missingValue): + valid = is_object(v) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" must be an object!") + return valid + + def v_array(self, v, k, m, missingValue): + valid = is_array(v) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" must be an array!") + return valid + + def v_file(self, v, k, m, missingValue): + valid = is_file(str(v)) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" must be a file!") + return valid + + def v_empty(self, v, k, m, missingValue): + valid = missingValue or is_null(v) or (not len(v) if is_array(v) or is_object(v) else not len(str(v).strip())) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" must be empty!") + return valid + + def v_maxitems(self, v, k, m, missingValue): + valid = len(v) <= self.inp + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(self.inp)) if not empty(self.msg) else "\""+k+"\" must have at most "+str(self.inp)+" items!") + return valid + + def v_minitems(self, v, k, m, missingValue): + valid = len(v) >= self.inp + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(self.inp)) if not empty(self.msg) else "\""+k+"\" must have at least "+str(self.inp)+" items!") + return valid + + def v_maxchars(self, v, k, m, missingValue): + valid = len(v) <= self.inp + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(self.inp)) if not empty(self.msg) else "\""+k+"\" must have at most "+str(self.inp)+" characters!") + return valid + + def v_minchars(self, v, k, m, missingValue): + valid = len(v) >= self.inp + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(self.inp)) if not empty(self.msg) else "\""+k+"\" must have at least "+str(self.inp)+" characters!") + return valid + + def v_maxsize(self, v, k, m, missingValue): + fs = False + try: + fs = os.path.getsize(str(v)) + except OSError: + fs = False + valid = False if fs is False else (fs <= self.inp) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(self.inp)) if not empty(self.msg) else "\""+k+"\" must have at most "+str(self.inp)+" bytes!") + return valid + + def v_minsize(self, v, k, m, missingValue): + fs = False + try: + fs = os.path.getsize(str(v)) + except OSError: + fs = False + valid = False if fs is False else (fs >= self.inp) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(self.inp)) if not empty(self.msg) else "\""+k+"\" must have at least "+str(self.inp)+" bytes!") + return valid + + def v_eq(self, v, k, m, missingValue): + val = self.inp + valm = val + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + valid = val == v + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must be equal to "+str(valm)+"!") + return valid + + def v_neq(self, v, k, m, missingValue): + val = self.inp + valm = val + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + valid = val != v + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must not be equal to "+str(valm)+"!") + return valid + + def v_gt(self, v, k, m, missingValue): + val = self.inp + valm = val + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + valid = v > val + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must be greater than "+str(valm)+"!") + return valid + + def v_gte(self, v, k, m, missingValue): + val = self.inp + valm = val + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + valid = v >= val + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must be greater than or equal to "+str(valm)+"!") + return valid + + def v_lt(self, v, k, m, missingValue): + val = self.inp + valm = val + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + valid = v < val + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must be less than "+str(valm)+"!") + return valid + + def v_lte(self, v, k, m, missingValue): + val = self.inp + valm = val + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + valid = v <= val + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must be less than or equal to "+str(valm)+"}!") + return valid + + def v_between(self, v, k, m, missingValue): + min = self.inp[0] + max = self.inp[1] + minm = min + maxm = max + if isinstance(min, FormalField): + minm = min.field if not empty(self.msg) else '"' + min.field + '"' + min = m.get(min.field) + if isinstance(max, FormalField): + maxm = max.field if not empty(self.msg) else '"' + max.field + '"' + max = m.get(max.field) + valid = (min <= v) and (v <= max) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', ','.join([str(minm), str(maxm)])) if not empty(self.msg) else "\""+k+"\" must be between "+str(minm)+" and "+str(maxm)+"!") + return valid + + def v_in(self, v, k, m, missingValue): + val = self.inp + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + else: + valm = ','.join(map(str, array(val))) if not empty(self.msg) else '[' + ','.join(map(str, array(val))) + ']' + valid = v in array(val) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must be one of "+str(valm)+"!") + return valid + + def v_not_in(self, v, k, m, missingValue): + val = self.inp + if isinstance(val, FormalField): + valm = val.field if not empty(self.msg) else '"' + val.field + '"' + val = m.get(val.field) + else: + valm = ','.join(map(str, array(val))) if not empty(self.msg) else '[' + ','.join(map(str, array(val))) + ']' + valid = v not in array(val) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', str(valm)) if not empty(self.msg) else "\""+k+"\" must not be one of "+str(valm)+"!") + return valid + + def v_match(self, v, k, m, missingValue): + rex = self.inp.getPattern() if isinstance(self.inp, FormalDateTime) else self.inp + valid = bool(re.match(rex, str(v))) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', self.inp.getFormat() if isinstance(self.inp, FormalDateTime) else str(self.inp)) if not empty(self.msg) else "\""+k+"\" must match " + (self.inp.getFormat() if isinstance(self.inp, FormalDateTime) else 'the') + " pattern!") + return valid + + def v_email(self, v, k, m, missingValue): + valid = bool(re.match(EMAIL_RE, str(v))) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" must be valid email pattern!") + return valid + + def v_url(self, v, k, m, missingValue): + valid = bool(re.match(URL_RE, str(v))) + if not valid: raise FormalException(self.msg.replace('{key}', k).replace('{args}', '') if not empty(self.msg) else "\""+k+"\" must be valid url pattern!") + return valid + +class FormalError: + def __init__(self, msg = '', key = list()): + self.msg = str(msg) + self.key = key + + def getMsg(self): + return self.msg + + def getKey(self): + return self.key + + def __str__(self): + return self.msg + +class Formal: + """ + Formal for Python, + https://github.com/foo123/Formal + """ + VERSION = "1.0.0" + + # export these + Exception = FormalException + Field = FormalField + DateTime = FormalDateTime + Type = FormalType + Validator = FormalValidator + Error = FormalError + + @staticmethod + def field(field): + return FormalField(field) + + @staticmethod + def datetime(format, locale = None): + return FormalDateTime(format, locale) + + @staticmethod + def typecast(type, args = None): + return FormalType(type, args) + + @staticmethod + def validate(validator, args = None, msg = None): + return FormalValidator(validator, args, msg) + + def __init__(self): + self.opts = {} + self.err = [] + self.data = None + self.option('WILDCARD', '*').option('SEPARATOR', '.').option('break_on_first_error', False).option('invalid_value_msg', 'Invalid Value in "{key}"!').option('missing_value_msg', 'Missing Value in "{key}"!').option('defaults', {}).option('typecasters', {}).option('validators', {}) + + def option(self, *args): + nargs = len(args) + if 1 == nargs: + key = str(args[0]) + return self.opts[key] if key in self.opts else None + elif 1 < nargs: + key = str(args[0]) + val = args[1] + self.opts[key] = val + return self + + def process(self, data): + WILDCARD = self.option('WILDCARD') + SEPARATOR = self.option('SEPARATOR') + self.data = None + self.err = [] + data = clone(data) + data = self.doMergeDefaults(data, self.option('defaults'), WILDCARD, SEPARATOR) + data = self.doTypecast(data, self.option('typecasters'), [], [], WILDCARD, SEPARATOR) + self.data = data + self.doValidate(data, self.option('validators'), [], [], WILDCARD, SEPARATOR) + self.data = None + return data + + def getErrors(self): + return self.err + + def get(self, field, _default = None, data = None): + if data is None: data = self.data + WILDCARD = self.option('WILDCARD') + SEPARATOR = self.option('SEPARATOR') + is_obj_arr = isinstance(data, (list, tuple, dict)) + result = None + if (is_string(field) or is_numeric(field)) and is_obj_arr: + stack = [(data, str(field))] + while len(stack): + o, key = stack.pop() + p = key.split(SEPARATOR) + i = 0 + l = len(p) + while i < l: + k = p[i] + i += 1 + if i < l: + if WILDCARD == k: + result = [] + k = SEPARATOR.join(p[i:]) + for kk in array_keys(o): + stack.append((o, key+SEPARATOR+k)) + break + elif array_key_exists(k, o): + o = key_value(k, o) + else: + return _default # key does not exist + else: + if WILDCARD == k: + result = array_values(o) + elif array_key_exists(k, o): + if isinstance(result, list): + result.append(key_value(k, o)) + else: + result = key_value(k, o) + return result + return _default + + def doMergeKeys(self, keys, _def): + defaults = _def + for k in reversed(keys): + o = {} + if is_array(k): + for kk in k: + o = set_key_value(kk, clone(defaults), o) + else: + o = set_key_value(k, clone(defaults), o) + defaults = o + return defaults + + def doMergeDefaults(self, data, defaults, WILDCARD = '*', SEPARATOR = '.'): + import json + if isinstance(data, (list, dict)) and isinstance(defaults, (list, dict)): + for key in array_keys(defaults): + _def = key_value(key, defaults) + kk = key.split(SEPARATOR) + n = len(kk) + if 1 < n: + o = data + keys = [] + doMerge = True + for i in range(n): + k = kk[i] + if WILDCARD == k: + ok = array_keys(o) + if not len(ok): + doMerge = False + break + keys.append(ok) + o = key_value(ok[0], o) + elif array_key_exists(k, o): + keys.append(k) + o = key_value(k, o) + elif i == n-1: + keys.append(k) + else: + doMerge = False + break + if doMerge: + data = self.doMergeDefaults(data, self.doMergeKeys(keys, _def), WILDCARD, SEPARATOR) + else: + if array_key_exists(key, data): + data_key = key_value(key, data) + if isinstance(data_key, (list, tuple, dict)) and isinstance(_def, (list, tuple, dict)): + data = set_key_value(key, self.doMergeDefaults(data_key, _def, WILDCARD, SEPARATOR), data) + elif is_null(data_key) or (is_string(data_key) and not len(data_key.strip())): + data = set_key_value(key, clone(_def), data) + else: + data = set_key_value(key, clone(_def), data) + elif is_null(data) or (is_string(data) and not len(data.strip())): + data = clone(defaults) + + return data + + def doTypecast(self, data, typecaster, key = list(), root = list(), WILDCARD = '*', SEPARATOR = '.'): + if isinstance(typecaster, FormalType): + n = len(key) + i = 0 + if i < n: + k = key[i] + i += 1 + if '' == k: + return data + elif WILDCARD == k: + if i < n: + rk = key[i:] + root = root + key[0:i-1] + for ok in array_keys(data): + data = set_key_value(ok, self.doTypecast(key_value(ok, data), typecaster, rk, root + [ok], WILDCARD, SEPARATOR), data) + else: + root = root + key[0:i-1] + for ok in array_keys(data): + data = self.doTypecast(data, typecaster, [ok], root, WILDCARD, SEPARATOR) + return data + elif array_key_exists(k, data): + rk = key[i:] + root = root + key[0:i] + data = set_key_value(k, self.doTypecast(key_value(k, data), typecaster, rk, root, WILDCARD, SEPARATOR), data) + else: + return data + else: + KEY = SEPARATOR.join(root + key) + data = typecaster.exec(data, KEY, self) + + elif isinstance(typecaster, (list, dict)): + for k in array_keys(typecaster): + data = self.doTypecast(data, key_value(k, typecaster), k.split(SEPARATOR) if empty(key) else key + k.split(SEPARATOR), root, WILDCARD, SEPARATOR) + + return data + + def doValidate(self, data, validator, key = list(), root = list(), WILDCARD = '*', SEPARATOR = '.'): + if self.option('break_on_first_error') and len(self.err): return + if isinstance(validator, FormalValidator): + n = len(key) + i = 0 + while i < n: + k = key[i] + i += 1 + if '' == k: + continue + elif WILDCARD == k: + if i < n: + rk = key[i:] + root = root + key[0:i-1] + for ok in array_keys(data): + self.doValidate(key_value(ok, data), validator, rk, root + [ok], WILDCARD, SEPARATOR) + else: + root = root + key[0:i-1] + for ok in array_keys(data): + self.doValidate(data, validator, [ok], root, WILDCARD, SEPARATOR) + return + elif array_key_exists(k, data): + data = key_value(k, data) + else: + KEY_ = root + key + KEY = SEPARATOR.join(KEY_) + err = None + try: + valid = validator.exec(None, KEY, self, True) + except FormalException as e: + valid = False + err = str(e) + if not valid: + self.err.append(FormalError(self.option('missing_value_msg').replace('{key}', KEY).replace('{args}', '') if empty(err) else err, KEY_)) + return + + KEY_ = root + key + KEY = SEPARATOR.join(KEY_) + err = None + try: + valid = validator.exec(data, KEY, self, False) + except FormalException as e: + valid = False + err = str(e) + if not valid: + self.err.append(FormalError(self.option('invalid_value_msg').replace('{key}', KEY).replace('{args}', '') if empty(err) else err, KEY_)) + + elif isinstance(validator, (list, dict)): + for k in array_keys(validator): + self.doValidate(data, key_value(k, validator), k.split(SEPARATOR) if empty(key) else key + k.split(SEPARATOR), root, WILDCARD, SEPARATOR) + + +__all__ = ['Formal'] diff --git a/src/python/__init__.py b/src/python/__init__.py new file mode 100644 index 0000000..2f8398b --- /dev/null +++ b/src/python/__init__.py @@ -0,0 +1 @@ +# dummy __init__.py \ No newline at end of file diff --git a/src/python/todo.txt b/src/python/todo.txt deleted file mode 100644 index 1333ed7..0000000 --- a/src/python/todo.txt +++ /dev/null @@ -1 +0,0 @@ -TODO diff --git a/test/python/out.txt b/test/python/out.txt new file mode 100644 index 0000000..020bef8 --- /dev/null +++ b/test/python/out.txt @@ -0,0 +1,59 @@ +{ + "foo": "", + "moo": [ + { + "choo": 1 + }, + { + "choo": 2 + }, + { + "choo": 3 + } + ], + "koo": [ + "", + "", + "" + ], + "num": [ + "0.1", + "1.2" + ], + "date": [ + "2012-11-02", + "20-11-02" + ] +} +{ + "foo": "bar", + "moo": [ + { + "choo": 1, + "foo": "bar" + }, + { + "choo": 2, + "foo": "bar" + }, + { + "choo": 3, + "foo": "bar" + } + ], + "koo": [ + "bar", + "bar", + "bar" + ], + "num": [ + 0.1, + 1.0 + ], + "date": [ + "2012-11-02", + "20-11-02" + ] +} +"date.1" should match Y-m-d ! +"date.0" must be equal to "date.1"! diff --git a/test/python/test.py b/test/python/test.py new file mode 100644 index 0000000..0ed0e15 --- /dev/null +++ b/test/python/test.py @@ -0,0 +1,76 @@ +import os, sys + +DIR = os.path.dirname(os.path.abspath(__file__)) + +def import_module(name, path): + import imp + try: + mod_fp, mod_path, mod_desc = imp.find_module(name, [path]) + mod = getattr( imp.load_module(name, mod_fp, mod_path, mod_desc), name ) + except ImportError as exc: + mod = None + sys.stderr.write("Error: failed to import module ({})".format(exc)) + finally: + if mod_fp: mod_fp.close() + return mod + +# import the Formal.py (as a) module, probably you will want to place this in another dir/package +Formal = import_module('Formal', os.path.join(DIR, '../../src/python/')) +if not Formal: + print ('Could not load the Formal Module') + sys.exit(1) +else: + pass + + +def test(): + import json + + formal = Formal().option('WILDCARD', '*').option('SEPARATOR', '.').option('break_on_first_error', False) + + formdata = { + 'foo' : '', + 'moo' : [ + {'choo' : 1}, + {'choo' : 2}, + {'choo' : 3}, + ], + + 'koo' : [ + '', + '', + '', + ], + + 'num' : [ + '0.1', + '1.2', + ], + + 'date' : [ + '2012-11-02', + '20-11-02', + ] + } + + data = formal.option('defaults', { + 'foo' : 'bar', + 'moo.*.foo' : 'bar', + 'koo.*' : 'bar' + }).option('typecasters', { + 'num.*' : Formal.typecast('composite', [Formal.typecast('float'), Formal.typecast('clamp', [0.0, 1.0]) + ])}).option('validators', { + 'date.*' : Formal.validate('match', Formal.datetime('Y-m-d'), '"{key}" should match {args} !'), + 'date.0' : Formal.validate('eq', Formal.field('date.1')) + }).process(formdata) + + err = formal.getErrors() + + print(json.dumps(formdata, indent=4)) + + print(json.dumps(data, indent=4)) + + print("\n".join(map(str, err))) + + +test() \ No newline at end of file diff --git a/test/python/todo.txt b/test/python/todo.txt deleted file mode 100644 index 1333ed7..0000000 --- a/test/python/todo.txt +++ /dev/null @@ -1 +0,0 @@ -TODO