Skip to content

Commit e484af6

Browse files
committed
spec compliance
1 parent 2e040d4 commit e484af6

File tree

13 files changed

+292
-50
lines changed

13 files changed

+292
-50
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "spec/JSON-Schema-Test-Suite"]
2+
path = spec/JSON-Schema-Test-Suite
3+
url = https://github.com/json-schema/JSON-Schema-Test-Suite.git

spec/JSON-Schema-Test-Suite

Submodule JSON-Schema-Test-Suite added at ed3391c

src/Constraint/Properties.php

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

33
namespace Yaoi\Schema\Constraint;
44

5+
use Yaoi\Schema\Exception;
56
use Yaoi\Schema\MagicMap;
67
use Yaoi\Schema\Schema;
78
use Yaoi\Schema\SchemaLoader;
@@ -50,6 +51,9 @@ public function import($data, ObjectItem $result, Schema $schema = null)
5051
} else {
5152
if (null !== $additionalProperties) {
5253
$traceHelper->push()->addData(SchemaLoader::ADDITIONAL_PROPERTIES);
54+
if ($additionalProperties === false) {
55+
throw new Exception('Additional properties not allowed', Exception::INVALID_VALUE);
56+
}
5357
$value = $additionalProperties->import($value);
5458
$traceHelper->pop();
5559
}

src/Constraint/Type.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function isValid($data)
3838
foreach ($this->types as $type) {
3939
switch ($type) {
4040
case self::OBJECT:
41-
$ok = is_object($data) || is_array($data);
41+
$ok = $data instanceof \stdClass;
4242
break;
4343
case self::_ARRAY:
4444
$ok = is_array($data);

src/Constraint/UniqueItems.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Yaoi\Schema\Constraint;
4+
5+
6+
class UniqueItems
7+
{
8+
/**
9+
* @param array $data
10+
* @return bool
11+
* @todo optimize a lot
12+
*/
13+
public static function isValid(array $data)
14+
{
15+
$index = array();
16+
foreach ($data as $value) {
17+
if (is_array($value) || $value instanceof \stdClass) {
18+
$value = json_encode($value);
19+
}
20+
if (is_bool($value)) {
21+
$value = '_______BOOL' . $value;
22+
}
23+
24+
$tmp = &$index[$value];
25+
if ($tmp !== null) {
26+
return false;
27+
}
28+
$tmp = true;
29+
}
30+
return true;
31+
}
32+
33+
}

src/Exception.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,4 @@ class Exception extends \Exception
66
{
77
const INVALID_VALUE = 1;
88
const NOT_IMPLEMENTED = 2;
9-
10-
protected $structureTrace = array();
11-
private $originalMessage;
12-
13-
public function __construct($message = "", $code = 0, Exception $previous = null)
14-
{
15-
$this->originalMessage = $message;
16-
parent::__construct($message, $code, $previous);
17-
}
18-
19-
public $constraint;
20-
public function setConstraint(Constraint $constraint)
21-
{
22-
$this->constraint = $constraint;
23-
return $this;
24-
}
259
}

src/Schema.php

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
use Yaoi\Schema\Constraint\Properties;
77
use Yaoi\Schema\Constraint\Ref;
88
use Yaoi\Schema\Constraint\Type;
9-
use Yaoi\Schema\Exception;
10-
use Yaoi\Schema\MagicMap;
11-
use Yaoi\Schema\StackTraceStorage;
9+
use Yaoi\Schema\Constraint\UniqueItems;
1210
use Yaoi\Schema\Structure\ObjectItem;
1311

1412
class Schema extends MagicMap
@@ -21,12 +19,16 @@ class Schema extends MagicMap
2119

2220
/** @var Properties */
2321
public $properties;
24-
25-
/** @var Schema */
22+
/** @var Schema|bool */
2623
public $additionalProperties;
2724

28-
/** @var Schema */
25+
26+
/** @var Schema|Schema[] */
2927
public $items;
28+
/** @var Schema|bool */
29+
public $additionalItems;
30+
/** @var bool */
31+
public $uniqueItems;
3032

3133
/** @var Ref */
3234
public $ref;
@@ -41,27 +43,24 @@ public function import($data)
4143

4244
if ($this->type !== null) {
4345
if (!$this->type->isValid($data)) {
44-
$message = ucfirst(implode(', ', $this->type->types) . ' required');
45-
if ($traceFrames = Schema::$traceHelper->getClean()) {
46-
throw new Exception($message . ' at ' . implode('->', $traceFrames), Exception::INVALID_VALUE);
47-
} else {
48-
throw new Exception($message, Exception::INVALID_VALUE);
49-
50-
}
51-
46+
$this->fail(ucfirst(implode(', ', $this->type->types) . ' required'));
5247
}
5348
}
5449

55-
if ($this->properties !== null) {
56-
if ($data instanceof \stdClass || is_array($data)) {
50+
if ($data instanceof \stdClass) {
51+
if ($this->properties !== null) {
5752
if (!$result instanceof ObjectItem) {
5853
$result = new ObjectItem();
5954
}
60-
$this->properties->import($data, $result, $this);
61-
}
55+
try {
56+
$this->properties->import($data, $result, $this);
57+
} catch (Exception $exception) {
58+
if ($exception->getCode() === Exception::INVALID_VALUE) {
59+
$this->fail($exception->getMessage());
60+
}
61+
}
6262

63-
} else {
64-
if ($data instanceof \stdClass || is_array($data)) {
63+
} else {
6564
if ($this->additionalProperties
6665
|| ($this->type && $this->type->has(Type::OBJECT))
6766
) {
@@ -80,10 +79,39 @@ public function import($data)
8079
}
8180
}
8281

83-
if ($this->items) {
84-
if (is_array($data)) {
82+
if (is_array($data)) {
83+
84+
if ($this->items instanceof Schema) {
85+
$items = array();
86+
$additionalItems = $this->items;
87+
} elseif ($this->items === null) { // items defaults to empty schema so everything is valid
88+
$items = array();
89+
$additionalItems = true;
90+
} else { // listed items
91+
$items = $this->items;
92+
$additionalItems = $this->additionalItems;
93+
}
94+
95+
if ($items || $additionalItems !== null) {
96+
$itemsLen = is_array($items) ? count($items) : 0;
97+
$index = 0;
8598
foreach ($data as &$value) {
86-
$value = $this->items->import($value);
99+
if ($index < $itemsLen) {
100+
$value = $items[$index]->import($value);
101+
} else {
102+
if ($additionalItems instanceof Schema) {
103+
$value = $additionalItems->import($value);
104+
} elseif ($additionalItems === false) {
105+
$this->fail('Unexpected array item');
106+
}
107+
}
108+
++$index;
109+
}
110+
}
111+
112+
if ($this->uniqueItems) {
113+
if (!UniqueItems::isValid($data)) {
114+
$this->fail('Array is not unique');
87115
}
88116
}
89117
}
@@ -93,6 +121,16 @@ public function import($data)
93121
}
94122

95123

124+
private function fail($message)
125+
{
126+
if ($traceFrames = Schema::$traceHelper->getClean()) {
127+
throw new Exception($message . ' at ' . implode('->', $traceFrames), Exception::INVALID_VALUE);
128+
} else {
129+
throw new Exception($message, Exception::INVALID_VALUE);
130+
}
131+
}
132+
133+
96134
public function export($data)
97135
{
98136
$result = $data;
@@ -106,15 +144,28 @@ public function export($data)
106144

107145
if ($this->type !== null) {
108146
if (!$this->type->isValid($data)) {
109-
throw new Exception('Invalid type', Exception::INVALID_VALUE);
147+
$message = ucfirst(implode(', ', $this->type->types) . ' required');
148+
if ($traceFrames = Schema::$traceHelper->getClean()) {
149+
throw new Exception($message . ' at ' . implode('->', $traceFrames), Exception::INVALID_VALUE);
150+
} else {
151+
throw new Exception($message, Exception::INVALID_VALUE);
152+
}
110153
}
111154
}
112155

113-
114156
if ($this->properties !== null && ($data instanceof ObjectItem)) {
115157
$result = $this->properties->export($data);
116158
}
117159

160+
if ($this->additionalItems) {
161+
if (is_array($data)) {
162+
foreach ($data as &$value) {
163+
$value = $this->additionalItems->export($value);
164+
}
165+
}
166+
}
167+
168+
118169
return $result;
119170
}
120171

@@ -132,6 +183,13 @@ public static function string()
132183
return $schema;
133184
}
134185

186+
public static function object()
187+
{
188+
$schema = new Schema();
189+
$schema->type = new Type(Type::OBJECT);
190+
return $schema;
191+
}
192+
135193
public static function create()
136194
{
137195
$schema = new Schema();
@@ -160,8 +218,6 @@ public function setType($type)
160218
}
161219

162220

163-
164-
165221
}
166222

167223
Schema::$traceHelper = new StackTraceStorage();

src/SchemaLoader.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ class SchemaLoader extends Base
1717
const ADDITIONAL_PROPERTIES = 'additionalProperties';
1818
const REF = '$ref';
1919

20+
const ITEMS = 'items';
21+
const ADDITIONAL_ITEMS = 'additionalItems';
22+
const UNIQUE_ITEMS = 'uniqueItems';
23+
2024
/** @var Schema */
2125
private $rootSchema;
2226

@@ -40,6 +44,10 @@ protected function readSchemaDeeper($schemaData, Schema $parentSchema = null)
4044
$this->rootData = $schemaData;
4145
}
4246

47+
if ($schemaData instanceof \stdClass) {
48+
$schemaData = (array)$schemaData;
49+
}
50+
4351
if (isset($schemaData[self::TYPE])) {
4452
$schema->type = new Type($schemaData[self::TYPE]);
4553
}
@@ -53,7 +61,39 @@ protected function readSchemaDeeper($schemaData, Schema $parentSchema = null)
5361
}
5462

5563
if (isset($schemaData[self::ADDITIONAL_PROPERTIES])) {
56-
$schema->additionalProperties = $this->readSchemaDeeper($schemaData[self::ADDITIONAL_PROPERTIES], $schema);
64+
$additionalProperties = $schemaData[self::ADDITIONAL_PROPERTIES];
65+
if ($additionalProperties instanceof \stdClass) {
66+
$schema->additionalProperties = $this->readSchemaDeeper($additionalProperties, $schema);
67+
} else {
68+
$schema->additionalProperties = $additionalProperties;
69+
}
70+
}
71+
72+
73+
if (isset($schemaData[self::ITEMS])) {
74+
$items = $schemaData[self::ITEMS];
75+
if (is_array($items)) {
76+
$schema->items = array();
77+
foreach ($items as $item) {
78+
$schema->items[] = $this->readSchemaDeeper($item);
79+
}
80+
} elseif ($items instanceof \stdClass) {
81+
$schema->items = $this->readSchemaDeeper($items);
82+
}
83+
}
84+
85+
86+
if (isset($schemaData[self::ADDITIONAL_ITEMS])) {
87+
$additionalItems = $schemaData[self::ADDITIONAL_ITEMS];
88+
if ($additionalItems instanceof \stdClass) {
89+
$schema->additionalItems = $this->readSchemaDeeper($additionalItems, $schema);
90+
} else {
91+
$schema->additionalItems = $additionalItems;
92+
}
93+
}
94+
95+
if (isset($schemaData[self::UNIQUE_ITEMS]) && $schemaData[self::UNIQUE_ITEMS] === true) {
96+
$schema->uniqueItems = true;
5797
}
5898

5999
// should resolve references on load

src/TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
* implement go-unnamed-style structures
22
* separate `additionalProperties` to an unnamed structure
33
* check export
4-
* add constraint to stack-trace during import
4+
* add constraint to stack-trace during import
5+
* check against spec test cases

tests/src/Naive/TypeObjectTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ public function testValid()
1717
'type' => 'object',
1818
)
1919
);
20-
$this->assertSame('123', $schema->import(array('aaa' => '123'))->aaa);
20+
$this->assertSame('123', $schema->import((object)array('aaa' => '123'))->aaa);
2121

22-
$object = $schema->import(array('3.45' => '123'));
22+
$object = $schema->import((object)array('3.45' => '123'));
2323
$this->assertSame('123', $object->{3.45});
2424

2525
$data = $schema->export($object);
26-
$this->assertSame(array('3.45' => '123'), $data);
26+
$this->assertSame((object)array('3.45' => '123'), $data);
2727
}
2828

2929
public function testInvalidObject()

0 commit comments

Comments
 (0)