diff --git a/.gitignore b/.gitignore index 090a1f0..de15c52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea .DS_Store +/vendor/ \ No newline at end of file diff --git a/README.md b/README.md index 4d398b9..9ba467f 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ `jsv4-php` is a data validator, using version 4 JSON Schemas. -Just include `jsv4.php` from your code, and use the static methods on the `Jsv4` class it defines. +Use composer autoload to load Jsv4 classes, and use the static methods on the `Jsv4` class it defines. Usage: -### `Jsv4::validate($data, $schema)` +### `Jsv4\Jsv4::validate($data, $schema)` This usage returns an object of the following shape. ```json @@ -33,17 +33,17 @@ The values in the `errors` array are similar to those for [tv4](https://github.c The `code` property corresponds to a constant corresponding to the nature of the validation error, e.g. `JSV4_INVALID_TYPE`. The names of these constants (and their values) match up exactly with the [constants from tv4](https://github.com/geraintluff/tv4/blob/master/source/api.js). -### `Jsv4::isValid($data, $schema)` +### `Jsv4\Jsv4::isValid($data, $schema)` If you just want to know the validation status, and don't care what the errors actually are, then this is a more concise way of getting it. It returns a boolean indicating whether the data correctly followed the schema. -### `Jsv4::coerce($data, $schema)` +### `Jsv4\Jsv4::coerce($data, $schema)` Sometimes, the data is not quite the correct shape - but it could be *made* the correct shape by simple modifications. -If you call `Jsv4::coerce($data, $schema)`, then it will attempt to change the data. +If you call `Jsv4\Jsv4::coerce($data, $schema)`, then it will attempt to change the data. If it is successful, then a modified version of the data can be found in `$result->value`. @@ -53,19 +53,19 @@ It's not psychic - in fact, it's quite limited. What it currently does is: Perhaps you are using data from `$_GET`, so everything's a string, but the schema says certain values should be integers or booleans. -`Jsv4::coerce()` will attempt to convert strings to numbers/booleans *only where the schema says*, leaving other numerically-value strings as strings. +`Jsv4\Jsv4::coerce()` will attempt to convert strings to numbers/booleans *only where the schema says*, leaving other numerically-value strings as strings. ### Missing properties Perhaps the API needs a complete object (described using `"required"` in the schema), but only a partial one was supplied. -`Jsv4::coerce()` will attempt to insert appropriate values for the missing properties, using a default (if it is defined in a nearby `"properties"` entry) or by creating a value if it knows the type. +`Jsv4\Jsv4::coerce()` will attempt to insert appropriate values for the missing properties, using a default (if it is defined in a nearby `"properties"` entry) or by creating a value if it knows the type. ## The `SchemaStore` class This class represents a collection of schemas. You include it from `schema-store.php`, and use it like this: ```php -$store = new SchemaStore(); +$store = new Jsv4\SchemaStore(); $store->add($url, $schema); $retrieved = $store->get($url); ``` diff --git a/composer.json b/composer.json index 9d28230..4518242 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,8 @@ }, "autoload": { - "classmap": [ - "jsv4.php", - "schema-store.php" - ] + "psr-4": { + "Jsv4\\": "src/Jsv4" + } } -} +} \ No newline at end of file diff --git a/jsv4.php b/jsv4.php deleted file mode 100644 index f6d0246..0000000 --- a/jsv4.php +++ /dev/null @@ -1,553 +0,0 @@ -valid; - } - - static public function coerce($data, $schema) { - if (is_object($data) || is_array($data)) { - $data = unserialize(serialize($data)); - } - $result = new Jsv4($data, $schema, FALSE, TRUE); - if ($result->valid) { - $result->value = $result->data; - } - return $result; - } - - static public function pointerJoin($parts) { - $result = ""; - foreach ($parts as $part) { - $part = str_replace("~", "~0", $part); - $part = str_replace("/", "~1", $part); - $result .= "/".$part; - } - return $result; - } - - static public function recursiveEqual($a, $b) { - if (is_object($a)) { - if (!is_object($b)) { - return FALSE; - } - foreach ($a as $key => $value) { - if (!isset($b->$key)) { - return FALSE; - } - if (!self::recursiveEqual($value, $b->$key)) { - return FALSE; - } - } - foreach ($b as $key => $value) { - if (!isset($a->$key)) { - return FALSE; - } - } - return TRUE; - } - if (is_array($a)) { - if (!is_array($b)) { - return FALSE; - } - foreach ($a as $key => $value) { - if (!isset($b[$key])) { - return FALSE; - } - if (!self::recursiveEqual($value, $b[$key])) { - return FALSE; - } - } - foreach ($b as $key => $value) { - if (!isset($a[$key])) { - return FALSE; - } - } - return TRUE; - } - return $a === $b; - } - - - private $data; - private $schema; - private $firstErrorOnly; - private $coerce; - public $valid; - public $errors; - - private function __construct(&$data, $schema, $firstErrorOnly=FALSE, $coerce=FALSE) { - $this->data =& $data; - $this->schema =& $schema; - $this->firstErrorOnly = $firstErrorOnly; - $this->coerce = $coerce; - $this->valid = TRUE; - $this->errors = array(); - - try { - $this->checkTypes(); - $this->checkEnum(); - $this->checkObject(); - $this->checkArray(); - $this->checkString(); - $this->checkNumber(); - $this->checkComposite(); - } catch (Jsv4Error $e) { - } - } - - private function fail($code, $dataPath, $schemaPath, $errorMessage, $subErrors=NULL) { - $this->valid = FALSE; - $error = new Jsv4Error($code, $dataPath, $schemaPath, $errorMessage, $subErrors); - $this->errors[] = $error; - if ($this->firstErrorOnly) { - throw $error; - } - } - - private function subResult(&$data, $schema, $allowCoercion=TRUE) { - return new Jsv4($data, $schema, $this->firstErrorOnly, $allowCoercion && $this->coerce); - } - - private function includeSubResult($subResult, $dataPrefix, $schemaPrefix) { - if (!$subResult->valid) { - $this->valid = FALSE; - foreach ($subResult->errors as $error) { - $this->errors[] = $error->prefix($dataPrefix, $schemaPrefix); - } - } - } - - private function checkTypes() { - if (isset($this->schema->type)) { - $types = $this->schema->type; - if (!is_array($types)) { - $types = array($types); - } - foreach ($types as $type) { - if ($type == "object" && is_object($this->data)) { - return; - } elseif ($type == "array" && is_array($this->data)) { - return; - } elseif ($type == "string" && is_string($this->data)) { - return; - } elseif ($type == "number" && !is_string($this->data) && is_numeric($this->data)) { - return; - } elseif ($type == "integer" && is_int($this->data)) { - return; - } elseif ($type == "boolean" && is_bool($this->data)) { - return; - } elseif ($type == "null" && $this->data === NULL) { - return; - } - } - - if ($this->coerce) { - foreach ($types as $type) { - if ($type == "number") { - if (is_numeric($this->data)) { - $this->data = (float)$this->data; - return; - } else if (is_bool($this->data)) { - $this->data = $this->data ? 1 : 0; - return; - } - } else if ($type == "integer") { - if ((int)$this->data == $this->data) { - $this->data = (int)$this->data; - return; - } - } else if ($type == "string") { - if (is_numeric($this->data)) { - $this->data = "".$this->data; - return; - } else if (is_bool($this->data)) { - $this->data = ($this->data) ? "true" : "false"; - return; - } else if (is_null($this->data)) { - $this->data = ""; - return; - } - } else if ($type == "boolean") { - if (is_numeric($this->data)) { - $this->data = ($this->data != "0"); - return; - } else if ($this->data == "yes" || $this->data == "true") { - $this->data = TRUE; - return; - } else if ($this->data == "no" || $this->data == "false") { - $this->data = FALSE; - return; - } else if ($this->data == NULL) { - $this->data = FALSE; - return; - } - } - } - } - - $type = gettype($this->data); - if ($type == "double") { - $type = ((int)$this->data == $this->data) ? "integer" : "number"; - } else if ($type == "NULL") { - $type = "null"; - } - $this->fail(JSV4_INVALID_TYPE, "", "/type", "Invalid type: $type"); - } - } - - private function checkEnum() { - if (isset($this->schema->enum)) { - foreach ($this->schema->enum as $option) { - if (self::recursiveEqual($this->data, $option)) { - return; - } - } - $this->fail(JSV4_ENUM_MISMATCH, "", "/enum", "Value must be one of the enum options"); - } - } - - private function checkObject() { - if (!is_object($this->data)) { - return; - } - if (isset($this->schema->required)) { - foreach ($this->schema->required as $index => $key) { - if (!array_key_exists($key, (array) $this->data)) { - if ($this->coerce && $this->createValueForProperty($key)) { - continue; - } - $this->fail(JSV4_OBJECT_REQUIRED, "", "/required/{$index}", "Missing required property: {$key}"); - } - } - } - $checkedProperties = array(); - if (isset($this->schema->properties)) { - foreach ($this->schema->properties as $key => $subSchema) { - $checkedProperties[$key] = TRUE; - if (array_key_exists($key, (array) $this->data)) { - $subResult = $this->subResult($this->data->$key, $subSchema); - $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("properties", $key))); - } - } - } - if (isset($this->schema->patternProperties)) { - foreach ($this->schema->patternProperties as $pattern => $subSchema) { - foreach ($this->data as $key => &$subValue) { - if (preg_match("/".str_replace("/", "\\/", $pattern)."/", $key)) { - $checkedProperties[$key] = TRUE; - $subResult = $this->subResult($this->data->$key, $subSchema); - $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("patternProperties", $pattern))); - } - } - } - } - if (isset($this->schema->additionalProperties)) { - $additionalProperties = $this->schema->additionalProperties; - foreach ($this->data as $key => &$subValue) { - if (isset($checkedProperties[$key])) { - continue; - } - if (!$additionalProperties) { - $this->fail(JSV4_OBJECT_ADDITIONAL_PROPERTIES, self::pointerJoin(array($key)), "/additionalProperties", "Additional properties not allowed"); - } else if (is_object($additionalProperties)) { - $subResult = $this->subResult($subValue, $additionalProperties); - $this->includeSubResult($subResult, self::pointerJoin(array($key)), "/additionalProperties"); - } - } - } - if (isset($this->schema->dependencies)) { - foreach ($this->schema->dependencies as $key => $dep) { - if (!isset($this->data->$key)) { - continue; - } - if (is_object($dep)) { - $subResult = $this->subResult($this->data, $dep); - $this->includeSubResult($subResult, "", self::pointerJoin(array("dependencies", $key))); - } else if (is_array($dep)) { - foreach ($dep as $index => $depKey) { - if (!isset($this->data->$depKey)) { - $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key, $index)), "Property $key depends on $depKey"); - } - } - } else { - if (!isset($this->data->$dep)) { - $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key)), "Property $key depends on $dep"); - } - } - } - } - if (isset($this->schema->minProperties)) { - if (count(get_object_vars($this->data)) < $this->schema->minProperties) { - $this->fail(JSV4_OBJECT_PROPERTIES_MINIMUM, "", "/minProperties", ($this->schema->minProperties == 1) ? "Object cannot be empty" : "Object must have at least {$this->schema->minProperties} defined properties"); - } - } - if (isset($this->schema->maxProperties)) { - if (count(get_object_vars($this->data)) > $this->schema->maxProperties) { - $this->fail(JSV4_OBJECT_PROPERTIES_MAXIMUM, "", "/minProperties", ($this->schema->maxProperties == 1) ? "Object must have at most one defined property" : "Object must have at most {$this->schema->maxProperties} defined properties"); - } - } - } - - private function checkArray() { - if (!is_array($this->data)) { - return; - } - if (isset($this->schema->items)) { - $items = $this->schema->items; - if (is_array($items)) { - foreach ($this->data as $index => &$subData) { - if (!is_numeric($index)) { - throw new Exception("Arrays must only be numerically-indexed"); - } - if (isset($items[$index])) { - $subResult = $this->subResult($subData, $items[$index]); - $this->includeSubResult($subResult, "/{$index}", "/items/{$index}"); - } else if (isset($this->schema->additionalItems)) { - $additionalItems = $this->schema->additionalItems; - if (!$additionalItems) { - $this->fail(JSV4_ARRAY_ADDITIONAL_ITEMS, "/{$index}", "/additionalItems", "Additional items (index ".count($items)." or more) are not allowed"); - } else if ($additionalItems !== TRUE) { - $subResult = $this->subResult($subData, $additionalItems); - $this->includeSubResult($subResult, "/{$index}", "/additionalItems"); - } - } - } - } else { - foreach ($this->data as $index => &$subData) { - if (!is_numeric($index)) { - throw new Exception("Arrays must only be numerically-indexed"); - } - $subResult = $this->subResult($subData, $items); - $this->includeSubResult($subResult, "/{$index}", "/items"); - } - } - } - if (isset($this->schema->minItems)) { - if (count($this->data) < $this->schema->minItems) { - $this->fail(JSV4_ARRAY_LENGTH_SHORT, "", "/minItems", "Array is too short (must have at least {$this->schema->minItems} items)"); - } - } - if (isset($this->schema->maxItems)) { - if (count($this->data) > $this->schema->maxItems) { - $this->fail(JSV4_ARRAY_LENGTH_LONG, "", "/maxItems", "Array is too long (must have at most {$this->schema->maxItems} items)"); - } - } - if (isset($this->schema->uniqueItems)) { - foreach ($this->data as $indexA => $itemA) { - foreach ($this->data as $indexB => $itemB) { - if ($indexA < $indexB) { - if (self::recursiveEqual($itemA, $itemB)) { - $this->fail(JSV4_ARRAY_UNIQUE, "", "/uniqueItems", "Array items must be unique (items $indexA and $indexB)"); - break 2; - } - } - } - } - } - } - - private function checkString() { - if (!is_string($this->data)) { - return; - } - if (isset($this->schema->minLength)) { - if (strlen($this->data) < $this->schema->minLength) { - $this->fail(JSV4_STRING_LENGTH_SHORT, "", "/minLength", "String must be at least {$this->schema->minLength} characters long"); - } - } - if (isset($this->schema->maxLength)) { - if (strlen($this->data) > $this->schema->maxLength) { - $this->fail(JSV4_STRING_LENGTH_LONG, "", "/maxLength", "String must be at most {$this->schema->maxLength} characters long"); - } - } - if (isset($this->schema->pattern)) { - $pattern = $this->schema->pattern; - $patternFlags = isset($this->schema->patternFlags) ? $this->schema->patternFlags : ''; - $result = preg_match("/".str_replace("/", "\\/", $pattern)."/".$patternFlags, $this->data); - if ($result === 0) { - $this->fail(JSV4_STRING_PATTERN, "", "/pattern", "String does not match pattern: $pattern"); - } - } - } - - private function checkNumber() { - if (is_string($this->data) || !is_numeric($this->data)) { - return; - } - if (isset($this->schema->multipleOf)) { - if (fmod($this->data/$this->schema->multipleOf, 1) != 0) { - $this->fail(JSV4_NUMBER_MULTIPLE_OF, "", "/multipleOf", "Number must be a multiple of {$this->schema->multipleOf}"); - } - } - if (isset($this->schema->minimum)) { - $minimum = $this->schema->minimum; - if (isset($this->schema->exclusiveMinimum) && $this->schema->exclusiveMinimum) { - if ($this->data <= $minimum) { - $this->fail(JSV4_NUMBER_MINIMUM_EXCLUSIVE, "", "", "Number must be > $minimum"); - } - } else { - if ($this->data < $minimum) { - $this->fail(JSV4_NUMBER_MINIMUM, "", "/minimum", "Number must be >= $minimum"); - } - } - } - if (isset($this->schema->maximum)) { - $maximum = $this->schema->maximum; - if (isset($this->schema->exclusiveMaximum) && $this->schema->exclusiveMaximum) { - if ($this->data >= $maximum) { - $this->fail(JSV4_NUMBER_MAXIMUM_EXCLUSIVE, "", "", "Number must be < $maximum"); - } - } else { - if ($this->data > $maximum) { - $this->fail(JSV4_NUMBER_MAXIMUM, "", "/maximum", "Number must be <= $maximum"); - } - } - } - } - - private function checkComposite() { - if (isset($this->schema->allOf)) { - foreach ($this->schema->allOf as $index => $subSchema) { - $subResult = $this->subResult($this->data, $subSchema, FALSE); - $this->includeSubResult($subResult, "", "/allOf/".(int)$index); - } - } - if (isset($this->schema->anyOf)) { - $failResults = array(); - foreach ($this->schema->anyOf as $index => $subSchema) { - $subResult = $this->subResult($this->data, $subSchema, FALSE); - if ($subResult->valid) { - return; - } - $failResults[] = $subResult; - } - $this->fail(JSV4_ANY_OF_MISSING, "", "/anyOf", "Value must satisfy at least one of the options", $failResults); - } - if (isset($this->schema->oneOf)) { - $failResults = array(); - $successIndex = NULL; - foreach ($this->schema->oneOf as $index => $subSchema) { - $subResult = $this->subResult($this->data, $subSchema, FALSE); - if ($subResult->valid) { - if ($successIndex === NULL) { - $successIndex = $index; - } else { - $this->fail(JSV4_ONE_OF_MULTIPLE, "", "/oneOf", "Value satisfies more than one of the options ($successIndex and $index)"); - } - continue; - } - $failResults[] = $subResult; - } - if ($successIndex === NULL) { - $this->fail(JSV4_ONE_OF_MISSING, "", "/oneOf", "Value must satisfy one of the options", $failResults); - } - } - if (isset($this->schema->not)) { - $subResult = $this->subResult($this->data, $this->schema->not, FALSE); - if ($subResult->valid) { - $this->fail(JSV4_NOT_PASSED, "", "/not", "Value satisfies prohibited schema"); - } - } - } - - private function createValueForProperty($key) { - $schema = NULL; - if (isset($this->schema->properties->$key)) { - $schema = $this->schema->properties->$key; - } else if (isset($this->schema->patternProperties)) { - foreach ($this->schema->patternProperties as $pattern => $subSchema) { - if (preg_match("/".str_replace("/", "\\/", $pattern)."/", $key)) { - $schema = $subSchema; - break; - } - } - } - if (!$schema && isset($this->schema->additionalProperties)) { - $schema = $this->schema->additionalProperties; - } - if ($schema) { - if (isset($schema->default)) { - $this->data->$key = unserialize(serialize($schema->default)); - return TRUE; - } - if (isset($schema->type)) { - $types = is_array($schema->type) ? $schema->type : array($schema->type); - if (in_array("null", $types)) { - $this->data->$key = NULL; - } elseif (in_array("boolean", $types)) { - $this->data->$key = TRUE; - } elseif (in_array("integer", $types) || in_array("number", $types)) { - $this->data->$key = 0; - } elseif (in_array("string", $types)) { - $this->data->$key = ""; - } elseif (in_array("object", $types)) { - $this->data->$key = new StdClass; - } elseif (in_array("array", $types)) { - $this->data->$key = array(); - } else { - return FALSE; - } - } - return TRUE; - } - return FALSE; - } -} - -class Jsv4Error extends Exception { - public $code; - public $dataPath; - public $schemaPath; - public $message; - - public function __construct($code, $dataPath, $schemaPath, $errorMessage, $subResults=NULL) { - parent::__construct($errorMessage); - $this->code = $code; - $this->dataPath = $dataPath; - $this->schemaPath = $schemaPath; - $this->message = $errorMessage; - if ($subResults) { - $this->subResults = $subResults; - } - } - - public function prefix($dataPrefix, $schemaPrefix) { - return new Jsv4Error($this->code, $dataPrefix.$this->dataPath, $schemaPrefix.$this->schemaPath, $this->message); - } -} - -?> diff --git a/schema-store.php b/schema-store.php deleted file mode 100644 index d25be7f..0000000 --- a/schema-store.php +++ /dev/null @@ -1,195 +0,0 @@ -$part)) { - $value =& $value->$part; - } else if ($strict) { - throw new Exception("Path does not exist: $path"); - } else { - return NULL; - } - } else if ($strict) { - throw new Exception("Path does not exist: $path"); - } else { - return NULL; - } - } - return $value; - } - - private static function isNumericArray($array) { - $count = count($array); - for ($i = 0; $i < $count; $i++) { - if (!isset($array[$i])) { - return FALSE; - } - } - return TRUE; - } - - private static function resolveUrl($base, $relative) { - if (parse_url($relative, PHP_URL_SCHEME) != '') { - // It's already absolute - return $relative; - } - $baseParts = parse_url($base); - if ($relative[0] == "?") { - $baseParts['query'] = substr($relative, 1); - unset($baseParts['fragment']); - } else if ($relative[0] == "#") { - $baseParts['fragment'] = substr($relative, 1); - } else if ($relative[0] == "/") { - if ($relative[1] == "/") { - return $baseParts['scheme'].$relative; - } - $baseParts['path'] = $relative; - unset($baseParts['query']); - unset($baseParts['fragment']); - } else { - $basePathParts = explode("/", $baseParts['path']); - $relativePathParts = explode("/", $relative); - array_pop($basePathParts); - while (count($relativePathParts)) { - if ($relativePathParts[0] == "..") { - array_shift($relativePathParts); - if (count($basePathParts)) { - array_pop($basePathParts); - } - } else if ($relativePathParts[0] == ".") { - array_shift($relativePathParts); - } else { - array_push($basePathParts, array_shift($relativePathParts)); - } - } - $baseParts['path'] = implode("/", $basePathParts); - if ($baseParts['path'][0] != '/') { - $baseParts['path'] = "/".$baseParts['path']; - } - } - - $result = ""; - if (isset($baseParts['scheme'])) { - $result .= $baseParts['scheme']."://"; - if (isset($baseParts['user'])) { - $result .= ":".$baseParts['user']; - if (isset($baseParts['pass'])) { - $result .= ":".$baseParts['pass']; - } - $result .= "@"; - } - $result .= $baseParts['host']; - if (isset($baseParts['port'])) { - $result .= ":".$baseParts['port']; - } - } - $result .= $baseParts["path"]; - if (isset($baseParts['query'])) { - $result .= "?".$baseParts['query']; - } - if (isset($baseParts['fragment'])) { - $result .= "#".$baseParts['fragment']; - } - return $result; - } - - private $schemas = array(); - private $refs = array(); - - public function missing() { - return array_keys($this->refs); - } - - public function add($url, $schema, $trusted=FALSE) { - $urlParts = explode("#", $url); - $baseUrl = array_shift($urlParts); - $fragment = urldecode(implode("#", $urlParts)); - - $trustBase = explode("?", $baseUrl); - $trustBase = $trustBase[0]; - - $this->schemas[$url] =& $schema; - $this->normalizeSchema($url, $schema, $trusted ? TRUE : $trustBase); - if ($fragment == "") { - $this->schemas[$baseUrl] = $schema; - } - if (isset($this->refs[$baseUrl])) { - foreach ($this->refs[$baseUrl] as $fullUrl => $refSchemas) { - foreach ($refSchemas as &$refSchema) { - $refSchema = $this->get($fullUrl); - } - unset($this->refs[$baseUrl][$fullUrl]); - } - if (count($this->refs[$baseUrl]) == 0) { - unset($this->refs[$baseUrl]); - } - } - } - - private function normalizeSchema($url, &$schema, $trustPrefix = '') { - if (is_array($schema) && !self::isNumericArray($schema)) { - $schema = (object)$schema; - } - if (is_object($schema)) { - if (isset($schema->{'$ref'})) { - $refUrl = $schema->{'$ref'} = self::resolveUrl($url, $schema->{'$ref'}); - if ($refSchema = $this->get($refUrl)) { - $schema = $refSchema; - return; - } else { - $urlParts = explode("#", $refUrl); - $baseUrl = array_shift($urlParts); - $fragment = urldecode(implode("#", $urlParts)); - $this->refs[$baseUrl][$refUrl][] =& $schema; - } - } else if (isset($schema->id) && is_string($schema->id)) { - $schema->id = $url = self::resolveUrl($url, $schema->id); - $regex = '/^'.preg_quote($trustPrefix, '/').'(?:[#\/?].*)?$/'; - if (($trustPrefix === TRUE || preg_match($regex, $schema->id)) && !isset($this->schemas[$schema->id])) { - $this->add($schema->id, $schema); - } - } - foreach ($schema as $key => &$value) { - if ($key != "enum") { - self::normalizeSchema($url, $value, $trustPrefix); - } - } - } else if (is_array($schema)) { - foreach ($schema as &$value) { - self::normalizeSchema($url, $value, $trustPrefix); - } - } - } - - public function get($url) { - if (isset($this->schemas[$url])) { - return $this->schemas[$url]; - } - $urlParts = explode("#", $url); - $baseUrl = array_shift($urlParts); - $fragment = urldecode(implode("#", $urlParts)); - if (isset($this->schemas[$baseUrl])) { - $schema = $this->schemas[$baseUrl]; - if ($schema && $fragment == "" || $fragment[0] == "/") { - $schema = self::pointerGet($schema, $fragment); - $this->add($url, $schema); - return $schema; - } - } - } -} - -?> diff --git a/src/Jsv4/Jsv4.php b/src/Jsv4/Jsv4.php new file mode 100644 index 0000000..a38caf1 --- /dev/null +++ b/src/Jsv4/Jsv4.php @@ -0,0 +1,557 @@ +valid; + } + + public static function coerce($data, $schema) + { + if (is_object($data) || is_array($data)) { + $data = unserialize(serialize($data)); + } + $result = new Jsv4($data, $schema, false, true); + if ($result->valid) { + $result->value = $result->data; + } + return $result; + } + + public static function pointerJoin($parts) + { + $result = ""; + foreach ($parts as $part) { + $part = str_replace("~", "~0", $part); + $part = str_replace("/", "~1", $part); + $result .= "/" . $part; + } + return $result; + } + + public static function recursiveEqual($a, $b) + { + if (is_object($a)) { + if (!is_object($b)) { + return false; + } + foreach ($a as $key => $value) { + if (!isset($b->$key)) { + return false; + } + if (!self::recursiveEqual($value, $b->$key)) { + return false; + } + } + foreach ($b as $key => $value) { + if (!isset($a->$key)) { + return false; + } + } + return true; + } + if (is_array($a)) { + if (!is_array($b)) { + return false; + } + foreach ($a as $key => $value) { + if (!isset($b[$key])) { + return false; + } + if (!self::recursiveEqual($value, $b[$key])) { + return false; + } + } + foreach ($b as $key => $value) { + if (!isset($a[$key])) { + return false; + } + } + return true; + } + return $a === $b; + } + + private function __construct(&$data, $schema, $firstErrorOnly = false, $coerce = false) + { + $this->data = & $data; + $this->schema = & $schema; + $this->firstErrorOnly = $firstErrorOnly; + $this->coerce = $coerce; + $this->valid = true; + $this->errors = array(); + + try { + $this->checkTypes(); + $this->checkEnum(); + $this->checkObject(); + $this->checkArray(); + $this->checkString(); + $this->checkNumber(); + $this->checkComposite(); + } catch (Jsv4Error $e) { + } + } + + private function fail($code, $dataPath, $schemaPath, $errorMessage, $subErrors = null) + { + $this->valid = false; + $error = new Jsv4Error($code, $dataPath, $schemaPath, $errorMessage, $subErrors); + $this->errors[] = $error; + if ($this->firstErrorOnly) { + throw $error; + } + } + + private function subResult(&$data, $schema, $allowCoercion = true) + { + return new Jsv4($data, $schema, $this->firstErrorOnly, $allowCoercion && $this->coerce); + } + + private function includeSubResult($subResult, $dataPrefix, $schemaPrefix) + { + if (!$subResult->valid) { + $this->valid = false; + foreach ($subResult->errors as $error) { + $this->errors[] = $error->prefix($dataPrefix, $schemaPrefix); + } + } + } + + private function checkTypes() + { + if (isset($this->schema->type)) { + $types = $this->schema->type; + if (!is_array($types)) { + $types = array($types); + } + foreach ($types as $type) { + if ($type == "object" && is_object($this->data)) { + return; + } elseif ($type == "array" && is_array($this->data)) { + return; + } elseif ($type == "string" && is_string($this->data)) { + return; + } elseif ($type == "number" && !is_string($this->data) && is_numeric($this->data)) { + return; + } elseif ($type == "integer" && is_int($this->data)) { + return; + } elseif ($type == "boolean" && is_bool($this->data)) { + return; + } elseif ($type == "null" && $this->data === null) { + return; + } + } + + if ($this->coerce) { + foreach ($types as $type) { + if ($type == "number") { + if (is_numeric($this->data)) { + $this->data = (float) $this->data; + return; + } elseif (is_bool($this->data)) { + $this->data = $this->data ? 1 : 0; + return; + } + } elseif ($type == "integer") { + if ((int) $this->data == $this->data) { + $this->data = (int) $this->data; + return; + } + } elseif ($type == "string") { + if (is_numeric($this->data)) { + $this->data = "" . $this->data; + return; + } elseif (is_bool($this->data)) { + $this->data = ($this->data) ? "true" : "false"; + return; + } elseif (is_null($this->data)) { + $this->data = ""; + return; + } + } elseif ($type == "boolean") { + if (is_numeric($this->data)) { + $this->data = ($this->data != "0"); + return; + } elseif ($this->data == "yes" || $this->data == "true") { + $this->data = true; + return; + } elseif ($this->data == "no" || $this->data == "false") { + $this->data = false; + return; + } elseif ($this->data == null) { + $this->data = false; + return; + } + } + } + } + + $type = strtolower(gettype($this->data)); + if ($type == "double") { + $type = ((int) $this->data == $this->data) ? "integer" : "number"; + } elseif ($type == "null") { + $type = "null"; + } + $this->fail(self::JSV4_INVALID_TYPE, "", "/type", "Invalid type: $type"); + } + } + + private function checkEnum() + { + if (isset($this->schema->enum)) { + foreach ($this->schema->enum as $option) { + if (self::recursiveEqual($this->data, $option)) { + return; + } + } + $this->fail(self::JSV4_ENUM_MISMATCH, "", "/enum", "Value must be one of the enum options"); + } + } + + private function checkObject() + { + if (!is_object($this->data)) { + return; + } + if (isset($this->schema->required)) { + foreach ($this->schema->required as $index => $key) { + if (!array_key_exists($key, (array) $this->data)) { + if ($this->coerce && $this->createValueForProperty($key)) { + continue; + } + $this->fail(self::JSV4_OBJECT_REQUIRED, "", "/required/{$index}", "Missing required property: {$key}"); + } + } + } + $checkedProperties = array(); + if (isset($this->schema->properties)) { + foreach ($this->schema->properties as $key => $subSchema) { + $checkedProperties[$key] = true; + if (array_key_exists($key, (array) $this->data)) { + $subResult = $this->subResult($this->data->$key, $subSchema); + $this->includeSubResult( + $subResult, + self::pointerJoin(array($key)), + self::pointerJoin(array("properties", $key)) + ); + } + } + } + if (isset($this->schema->patternProperties)) { + foreach ($this->schema->patternProperties as $pattern => $subSchema) { + foreach ($this->data as $key => &$subValue) { + if (preg_match("/" . str_replace("/", "\\/", $pattern) . "/", $key)) { + $checkedProperties[$key] = true; + $subResult = $this->subResult($this->data->$key, $subSchema); + $this->includeSubResult( + $subResult, + self::pointerJoin(array($key)), + self::pointerJoin(array("patternProperties", $pattern)) + ); + } + } + } + } + if (isset($this->schema->additionalProperties)) { + $additionalProperties = $this->schema->additionalProperties; + foreach ($this->data as $key => &$subValue) { + if (isset($checkedProperties[$key])) { + continue; + } + if (!$additionalProperties) { + $this->fail(self::JSV4_OBJECT_ADDITIONAL_PROPERTIES, self::pointerJoin(array($key)), "/additionalProperties", "Additional properties not allowed"); + } elseif (is_object($additionalProperties)) { + $subResult = $this->subResult($subValue, $additionalProperties); + $this->includeSubResult($subResult, self::pointerJoin(array($key)), "/additionalProperties"); + } + } + } + if (isset($this->schema->dependencies)) { + foreach ($this->schema->dependencies as $key => $dep) { + if (!isset($this->data->$key)) { + continue; + } + if (is_object($dep)) { + $subResult = $this->subResult($this->data, $dep); + $this->includeSubResult($subResult, "", self::pointerJoin(array("dependencies", $key))); + } elseif (is_array($dep)) { + foreach ($dep as $index => $depKey) { + if (!isset($this->data->$depKey)) { + $this->fail(self::JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key, $index)), "Property $key depends on $depKey"); + } + } + } else { + if (!isset($this->data->$dep)) { + $this->fail(self::JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key)), "Property $key depends on $dep"); + } + } + } + } + if (isset($this->schema->minProperties)) { + if (count(get_object_vars($this->data)) < $this->schema->minProperties) { + $this->fail(self::JSV4_OBJECT_PROPERTIES_MINIMUM, "", "/minProperties", ($this->schema->minProperties == 1) ? "Object cannot be empty" : "Object must have at least {$this->schema->minProperties} defined properties"); + } + } + if (isset($this->schema->maxProperties)) { + if (count(get_object_vars($this->data)) > $this->schema->maxProperties) { + $this->fail(self::JSV4_OBJECT_PROPERTIES_MAXIMUM, "", "/minProperties", ($this->schema->maxProperties == 1) ? "Object must have at most one defined property" : "Object must have at most {$this->schema->maxProperties} defined properties"); + } + } + } + + private function checkArray() + { + if (!is_array($this->data)) { + return; + } + if (isset($this->schema->items)) { + $items = $this->schema->items; + if (is_array($items)) { + foreach ($this->data as $index => &$subData) { + if (!is_numeric($index)) { + throw new Exception("Arrays must only be numerically-indexed"); + } + if (isset($items[$index])) { + $subResult = $this->subResult($subData, $items[$index]); + $this->includeSubResult($subResult, "/{$index}", "/items/{$index}"); + } elseif (isset($this->schema->additionalItems)) { + $additionalItems = $this->schema->additionalItems; + if (!$additionalItems) { + $this->fail(self::JSV4_ARRAY_ADDITIONAL_ITEMS, "/{$index}", "/additionalItems", "Additional items (index " . count($items) . " or more) are not allowed"); + } elseif ($additionalItems !== true) { + $subResult = $this->subResult($subData, $additionalItems); + $this->includeSubResult($subResult, "/{$index}", "/additionalItems"); + } + } + } + } else { + foreach ($this->data as $index => &$subData) { + if (!is_numeric($index)) { + throw new Exception("Arrays must only be numerically-indexed"); + } + $subResult = $this->subResult($subData, $items); + $this->includeSubResult($subResult, "/{$index}", "/items"); + } + } + } + if (isset($this->schema->minItems)) { + if (count($this->data) < $this->schema->minItems) { + $this->fail(self::JSV4_ARRAY_LENGTH_SHORT, "", "/minItems", "Array is too short (must have at least {$this->schema->minItems} items)"); + } + } + if (isset($this->schema->maxItems)) { + if (count($this->data) > $this->schema->maxItems) { + $this->fail(self::JSV4_ARRAY_LENGTH_LONG, "", "/maxItems", "Array is too long (must have at most {$this->schema->maxItems} items)"); + } + } + if (isset($this->schema->uniqueItems)) { + foreach ($this->data as $indexA => $itemA) { + foreach ($this->data as $indexB => $itemB) { + if ($indexA < $indexB) { + if (self::recursiveEqual($itemA, $itemB)) { + $this->fail(self::JSV4_ARRAY_UNIQUE, "", "/uniqueItems", "Array items must be unique (items $indexA and $indexB)"); + break 2; + } + } + } + } + } + } + + private function checkString() + { + if (!is_string($this->data)) { + return; + } + if (isset($this->schema->minLength)) { + if (strlen($this->data) < $this->schema->minLength) { + $this->fail(self::JSV4_STRING_LENGTH_SHORT, "", "/minLength", "String must be at least {$this->schema->minLength} characters long"); + } + } + if (isset($this->schema->maxLength)) { + if (strlen($this->data) > $this->schema->maxLength) { + $this->fail(self::JSV4_STRING_LENGTH_LONG, "", "/maxLength", "String must be at most {$this->schema->maxLength} characters long"); + } + } + if (isset($this->schema->pattern)) { + $pattern = $this->schema->pattern; + $patternFlags = isset($this->schema->patternFlags) ? $this->schema->patternFlags : ''; + $result = preg_match("/" . str_replace("/", "\\/", $pattern) . "/" . $patternFlags, $this->data); + if ($result === 0) { + $this->fail(self::JSV4_STRING_PATTERN, "", "/pattern", "String does not match pattern: $pattern"); + } + } + } + + private function checkNumber() + { + if (is_string($this->data) || !is_numeric($this->data)) { + return; + } + if (isset($this->schema->multipleOf)) { + if (fmod($this->data / $this->schema->multipleOf, 1) != 0) { + $this->fail(self::JSV4_NUMBER_MULTIPLE_OF, "", "/multipleOf", "Number must be a multiple of {$this->schema->multipleOf}"); + } + } + if (isset($this->schema->minimum)) { + $minimum = $this->schema->minimum; + if (isset($this->schema->exclusiveMinimum) && $this->schema->exclusiveMinimum) { + if ($this->data <= $minimum) { + $this->fail(self::JSV4_NUMBER_MINIMUM_EXCLUSIVE, "", "", "Number must be > $minimum"); + } + } else { + if ($this->data < $minimum) { + $this->fail(self::JSV4_NUMBER_MINIMUM, "", "/minimum", "Number must be >= $minimum"); + } + } + } + if (isset($this->schema->maximum)) { + $maximum = $this->schema->maximum; + if (isset($this->schema->exclusiveMaximum) && $this->schema->exclusiveMaximum) { + if ($this->data >= $maximum) { + $this->fail(self::JSV4_NUMBER_MAXIMUM_EXCLUSIVE, "", "", "Number must be < $maximum"); + } + } else { + if ($this->data > $maximum) { + $this->fail(self::JSV4_NUMBER_MAXIMUM, "", "/maximum", "Number must be <= $maximum"); + } + } + } + } + + private function checkComposite() + { + if (isset($this->schema->allOf)) { + foreach ($this->schema->allOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, false); + $this->includeSubResult($subResult, "", "/allOf/" . (int) $index); + } + } + if (isset($this->schema->anyOf)) { + $failResults = array(); + foreach ($this->schema->anyOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, false); + if ($subResult->valid) { + return; + } + $failResults[] = $subResult; + } + $this->fail(self::JSV4_ANY_OF_MISSING, "", "/anyOf", "Value must satisfy at least one of the options", $failResults); + } + if (isset($this->schema->oneOf)) { + $failResults = array(); + $successIndex = null; + foreach ($this->schema->oneOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, false); + if ($subResult->valid) { + if ($successIndex === null) { + $successIndex = $index; + } else { + $this->fail(self::JSV4_ONE_OF_MULTIPLE, "", "/oneOf", "Value satisfies more than one of the options ($successIndex and $index)"); + } + continue; + } + $failResults[] = $subResult; + } + if ($successIndex === null) { + $this->fail(self::JSV4_ONE_OF_MISSING, "", "/oneOf", "Value must satisfy one of the options", $failResults); + } + } + if (isset($this->schema->not)) { + $subResult = $this->subResult($this->data, $this->schema->not, false); + if ($subResult->valid) { + $this->fail(self::JSV4_NOT_PASSED, "", "/not", "Value satisfies prohibited schema"); + } + } + } + + private function createValueForProperty($key) + { + $schema = null; + if (isset($this->schema->properties->$key)) { + $schema = $this->schema->properties->$key; + } else if (isset($this->schema->patternProperties)) { + foreach ($this->schema->patternProperties as $pattern => $subSchema) { + if (preg_match("/" . str_replace("/", "\\/", $pattern) . "/", $key)) { + $schema = $subSchema; + break; + } + } + } + if (!$schema && isset($this->schema->additionalProperties)) { + $schema = $this->schema->additionalProperties; + } + if ($schema) { + if (isset($schema->default)) { + $this->data->$key = unserialize(serialize($schema->default)); + return true; + } + if (isset($schema->type)) { + $types = is_array($schema->type) ? $schema->type : array($schema->type); + if (in_array("null", $types)) { + $this->data->$key = null; + } elseif (in_array("boolean", $types)) { + $this->data->$key = true; + } elseif (in_array("integer", $types) || in_array("number", $types)) { + $this->data->$key = 0; + } elseif (in_array("string", $types)) { + $this->data->$key = ""; + } elseif (in_array("object", $types)) { + $this->data->$key = new \StdClass; + } elseif (in_array("array", $types)) { + $this->data->$key = array(); + } else { + return false; + } + } + return true; + } + return false; + } +} diff --git a/src/Jsv4/Jsv4Error.php b/src/Jsv4/Jsv4Error.php new file mode 100644 index 0000000..30ef87b --- /dev/null +++ b/src/Jsv4/Jsv4Error.php @@ -0,0 +1,33 @@ +code = $code; + $this->dataPath = $dataPath; + $this->schemaPath = $schemaPath; + $this->message = $errorMessage; + if ($subResults) { + $this->subResults = $subResults; + } + } + + public function prefix($dataPrefix, $schemaPrefix) + { + return new Jsv4Error( + $this->code, + $dataPrefix . $this->dataPath, + $schemaPrefix . $this->schemaPath, + $this->message + ); + } +} diff --git a/src/Jsv4/SchemaStore.php b/src/Jsv4/SchemaStore.php new file mode 100644 index 0000000..5e9078c --- /dev/null +++ b/src/Jsv4/SchemaStore.php @@ -0,0 +1,203 @@ +$part)) { + $value = & $value->$part; + } elseif ($strict) { + throw new Exception("Path does not exist: $path"); + } else { + return null; + } + } elseif ($strict) { + throw new Exception("Path does not exist: $path"); + } else { + return null; + } + } + return $value; + } + + private static function isNumericArray($array) + { + $count = count($array); + for ($i = 0; $i < $count; $i++) { + if (!isset($array[$i])) { + return false; + } + } + return true; + } + + private static function resolveUrl($base, $relative) + { + if (parse_url($relative, PHP_URL_SCHEME) != '') { + // It's already absolute + return $relative; + } + $baseParts = parse_url($base); + if ($relative[0] == "?") { + $baseParts['query'] = substr($relative, 1); + unset($baseParts['fragment']); + } elseif ($relative[0] == "#") { + $baseParts['fragment'] = substr($relative, 1); + } elseif ($relative[0] == "/") { + if ($relative[1] == "/") { + return $baseParts['scheme'] . $relative; + } + $baseParts['path'] = $relative; + unset($baseParts['query']); + unset($baseParts['fragment']); + } else { + $basePathParts = explode("/", $baseParts['path']); + $relativePathParts = explode("/", $relative); + array_pop($basePathParts); + while (count($relativePathParts)) { + if ($relativePathParts[0] == "..") { + array_shift($relativePathParts); + if (count($basePathParts)) { + array_pop($basePathParts); + } + } elseif ($relativePathParts[0] == ".") { + array_shift($relativePathParts); + } else { + array_push($basePathParts, array_shift($relativePathParts)); + } + } + $baseParts['path'] = implode("/", $basePathParts); + if ($baseParts['path'][0] != '/') { + $baseParts['path'] = "/" . $baseParts['path']; + } + } + + $result = ""; + if (isset($baseParts['scheme'])) { + $result .= $baseParts['scheme'] . "://"; + if (isset($baseParts['user'])) { + $result .= ":" . $baseParts['user']; + if (isset($baseParts['pass'])) { + $result .= ":" . $baseParts['pass']; + } + $result .= "@"; + } + $result .= $baseParts['host']; + if (isset($baseParts['port'])) { + $result .= ":" . $baseParts['port']; + } + } + $result .= $baseParts["path"]; + if (isset($baseParts['query'])) { + $result .= "?" . $baseParts['query']; + } + if (isset($baseParts['fragment'])) { + $result .= "#" . $baseParts['fragment']; + } + return $result; + } + + private $schemas = array(); + private $refs = array(); + + public function missing() + { + return array_keys($this->refs); + } + + public function add($url, $schema, $trusted = false) + { + $urlParts = explode("#", $url); + $baseUrl = array_shift($urlParts); + $fragment = urldecode(implode("#", $urlParts)); + + $trustBase = explode("?", $baseUrl); + $trustBase = $trustBase[0]; + + $this->schemas[$url] = & $schema; + $this->normalizeSchema($url, $schema, $trusted ? true : $trustBase); + if ($fragment == "") { + $this->schemas[$baseUrl] = $schema; + } + if (isset($this->refs[$baseUrl])) { + foreach ($this->refs[$baseUrl] as $fullUrl => $refSchemas) { + foreach ($refSchemas as &$refSchema) { + $refSchema = $this->get($fullUrl); + } + unset($this->refs[$baseUrl][$fullUrl]); + } + if (count($this->refs[$baseUrl]) == 0) { + unset($this->refs[$baseUrl]); + } + } + } + + private function normalizeSchema($url, &$schema, $trustPrefix = '') + { + if (is_array($schema) && !self::isNumericArray($schema)) { + $schema = (object) $schema; + } + if (is_object($schema)) { + if (isset($schema->{'$ref'})) { + $refUrl = $schema->{'$ref'} = self::resolveUrl($url, $schema->{'$ref'}); + if ($refSchema = $this->get($refUrl)) { + $schema = $refSchema; + return; + } else { + $urlParts = explode("#", $refUrl); + $baseUrl = array_shift($urlParts); + $fragment = urldecode(implode("#", $urlParts)); + $this->refs[$baseUrl][$refUrl][] = & $schema; + } + } elseif (isset($schema->id) && is_string($schema->id)) { + $schema->id = $url = self::resolveUrl($url, $schema->id); + $regex = '/^' . preg_quote($trustPrefix, '/') . '(?:[#\/?].*)?$/'; + if (($trustPrefix === true || preg_match($regex, $schema->id)) && !isset($this->schemas[$schema->id])) { + $this->add($schema->id, $schema); + } + } + foreach ($schema as $key => &$value) { + if ($key != "enum") { + self::normalizeSchema($url, $value, $trustPrefix); + } + } + } elseif (is_array($schema)) { + foreach ($schema as &$value) { + self::normalizeSchema($url, $value, $trustPrefix); + } + } + } + + public function get($url) + { + if (isset($this->schemas[$url])) { + return $this->schemas[$url]; + } + $urlParts = explode("#", $url); + $baseUrl = array_shift($urlParts); + $fragment = urldecode(implode("#", $urlParts)); + if (isset($this->schemas[$baseUrl])) { + $schema = $this->schemas[$baseUrl]; + if ($schema && $fragment == "" || $fragment[0] == "/") { + $schema = self::pointerGet($schema, $fragment); + $this->add($url, $schema); + return $schema; + } + } + } +} diff --git a/test-utils.php b/test-utils.php deleted file mode 100644 index c35f645..0000000 --- a/test-utils.php +++ /dev/null @@ -1,85 +0,0 @@ - $value) { - if (!isset($b->$key)) { - return FALSE; - } - if (!recursiveEqual($value, $b->$key)) { - return FALSE; - } - } - foreach ($b as $key => $value) { - if (!isset($a->$key)) { - return FALSE; - } - } - return TRUE; - } - if (is_array($a)) { - if (!is_array($b)) { - return FALSE; - } - foreach ($a as $key => $value) { - if (!isset($b[$key])) { - return FALSE; - } - if (!recursiveEqual($value, $b[$key])) { - return FALSE; - } - } - foreach ($b as $key => $value) { - if (!isset($a[$key])) { - return FALSE; - } - } - return TRUE; - } - return $a === $b; -} - -function pointerGet(&$value, $path="", $strict=FALSE) { - if ($path == "") { - return $value; - } else if ($path[0] != "/") { - throw new Exception("Invalid path: $path"); - } - $parts = explode("/", $path); - array_shift($parts); - foreach ($parts as $part) { - $part = str_replace("~1", "/", $part); - $part = str_replace("~0", "~", $part); - if (is_array($value) && is_numeric($part)) { - $value =& $value[$part]; - } else if (is_object($value)) { - if (isset($value->$part)) { - $value =& $value->$part; - } else if ($strict) { - throw new Exception("Path does not exist: $path"); - } else { - return NULL; - } - } else if ($strict) { - throw new Exception("Path does not exist: $path"); - } else { - return NULL; - } - } - return $value; -} - -function pointerJoin($parts) { - $result = ""; - foreach ($parts as $part) { - $part = str_replace("~", "~0", $part); - $part = str_replace("/", "~1", $part); - $result .= "/".$part; - } - return $result; -} - -?> \ No newline at end of file diff --git a/test.php b/test.php deleted file mode 100644 index a3fc508..0000000 --- a/test.php +++ /dev/null @@ -1,129 +0,0 @@ -method == "validate") { - $result = Jsv4::validate($test->data, $test->schema); - } else if ($test->method == "isValid") { - $result = Jsv4::isValid($test->data, $test->schema); - } else if ($test->method == "coerce") { - $result = Jsv4::coerce($test->data, $test->schema); - } else { - $failedTests[$key][] = ("Unknown method: {$test->method}"); - return; - } - if (is_object($test->result)) { - foreach ($test->result as $path => $expectedValue) { - $actualValue = pointerGet($result, $path, TRUE); - if (!recursiveEqual($actualValue, $expectedValue)) { - $failedTests[$key][] = "$path does not match - should be:\n ".json_encode($expectedValue)."\nwas:\n ".json_encode($actualValue); - } - } - } else { - if (!recursiveEqual($test->result, $result)) { - $failedTests[$key][] = "$path does not match - should be:\n ".json_encode($test->result)."\nwas:\n ".json_encode($result); - } - } - } catch (Exception $e) { - $failedTests[$key][] = $e->getMessage(); - $failedTests[$key][] .= " ".str_replace("\n", "\n ", $e->getTraceAsString()); - } -} - -function runPhpTest($key, $filename) { - global $totalTestCount; - global $failedTests; - $totalTestCount++; - - try { - include_once $filename; - } catch (Exception $e) { - $failedTests[$key][] = $e->getMessage(); - $failedTests[$key][] .= " ".str_replace("\n", "\n ", $e->getTraceAsString()); - } -} - -function runTests($directory, $indent="") { - global $failedTests; - if ($directory[strlen($directory) - 1] != "/") { - $directory .= "/"; - } - $baseName = basename($directory); - - $testCount = 0; - $testFileCount = 0; - - $entries = scandir($directory); - foreach ($entries as $entry) { - $filename = $directory.$entry; - if (stripos($entry, '.php') && is_file($filename)) { - $key = substr($filename, 0, strlen($filename) - 4); - runPhpTest($key, $filename); - } else if (stripos($entry, '.json') && is_file($filename)) { - $testFileCount++; - $tests = json_decode(file_get_contents($filename)); - if ($tests == NULL) { - $testCount++; - $failedTests[$filename] = "Error parsing JSON"; - continue; - } - if (!is_array($tests)) { - $tests = array($tests); - } - foreach ($tests as $index => $test) { - $key = substr($filename, 0, strlen($filename) - 5); - if (isset($test->title)) { - $key .= ": {$test->title}"; - } else { - $key .= ": #{$index}"; - } - runJsonTest($key, $test); - $testCount++; - } - } - } - if ($testCount) { - echo "{$indent}{$baseName}/ \t({$testCount} tests in {$testFileCount} files)\n"; - } else { - echo "{$indent}{$baseName}/\n"; - } - foreach ($entries as $entry) { - $filename = $directory.$entry; - if (strpos($entry, '.') === FALSE && is_dir($filename)) { - runTests($filename, $indent.str_repeat(" ", strlen($baseName) + 1)); - } - } -} - -runTests("tests/"); - -echo "\n\n"; -if (count($failedTests) == 0) { - echo "Passed all {$totalTestCount} tests\n"; -} else { - echo "Failed ".count($failedTests)."/{$totalTestCount} tests\n"; - foreach ($failedTests as $key => $failedTest) { - if (is_array($failedTest)) { - $failedTest = implode("\n", $failedTest); - } - echo "\n"; - echo "FAILED $key:\n"; - echo str_repeat("-", strlen($key) + 10)."\n"; - echo " | ".str_replace("\n", "\n | ", $failedTest)."\n"; - } -} - -?> \ No newline at end of file diff --git a/tests/03 - schema store/02 - add using id.php b/tests/03 - schema store/02 - add using id.php deleted file mode 100644 index 718f2c3..0000000 --- a/tests/03 - schema store/02 - add using id.php +++ /dev/null @@ -1,73 +0,0 @@ -add($url, $schema); - -if (!recursiveEqual($store->get($url."#foo"), $schema->properties->foo)) { - throw new Exception("#foo not found"); -} - -if (!recursiveEqual($store->get($url."?baz=1"), $schema->properties->baz)) { - throw new Exception("?baz=1 not found"); -} - -if (!recursiveEqual($store->get($url."/foobar"), $schema->properties->foobar)) { - throw new Exception("/foobar not found"); -} - -if (!recursiveEqual($store->get($url."/foo#bar"), $schema->properties->nestedSchema->nested)) { - throw new Exception("/foo#bar not found"); -} - -if ($store->get($urlBase."bar")) { - throw new Exception("/bar should not be indexed, as it should not be trusted"); -} - -if ($store->get($url."-foo")) { - throw new Exception("/test-schema-foo should not be indexed, as it should not be trusted"); -} - -if ($store->get("http://somewhere-else.com/test-schema")) { - throw new Exception("http://somewhere-else.com/test-schema should not be indexed, as it should not be trusted"); -} - -$store->add($url, $schema, TRUE); - -if (!recursiveEqual($store->get($urlBase."bar"), $schema->properties->bar)) { - throw new Exception("/bar not found"); -} - -?> \ No newline at end of file diff --git a/tests/03 - schema store/03 - references.php b/tests/03 - schema store/03 - references.php deleted file mode 100644 index ca6f5b6..0000000 --- a/tests/03 - schema store/03 - references.php +++ /dev/null @@ -1,87 +0,0 @@ -add($url, $schema); -$schema = $store->get($url); -if ($schema->properties->foo != $schema->definitions->foo) { - throw new Exception('$ref was not resolved'); -} - -// Add external $ref, and don't resolve it -// While we're at it, use an array, not an object -$schema = array( - "title" => "Test schema 2", - "properties" => array( - "foo" => array('$ref' => "somewhere-else") - ) -); -$store->add($urlBase."test-schema-2", $schema); -$schema = $store->get($urlBase."test-schema-2"); -if (!$schema->properties->foo->{'$ref'}) { - throw new Exception('$ref should still exist'); -} -if (!recursiveEqual($store->missing(), array($urlBase."somewhere-else"))) { - throw new Exception('$store->missing() is not correct: '.json_encode($store->missing()).' is not '.json_encode(array($urlBase."somewhere-else"))); -} - -$otherSchema = json_decode('{ - "title": "Somewhere else", - "items": [ - {"$ref": "'.$urlBase."test-schema-2".'"} - ] -}'); -$store->add($urlBase."somewhere-else", $otherSchema); -$fooSchema = $schema->properties->foo; -if (property_exists($fooSchema, '$ref')) { - throw new Exception('$ref should have been resolved'); -} -if ($fooSchema->title != "Somewhere else") { - throw new Exception('$ref does not point to correct place'); -} -if ($fooSchema->items[0]->title != "Test schema 2") { - throw new Exception('$ref in somewhere-else was not resolved'); -} -if (count($store->missing())) { - throw new Exception('There should be no more missing schemas'); -} - -// Add external $ref twice -$schema = json_decode('{ - "title": "Test schema 3 a", - "properties": { - "foo1": {"$ref": "'.$urlBase.'test-schema-3-b#/foo"}, - "foo2": {"$ref": "'.$urlBase.'test-schema-3-b#/foo"} - } -}'); -$store->add($urlBase."test-schema-3-a", $schema); -$schema = json_decode('{ - "title": "Test schema 3 b", - "foo": {"type": "object"} -}'); -$schema = $store->add($urlBase."test-schema-3-b", $schema); -$schema = $store->get($urlBase."test-schema-3-a"); -if (property_exists($schema->properties->foo1, '$ref')) { - throw new Exception('$ref was not resolved for foo1'); -} -if (property_exists($schema->properties->foo2, '$ref')) { - throw new Exception('$ref was not resolved for foo2'); -} -?> \ No newline at end of file diff --git a/tests/01 - pure validation/00 - API/01 - validate.json b/tests/cases/01 - pure validation/00 - API/01 - validate.json similarity index 100% rename from tests/01 - pure validation/00 - API/01 - validate.json rename to tests/cases/01 - pure validation/00 - API/01 - validate.json diff --git a/tests/01 - pure validation/00 - API/02 - isValid.json b/tests/cases/01 - pure validation/00 - API/02 - isValid.json similarity index 100% rename from tests/01 - pure validation/00 - API/02 - isValid.json rename to tests/cases/01 - pure validation/00 - API/02 - isValid.json diff --git a/tests/01 - pure validation/01 - basic constraints/01 - enum.json b/tests/cases/01 - pure validation/01 - basic constraints/01 - enum.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/01 - enum.json rename to tests/cases/01 - pure validation/01 - basic constraints/01 - enum.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/01 - passes all types by default.json b/tests/cases/01 - pure validation/01 - basic constraints/types/01 - passes all types by default.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/01 - passes all types by default.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/01 - passes all types by default.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/02 - object.json b/tests/cases/01 - pure validation/01 - basic constraints/types/02 - object.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/02 - object.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/02 - object.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/03 - array.json b/tests/cases/01 - pure validation/01 - basic constraints/types/03 - array.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/03 - array.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/03 - array.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/04 - string.json b/tests/cases/01 - pure validation/01 - basic constraints/types/04 - string.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/04 - string.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/04 - string.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/05 - number.json b/tests/cases/01 - pure validation/01 - basic constraints/types/05 - number.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/05 - number.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/05 - number.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/06- integer.json b/tests/cases/01 - pure validation/01 - basic constraints/types/06- integer.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/06- integer.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/06- integer.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/07 - boolean.json b/tests/cases/01 - pure validation/01 - basic constraints/types/07 - boolean.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/07 - boolean.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/07 - boolean.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/08 - null.json b/tests/cases/01 - pure validation/01 - basic constraints/types/08 - null.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/08 - null.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/08 - null.json diff --git a/tests/01 - pure validation/01 - basic constraints/types/09 - multiple types.json b/tests/cases/01 - pure validation/01 - basic constraints/types/09 - multiple types.json similarity index 100% rename from tests/01 - pure validation/01 - basic constraints/types/09 - multiple types.json rename to tests/cases/01 - pure validation/01 - basic constraints/types/09 - multiple types.json diff --git a/tests/01 - pure validation/02 - object constraints/01 - properties.json b/tests/cases/01 - pure validation/02 - object constraints/01 - properties.json similarity index 100% rename from tests/01 - pure validation/02 - object constraints/01 - properties.json rename to tests/cases/01 - pure validation/02 - object constraints/01 - properties.json diff --git a/tests/01 - pure validation/02 - object constraints/02 -patternProperties.json b/tests/cases/01 - pure validation/02 - object constraints/02 -patternProperties.json similarity index 100% rename from tests/01 - pure validation/02 - object constraints/02 -patternProperties.json rename to tests/cases/01 - pure validation/02 - object constraints/02 -patternProperties.json diff --git a/tests/01 - pure validation/02 - object constraints/03 - additionalProperties.json b/tests/cases/01 - pure validation/02 - object constraints/03 - additionalProperties.json similarity index 100% rename from tests/01 - pure validation/02 - object constraints/03 - additionalProperties.json rename to tests/cases/01 - pure validation/02 - object constraints/03 - additionalProperties.json diff --git a/tests/01 - pure validation/02 - object constraints/04 - required.json b/tests/cases/01 - pure validation/02 - object constraints/04 - required.json similarity index 100% rename from tests/01 - pure validation/02 - object constraints/04 - required.json rename to tests/cases/01 - pure validation/02 - object constraints/04 - required.json diff --git a/tests/01 - pure validation/02 - object constraints/05 - dependencies.json b/tests/cases/01 - pure validation/02 - object constraints/05 - dependencies.json similarity index 100% rename from tests/01 - pure validation/02 - object constraints/05 - dependencies.json rename to tests/cases/01 - pure validation/02 - object constraints/05 - dependencies.json diff --git a/tests/01 - pure validation/02 - object constraints/06 - min-max properties.json b/tests/cases/01 - pure validation/02 - object constraints/06 - min-max properties.json similarity index 100% rename from tests/01 - pure validation/02 - object constraints/06 - min-max properties.json rename to tests/cases/01 - pure validation/02 - object constraints/06 - min-max properties.json diff --git a/tests/01 - pure validation/03 - array constraints/01 - items.json b/tests/cases/01 - pure validation/03 - array constraints/01 - items.json similarity index 100% rename from tests/01 - pure validation/03 - array constraints/01 - items.json rename to tests/cases/01 - pure validation/03 - array constraints/01 - items.json diff --git a/tests/01 - pure validation/03 - array constraints/02 - additionalItems.json b/tests/cases/01 - pure validation/03 - array constraints/02 - additionalItems.json similarity index 100% rename from tests/01 - pure validation/03 - array constraints/02 - additionalItems.json rename to tests/cases/01 - pure validation/03 - array constraints/02 - additionalItems.json diff --git a/tests/01 - pure validation/03 - array constraints/03 - length.json b/tests/cases/01 - pure validation/03 - array constraints/03 - length.json similarity index 100% rename from tests/01 - pure validation/03 - array constraints/03 - length.json rename to tests/cases/01 - pure validation/03 - array constraints/03 - length.json diff --git a/tests/01 - pure validation/03 - array constraints/04 -unique.json b/tests/cases/01 - pure validation/03 - array constraints/04 -unique.json similarity index 100% rename from tests/01 - pure validation/03 - array constraints/04 -unique.json rename to tests/cases/01 - pure validation/03 - array constraints/04 -unique.json diff --git a/tests/01 - pure validation/04 - string constraints/01 - length.json b/tests/cases/01 - pure validation/04 - string constraints/01 - length.json similarity index 100% rename from tests/01 - pure validation/04 - string constraints/01 - length.json rename to tests/cases/01 - pure validation/04 - string constraints/01 - length.json diff --git a/tests/01 - pure validation/04 - string constraints/02 - pattern.json b/tests/cases/01 - pure validation/04 - string constraints/02 - pattern.json similarity index 100% rename from tests/01 - pure validation/04 - string constraints/02 - pattern.json rename to tests/cases/01 - pure validation/04 - string constraints/02 - pattern.json diff --git a/tests/01 - pure validation/05 - number constraints/01 - multipleOf.json b/tests/cases/01 - pure validation/05 - number constraints/01 - multipleOf.json similarity index 100% rename from tests/01 - pure validation/05 - number constraints/01 - multipleOf.json rename to tests/cases/01 - pure validation/05 - number constraints/01 - multipleOf.json diff --git a/tests/01 - pure validation/05 - number constraints/02 - minimum.json b/tests/cases/01 - pure validation/05 - number constraints/02 - minimum.json similarity index 100% rename from tests/01 - pure validation/05 - number constraints/02 - minimum.json rename to tests/cases/01 - pure validation/05 - number constraints/02 - minimum.json diff --git a/tests/01 - pure validation/05 - number constraints/03 - maximum.json b/tests/cases/01 - pure validation/05 - number constraints/03 - maximum.json similarity index 100% rename from tests/01 - pure validation/05 - number constraints/03 - maximum.json rename to tests/cases/01 - pure validation/05 - number constraints/03 - maximum.json diff --git a/tests/01 - pure validation/06 - composite/01 - allOf.json b/tests/cases/01 - pure validation/06 - composite/01 - allOf.json similarity index 100% rename from tests/01 - pure validation/06 - composite/01 - allOf.json rename to tests/cases/01 - pure validation/06 - composite/01 - allOf.json diff --git a/tests/01 - pure validation/06 - composite/02 - anyOf.json b/tests/cases/01 - pure validation/06 - composite/02 - anyOf.json similarity index 100% rename from tests/01 - pure validation/06 - composite/02 - anyOf.json rename to tests/cases/01 - pure validation/06 - composite/02 - anyOf.json diff --git a/tests/01 - pure validation/06 - composite/03 - oneOf.json b/tests/cases/01 - pure validation/06 - composite/03 - oneOf.json similarity index 100% rename from tests/01 - pure validation/06 - composite/03 - oneOf.json rename to tests/cases/01 - pure validation/06 - composite/03 - oneOf.json diff --git a/tests/01 - pure validation/06 - composite/04 - not.json b/tests/cases/01 - pure validation/06 - composite/04 - not.json similarity index 100% rename from tests/01 - pure validation/06 - composite/04 - not.json rename to tests/cases/01 - pure validation/06 - composite/04 - not.json diff --git a/tests/02 - coercive validation/00 - API/01 - coerce.json b/tests/cases/02 - coercive validation/00 - API/01 - coerce.json similarity index 100% rename from tests/02 - coercive validation/00 - API/01 - coerce.json rename to tests/cases/02 - coercive validation/00 - API/01 - coerce.json diff --git a/tests/02 - coercive validation/01 - simple type juggling.json b/tests/cases/02 - coercive validation/01 - simple type juggling.json similarity index 100% rename from tests/02 - coercive validation/01 - simple type juggling.json rename to tests/cases/02 - coercive validation/01 - simple type juggling.json diff --git a/tests/02 - coercive validation/02 - missing properties.json b/tests/cases/02 - coercive validation/02 - missing properties.json similarity index 100% rename from tests/02 - coercive validation/02 - missing properties.json rename to tests/cases/02 - coercive validation/02 - missing properties.json diff --git a/tests/03 - schema store/01 - fetch and retrieve.php b/tests/cases/03 - schema store/01 - fetch and retrieve.php similarity index 51% rename from tests/03 - schema store/01 - fetch and retrieve.php rename to tests/cases/03 - schema store/01 - fetch and retrieve.php index e637f8f..3d8338f 100644 --- a/tests/03 - schema store/01 - fetch and retrieve.php +++ b/tests/cases/03 - schema store/01 - fetch and retrieve.php @@ -1,6 +1,6 @@ add($url, $schema); if (!recursiveEqual($store->get($url), $schema)) { - throw new Exception("Not equal"); + throw new Exception("Not equal"); } -if (!recursiveEqual($store->get($url."#/title"), $schema->title)) { - throw new Exception("Not equal"); +if (!recursiveEqual($store->get($url . "#/title"), $schema->title)) { + throw new Exception("Not equal"); } - -?> \ No newline at end of file diff --git a/tests/cases/03 - schema store/02 - add using id.php b/tests/cases/03 - schema store/02 - add using id.php new file mode 100644 index 0000000..d32e3a6 --- /dev/null +++ b/tests/cases/03 - schema store/02 - add using id.php @@ -0,0 +1,71 @@ +add($url, $schema); + +if (!recursiveEqual($store->get($url . "#foo"), $schema->properties->foo)) { + throw new Exception("#foo not found"); +} + +if (!recursiveEqual($store->get($url . "?baz=1"), $schema->properties->baz)) { + throw new Exception("?baz=1 not found"); +} + +if (!recursiveEqual($store->get($url . "/foobar"), $schema->properties->foobar)) { + throw new Exception("/foobar not found"); +} + +if (!recursiveEqual($store->get($url . "/foo#bar"), $schema->properties->nestedSchema->nested)) { + throw new Exception("/foo#bar not found"); +} + +if ($store->get($urlBase . "bar")) { + throw new Exception("/bar should not be indexed, as it should not be trusted"); +} + +if ($store->get($url . "-foo")) { + throw new Exception("/test-schema-foo should not be indexed, as it should not be trusted"); +} + +if ($store->get("http://somewhere-else.com/test-schema")) { + throw new Exception("http://somewhere-else.com/test-schema should not be indexed, as it should not be trusted"); +} + +$store->add($url, $schema, true); + +if (!recursiveEqual($store->get($urlBase . "bar"), $schema->properties->bar)) { + throw new Exception("/bar not found"); +} diff --git a/tests/cases/03 - schema store/03 - references.php b/tests/cases/03 - schema store/03 - references.php new file mode 100644 index 0000000..3d956f8 --- /dev/null +++ b/tests/cases/03 - schema store/03 - references.php @@ -0,0 +1,86 @@ +add($url, $schema); +$schema = $store->get($url); +if ($schema->properties->foo != $schema->definitions->foo) { + throw new Exception('$ref was not resolved'); +} + +// Add external $ref, and don't resolve it +// While we're at it, use an array, not an object +$schema = array( + "title" => "Test schema 2", + "properties" => array( + "foo" => array('$ref' => "somewhere-else") + ) +); +$store->add($urlBase . "test-schema-2", $schema); +$schema = $store->get($urlBase . "test-schema-2"); +if (!$schema->properties->foo->{'$ref'}) { + throw new Exception('$ref should still exist'); +} +if (!recursiveEqual($store->missing(), array($urlBase . "somewhere-else"))) { + throw new Exception('$store->missing() is not correct: ' . json_encode($store->missing()) . ' is not ' . json_encode(array($urlBase . "somewhere-else"))); +} + +$otherSchema = json_decode('{ + "title": "Somewhere else", + "items": [ + {"$ref": "' . $urlBase . "test-schema-2" . '"} + ] +}'); +$store->add($urlBase . "somewhere-else", $otherSchema); +$fooSchema = $schema->properties->foo; +if (property_exists($fooSchema, '$ref')) { + throw new Exception('$ref should have been resolved'); +} +if ($fooSchema->title != "Somewhere else") { + throw new Exception('$ref does not point to correct place'); +} +if ($fooSchema->items[0]->title != "Test schema 2") { + throw new Exception('$ref in somewhere-else was not resolved'); +} +if (count($store->missing())) { + throw new Exception('There should be no more missing schemas'); +} + +// Add external $ref twice +$schema = json_decode('{ + "title": "Test schema 3 a", + "properties": { + "foo1": {"$ref": "' . $urlBase . 'test-schema-3-b#/foo"}, + "foo2": {"$ref": "' . $urlBase . 'test-schema-3-b#/foo"} + } +}'); +$store->add($urlBase . "test-schema-3-a", $schema); +$schema = json_decode('{ + "title": "Test schema 3 b", + "foo": {"type": "object"} +}'); +$schema = $store->add($urlBase . "test-schema-3-b", $schema); +$schema = $store->get($urlBase . "test-schema-3-a"); +if (property_exists($schema->properties->foo1, '$ref')) { + throw new Exception('$ref was not resolved for foo1'); +} +if (property_exists($schema->properties->foo2, '$ref')) { + throw new Exception('$ref was not resolved for foo2'); +} diff --git a/tests/test.php b/tests/test.php new file mode 100644 index 0000000..a37e80e --- /dev/null +++ b/tests/test.php @@ -0,0 +1,131 @@ +method == "validate") { + $result = Jsv4::validate($test->data, $test->schema); + } elseif ($test->method == "isValid") { + $result = Jsv4::isValid($test->data, $test->schema); + } elseif ($test->method == "coerce") { + $result = Jsv4::coerce($test->data, $test->schema); + } else { + $failedTests[$key][] = ("Unknown method: {$test->method}"); + return; + } + if (is_object($test->result)) { + foreach ($test->result as $path => $expectedValue) { + $actualValue = pointerGet($result, $path, true); + if (!recursiveEqual($actualValue, $expectedValue)) { + $failedTests[$key][] = "$path does not match - should be:\n " . json_encode($expectedValue) . "\nwas:\n " . json_encode($actualValue); + } + } + } else { + if (!recursiveEqual($test->result, $result)) { + $failedTests[$key][] = "$path does not match - should be:\n " . json_encode($test->result) . "\nwas:\n " . json_encode($result); + } + } + } catch (Exception $e) { + $failedTests[$key][] = $e->getMessage(); + $failedTests[$key][] .= " " . str_replace("\n", "\n ", $e->getTraceAsString()); + } +} + +function runPhpTest($key, $filename) +{ + global $totalTestCount; + global $failedTests; + $totalTestCount++; + + try { + include_once $filename; + } catch (Exception $e) { + $failedTests[$key][] = $e->getMessage(); + $failedTests[$key][] .= " " . str_replace("\n", "\n ", $e->getTraceAsString()); + } +} + +function runTests($directory, $indent = "") +{ + global $failedTests; + if ($directory[strlen($directory) - 1] != "/") { + $directory .= "/"; + } + $baseName = basename($directory); + + $testCount = 0; + $testFileCount = 0; + + $entries = scandir($directory); + foreach ($entries as $entry) { + $filename = $directory . $entry; + if (stripos($entry, '.php') && is_file($filename)) { + $key = substr($filename, 0, strlen($filename) - 4); + runPhpTest($key, $filename); + } elseif (stripos($entry, '.json') && is_file($filename)) { + $testFileCount++; + $tests = json_decode(file_get_contents($filename)); + if ($tests == null) { + $testCount++; + $failedTests[$filename] = "Error parsing JSON"; + continue; + } + if (!is_array($tests)) { + $tests = array($tests); + } + foreach ($tests as $index => $test) { + $key = substr($filename, 0, strlen($filename) - 5); + if (isset($test->title)) { + $key .= ": {$test->title}"; + } else { + $key .= ": #{$index}"; + } + runJsonTest($key, $test); + $testCount++; + } + } + } + if ($testCount) { + echo "{$indent}{$baseName}/ \t({$testCount} tests in {$testFileCount} files)\n"; + } else { + echo "{$indent}{$baseName}/\n"; + } + foreach ($entries as $entry) { + $filename = $directory . $entry; + if (strpos($entry, '.') === false && is_dir($filename)) { + runTests($filename, $indent . str_repeat(" ", strlen($baseName) + 1)); + } + } +} + +runTests("cases/"); + +echo "\n\n"; +if (count($failedTests) == 0) { + echo "Passed all {$totalTestCount} tests\n"; +} else { + echo "Failed " . count($failedTests) . "/{$totalTestCount} tests\n"; + foreach ($failedTests as $key => $failedTest) { + if (is_array($failedTest)) { + $failedTest = implode("\n", $failedTest); + } + echo "\n"; + echo "FAILED $key:\n"; + echo str_repeat("-", strlen($key) + 10) . "\n"; + echo " | " . str_replace("\n", "\n | ", $failedTest) . "\n"; + } +} diff --git a/tests/utils/helpers.php b/tests/utils/helpers.php new file mode 100644 index 0000000..968f483 --- /dev/null +++ b/tests/utils/helpers.php @@ -0,0 +1,86 @@ + $value) { + if (!isset($b->$key)) { + return false; + } + if (!recursiveEqual($value, $b->$key)) { + return false; + } + } + foreach ($b as $key => $value) { + if (!isset($a->$key)) { + return false; + } + } + return true; + } + if (is_array($a)) { + if (!is_array($b)) { + return false; + } + foreach ($a as $key => $value) { + if (!isset($b[$key])) { + return false; + } + if (!recursiveEqual($value, $b[$key])) { + return false; + } + } + foreach ($b as $key => $value) { + if (!isset($a[$key])) { + return false; + } + } + return true; + } + return $a === $b; +} + +function pointerGet(&$value, $path = "", $strict = false) +{ + if ($path == "") { + return $value; + } elseif ($path[0] != "/") { + throw new Exception("Invalid path: $path"); + } + $parts = explode("/", $path); + array_shift($parts); + foreach ($parts as $part) { + $part = str_replace("~1", "/", $part); + $part = str_replace("~0", "~", $part); + if (is_array($value) && is_numeric($part)) { + $value = & $value[$part]; + } elseif (is_object($value)) { + if (isset($value->$part)) { + $value = & $value->$part; + } elseif ($strict) { + throw new Exception("Path does not exist: $path"); + } else { + return null; + } + } elseif ($strict) { + throw new Exception("Path does not exist: $path"); + } else { + return null; + } + } + return $value; +} + +function pointerJoin($parts) +{ + $result = ""; + foreach ($parts as $part) { + $part = str_replace("~", "~0", $part); + $part = str_replace("/", "~1", $part); + $result .= "/" . $part; + } + return $result; +}