Skip to content

Commit

Permalink
centralize errors (#364)
Browse files Browse the repository at this point in the history
* centralize errors

* isolate 'more' info

* throw exception for missing error message

* swap args
  • Loading branch information
shmax authored and bighappyface committed Mar 7, 2017
1 parent 72b94c1 commit 3dba977
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 70 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
}
}],
"require": {
"php": ">=5.3.3"
"php": ">=5.3.3",
"marc-mabe/php-enum":"2.3.1"
},
"require-dev": {
"json-schema/JSON-Schema-Test-Suite": "1.2.0",
Expand Down
104 changes: 104 additions & 0 deletions src/JsonSchema/ConstraintError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace JsonSchema;

class ConstraintError extends \MabeEnum\Enum
{
const ADDITIONAL_ITEMS = 'additionalItems';
const ADDITIONAL_PROPERTIES = 'additionalProp';
const ALL_OF = 'allOf';
const ANY_OF = 'anyOf';
const DEPENDENCIES = 'dependencies';
const DISALLOW = 'disallow';
const DIVISIBLE_BY = 'divisibleBy';
const ENUM = 'enum';
const EXCLUSIVE_MINIMUM = 'exclusiveMinimum';
const EXCLUSIVE_MAXIMUM = 'exclusiveMaximum';
const FORMAT_COLOR = 'colorFormat';
const FORMAT_DATE = 'dateFormat';
const FORMAT_DATE_TIME = 'dateTimeFormat';
const FORMAT_DATE_UTC = 'dateUtcFormat';
const FORMAT_EMAIL = 'emailFormat';
const FORMAT_HOSTNAME = 'styleHostName';
const FORMAT_IP = 'ipFormat';
const FORMAT_PHONE = 'phoneFormat';
const FORMAT_REGEX= 'regexFormat';
const FORMAT_STYLE = 'styleFormat';
const FORMAT_TIME = 'timeFormat';
const FORMAT_URL = 'urlFormat';
const LENGTH_MAX = 'maxLength';
const LENGTH_MIN = 'minLength';
const MAXIMUM = 'maximum';
const MIN_ITEMS = 'minItems';
const MINIMUM = 'minimum';
const MISSING_MAXIMUM = 'missingMaximum';
const MISSING_MINIMUM = 'missingMinimum';
const MAX_ITEMS = 'maxItems';
const MULTIPLE_OF = 'multipleOf';
const NOT = 'not';
const ONE_OF = 'oneOf';
const REQUIRED = 'required';
const REQUIRED_D3 = 'selfRequired';
const REQUIRES = 'requires';
const PATTERN = 'pattern';
const PREGEX_INVALID = 'pregrex';
const PROPERTIES_MIN = 'minProperties';
const PROPERTIES_MAX = 'maxProperties';
const TYPE = 'type';
const UNIQUE_ITEMS = 'uniqueItems';

public function getMessage()
{
$name = $this->getValue();
static $messages = array(
self::ADDITIONAL_ITEMS => 'The item %s[%s] is not defined and the definition does not allow additional items',
self::ADDITIONAL_PROPERTIES => 'The property %s is not defined and the definition does not allow additional properties',
self::ALL_OF => 'Failed to match all schemas',
self::ANY_OF => 'Failed to match at least one schema',
self::DEPENDENCIES => '%s depends on %s, which is missing',
self::DISALLOW => 'Disallowed value was matched',
self::DIVISIBLE_BY => 'Is not divisible by %d',
self::ENUM => 'Does not have a value in the enumeration %s',
self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d',
self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d',
self::FORMAT_COLOR => 'Invalid color',
self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD',
self::FORMAT_DATE_TIME => 'Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm',
self::FORMAT_DATE_UTC => 'Invalid time %s, expected integer of milliseconds since Epoch',
self::FORMAT_EMAIL => 'Invalid email',
self::FORMAT_HOSTNAME => 'Invalid hostname',
self::FORMAT_IP => 'Invalid IP address',
self::FORMAT_PHONE => 'Invalid phone number',
self::FORMAT_REGEX=> 'Invalid regex format %s',
self::FORMAT_STYLE => 'Invalid style',
self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss',
self::FORMAT_URL => 'Invalid URL format',
self::LENGTH_MAX => 'Must be at most %d characters long',
self::LENGTH_MIN => 'Must be at least %d characters long',
self::MAX_ITEMS => 'There must be a maximum of %d items in the array',
self::MAXIMUM => 'Must have a maximum value less than or equal to %d',
self::MIN_ITEMS => 'There must be a minimum of %d items in the array',
self::MINIMUM => 'Must have a minimum value greater than or equal to %d',
self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum',
self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum',
self::MULTIPLE_OF => 'Must be a multiple of %d',
self::NOT => 'Matched a schema which it should not',
self::ONE_OF => 'Failed to match exactly one schema',
self::REQUIRED => 'The property %s is required',
self::REQUIRED_D3 => 'Is missing and it is required',
self::REQUIRES => 'The presence of the property %s requires that %s also be present',
self::PATTERN => 'Does not match the regex pattern %s',
self::PREGEX_INVALID => 'The pattern %s is invalid',
self::PROPERTIES_MIN => 'Must contain a minimum of %d properties',
self::PROPERTIES_MAX => 'Must contain no more than %d properties',
self::TYPE => '%s value found, but %s is required',
self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array'
);

if (!isset($messages[$name])) {
throw new InvalidArgumentException('Missing error message for ' . $name);
}

return $messages[$name];
}
}
22 changes: 15 additions & 7 deletions src/JsonSchema/Constraints/BaseConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Constraints;

use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\ValidationException;

Expand Down Expand Up @@ -36,23 +37,30 @@ public function __construct(Factory $factory = null)
$this->factory = $factory ?: new Factory();
}

public function addError(JsonPointer $path = null, $message, $constraint = '', array $more = null)
public function addError(ConstraintError $constraint, JsonPointer $path = null, array $more = array())
{
$message = $constraint ? $constraint->getMessage() : '';
$name = $constraint ? $constraint->getValue() : '';
$error = array(
'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')),
'pointer' => ltrim(strval($path ?: new JsonPointer('')), '#'),
'message' => $message,
'constraint' => $constraint,
'message' => ucfirst(vsprintf($message, array_map(function ($val) {
if (is_scalar($val)) {
return $val;
}

return json_encode($val);
}, array_values($more)))),
'constraint' => array(
'name' => $name,
'params' => $more
)
);

if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) {
throw new ValidationException(sprintf('Error validating %s: %s', $error['pointer'], $error['message']));
}

if (is_array($more) && count($more) > 0) {
$error += $more;
}

$this->errors[] = $error;
}

Expand Down
16 changes: 12 additions & 4 deletions src/JsonSchema/Constraints/CollectionConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Constraints;

use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;

/**
Expand All @@ -26,12 +27,12 @@ public function check(&$value, $schema = null, JsonPointer $path = null, $i = nu
{
// Verify minItems
if (isset($schema->minItems) && count($value) < $schema->minItems) {
$this->addError($path, 'There must be a minimum of ' . $schema->minItems . ' items in the array', 'minItems', array('minItems' => $schema->minItems));
$this->addError(ConstraintError::MIN_ITEMS(), $path, array('minItems' => $schema->minItems));
}

// Verify maxItems
if (isset($schema->maxItems) && count($value) > $schema->maxItems) {
$this->addError($path, 'There must be a maximum of ' . $schema->maxItems . ' items in the array', 'maxItems', array('maxItems' => $schema->maxItems));
$this->addError(ConstraintError::MAX_ITEMS(), $path, array('maxItems' => $schema->maxItems));
}

// Verify uniqueItems
Expand All @@ -43,7 +44,7 @@ public function check(&$value, $schema = null, JsonPointer $path = null, $i = nu
}, $value);
}
if (count(array_unique($unique)) != count($value)) {
$this->addError($path, 'There are no duplicates allowed in the array', 'uniqueItems');
$this->addError(ConstraintError::UNIQUE_ITEMS(), $path);
}
}

Expand Down Expand Up @@ -124,7 +125,14 @@ protected function validateItems(&$value, $schema = null, JsonPointer $path = nu
$this->checkUndefined($v, $schema->additionalItems, $path, $k);
} else {
$this->addError(
$path, 'The item ' . $i . '[' . $k . '] is not defined and the definition does not allow additional items', 'additionalItems', array('additionalItems' => $schema->additionalItems));
ConstraintError::ADDITIONAL_ITEMS(),
$path,
array(
'item' => $i,
'property' => $k,
'additionalItems' => $schema->additionalItems
)
);
}
} else {
// Should be valid against an empty schema
Expand Down
10 changes: 5 additions & 5 deletions src/JsonSchema/Constraints/ConstraintInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Constraints;

use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;

/**
Expand All @@ -35,12 +36,11 @@ public function addErrors(array $errors);
/**
* adds an error
*
* @param JsonPointer|null $path
* @param string $message
* @param string $constraint the constraint/rule that is broken, e.g.: 'minLength'
* @param array $more more array elements to add to the error
* @param ConstraintError $constraint the constraint/rule that is broken, e.g.: ConstraintErrors::LENGTH_MIN()
* @param JsonPointer |null $path
* @param array $more more array elements to add to the error
*/
public function addError(JsonPointer $path = null, $message, $constraint='', array $more = null);
public function addError(ConstraintError $constraint, JsonPointer $path = null, array $more = array());

/**
* checks if the validator has not raised errors
Expand Down
3 changes: 2 additions & 1 deletion src/JsonSchema/Constraints/EnumConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Constraints;

use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;

/**
Expand Down Expand Up @@ -49,6 +50,6 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i =
}
}

$this->addError($path, 'Does not have a value in the enumeration ' . json_encode($schema->enum), 'enum', array('enum' => $schema->enum));
$this->addError(ConstraintError::ENUM(), $path, array('enum' => $schema->enum));
}
}
45 changes: 32 additions & 13 deletions src/JsonSchema/Constraints/FormatConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Constraints;

use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Rfc3339;

Expand All @@ -33,49 +34,67 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i =
switch ($schema->format) {
case 'date':
if (!$date = $this->validateDateTime($element, 'Y-m-d')) {
$this->addError($path, sprintf('Invalid date %s, expected format YYYY-MM-DD', json_encode($element)), 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_DATE(), $path, array(
'date' => $element,
'format' => $schema->format
)
);
}
break;

case 'time':
if (!$this->validateDateTime($element, 'H:i:s')) {
$this->addError($path, sprintf('Invalid time %s, expected format hh:mm:ss', json_encode($element)), 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_TIME(), $path, array(
'time' => json_encode($element),
'format' => $schema->format,
)
);
}
break;

case 'date-time':
if (null === Rfc3339::createFromString($element)) {
$this->addError($path, sprintf('Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', json_encode($element)), 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, array(
'dateTime' => json_encode($element),
'format' => $schema->format
)
);
}
break;

case 'utc-millisec':
if (!$this->validateDateTime($element, 'U')) {
$this->addError($path, sprintf('Invalid time %s, expected integer of milliseconds since Epoch', json_encode($element)), 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, array(
'value' => $element,
'format' => $schema->format));
}
break;

case 'regex':
if (!$this->validateRegex($element)) {
$this->addError($path, 'Invalid regex format ' . $element, 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_REGEX(), $path, array(
'value' => $element,
'format' => $schema->format
)
);
}
break;

case 'color':
if (!$this->validateColor($element)) {
$this->addError($path, 'Invalid color', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_COLOR(), $path, array('format' => $schema->format));
}
break;

case 'style':
if (!$this->validateStyle($element)) {
$this->addError($path, 'Invalid style', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_STYLE(), $path, array('format' => $schema->format));
}
break;

case 'phone':
if (!$this->validatePhone($element)) {
$this->addError($path, 'Invalid phone number', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_PHONE(), $path, array('format' => $schema->format));
}
break;

Expand All @@ -99,34 +118,34 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i =
$validURL = null;
}
if ($validURL === null) {
$this->addError($path, 'Invalid URL format', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_URL(), $path, array('format' => $schema->format));
}
}
break;

case 'email':
if (null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE)) {
$this->addError($path, 'Invalid email', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_EMAIL(), $path, array('format' => $schema->format));
}
break;

case 'ip-address':
case 'ipv4':
if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) {
$this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_IP(), $path, array('format' => $schema->format));
}
break;

case 'ipv6':
if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) {
$this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_IP(), $path, array('format' => $schema->format));
}
break;

case 'host-name':
case 'hostname':
if (!$this->validateHostname($element)) {
$this->addError($path, 'Invalid hostname', 'format', array('format' => $schema->format));
$this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, array('format' => $schema->format));
}
break;

Expand Down
Loading

0 comments on commit 3dba977

Please sign in to comment.