Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

centralize errors #364

Merged
merged 4 commits into from
Mar 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May we please also have a setMessage method that can add / overwrite messages, and store the messages on the class rather than inside getMessage? This would be useful for local translations and people's custom errors that never make it as far as upstream.

Would be good if this could accept either a single error message, or an array of them to allow setting multiple errors at once. e.g.:

public static function setMessage($id, $message)
{
    if (is_array($id)) {
        self::$messages = array_merge(self::$messages, $id);
    } else {
        self::$messages[$id] = $message;
    }
}

Copy link
Collaborator Author

@shmax shmax Mar 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @erayd and I already came to an understanding on this, but just for those following along at home ErrorConstraint is not a localization interface. It's just a central repository for the natural language strings that we already had floating around in the code. Its only relevance to localization is as a reference.

{
$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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of json-encoding complex types. Good idea 👍.

}, array_values($more)))),
'constraint' => array(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above - if you feed it to the constructor, then this becomes unnecessary.

'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