Add support for callable form validation rules
diff --git a/system/libraries/Form_validation.php b/system/libraries/Form_validation.php
index 0c4f949..0a80888 100644
--- a/system/libraries/Form_validation.php
+++ b/system/libraries/Form_validation.php
@@ -144,7 +144,7 @@
 	 * Set Rules
 	 *
 	 * This function takes an array of field names and validation
-	 * rules as input, any custom error messages, validates the info, 
+	 * rules as input, any custom error messages, validates the info,
 	 * and stores it
 	 *
 	 * @param	mixed	$field
@@ -153,7 +153,7 @@
 	 * @param	array	$errors
 	 * @return	CI_Form_validation
 	 */
-	public function set_rules($field, $label = '', $rules = '', $errors = array())
+	public function set_rules($field, $label = '', $rules = array(), $errors = array())
 	{
 		// No reason to set rules if we have no POST data
 		// or a validation array has not been specified
@@ -187,26 +187,32 @@
 			return $this;
 		}
 
-		// Convert an array of rules to a string
-		if (is_array($rules))
-		{
-			$rules = implode('|', $rules);
-		}
-
 		// No fields? Nothing to do...
-		if ( ! is_string($field) OR ! is_string($rules) OR $field === '')
+		if ( ! is_string($field) OR $field === '')
 		{
 			return $this;
 		}
+		elseif ( ! is_array($rules))
+		{
+			// BC: Convert pipe-separated rules string to an array
+			if (is_string($rules))
+			{
+				$rules = explode('|', $rules);
+			}
+			else
+			{
+				return $this;
+			}
+		}
 
 		// If the field label wasn't passed we use the field name
 		$label = ($label === '') ? $field : $label;
 
 		// Is the field name an array? If it is an array, we break it apart
 		// into its components so that we can fetch the corresponding POST data later
-		$indexes = array();
-		if (preg_match_all('/\[(.*?)\]/', $field, $matches))
+		if (($is_array = (bool) preg_match_all('/\[(.*?)\]/', $field, $matches)) === TRUE)
 		{
+			$indexes = array();
 			sscanf($field, '%[^[][', $indexes[0]);
 
 			for ($i = 0, $c = count($matches[0]); $i < $c; $i++)
@@ -219,10 +225,6 @@
 
 			$is_array = TRUE;
 		}
-		else
-		{
-			$is_array	= FALSE;
-		}
 
 		// Build our master array
 		$this->_field_data[$field] = array(
@@ -468,7 +470,7 @@
 				continue;
 			}
 
-			$this->_execute($row, explode('|', $row['rules']), $this->_field_data[$field]['postdata']);
+			$this->_execute($row, $row['rules'], $this->_field_data[$field]['postdata']);
 		}
 
 		// Did we end up with any errors?
@@ -591,19 +593,33 @@
 		if ( ! in_array('required', $rules) && ($postdata === NULL OR $postdata === ''))
 		{
 			// Before we bail out, does the rule contain a callback?
-			if (preg_match('/(callback_\w+(\[.*?\])?)/', implode(' ', $rules), $match))
+			foreach ($rules as &$rule)
 			{
-				$callback = TRUE;
-				$rules = array(1 => $match[1]);
+				if (is_string($rule))
+				{
+					if (strncmp($rule, 'callback_', 9) === 0)
+					{
+						$callback = TRUE;
+						$rules = array(1 => $rule);
+						break;
+					}
+				}
+				elseif (is_callable($rule))
+				{
+					$callback = TRUE;
+					$rules = array(1 => $rule);
+					break;
+				}
 			}
-			else
+
+			if ( ! $callback)
 			{
 				return;
 			}
 		}
 
 		// Isset Test. Typically this rule will only apply to checkboxes.
-		if (($postdata === NULL OR $postdata === '') && $callback === FALSE)
+		if (($postdata === NULL OR $postdata === '') && ! $callback)
 		{
 			if (in_array('isset', $rules, TRUE) OR in_array('required', $rules))
 			{
@@ -668,39 +684,55 @@
 				// somebody messing with the form on the client side, so we'll just consider
 				// it an empty field
 				$postdata = is_array($this->_field_data[$row['field']]['postdata'])
-						? NULL
-						: $this->_field_data[$row['field']]['postdata'];
+					? NULL
+					: $this->_field_data[$row['field']]['postdata'];
 			}
 
 			// Is the rule a callback?
-			$callback = FALSE;
-			if (strpos($rule, 'callback_') === 0)
+			$callback = $callable = FALSE;
+			if (is_string($rule))
 			{
-				$rule = substr($rule, 9);
-				$callback = TRUE;
+				if (strpos($rule, 'callback_') === 0)
+				{
+					$rule = substr($rule, 9);
+					$callback = TRUE;
+				}
+			}
+			elseif (is_callable($rule))
+			{
+				$callable = TRUE;
 			}
 
 			// Strip the parameter (if exists) from the rule
 			// Rules can contain a parameter: max_length[5]
 			$param = FALSE;
-			if (preg_match('/(.*?)\[(.*)\]/', $rule, $match))
+			if ( ! $callable && preg_match('/(.*?)\[(.*)\]/', $rule, $match))
 			{
 				$rule = $match[1];
 				$param = $match[2];
 			}
 
 			// Call the function that corresponds to the rule
-			if ($callback === TRUE)
+			if ($callback OR $callable)
 			{
-				if ( ! method_exists($this->CI, $rule))
+				if ($callback)
 				{
-					log_message('debug', 'Unable to find callback validation rule: '.$rule);
-					$result = FALSE;
+					if ( ! method_exists($this->CI, $rule))
+					{
+						log_message('debug', 'Unable to find callback validation rule: '.$rule);
+						$result = FALSE;
+					}
+					else
+					{
+						// Run the function and grab the result
+						$result = $this->CI->$rule($postdata, $param);
+					}
 				}
 				else
 				{
-					// Run the function and grab the result
-					$result = $this->CI->$rule($postdata, $param);
+					$result = is_array($rule)
+						? $rule[0]->{$rule[1]}($postdata, $param)
+						: $rule($postdata, $param);
 				}
 
 				// Re-assign the result to the master data array
@@ -725,6 +757,7 @@
 				// Users can use any native PHP function call that has one param.
 				if (function_exists($rule))
 				{
+					// Native PHP functions issue warnings if you pass them more parameters than they use
 					$result = ($param !== FALSE) ? $rule($postdata, $param) : $rule($postdata);
 
 					if ($_in_array === TRUE)