diff --git a/src/RomaricDrigon/MetaYaml/Exception/NodeValidatorException.php b/src/RomaricDrigon/MetaYaml/Exception/NodeValidatorException.php index 7c7328d..ad2aff5 100644 --- a/src/RomaricDrigon/MetaYaml/Exception/NodeValidatorException.php +++ b/src/RomaricDrigon/MetaYaml/Exception/NodeValidatorException.php @@ -5,16 +5,50 @@ class NodeValidatorException extends \Exception { private $node_path; + private $paths = []; + private $messages = []; - public function __construct($node_path, $message) + public function __construct($node_path, $messages, $paths) { $this->node_path = $node_path; + if (!is_array($messages)) { + $messages = [$messages]; + } + if (!is_array($paths)) { + $paths = [$paths]; + } + $this->paths = $paths; + $this->messages = $messages; - parent::__construct($message); + parent::__construct(current($messages)); } public function getNodePath() { return $this->node_path; } + + public function getPaths() + { + return $this->paths; + } + + public function getMessages() + { + return $this->messages; + } + + public function getReport() + { + $candidates = []; + $max_length = 0; + foreach($this->paths as $index => $path) { + $length = count(explode('/', $path)); + $candidates[$length][] = $result[] = $path . ': ' . $this->messages[$index]; + if ($length > $max_length) { + $max_length = $length; + } + } + return implode("\n", $candidates[$max_length]); + } } diff --git a/src/RomaricDrigon/MetaYaml/MetaYaml.php b/src/RomaricDrigon/MetaYaml/MetaYaml.php index 48e2882..602d7d7 100644 --- a/src/RomaricDrigon/MetaYaml/MetaYaml.php +++ b/src/RomaricDrigon/MetaYaml/MetaYaml.php @@ -2,6 +2,7 @@ namespace RomaricDrigon\MetaYaml; +use RomaricDrigon\MetaYaml\Exception\NodeValidatorException; use RomaricDrigon\MetaYaml\SchemaValidator; use RomaricDrigon\MetaYaml\Loader\JsonLoader; @@ -23,8 +24,8 @@ public function __construct(array $schema, $validate = false) // we validate the schema using the meta schema, defining the structure of our schema try { $this->validateSchema(); - } catch (\Exception $e) { - throw new \Exception("Unable to validate schema with error: {$e->getMessage()}"); + } catch (NodeValidatorException $e) { + throw new \Exception("Unable to validate schema with error: {$e->getMessage()}\n{$e->getReport()}"); } } } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/ArrayNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/ArrayNodeValidator.php index d4b2bcf..fd849af 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/ArrayNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/ArrayNodeValidator.php @@ -11,7 +11,7 @@ public function validate($name, $node, $data) if ($this->checkRequired($name, $node, $data)) return true; if (! is_array($data)) { - throw new NodeValidatorException($name, "The node '$name' is not an array"); + throw new NodeValidatorException($name, "The node '$name' is not an array", $this->path); } $this->checkEmpty($name, $node, $data); @@ -20,7 +20,8 @@ public function validate($name, $node, $data) $this->schema_validator->validateNode($name.'.'.$key, $value[$this->schema_validator->getFullName('type')], $value, - isset($data[$key]) ? $data[$key] : null // isset(null) is false, but no big deal + isset($data[$key]) ? $data[$key] : null, // isset(null) is false, but no big deal + $this->path . '/' .$key ); if (array_key_exists($key, $data)) { @@ -35,14 +36,14 @@ public function validate($name, $node, $data) } throw new NodeValidatorException($name, - "The node '$name' has not allowed extra key(s): ".implode(', ', array_keys($data))); + "The node '$name' has not allowed extra key(s): ".implode(', ', array_keys($data)), $this->path); } protected function checkEmpty($name, array $node, $data) { if ($data === array() && isset($node[$this->schema_validator->getFullName('not_empty')]) && $node[$this->schema_validator->getFullName('not_empty')]) { - throw new NodeValidatorException($name, "The node '$name' can not be empty"); + throw new NodeValidatorException($name, "The node '$name' can not be empty", $this->path); } } } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/BooleanNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/BooleanNodeValidator.php index de9489c..99e37c1 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/BooleanNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/BooleanNodeValidator.php @@ -15,7 +15,7 @@ public function validate($name, $node, $data) if (is_bool($data) || (! $strict && ($data == 'true' || $data == 'false'))) { return true; } else { - throw new NodeValidatorException($name, "The node '$name' is not a boolean"); + throw new NodeValidatorException($name, "The node '$name' is not a boolean", $this->path); } } } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/ChoiceNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/ChoiceNodeValidator.php index a35a5a8..91da0f5 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/ChoiceNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/ChoiceNodeValidator.php @@ -13,11 +13,13 @@ public function validate($name, $node, $data) $valid = false; $message = ''; $count_levels = -1; + $sub_messages = []; + $sub_paths = []; foreach ($node[$this->schema_validator->getFullName('choices')] as $choice_config) { try { $this->schema_validator->validateNode($name, $choice_config[$this->schema_validator->getFullName('type')], - $choice_config, $data); + $choice_config, $data, $this->path); $valid = true; break; } catch (NodeValidatorException $e) { @@ -27,11 +29,17 @@ public function validate($name, $node, $data) $message = $e->getMessage(); $count_levels = $current_count_levels; } + + $sub_messages = array_merge($sub_messages, $e->getMessages()); + $sub_paths = array_merge($sub_paths, $e->getPaths()); + } } if (! $valid) { - throw new NodeValidatorException($name, "The choice node '$name' is invalid with error: $message"); + $messages = array_merge($sub_messages, ["The choice node '$name' is invalid with error: $message"]); + $paths = array_merge($sub_paths, [$this->path]); + throw new NodeValidatorException($name, $messages, $paths); } return true; diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/EnumNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/EnumNodeValidator.php index 3ab7162..a04627e 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/EnumNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/EnumNodeValidator.php @@ -33,7 +33,7 @@ public function validate($name, $node, $data) } if (! in_array($data, $haystack, $strict)) { - throw new NodeValidatorException($name, "The value '$data' is not allowed for node '$name'"); + throw new NodeValidatorException($name, "The value '$data' is not allowed for node '$name'", $this->path); } return true; diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidator.php index 063140f..90dbb17 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidator.php @@ -8,19 +8,25 @@ abstract class NodeValidator implements NodeValidatorInterface { protected $schema_validator; + protected $path; - public function __construct(SchemaValidator $schema_validator) + public function __construct(SchemaValidator $schema_validator) { $this->schema_validator = $schema_validator; } + public function setPath($path) + { + $this->path = $path; + } + protected function checkRequired($name, array $node, $data) { if (! is_null($data)) return false; // ok anyway if (isset($node[$this->schema_validator->getFullName('required')]) && $node[$this->schema_validator->getFullName('required')]) { - throw new NodeValidatorException($name, sprintf("The node '$name' is required")); + throw new NodeValidatorException($name, sprintf("The node '$name' is required"), $this->path); } else { return true; // data null & not required, stop further validations } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidatorFactory.php b/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidatorFactory.php index adbcbab..3623e1f 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidatorFactory.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/NodeValidatorFactory.php @@ -29,7 +29,7 @@ public function getValidator($name, $type, SchemaValidator $validator) case 'partial': return new PartialNodeValidator($validator); default: - throw new NodeValidatorException($name, 'Unknown validator type : '.$type); + throw new NodeValidatorException($name, 'Unknown validator type : '.$type, $this->path); } } } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/NumberNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/NumberNodeValidator.php index 9fad3b4..26752da 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/NumberNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/NumberNodeValidator.php @@ -14,7 +14,7 @@ public function validate($name, $node, $data) $strict = isset($node[$this->schema_validator->getFullName('strict')]) && isset($node[$this->schema_validator->getFullName('strict')]); if (! is_numeric($data) || ($strict && ! (is_integer($data) || is_float($data)))) { - throw new NodeValidatorException($name, "The node '$name' is not a number"); + throw new NodeValidatorException($name, "The node '$name' is not a number", $this->path); } return true; @@ -24,7 +24,7 @@ protected function checkEmpty($name, array $node, $data) { if (($data === '0' || $data === 0 || $data === 0.0) && isset($node[$this->schema_validator->getFullName('not_empty')]) && $node[$this->schema_validator->getFullName('not_empty')]) { - throw new NodeValidatorException($name, "The node '$name' can not be empty"); + throw new NodeValidatorException($name, "The node '$name' can not be empty", $this->path); } } } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/PartialNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/PartialNodeValidator.php index b9733af..1897a12 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/PartialNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/PartialNodeValidator.php @@ -9,6 +9,6 @@ public function validate($name, $node, $data) if ($this->checkRequired($name, $node, $data)) return true; // we will validate using the partial defined in schema -> _partials -> name - return $this->schema_validator->validatePartial($node[$this->schema_validator->getFullName('partial')], $data); + return $this->schema_validator->validatePartial($node[$this->schema_validator->getFullName('partial')], $data, $this->path); } } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/PatternNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/PatternNodeValidator.php index 0ed57c2..b31cd87 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/PatternNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/PatternNodeValidator.php @@ -13,7 +13,8 @@ public function validate($name, $node, $data) // preg_match return false if there's an error, 0 if not found, 1 if found if (1 !== preg_match($node[$this->schema_validator->getFullName('pattern')], $data)) { throw new NodeValidatorException($name, - "Node '$name' does not match pattern '{$node[$this->schema_validator->getFullName('pattern')]}'" + "Node '$name' does not match pattern '{$node[$this->schema_validator->getFullName('pattern')]}'", + $this->path ); } diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/PrototypeNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/PrototypeNodeValidator.php index 041f89e..aeed672 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/PrototypeNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/PrototypeNodeValidator.php @@ -11,7 +11,7 @@ public function validate($name, $node, $data) if ($this->checkRequired($name, $node, $data)) return true; if (! is_array($data)) { - throw new NodeValidatorException($name, "The node '$name' is not an array"); + throw new NodeValidatorException($name, "The node '$name' is not an array", $this->path); } // get min and max number of prototype repetition @@ -21,19 +21,20 @@ public function validate($name, $node, $data) foreach ($data as $key => $subdata) { if ($n >= $max) { // because we count from 0 - throw new NodeValidatorException($name, "Prototype node '$name' has too much children"); + throw new NodeValidatorException($name, "Prototype node '$name' has too much children", $this->path); } $this->schema_validator->validateNode($name.'.'.$key, $node[$this->schema_validator->getFullName('prototype')][$this->schema_validator->getFullName('type')], $node[$this->schema_validator->getFullName('prototype')], - $subdata); + $subdata, + $this->path . '/' . $key); $n++; } if ($n < $min) { - throw new NodeValidatorException($name, "Prototype node '$name' has not enough children"); + throw new NodeValidatorException($name, "Prototype node '$name' has not enough children", $this->path); } return true; diff --git a/src/RomaricDrigon/MetaYaml/NodeValidator/TextNodeValidator.php b/src/RomaricDrigon/MetaYaml/NodeValidator/TextNodeValidator.php index 13381dc..cea693a 100644 --- a/src/RomaricDrigon/MetaYaml/NodeValidator/TextNodeValidator.php +++ b/src/RomaricDrigon/MetaYaml/NodeValidator/TextNodeValidator.php @@ -15,7 +15,7 @@ public function validate($name, $node, $data) (isset($node[$this->schema_validator->getFullName('strict')]) && $node[$this->schema_validator->getFullName('strict')] && ! is_string($data))) { - throw new NodeValidatorException($name, "The node '$name' is not a text value"); + throw new NodeValidatorException($name, "The node '$name' is not a text value", $this->path); } return true; @@ -25,7 +25,7 @@ protected function checkEmpty($name, array $node, $data) { if ($data === '' && isset($node[$this->schema_validator->getFullName('not_empty')]) && $node[$this->schema_validator->getFullName('not_empty')]) { - throw new NodeValidatorException($name, "The node '$name' can not be empty"); + throw new NodeValidatorException($name, "The node '$name' can not be empty", $this->path); } } } diff --git a/src/RomaricDrigon/MetaYaml/SchemaValidator.php b/src/RomaricDrigon/MetaYaml/SchemaValidator.php index d51a536..8c0bc88 100644 --- a/src/RomaricDrigon/MetaYaml/SchemaValidator.php +++ b/src/RomaricDrigon/MetaYaml/SchemaValidator.php @@ -41,14 +41,18 @@ public function getFullName($name) // validate nodes - public function validateNode($name, $type, $node, $data) + /** + * @throws Exception\NodeValidatorException + */ + public function validateNode($name, $type, $node, $data, $path = '') { $validator = $this->factory->getValidator($name, $type, $this); + $validator->setPath($path); return $validator->validate($name, $node, $data); } - public function validatePartial($name, $data) + public function validatePartial($name, $data, $path = '') { if (! isset($this->schema_config['partials']) || ! isset($this->schema_config['partials'][$name])) { throw new \Exception("You're using a partial but partial '$name' is not defined in your schema"); @@ -57,7 +61,8 @@ public function validatePartial($name, $data) return $this->validateNode($name, $this->schema_config['partials'][$name][$this->getFullName('type')], $this->schema_config['partials'][$name], - $data + $data, + $path ); } }