Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 39aa0db

Browse files
committedJan 21, 2018
implementing draft-06, draft-07, ajv tests passed
1 parent 8da5f1c commit 39aa0db

28 files changed

+2301
-125
lines changed
 

‎.gitmodules

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "spec/JSON-Schema-Test-Suite"]
22
path = spec/JSON-Schema-Test-Suite
3-
url = https://github.com/swaggest/JSON-Schema-Test-Suite.git
3+
url = https://github.com/swaggest/JSON-Schema-Test-Suite.git
4+
[submodule "spec/ajv"]
5+
path = spec/ajv
6+
url = https://github.com/epoberezkin/ajv.git

‎README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
High definition PHP structures with JSON-schema based validation.
99

10+
Supported schemas:
11+
[JSON Schema Draft 7](http://json-schema.org/specification-links.html#draft-7)
12+
[JSON Schema Draft 6](http://json-schema.org/specification-links.html#draft-6)
13+
[JSON Schema Draft 4](http://json-schema.org/specification-links.html#draft-4)
14+
1015
## Installation
1116

1217
```
@@ -86,6 +91,17 @@ JSON
8691
)); // Exception: Required property missing: id at #->properties:orders->items[1]->#/definitions/order
8792
```
8893

94+
You can also call `Schema::import` on string `uri` to schema json data.
95+
```php
96+
$schema = Schema::import('http://localhost:1234/my_schema.json');
97+
```
98+
99+
Or with boolean argument.
100+
```php
101+
$schema = Schema::import(true); // permissive schema, always validates
102+
$schema = Schema::import(false); // restrictive schema, always invalidates
103+
```
104+
89105
### PHP structured classes with validation
90106

91107
```php
@@ -403,4 +419,14 @@ $schema = SwaggerSchema::schema()->in(json_decode(
403419
file_get_contents(__DIR__ . '/../../../../spec/petstore-swagger.json')
404420
), $context);
405421
$this->assertInstanceOf(CustomSchema::className(), $schema->definitions['User']);
406-
```
422+
```
423+
424+
## Code quality and test coverage
425+
426+
Some code quality best practices are intentionally violated here
427+
(see [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/swaggest/php-json-schema/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/swaggest/php-json-schema/?branch=master)
428+
) to allow best performance at maintenance cost.
429+
430+
Those violations are secured by comprehensive test coverage:
431+
* draft-04, draft-06, draft-07 of [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
432+
* test cases (excluding `$data` and few tests) of [epoberezkin/ajv](https://github.com/epoberezkin/ajv/tree/master/spec) (a mature js implementation)

‎composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"type": "library",
55
"require": {
66
"php": ">=5.4",
7-
"phplang/scope-exit": "^1.0"
7+
"phplang/scope-exit": "^1.0",
8+
"swaggest/json-diff": "^1.0"
89
},
910
"require-dev": {
1011
"phpunit/phpunit": "4.8.23",

‎spec/JSON-Schema-Test-Suite

‎spec/ajv

Submodule ajv added at ef40fbb

‎spec/json-schema-draft6.json

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-06/schema#",
3+
"$id": "http://json-schema.org/draft-06/schema#",
4+
"title": "Core schema meta-schema",
5+
"definitions": {
6+
"schemaArray": {
7+
"type": "array",
8+
"minItems": 1,
9+
"items": { "$ref": "#" }
10+
},
11+
"nonNegativeInteger": {
12+
"type": "integer",
13+
"minimum": 0
14+
},
15+
"nonNegativeIntegerDefault0": {
16+
"allOf": [
17+
{ "$ref": "#/definitions/nonNegativeInteger" },
18+
{ "default": 0 }
19+
]
20+
},
21+
"simpleTypes": {
22+
"enum": [
23+
"array",
24+
"boolean",
25+
"integer",
26+
"null",
27+
"number",
28+
"object",
29+
"string"
30+
]
31+
},
32+
"stringArray": {
33+
"type": "array",
34+
"items": { "type": "string" },
35+
"uniqueItems": true,
36+
"default": []
37+
}
38+
},
39+
"type": ["object", "boolean"],
40+
"properties": {
41+
"$id": {
42+
"type": "string",
43+
"format": "uri-reference"
44+
},
45+
"$schema": {
46+
"type": "string",
47+
"format": "uri"
48+
},
49+
"$ref": {
50+
"type": "string",
51+
"format": "uri-reference"
52+
},
53+
"title": {
54+
"type": "string"
55+
},
56+
"description": {
57+
"type": "string"
58+
},
59+
"default": {},
60+
"examples": {
61+
"type": "array",
62+
"items": {}
63+
},
64+
"multipleOf": {
65+
"type": "number",
66+
"exclusiveMinimum": 0
67+
},
68+
"maximum": {
69+
"type": "number"
70+
},
71+
"exclusiveMaximum": {
72+
"type": "number"
73+
},
74+
"minimum": {
75+
"type": "number"
76+
},
77+
"exclusiveMinimum": {
78+
"type": "number"
79+
},
80+
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
81+
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
82+
"pattern": {
83+
"type": "string",
84+
"format": "regex"
85+
},
86+
"additionalItems": { "$ref": "#" },
87+
"items": {
88+
"anyOf": [
89+
{ "$ref": "#" },
90+
{ "$ref": "#/definitions/schemaArray" }
91+
],
92+
"default": {}
93+
},
94+
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
95+
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
96+
"uniqueItems": {
97+
"type": "boolean",
98+
"default": false
99+
},
100+
"contains": { "$ref": "#" },
101+
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
102+
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
103+
"required": { "$ref": "#/definitions/stringArray" },
104+
"additionalProperties": { "$ref": "#" },
105+
"definitions": {
106+
"type": "object",
107+
"additionalProperties": { "$ref": "#" },
108+
"default": {}
109+
},
110+
"properties": {
111+
"type": "object",
112+
"additionalProperties": { "$ref": "#" },
113+
"default": {}
114+
},
115+
"patternProperties": {
116+
"type": "object",
117+
"additionalProperties": { "$ref": "#" },
118+
"default": {}
119+
},
120+
"dependencies": {
121+
"type": "object",
122+
"additionalProperties": {
123+
"anyOf": [
124+
{ "$ref": "#" },
125+
{ "$ref": "#/definitions/stringArray" }
126+
]
127+
}
128+
},
129+
"propertyNames": { "$ref": "#" },
130+
"const": {},
131+
"enum": {
132+
"type": "array",
133+
"minItems": 1,
134+
"uniqueItems": true
135+
},
136+
"type": {
137+
"anyOf": [
138+
{ "$ref": "#/definitions/simpleTypes" },
139+
{
140+
"type": "array",
141+
"items": { "$ref": "#/definitions/simpleTypes" },
142+
"minItems": 1,
143+
"uniqueItems": true
144+
}
145+
]
146+
},
147+
"format": { "type": "string" },
148+
"allOf": { "$ref": "#/definitions/schemaArray" },
149+
"anyOf": { "$ref": "#/definitions/schemaArray" },
150+
"oneOf": { "$ref": "#/definitions/schemaArray" },
151+
"not": { "$ref": "#" }
152+
},
153+
"default": {}
154+
}

‎spec/json-schema-draft7.json

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "http://json-schema.org/draft-07/schema#",
4+
"title": "Core schema meta-schema",
5+
"definitions": {
6+
"schemaArray": {
7+
"type": "array",
8+
"minItems": 1,
9+
"items": { "$ref": "#" }
10+
},
11+
"nonNegativeInteger": {
12+
"type": "integer",
13+
"minimum": 0
14+
},
15+
"nonNegativeIntegerDefault0": {
16+
"allOf": [
17+
{ "$ref": "#/definitions/nonNegativeInteger" },
18+
{ "default": 0 }
19+
]
20+
},
21+
"simpleTypes": {
22+
"enum": [
23+
"array",
24+
"boolean",
25+
"integer",
26+
"null",
27+
"number",
28+
"object",
29+
"string"
30+
]
31+
},
32+
"stringArray": {
33+
"type": "array",
34+
"items": { "type": "string" },
35+
"uniqueItems": true,
36+
"default": []
37+
}
38+
},
39+
"type": ["object", "boolean"],
40+
"properties": {
41+
"$id": {
42+
"type": "string",
43+
"format": "uri-reference"
44+
},
45+
"$schema": {
46+
"type": "string",
47+
"format": "uri"
48+
},
49+
"$ref": {
50+
"type": "string",
51+
"format": "uri-reference"
52+
},
53+
"$comment": {
54+
"type": "string"
55+
},
56+
"title": {
57+
"type": "string"
58+
},
59+
"description": {
60+
"type": "string"
61+
},
62+
"default": true,
63+
"readOnly": {
64+
"type": "boolean",
65+
"default": false
66+
},
67+
"examples": {
68+
"type": "array",
69+
"items": true
70+
},
71+
"multipleOf": {
72+
"type": "number",
73+
"exclusiveMinimum": 0
74+
},
75+
"maximum": {
76+
"type": "number"
77+
},
78+
"exclusiveMaximum": {
79+
"type": "number"
80+
},
81+
"minimum": {
82+
"type": "number"
83+
},
84+
"exclusiveMinimum": {
85+
"type": "number"
86+
},
87+
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
88+
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
89+
"pattern": {
90+
"type": "string",
91+
"format": "regex"
92+
},
93+
"additionalItems": { "$ref": "#" },
94+
"items": {
95+
"anyOf": [
96+
{ "$ref": "#" },
97+
{ "$ref": "#/definitions/schemaArray" }
98+
],
99+
"default": true
100+
},
101+
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
102+
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
103+
"uniqueItems": {
104+
"type": "boolean",
105+
"default": false
106+
},
107+
"contains": { "$ref": "#" },
108+
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
109+
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
110+
"required": { "$ref": "#/definitions/stringArray" },
111+
"additionalProperties": { "$ref": "#" },
112+
"definitions": {
113+
"type": "object",
114+
"additionalProperties": { "$ref": "#" },
115+
"default": {}
116+
},
117+
"properties": {
118+
"type": "object",
119+
"additionalProperties": { "$ref": "#" },
120+
"default": {}
121+
},
122+
"patternProperties": {
123+
"type": "object",
124+
"additionalProperties": { "$ref": "#" },
125+
"propertyNames": { "format": "regex" },
126+
"default": {}
127+
},
128+
"dependencies": {
129+
"type": "object",
130+
"additionalProperties": {
131+
"anyOf": [
132+
{ "$ref": "#" },
133+
{ "$ref": "#/definitions/stringArray" }
134+
]
135+
}
136+
},
137+
"propertyNames": { "$ref": "#" },
138+
"const": true,
139+
"enum": {
140+
"type": "array",
141+
"items": true,
142+
"minItems": 1,
143+
"uniqueItems": true
144+
},
145+
"type": {
146+
"anyOf": [
147+
{ "$ref": "#/definitions/simpleTypes" },
148+
{
149+
"type": "array",
150+
"items": { "$ref": "#/definitions/simpleTypes" },
151+
"minItems": 1,
152+
"uniqueItems": true
153+
}
154+
]
155+
},
156+
"format": { "type": "string" },
157+
"contentMediaType": { "type": "string" },
158+
"contentEncoding": { "type": "string" },
159+
"if": {"$ref": "#"},
160+
"then": {"$ref": "#"},
161+
"else": {"$ref": "#"},
162+
"allOf": { "$ref": "#/definitions/schemaArray" },
163+
"anyOf": { "$ref": "#/definitions/schemaArray" },
164+
"oneOf": { "$ref": "#/definitions/schemaArray" },
165+
"not": { "$ref": "#" }
166+
},
167+
"default": true
168+
}

‎src/Constraint/Content.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Constraint;
4+
5+
use Swaggest\JsonSchema\Context;
6+
use Swaggest\JsonSchema\Exception\ContentException;
7+
8+
class Content
9+
{
10+
const MEDIA_TYPE_APPLICATION_JSON = 'application/json';
11+
const ENCODING_BASE64 = 'base64';
12+
13+
const BASE64_INVALID_REGEX = '_[^A-Za-z0-9+/=]+_';
14+
15+
16+
/**
17+
* @param Context $options
18+
* @param string|null $encoding
19+
* @param string|null $mediaType
20+
* @param string $data
21+
* @param bool $import
22+
* @return bool|mixed|string
23+
* @throws ContentException
24+
*/
25+
public static function process(Context $options, $encoding, $mediaType, $data, $import = true)
26+
{
27+
if ($import) {
28+
if ($encoding !== null) {
29+
switch ($encoding) {
30+
case self::ENCODING_BASE64:
31+
if ($options->strictBase64Validation && preg_match(self::BASE64_INVALID_REGEX, $data)) {
32+
throw new ContentException('Invalid base64 string');
33+
}
34+
$data = base64_decode($data);
35+
if ($data === false) {
36+
throw new ContentException('Unable to decode base64');
37+
}
38+
break;
39+
}
40+
}
41+
42+
if ($mediaType !== null) {
43+
switch ($mediaType) {
44+
case self::MEDIA_TYPE_APPLICATION_JSON:
45+
$data = json_decode($data);
46+
$lastErrorCode = json_last_error();
47+
if ($lastErrorCode !== JSON_ERROR_NONE) {
48+
// TODO add readable error message
49+
throw new ContentException('Unable to decode json, err code: ' . $lastErrorCode);
50+
}
51+
break;
52+
53+
}
54+
}
55+
56+
return $data;
57+
} else {
58+
// export
59+
60+
if ($mediaType !== null) {
61+
switch ($mediaType) {
62+
case self::MEDIA_TYPE_APPLICATION_JSON:
63+
$data = json_encode($data);
64+
$lastErrorCode = json_last_error();
65+
if ($lastErrorCode !== JSON_ERROR_NONE) {
66+
// TODO add readable error message
67+
throw new ContentException('Unable to encode json, err code: ' . $lastErrorCode);
68+
}
69+
break;
70+
}
71+
}
72+
73+
if ($encoding !== null) {
74+
switch ($encoding) {
75+
case self::ENCODING_BASE64:
76+
$data = base64_encode($data);
77+
break;
78+
79+
}
80+
}
81+
82+
return $data;
83+
}
84+
85+
}
86+
}

‎src/Constraint/Format.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Constraint;
4+
5+
use Swaggest\JsonSchema\Constraint\Format\IdnHostname;
6+
use Swaggest\JsonSchema\Constraint\Format\Iri;
7+
use Swaggest\JsonSchema\Constraint\Format\Uri;
8+
9+
class Format
10+
{
11+
const DATE_REGEX_PART = '(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])';
12+
const TIME_REGEX_PART = '([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))?';
13+
/**
14+
* @see http://stackoverflow.com/a/1420225
15+
*/
16+
const HOSTNAME_REGEX = '/^
17+
(?=.{1,255}$)
18+
[0-9a-z]
19+
(([0-9a-z]|-){0,61}[0-9a-z])?
20+
(\.[0-9a-z](?:(?:[0-9a-z]|-){0,61}[0-9a-z])?)*
21+
\.?
22+
$/ix';
23+
24+
const JSON_POINTER_REGEX = '_^(?:/|(?:/[^/#]*)*)$_';
25+
const JSON_POINTER_RELATIVE_REGEX = '~^(0|[1-9][0-9]*)((?:/[^/#]*)*)(#?)$~';
26+
const JSON_POINTER_UNESCAPED_TILDE = '/~([^01]|$)/';
27+
28+
public static function validationError($format, $data)
29+
{
30+
switch ($format) {
31+
case 'date-time':
32+
return self::dateTimeError($data);
33+
case 'date':
34+
return preg_match('/^' . self::DATE_REGEX_PART . '$/i', $data) ? null : 'Invalid date';
35+
case 'time':
36+
return preg_match('/^' . self::TIME_REGEX_PART . '$/i', $data) ? null : 'Invalid time';
37+
case 'uri':
38+
return Uri::validationError($data, Uri::IS_SCHEME_REQUIRED);
39+
case 'iri':
40+
return Iri::validationError($data);
41+
case 'email':
42+
return filter_var($data, FILTER_VALIDATE_EMAIL) ? null : 'Invalid email';
43+
case 'idn-email':
44+
return count(explode('@', $data, 3)) === 2 ? null : 'Invalid email';
45+
case 'ipv4':
46+
return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? null : 'Invalid ipv4';
47+
case 'ipv6':
48+
return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? null : 'Invalid ipv6';
49+
case 'hostname':
50+
return preg_match(self::HOSTNAME_REGEX, $data) ? null : 'Invalid hostname';
51+
case 'idn-hostname':
52+
return IdnHostname::validationError($data);
53+
case 'regex':
54+
return self::regexError($data);
55+
case 'json-pointer':
56+
return self::jsonPointerError($data);
57+
case 'relative-json-pointer':
58+
return self::jsonPointerError($data, true);
59+
case 'uri-reference':
60+
return Uri::validationError($data, Uri::IS_URI_REFERENCE);
61+
case 'iri-reference':
62+
return Iri::validationError($data, Uri::IS_URI_REFERENCE);
63+
case 'uri-template':
64+
return Uri::validationError($data, Uri::IS_URI_TEMPLATE);
65+
}
66+
return null;
67+
}
68+
69+
public static function dateTimeError($data)
70+
{
71+
return preg_match('/^' . self::DATE_REGEX_PART . 'T' . self::TIME_REGEX_PART . '$/i', $data)
72+
? null
73+
: 'Invalid date-time: ' . $data;
74+
}
75+
76+
public static function regexError($data)
77+
{
78+
if (substr($data, -2) === '\Z') {
79+
return 'Invalid regex: \Z is not supported';
80+
}
81+
if (substr($data, 0, 2) === '\A') {
82+
return 'Invalid regex: \A is not supported';
83+
}
84+
85+
86+
$d = null;
87+
foreach (array('/', '_', '~', '#', '!', '%', '`', '=') as $delimiter) {
88+
if (strpos($data, $delimiter) === false) {
89+
$d = $delimiter;
90+
break;
91+
}
92+
}
93+
return @preg_match($d . $data . $d, '') === false ? 'Invalid regex: ' . $data : null;
94+
}
95+
96+
public static function jsonPointerError($data, $isRelative = false)
97+
{
98+
if (preg_match(self::JSON_POINTER_UNESCAPED_TILDE, $data)) {
99+
return 'Invalid json-pointer: unescaped ~';
100+
}
101+
if ($isRelative) {
102+
return preg_match(self::JSON_POINTER_RELATIVE_REGEX, $data) ? null : 'Invalid relative json-pointer';
103+
} else {
104+
return preg_match(self::JSON_POINTER_REGEX, $data) ? null : 'Invalid json-pointer';
105+
}
106+
}
107+
}

‎src/Constraint/Format/IdnHostname.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Constraint\Format;
4+
5+
use Swaggest\JsonSchema\Constraint\Format;
6+
7+
class IdnHostname
8+
{
9+
/**
10+
* @see http://www.unicode.org/faq/idn.html
11+
* @see https://gist.github.com/rxu/0660eef7a2f9e7992db6
12+
* @param string $data
13+
* @return null|string
14+
*/
15+
public static function validationError($data)
16+
{
17+
$error = Iri::unicodeValidationError($data, $sanitized);
18+
if ($error !== null) {
19+
return $error;
20+
}
21+
return preg_match(Format::HOSTNAME_REGEX, $sanitized) ? null : 'Invalid idn-hostname: ' . $data;
22+
}
23+
}

‎src/Constraint/Format/Iri.php

Lines changed: 703 additions & 0 deletions
Large diffs are not rendered by default.

‎src/Constraint/Format/Uri.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
/**
3+
* Created by PhpStorm.
4+
* User: vpoturaev
5+
* Date: 1/19/18
6+
* Time: 15:05
7+
*/
8+
9+
namespace Swaggest\JsonSchema\Constraint\Format;
10+
11+
12+
use Swaggest\JsonSchema\Constraint\Format;
13+
14+
class Uri
15+
{
16+
const IS_URI_REFERENCE = 1;
17+
const IS_URI_TEMPLATE = 2;
18+
const IS_SCHEME_REQUIRED = 8;
19+
20+
public static function validationError($data, $options = 0)
21+
{
22+
if ($options === Uri::IS_URI_TEMPLATE) {
23+
$opened = false;
24+
for ($i = 0; $i < strlen($data); ++$i) {
25+
if ($data[$i] === '{') {
26+
if ($opened) {
27+
return 'Invalid uri-template: unexpected "{"';
28+
} else {
29+
$opened = true;
30+
}
31+
} elseif ($data[$i] === '}') {
32+
if ($opened) {
33+
$opened = false;
34+
} else {
35+
return 'Invalid uri-template: unexpected "}"';
36+
}
37+
}
38+
}
39+
if ($opened) {
40+
return 'Invalid uri-template: unexpected end of string';
41+
}
42+
}
43+
44+
$uri = parse_url($data);
45+
if (!$uri) {
46+
return 'Malformed URI';
47+
}
48+
if (($options & self::IS_SCHEME_REQUIRED) && (!isset($uri['scheme']) || $uri['scheme'] === '')) {
49+
return 'Missing scheme in URI';
50+
}
51+
if (isset($uri['host'])) {
52+
$host = $uri['host'];
53+
if (!preg_match(Format::HOSTNAME_REGEX, $host)) {
54+
// stripping [ ]
55+
if ($host[0] === '[' && $host[strlen($host) - 1] === ']') {
56+
$host = substr($host, 1, -1);
57+
}
58+
if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
59+
return 'Malformed host in URI: ' . $host;
60+
}
61+
}
62+
}
63+
64+
if (isset($uri['path'])) {
65+
if (strpos($uri['path'], '\\') !== false) {
66+
return 'Invalid path: unescaped backslash';
67+
}
68+
}
69+
70+
if (isset($uri['fragment'])) {
71+
if (strpos($uri['fragment'], '\\') !== false) {
72+
return 'Invalid fragment: unescaped backslash';
73+
}
74+
}
75+
76+
return null;
77+
}
78+
}

‎src/Constraint/Type.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Swaggest\JsonSchema\Constraint;
44

5+
use Swaggest\JsonSchema\Schema;
6+
57
class Type implements Constraint
68
{
79
// TODO deprecate in favour of JsonSchema::<TYPE> ?
@@ -53,7 +55,7 @@ public static function readString($types, &$data)
5355
return false;
5456
}
5557

56-
public static function isValid($types, $data)
58+
public static function isValid($types, $data, $version)
5759
{
5860
if (!is_array($types)) {
5961
$types = array($types);
@@ -71,7 +73,10 @@ public static function isValid($types, $data)
7173
$ok = is_string($data);
7274
break;
7375
case self::INTEGER:
74-
$ok = is_int($data);
76+
$ok = is_int($data)
77+
|| (is_float($data)
78+
&& ((ceil($data) === $data && $version !== Schema::VERSION_DRAFT_04) // float without fraction is int
79+
|| abs($data) > PHP_INT_MAX)); // big float accepted for int
7580
break;
7681
case self::NUMBER:
7782
$ok = is_int($data) || is_float($data);

‎src/Context.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@ class Context extends MagicMap
3030
/** @var bool allow soft cast from to/strings */
3131
public $tolerateStrings = false;
3232

33+
/** @var bool do not tolerate special symbols even if base64_decode accepts string */
34+
public $strictBase64Validation = false;
35+
36+
public $unpackContentMediaType = true;
37+
3338
/** @var string property mapping set name */
3439
public $mapping = Schema::DEFAULT_MAPPING;
3540

41+
public $version = Schema::VERSION_AUTO;
42+
3643
/**
3744
* @param boolean $skipValidation
3845
* @return Context

‎src/Exception/ConstException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Exception;
4+
5+
use Swaggest\JsonSchema\InvalidValue;
6+
7+
class ConstException extends InvalidValue
8+
{
9+
10+
}

‎src/Exception/ContentException.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Exception;
4+
5+
6+
use Swaggest\JsonSchema\InvalidValue;
7+
8+
class ContentException extends InvalidValue
9+
{
10+
11+
}

‎src/JsonSchema.php

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
namespace Swaggest\JsonSchema;
88

99
use Swaggest\JsonSchema\Constraint\Properties;
10+
use Swaggest\JsonSchema\Constraint\Type;
1011
use Swaggest\JsonSchema\Schema as JsonBasicSchema;
1112
use Swaggest\JsonSchema\Structure\ClassStructure;
1213

1314

1415
/**
1516
* Core schema meta-schema
17+
*
18+
* // draft6
19+
* @property mixed $const
1620
*/
1721
class JsonSchema extends ClassStructure {
1822
const _ARRAY = 'array';
@@ -49,13 +53,13 @@ class JsonSchema extends ClassStructure {
4953
/** @var float */
5054
public $maximum;
5155

52-
/** @var bool */
56+
/** @var bool|float */
5357
public $exclusiveMaximum;
5458

5559
/** @var float */
5660
public $minimum;
5761

58-
/** @var bool */
62+
/** @var bool|float */
5963
public $exclusiveMinimum;
6064

6165
/** @var int */
@@ -130,6 +134,30 @@ class JsonSchema extends ClassStructure {
130134
/** @var JsonSchema Core schema meta-schema */
131135
public $not;
132136

137+
138+
// draft6
139+
/** @var JsonSchema */
140+
public $contains;
141+
142+
/** @var JsonSchema */
143+
public $propertyNames;
144+
145+
// draft7
146+
/** @var JsonSchema */
147+
public $if;
148+
149+
/** @var JsonSchema */
150+
public $then;
151+
152+
/** @var JsonSchema */
153+
public $else;
154+
155+
/** @var string */
156+
public $contentMediaType;
157+
158+
/** @var string */
159+
public $contentEncoding;
160+
133161
/**
134162
* @param Properties|static $properties
135163
* @param JsonBasicSchema $ownerSchema
@@ -138,7 +166,7 @@ public static function setUpProperties($properties, JsonBasicSchema $ownerSchema
138166
{
139167
$properties->id = JsonBasicSchema::string();
140168
$properties->id->format = 'uri';
141-
$properties->schema = JsonBasicSchema::string();
169+
$properties->schema = JsonBasicSchema::string();
142170
$properties->schema->format = 'uri';
143171
$ownerSchema->addPropertyMapping('$schema', self::names()->schema);
144172
$properties->title = JsonBasicSchema::string();
@@ -148,11 +176,15 @@ public static function setUpProperties($properties, JsonBasicSchema $ownerSchema
148176
$properties->multipleOf->minimum = 0;
149177
$properties->multipleOf->exclusiveMinimum = true;
150178
$properties->maximum = JsonBasicSchema::number();
151-
$properties->exclusiveMaximum = JsonBasicSchema::boolean();
152-
$properties->exclusiveMaximum->default = false;
179+
$properties->exclusiveMaximum = new JsonBasicSchema(); // draft6
180+
$properties->exclusiveMaximum->type = array(Type::BOOLEAN, Type::NUMBER); // draft6
181+
//$properties->exclusiveMaximum = JsonBasicSchema::boolean(); // draft6
182+
//$properties->exclusiveMaximum->default = false; // draft6
153183
$properties->minimum = JsonBasicSchema::number();
154-
$properties->exclusiveMinimum = JsonBasicSchema::boolean();
155-
$properties->exclusiveMinimum->default = false;
184+
$properties->exclusiveMinimum = new JsonBasicSchema(); // draft6
185+
$properties->exclusiveMinimum->type = array(Type::BOOLEAN, Type::NUMBER); // draft6
186+
//$properties->exclusiveMinimum = JsonBasicSchema::boolean(); // draft6
187+
//$properties->exclusiveMinimum->default = false; // draft6
156188
$properties->maxLength = JsonBasicSchema::integer();
157189
$properties->maxLength->minimum = 0;
158190
$properties->minLength = new JsonBasicSchema();
@@ -192,7 +224,7 @@ public static function setUpProperties($properties, JsonBasicSchema $ownerSchema
192224
$properties->minProperties->allOf[1]->default = 0;
193225
$properties->required = JsonBasicSchema::arr();
194226
$properties->required->items = JsonBasicSchema::string();
195-
$properties->required->minItems = 1;
227+
//$properties->required->minItems = 1; // disabled by draft6
196228
$properties->required->uniqueItems = true;
197229
$properties->additionalProperties = new JsonBasicSchema();
198230
$properties->additionalProperties->anyOf[0] = JsonBasicSchema::boolean();
@@ -216,7 +248,7 @@ public static function setUpProperties($properties, JsonBasicSchema $ownerSchema
216248
$properties->dependencies->additionalProperties->anyOf[0] = JsonBasicSchema::schema();
217249
$properties->dependencies->additionalProperties->anyOf[1] = JsonBasicSchema::arr();
218250
$properties->dependencies->additionalProperties->anyOf[1]->items = JsonBasicSchema::string();
219-
$properties->dependencies->additionalProperties->anyOf[1]->minItems = 1;
251+
//$properties->dependencies->additionalProperties->anyOf[1]->minItems = 1; // disabled by draft6
220252
$properties->dependencies->additionalProperties->anyOf[1]->uniqueItems = true;
221253
$properties->enum = JsonBasicSchema::arr();
222254
$properties->enum->minItems = 1;
@@ -258,12 +290,14 @@ public static function setUpProperties($properties, JsonBasicSchema $ownerSchema
258290
$properties->oneOf->items = JsonBasicSchema::schema();
259291
$properties->oneOf->minItems = 1;
260292
$properties->not = JsonBasicSchema::schema();
261-
$ownerSchema->type = 'object';
293+
$ownerSchema->type = array(self::OBJECT, self::BOOLEAN);
262294
$ownerSchema->id = 'http://json-schema.org/draft-04/schema#';
263295
$ownerSchema->schema = 'http://json-schema.org/draft-04/schema#';
264296
$ownerSchema->description = 'Core schema meta-schema';
265297
$ownerSchema->default = (object)array (
266298
);
299+
// disabled by draft6
300+
/*
267301
$ownerSchema->dependencies = (object)array (
268302
'exclusiveMaximum' =>
269303
array (
@@ -274,7 +308,22 @@ public static function setUpProperties($properties, JsonBasicSchema $ownerSchema
274308
0 => 'minimum',
275309
),
276310
);
277-
}
311+
*/
312+
313+
314+
// draft6
315+
$properties->const = (object)array();
316+
$properties->contains = JsonBasicSchema::schema();
317+
$properties->propertyNames = JsonBasicSchema::schema();
318+
319+
// draft7
320+
$properties->if = JsonBasicSchema::schema();
321+
$properties->then = JsonBasicSchema::schema();
322+
$properties->else = JsonBasicSchema::schema();
323+
324+
$properties->contentEncoding = JsonBasicSchema::string();
325+
$properties->contentMediaType = JsonBasicSchema::string();
326+
}
278327

279328
/**
280329
* @param string $id

‎src/RefResolver.php

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,27 @@ public function setupResolutionScope($id, $data)
5454
{
5555
$rootResolver = $this->rootResolver ? $this->rootResolver : $this;
5656

57-
$prev = $this->updateResolutionScope($id);
57+
$prev = $rootResolver->updateResolutionScope($id);
5858

5959
$refParts = explode('#', $rootResolver->resolutionScope, 2);
60-
if ($refParts[0] && empty($refParts[1])) {
61-
if (!isset($rootResolver->remoteRefResolvers[$refParts[0]])) {
62-
$resolver = new RefResolver($data);
60+
61+
if ($refParts[0]) { // external uri
62+
$resolver = &$rootResolver->remoteRefResolvers[$refParts[0]];
63+
if ($resolver === null) {
64+
$resolver = new RefResolver();
6365
$resolver->rootResolver = $rootResolver;
6466
$resolver->url = $refParts[0];
6567
$this->remoteRefResolvers[$refParts[0]] = $resolver;
6668
}
69+
} else { // local uri
70+
$resolver = $this;
71+
}
72+
73+
if (empty($refParts[1])) {
74+
$resolver->rootData = $data;
75+
} else {
76+
$refPath = '#' . $refParts[1];
77+
$resolver->refs[$refPath] = new Ref($refPath, $data);
6778
}
6879

6980
return $prev;
@@ -74,7 +85,7 @@ public function setupResolutionScope($id, $data)
7485
/** @var Ref[] */
7586
private $refs = array();
7687

77-
/** @var RefResolver[] */
88+
/** @var RefResolver[]|null[] */
7889
private $remoteRefResolvers = array();
7990

8091
/** @var RemoteRefProvider */
@@ -84,7 +95,7 @@ public function setupResolutionScope($id, $data)
8495
* RefResolver constructor.
8596
* @param JsonSchema $rootData
8697
*/
87-
public function __construct($rootData)
98+
public function __construct($rootData = null)
8899
{
89100
$this->rootData = $rootData;
90101
}
@@ -104,6 +115,11 @@ private function getRefProvider()
104115
return $this->refProvider;
105116
}
106117

118+
private function registerIdData($id, $data)
119+
{
120+
121+
}
122+
107123

108124
/**
109125
* @param string $referencePath
@@ -140,8 +156,11 @@ public function resolveReference($referencePath)
140156
/** @var JsonSchema $branch */
141157
$branch = &$refResolver->rootData;
142158
while (!empty($path)) {
143-
if (isset($branch->id) && is_string($branch->id)) {
144-
$refResolver->updateResolutionScope($branch->id);
159+
if (isset($branch->{Schema::ID_D4}) && is_string($branch->{Schema::ID_D4})) {
160+
$refResolver->updateResolutionScope($branch->{Schema::ID_D4});
161+
}
162+
if (isset($branch->{Schema::ID}) && is_string($branch->{Schema::ID})) {
163+
$refResolver->updateResolutionScope($branch->{Schema::ID});
145164
}
146165

147166
$folder = array_shift($path);

‎src/RemoteRef/Preloaded.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ public function __construct()
1212
{
1313
$this->setSchemaData('http://json-schema.org/draft-04/schema',
1414
json_decode(file_get_contents(__DIR__ . '/../../spec/json-schema.json')));
15+
$this->setSchemaData('http://json-schema.org/draft-06/schema',
16+
json_decode(file_get_contents(__DIR__ . '/../../spec/json-schema-draft6.json')));
17+
$this->setSchemaData('http://json-schema.org/draft-07/schema',
18+
json_decode(file_get_contents(__DIR__ . '/../../spec/json-schema-draft7.json')));
1519
}
1620

1721
public function getSchemaData($url)

‎src/Schema.php

Lines changed: 252 additions & 30 deletions
Large diffs are not rendered by default.

‎tests/src/PHPUnit/Example/ExampleTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,15 @@ public function testNestedStructure()
160160
$info = new UserInfo();
161161
$info->firstName = 'John';
162162
$info->lastName = 'Doe';
163-
$info->birthDay = '1970-01-01';
163+
$info->birthDay = '1970-01-01T00:00:00Z';
164164
$user->info = $info;
165165

166166
$json = <<<JSON
167167
{
168168
"id": 1,
169169
"firstName": "John",
170170
"lastName": "Doe",
171-
"birthDay": "1970-01-01"
171+
"birthDay": "1970-01-01T00:00:00Z"
172172
}
173173
JSON;
174174
$exported = User::export($user);

‎tests/src/PHPUnit/RefTest.php

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Swaggest\JsonSchema\RefResolver;
1212
use Swaggest\JsonSchema\RemoteRef\Preloaded;
1313
use Swaggest\JsonSchema\Schema;
14-
use Swaggest\JsonSchema\Tests\PHPUnit\Spec\SpecTest;
14+
use Swaggest\JsonSchema\Tests\PHPUnit\Spec\Draft4Test;
1515

1616
class RefTest extends \PHPUnit_Framework_TestCase
1717
{
@@ -308,7 +308,7 @@ public function testScopeChange()
308308
);
309309

310310
$options = new Context();
311-
$options->remoteRefProvider = SpecTest::getProvider();
311+
$options->remoteRefProvider = Draft4Test::getProvider();
312312
$schema = Schema::import($testData->schema, $options);
313313

314314
$schema->in($testData->tests[0]->data);
@@ -356,7 +356,7 @@ public function testScopeChangeSubschema()
356356
JSON
357357
);
358358
$options = new Context();
359-
$options->remoteRefProvider = SpecTest::getProvider();
359+
$options->remoteRefProvider = Draft4Test::getProvider();
360360
$schema = Schema::import($testData->schema, $options);
361361

362362
$schema->in($testData->tests[0]->data);
@@ -392,7 +392,7 @@ public function testExtRef()
392392
);
393393

394394
$schema = Schema::import($testData->schema, new Context(
395-
SpecTest::getProvider())
395+
Draft4Test::getProvider())
396396
);
397397
$schema->in($testData->tests[0]->data);
398398
$this->setExpectedException(get_class(new InvalidValue()));
@@ -425,12 +425,101 @@ public function testRefWithinRef()
425425
JSON
426426
);
427427
$schema = Schema::import($testData->schema, new Context(
428-
SpecTest::getProvider())
428+
Draft4Test::getProvider())
429429
);
430430
$schema->in($testData->tests[0]->data);
431431
$this->setExpectedException(get_class(new InvalidValue()));
432432
$schema->in($testData->tests[1]->data);
433433

434434
}
435435

436+
437+
public function testDraft6RefRemoteBaseUriChangeInvalid()
438+
{
439+
$testData = json_decode(<<<'JSON'
440+
{
441+
"description": "base URI change",
442+
"schema": {
443+
"$id": "http://localhost:1234/",
444+
"items": {
445+
"$id": "folder/",
446+
"items": {"$ref": "folderInteger.json"}
447+
}
448+
},
449+
"tests": [
450+
{
451+
"description": "base URI change ref valid",
452+
"data": [[1]],
453+
"valid": true
454+
},
455+
{
456+
"description": "base URI change ref invalid",
457+
"data": [["a"]],
458+
"valid": false
459+
}
460+
]
461+
}
462+
JSON
463+
);
464+
$schema = Schema::import($testData->schema, new Context(
465+
Draft4Test::getProvider())
466+
);
467+
$schema->in($testData->tests[0]->data);
468+
$this->setExpectedException(get_class(new InvalidValue()));
469+
$schema->in($testData->tests[1]->data);
470+
471+
}
472+
473+
474+
public function testDraft4RefRemoteBaseUriChangeInvalid()
475+
{
476+
$testData = json_decode(<<<'JSON'
477+
{
478+
"description": "base URI change",
479+
"schema": {
480+
"id": "http://localhost:1234/",
481+
"items": {
482+
"id": "folder/",
483+
"items": {"$ref": "folderInteger.json"}
484+
}
485+
},
486+
"tests": [
487+
{
488+
"description": "base URI change ref valid",
489+
"data": [[1]],
490+
"valid": true
491+
},
492+
{
493+
"description": "base URI change ref invalid",
494+
"data": [["a"]],
495+
"valid": false
496+
}
497+
]
498+
}
499+
JSON
500+
);
501+
$schema = Schema::import($testData->schema, new Context(
502+
Draft4Test::getProvider())
503+
);
504+
$schema->in($testData->tests[0]->data);
505+
$this->setExpectedException(get_class(new InvalidValue()));
506+
$schema->in($testData->tests[1]->data);
507+
508+
}
509+
510+
511+
public function testBoolSchema()
512+
{
513+
$schema = Schema::import(json_decode(<<<'JSON'
514+
{
515+
"$ref": "#\/definitions\/bool",
516+
"definitions": {
517+
"bool": false
518+
}
519+
}
520+
JSON
521+
));
522+
$this->setExpectedException(get_class(new InvalidValue()));
523+
$schema->in("foo");
524+
}
436525
}

‎tests/src/PHPUnit/Spec/AjvTest.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Tests\PHPUnit\Spec;
4+
5+
6+
use Swaggest\JsonSchema\RemoteRef\Preloaded;
7+
use Swaggest\JsonSchema\Schema;
8+
9+
class AjvTest extends SchemaTestSuite
10+
{
11+
const SCHEMA_VERSION = Schema::VERSION_AUTO;
12+
13+
public static function getProvider()
14+
{
15+
static $refProvider = null;
16+
17+
if (null === $refProvider) {
18+
$refProvider = new Preloaded();
19+
$refProvider
20+
->setSchemaData(
21+
'http://localhost:1234/integer.json',
22+
json_decode(file_get_contents(__DIR__
23+
. '/../../../../spec/JSON-Schema-Test-Suite/remotes/integer.json')))
24+
->setSchemaData(
25+
'http://localhost:1234/subSchemas.json',
26+
json_decode(file_get_contents(__DIR__
27+
. '/../../../../spec/JSON-Schema-Test-Suite/remotes/subSchemas.json')))
28+
->setSchemaData(
29+
'http://localhost:1234/name.json',
30+
json_decode(file_get_contents(__DIR__
31+
. '/../../../../spec/JSON-Schema-Test-Suite/remotes/name.json')))
32+
->setSchemaData(
33+
'http://localhost:1234/folder/folderInteger.json',
34+
json_decode(file_get_contents(__DIR__
35+
. '/../../../../spec/JSON-Schema-Test-Suite/remotes/folder/folderInteger.json')));
36+
37+
38+
$refProvider->setSchemaData('http://swagger.io/v2/schema.json', json_decode(file_get_contents(__DIR__
39+
. '/../../../../spec/swagger-schema.json')));
40+
41+
$dir = __DIR__ . '/../../../../spec/ajv/spec/remotes/';
42+
foreach (new \DirectoryIterator($dir) as $path) {
43+
if ($path === '.' || $path === '..') {
44+
continue;
45+
}
46+
$refProvider->setSchemaData('http://localhost:1234/'
47+
. $path, json_decode(file_get_contents($dir . $path)));
48+
}
49+
}
50+
51+
return $refProvider;
52+
}
53+
54+
55+
protected function skipTest($name)
56+
{
57+
static $skip = array(
58+
'format.json validation of uuid strings: not valid uuid' => 1,
59+
'format.json validation of JSON-pointer URI fragment strings: not a valid JSON-pointer as uri fragment (% not URL-encoded)' => 1,
60+
'format.json validation of URL strings: an invalid URL string' => 1,
61+
'62_resolution_scope_change.json change resolution scope - change filename (#62): string is valid' => 1,
62+
);
63+
64+
// debug particular test
65+
//return '1_ids_in_refs.json IDs in refs with root id: valid' !== $name;
66+
67+
return isset($skip[$name]);
68+
}
69+
70+
71+
public function specOptionalProvider()
72+
{
73+
$path = __DIR__ . '/../../../../spec/ajv/spec/tests/issues';
74+
return $this->provider($path);
75+
}
76+
77+
public function specProvider()
78+
{
79+
$path = __DIR__ . '/../../../../spec/ajv/spec/tests/rules';
80+
return $this->provider($path);
81+
}
82+
83+
84+
public function specExtrasProvider()
85+
{
86+
$path = __DIR__ . '/../../../../spec/ajv/spec/extras';
87+
return $this->provider($path);
88+
}
89+
90+
/**
91+
* @dataProvider specExtrasProvider
92+
* @param $schemaData
93+
* @param $data
94+
* @param $isValid
95+
* @param $name
96+
* @throws \Exception
97+
*/
98+
public function testSpecExtras($schemaData, $data, $isValid, $name)
99+
{
100+
if ($this->skipTest($name)) {
101+
$this->markTestSkipped();
102+
return;
103+
}
104+
$this->runSpecTest($schemaData, $data, $isValid, $name, static::SCHEMA_VERSION);
105+
}
106+
107+
public function specSchemasProvider()
108+
{
109+
$path = __DIR__ . '/../../../../spec/ajv/spec/tests/schemas';
110+
return $this->provider($path);
111+
}
112+
113+
/**
114+
* @dataProvider specSchemasProvider
115+
* @param $schemaData
116+
* @param $data
117+
* @param $isValid
118+
* @param $name
119+
* @throws \Exception
120+
*/
121+
public function testSpecSchemas($schemaData, $data, $isValid, $name)
122+
{
123+
if ($this->skipTest($name)) {
124+
$this->markTestSkipped();
125+
return;
126+
}
127+
$this->runSpecTest($schemaData, $data, $isValid, $name, static::SCHEMA_VERSION);
128+
}
129+
130+
public function specDataProvider()
131+
{
132+
$path = __DIR__ . '/../../../../spec/ajv/spec/extras/$data';
133+
return $this->provider($path);
134+
}
135+
136+
/**
137+
* @dataProvider specDataProvider
138+
* @param $schemaData
139+
* @param $data
140+
* @param $isValid
141+
* @param $name
142+
* @throws \Exception
143+
*/
144+
/*
145+
public function testSpecData($schemaData, $data, $isValid, $name)
146+
{
147+
$this->markTestSkipped();
148+
if ($this->skipTest($name)) {
149+
$this->markTestSkipped();
150+
return;
151+
}
152+
$this->runSpecTest($schemaData, $data, $isValid, $name, static::SCHEMA_VERSION);
153+
}
154+
*/
155+
156+
}

‎tests/src/PHPUnit/Spec/Draft4Test.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Tests\PHPUnit\Spec;
4+
5+
6+
class Draft4Test extends SchemaTestSuite
7+
{
8+
protected function skipTest($name)
9+
{
10+
return false;
11+
}
12+
13+
14+
public function specOptionalProvider()
15+
{
16+
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft4/optional';
17+
return $this->provider($path);
18+
}
19+
20+
public function specProvider()
21+
{
22+
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft4';
23+
return $this->provider($path);
24+
}
25+
}

‎tests/src/PHPUnit/Spec/Draft6Test.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Tests\PHPUnit\Spec;
4+
5+
6+
use Swaggest\JsonSchema\Schema;
7+
8+
class Draft6Test extends Draft4Test
9+
{
10+
const SCHEMA_VERSION = Schema::VERSION_DRAFT_06;
11+
12+
public function specProvider()
13+
{
14+
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft6';
15+
return $this->provider($path);
16+
}
17+
18+
public function specOptionalProvider()
19+
{
20+
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft6/optional';
21+
return $this->provider($path);
22+
}
23+
24+
}

‎tests/src/PHPUnit/Spec/Draft7Test.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Swaggest\JsonSchema\Tests\PHPUnit\Spec;
4+
5+
6+
use Swaggest\JsonSchema\Schema;
7+
8+
class Draft7Test extends Draft4Test
9+
{
10+
const SCHEMA_VERSION = Schema::VERSION_DRAFT_07;
11+
12+
protected function skipTest($name)
13+
{
14+
static $skip = array(
15+
'iri.json validation of IRIs: a valid IRI based on IPv6' => 1,
16+
);
17+
return isset($skip[$name]);
18+
}
19+
20+
public function specProvider()
21+
{
22+
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft7';
23+
return $this->provider($path);
24+
}
25+
26+
public function specOptionalProvider()
27+
{
28+
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft7/optional';
29+
return $this->provider($path);
30+
}
31+
32+
33+
public function specFormatProvider()
34+
{
35+
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft7/optional/format';
36+
return $this->provider($path);
37+
}
38+
39+
/**
40+
* @dataProvider specFormatProvider
41+
* @param $schemaData
42+
* @param $data
43+
* @param $isValid
44+
* @param $name
45+
* @throws \Exception
46+
*/
47+
public function testSpecFormat($schemaData, $data, $isValid, $name)
48+
{
49+
if ($this->skipTest($name)) {
50+
$this->markTestSkipped();
51+
return;
52+
}
53+
$this->runSpecTest($schemaData, $data, $isValid, $name, static::SCHEMA_VERSION);
54+
}
55+
56+
57+
}

‎tests/src/PHPUnit/Spec/SpecTest.php renamed to ‎tests/src/PHPUnit/Spec/SchemaTestSuite.php

Lines changed: 145 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
<?php
2+
/**
3+
* Created by PhpStorm.
4+
* User: vpoturaev
5+
* Date: 1/19/18
6+
* Time: 15:39
7+
*/
28

39
namespace Swaggest\JsonSchema\Tests\PHPUnit\Spec;
410

11+
512
use Swaggest\JsonSchema\Context;
613
use Swaggest\JsonSchema\InvalidValue;
7-
use Swaggest\JsonSchema\RemoteRef\Preloaded;
814
use Swaggest\JsonSchema\Schema;
15+
use Swaggest\JsonSchema\RemoteRef\Preloaded;
916

10-
class SpecTest extends \PHPUnit_Framework_TestCase
17+
abstract class SchemaTestSuite extends \PHPUnit_Framework_TestCase
1118
{
19+
const SCHEMA_VERSION = Schema::VERSION_DRAFT_04;
20+
1221
public static function getProvider()
1322
{
1423
static $refProvider = null;
@@ -37,48 +46,169 @@ public static function getProvider()
3746
return $refProvider;
3847
}
3948

49+
abstract protected function skipTest($name);
50+
51+
abstract protected function specProvider();
52+
53+
abstract protected function specOptionalProvider();
54+
55+
protected function provider($path)
56+
{
57+
if (!file_exists($path)) {
58+
//$this->markTestSkipped('No spec tests found, please run `git submodule bla-bla`');
59+
}
60+
61+
$testCases = array();
62+
63+
if ($handle = opendir($path)) {
64+
while (false !== ($entry = readdir($handle))) {
65+
if ($entry != "." && $entry != "..") {
66+
if ('.json' !== substr($entry, -5)) {
67+
continue;
68+
}
69+
70+
//if ($entry !== 'refRemote.json') {
71+
//continue;
72+
//}
73+
74+
//echo "$entry\n";
75+
/** @var _SpecTest[] $tests */
76+
// $tests = json_decode(file_get_contents($path . '/' . $entry), false, 512, JSON_BIGINT_AS_STRING);
77+
$tests = json_decode(file_get_contents($path . '/' . $entry));
78+
79+
foreach ($tests as $test) {
80+
foreach ($test->tests as $case) {
81+
/*if ($case->description !== 'changed scope ref invalid') {
82+
continue;
83+
}
84+
*/
85+
86+
$name = $entry . ' ' . $test->description . ': ' . $case->description;
87+
if (!isset($test->schema)) {
88+
if (isset($test->schemas)) {
89+
foreach ($test->schemas as $i => $schema) {
90+
$testCases[$name . '_' . $i] = array(
91+
'schema' => $schema,
92+
'data' => $case->data,
93+
'isValid' => $case->valid,
94+
'name' => $name,
95+
);
96+
}
97+
}
98+
continue;
99+
}
100+
$testCases[$name] = array(
101+
'schema' => $test->schema,
102+
'data' => $case->data,
103+
'isValid' => $case->valid,
104+
'name' => $name,
105+
);
106+
}
107+
}
108+
}
109+
}
110+
closedir($handle);
111+
}
112+
113+
//print_r($testCases);
114+
115+
return $testCases;
116+
}
117+
118+
40119
/**
41-
* @dataProvider provider
120+
* @dataProvider specProvider
42121
* @param $schemaData
43122
* @param $data
44123
* @param $isValid
45-
* @throws InvalidValue
124+
* @param $name
125+
* @throws \Exception
46126
*/
47-
public function testSpecDraft4($schemaData, $data, $isValid)
127+
public function testSpec($schemaData, $data, $isValid, $name)
48128
{
49-
$refProvider = self::getProvider();
129+
if ($this->skipTest($name)) {
130+
$this->markTestSkipped();
131+
return;
132+
}
133+
$this->runSpecTest($schemaData, $data, $isValid, $name, static::SCHEMA_VERSION);
134+
}
135+
136+
/**
137+
* @dataProvider specOptionalProvider
138+
* @param $schemaData
139+
* @param $data
140+
* @param $isValid
141+
* @param $name
142+
* @throws \Exception
143+
*/
144+
public function testSpecOptional($schemaData, $data, $isValid, $name)
145+
{
146+
if ($this->skipTest($name)) {
147+
$this->markTestSkipped();
148+
return;
149+
}
150+
$this->runSpecTest($schemaData, $data, $isValid, $name, static::SCHEMA_VERSION);
151+
}
152+
153+
/**
154+
* @param $schemaData
155+
* @param $data
156+
* @param $isValid
157+
* @param $name
158+
* @param $version
159+
* @throws \Exception
160+
*/
161+
protected function runSpecTest($schemaData, $data, $isValid, $name, $version)
162+
{
163+
$refProvider = static::getProvider();
50164

51165
$actualValid = true;
52166
$error = '';
53167
try {
54168
$options = new Context();
55169
$options->setRemoteRefProvider($refProvider);
170+
$options->version = $version;
171+
$options->strictBase64Validation = true;
172+
56173
$schema = Schema::import($schemaData, $options);
57-
$res = $schema->in($data);
174+
175+
$res = $schema->in($data, $options);
58176

59177
$exported = $schema->out($res);
60-
$this->assertEquals($data, $exported);
178+
179+
$res = $schema->in($exported, $options);
180+
$exported2 = $schema->out($res);
181+
182+
$this->assertEquals($exported2, $exported, $name);
61183
} catch (InvalidValue $exception) {
62184
$actualValid = false;
63185
$error = $exception->getMessage();
64186
}
65187

66-
$this->assertSame($isValid, $actualValid, "Schema:\n" . json_encode($schemaData, JSON_PRETTY_PRINT)
67-
. "\nData:\n" . json_encode($data, JSON_PRETTY_PRINT)
188+
$this->assertSame($isValid, $actualValid,
189+
"Test: $name\n"
190+
. "Schema:\n" . json_encode($schemaData, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES)
191+
. "\nData:\n" . json_encode($data, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES)
68192
. "\nError: " . $error . "\n");
193+
69194
}
70195

71196

72197
/**
73-
* @dataProvider provider
198+
* @dataProvider specProvider
74199
* @param $schemaData
75200
* @param $data
76201
* @param $isValid
77-
* @throws InvalidValue
202+
* @param $name
78203
*/
79-
public function testSpecDraft4SkipValidation($schemaData, $data, $isValid)
204+
public function testSpecSkipValidation($schemaData, $data, $isValid, $name)
80205
{
81-
$refProvider = self::getProvider();
206+
$this->runSpecTestSkipValidation($schemaData, $data, $isValid, $name);
207+
}
208+
209+
private function runSpecTestSkipValidation($schemaData, $data, $isValid, $name)
210+
{
211+
$refProvider = static::getProvider();
82212

83213
$actualValid = true;
84214
$error = '';
@@ -105,60 +235,12 @@ public function testSpecDraft4SkipValidation($schemaData, $data, $isValid)
105235
. "\nError: " . $error . "\n");
106236
}
107237

108-
109-
public function provider()
110-
{
111-
$path = __DIR__ . '/../../../../spec/JSON-Schema-Test-Suite/tests/draft4';
112-
if (!file_exists($path)) {
113-
//$this->markTestSkipped('No spec tests found, please run `git submodule bla-bla`');
114-
}
115-
116-
$testCases = array();
117-
118-
if ($handle = opendir($path)) {
119-
while (false !== ($entry = readdir($handle))) {
120-
if ($entry != "." && $entry != "..") {
121-
if ('.json' !== substr($entry, -5)) {
122-
continue;
123-
}
124-
125-
//if ($entry !== 'refRemote.json') {
126-
//continue;
127-
//}
128-
129-
//echo "$entry\n";
130-
/** @var _SpecTest[] $tests */
131-
$tests = json_decode(file_get_contents($path . '/' . $entry));
132-
foreach ($tests as $test) {
133-
foreach ($test->tests as $case) {
134-
/*if ($case->description !== 'changed scope ref invalid') {
135-
continue;
136-
}
137-
*/
138-
139-
$testCases[$entry . ' ' . $test->description . ': ' . $case->description] = array(
140-
'schema' => $test->schema,
141-
'data' => $case->data,
142-
'isValid' => $case->valid,
143-
);
144-
}
145-
}
146-
}
147-
}
148-
closedir($handle);
149-
}
150-
151-
//print_r($testCases);
152-
153-
return $testCases;
154-
}
155-
156-
157238
}
158239

159240
/**
160241
* @property $description
161242
* @property $schema
243+
* @property $schemas
162244
* @property _SpecTestCase[] $tests
163245
*/
164246
class _SpecTest

‎tools/iri-unicode-exclude.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/* Remove certain exceptions:
4+
** U+0640 ARABIC TATWEEL
5+
** U+07FA NKO LAJANYALAN
6+
** U+302E HANGUL SINGLE DOT TONE MARK
7+
** U+302F HANGUL DOUBLE DOT TONE MARK
8+
** U+3031 VERTICAL KANA REPEAT MARK
9+
** U+3032 VERTICAL KANA REPEAT WITH VOICED SOUND MARK
10+
** ..
11+
** U+3035 VERTICAL KANA REPEAT MARK LOWER HALF
12+
** U+303B VERTICAL IDEOGRAPHIC ITERATION MARK
13+
*/
14+
function certainExceptions()
15+
{
16+
$u = '\u0640 \u07FA \u302E \u302F \u3031 \u3032 \u3033 \u3034 \u3035 \u303B';
17+
$s = json_decode('"' . $u . '"');
18+
$ue = explode(' ', $u);
19+
foreach (explode(' ', $s) as $ci => $char) {
20+
$esc = '';
21+
for ($i = 0; $i < strlen($char); ++$i) {
22+
$esc .= '\\x' . strtoupper(dechex(ord($char[$i])));
23+
}
24+
echo '"' . $esc, '" => \'' . $ue[$ci], "',\n";
25+
}
26+
}
27+
28+
function excludes()
29+
{
30+
certainExceptions();
31+
echo '// Remove characters used for archaic Hangul (Korean) - \p{HST=L} and \p{HST=V}', "\n";
32+
echo '// as per http://unicode.org/Public/UNIDATA/HangulSyllableType.txt', "\n";
33+
makeRangeFromPreg('\x{1100}-\x{115F}');
34+
makeRangeFromPreg('\x{A960}-\x{A97C}');
35+
makeRangeFromPreg('\x{1160}-\x{11A7}');
36+
makeRangeFromPreg('\x{D7B0}-\x{D7C6}');
37+
38+
echo '// Remove three blocks of technical or archaic symbols.', "\n";
39+
echo '// \p{block=Combining_Diacritical_Marks_For_Symbols}', "\n";
40+
makeRangeFromPreg('\x{20D0}-\x{20FF}');
41+
echo '// \p{block=Musical_Symbols}', "\n";
42+
makeRangeFromPreg('\x{1D100}-\x{1D1FF}');
43+
echo '// \p{block=Ancient_Greek_Musical_Notation}', "\n";
44+
makeRangeFromPreg('\x{1D200}-\x{1D24F}');
45+
}
46+
47+
function makeRangeFromPreg($s)
48+
{
49+
list($start, $end) = explode('-', $s);
50+
$startHex = substr($start, 3, -1);
51+
$endHex = substr($end, 3, -1);
52+
$startDec = hexdec($startHex);
53+
$endDec = hexdec($endHex);
54+
for ($j = $startDec; $j <= $endDec; ++$j) {
55+
$u = '\u' . dechex($j);
56+
$char = json_decode('"' . $u . '"');
57+
$esc = '';
58+
59+
for ($i = 0; $i < strlen($char); ++$i) {
60+
$esc .= '\\x' . strtoupper(dechex(ord($char[$i])));
61+
}
62+
echo '"' . $esc, '" => \'' . $u, "',\n";
63+
}
64+
}
65+
66+
excludes();

0 commit comments

Comments
 (0)
Please sign in to comment.