Skip to content

Commit 27238e6

Browse files
committed
add id schema and content methods
1 parent 3c3a581 commit 27238e6

9 files changed

Lines changed: 347 additions & 1 deletion

File tree

docs/json-schema/schema-types/string.mdx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,55 @@ $timeSchema = Schema::string('appointment_time')
147147
Formats marked with † require JSON Schema Draft-07 or later. Formats marked with * require Draft 2019-09 or later.
148148
</Note>
149149

150+
## Content Annotations
151+
152+
Describe encoded content carried by a string using `contentEncoding` and `contentMediaType`, and (optionally) validate the decoded content with `contentSchema`:
153+
154+
```php
155+
use Cortex\JsonSchema\Enums\SchemaVersion;
156+
use Cortex\JsonSchema\Enums\SchemaFormat;
157+
158+
$payloadSchema = Schema::string('payload', SchemaVersion::Draft_2019_09)
159+
->contentEncoding('base64')
160+
->contentMediaType('application/json')
161+
->contentSchema(
162+
Schema::object()
163+
->properties(
164+
Schema::string('event_id')->required(),
165+
Schema::string('type')->required(),
166+
Schema::string('created_at')->format(SchemaFormat::DateTime)->required(),
167+
Schema::object('data')
168+
->properties(
169+
Schema::string('user_id')->required(),
170+
Schema::string('email')->format(SchemaFormat::Email),
171+
)
172+
->required(),
173+
),
174+
);
175+
```
176+
177+
<Note>
178+
`contentSchema` requires JSON Schema Draft 2019-09 or later. `contentEncoding` and `contentMediaType` are available in all supported versions.
179+
</Note>
180+
181+
Validation examples:
182+
183+
```php
184+
$payloadSchema->isValid(base64_encode(
185+
json_encode([
186+
'event_id' => 'evt_123',
187+
'type' => 'user.created',
188+
'created_at' => '2024-03-14T12:00:00Z',
189+
'data' => [
190+
'user_id' => 'usr_1',
191+
'email' => 'ada@example.com',
192+
],
193+
], JSON_THROW_ON_ERROR),
194+
)); // true (base64 encoded string value and matches the content schema)
195+
196+
$payloadSchema->isValid(123); // false (not a string)
197+
```
198+
150199
## Enumeration Values
151200

152201
Restrict strings to a specific set of allowed values:
@@ -196,6 +245,10 @@ $passwordSchema = Schema::string('password')
196245
->pattern('(?=.*[a-z])(?=.*[A-Z])(?=.*\d)');
197246
```
198247

248+
<Note>
249+
Read-only and write-only annotations require JSON Schema Draft-07 or later.
250+
</Note>
251+
199252
## Complex Example
200253

201254
Here's a comprehensive example combining multiple string validation features:

src/Converters/JsonConverter.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Cortex\JsonSchema\Types\BooleanSchema;
1919
use Cortex\JsonSchema\Types\IntegerSchema;
2020
use Cortex\JsonSchema\Contracts\JsonSchema;
21+
use Cortex\JsonSchema\Types\AbstractSchema;
2122
use Cortex\JsonSchema\Exceptions\SchemaException;
2223

2324
class JsonConverter implements Converter
@@ -149,6 +150,16 @@ private function getConstValue(string $key): bool|float|int|string|null
149150
return null;
150151
}
151152

153+
/**
154+
* Apply shared fields to the schema.
155+
*/
156+
private function applyId(AbstractSchema $schema): void
157+
{
158+
if (($id = $this->getString('$id')) !== null) {
159+
$schema->id($id);
160+
}
161+
}
162+
152163
/**
153164
* Detect schema version from $schema URI.
154165
*/
@@ -166,6 +177,7 @@ private function detectSchemaVersion(string $schemaUri): SchemaVersion
166177
private function createStringSchema(?string $title): StringSchema
167178
{
168179
$stringSchema = new StringSchema($title, $this->schemaVersion);
180+
$this->applyId($stringSchema);
169181

170182
if (($minLength = $this->getInt('minLength')) !== null) {
171183
$stringSchema->minLength($minLength);
@@ -179,6 +191,23 @@ private function createStringSchema(?string $title): StringSchema
179191
$stringSchema->pattern($pattern);
180192
}
181193

194+
if (($contentEncoding = $this->getString('contentEncoding')) !== null) {
195+
$stringSchema->contentEncoding($contentEncoding);
196+
}
197+
198+
if (($contentMediaType = $this->getString('contentMediaType')) !== null) {
199+
$stringSchema->contentMediaType($contentMediaType);
200+
}
201+
202+
$contentSchema = $this->getValue('contentSchema');
203+
204+
if (is_array($contentSchema)) {
205+
$converter = new self($contentSchema, $this->schemaVersion);
206+
$stringSchema->contentSchema($converter->convert());
207+
} elseif (is_bool($contentSchema)) {
208+
$stringSchema->contentSchema($contentSchema);
209+
}
210+
182211
if (($format = $this->getString('format')) !== null) {
183212
$stringSchema->format($format);
184213
}
@@ -218,6 +247,7 @@ private function createStringSchema(?string $title): StringSchema
218247
private function createNumberSchema(?string $title): NumberSchema
219248
{
220249
$numberSchema = new NumberSchema($title, $this->schemaVersion);
250+
$this->applyId($numberSchema);
221251

222252
if (($minimum = $this->getFloat('minimum')) !== null) {
223253
$numberSchema->minimum($minimum);
@@ -262,6 +292,7 @@ private function createNumberSchema(?string $title): NumberSchema
262292
private function createIntegerSchema(?string $title): IntegerSchema
263293
{
264294
$integerSchema = new IntegerSchema($title, $this->schemaVersion);
295+
$this->applyId($integerSchema);
265296

266297
if (($minimum = $this->getInt('minimum')) !== null) {
267298
$integerSchema->minimum($minimum);
@@ -306,6 +337,7 @@ private function createIntegerSchema(?string $title): IntegerSchema
306337
private function createBooleanSchema(?string $title): BooleanSchema
307338
{
308339
$booleanSchema = new BooleanSchema($title, $this->schemaVersion);
340+
$this->applyId($booleanSchema);
309341

310342
if (($const = $this->getConstValue('const')) !== null) {
311343
$booleanSchema->const($const);
@@ -329,6 +361,7 @@ private function createBooleanSchema(?string $title): BooleanSchema
329361
private function createArraySchema(?string $title): ArraySchema
330362
{
331363
$arraySchema = new ArraySchema($title, $this->schemaVersion);
364+
$this->applyId($arraySchema);
332365

333366
if (($items = $this->getArray('items')) !== null) {
334367
$converter = new self($items, $this->schemaVersion);
@@ -372,6 +405,7 @@ private function createArraySchema(?string $title): ArraySchema
372405
private function createObjectSchema(?string $title): ObjectSchema
373406
{
374407
$objectSchema = new ObjectSchema($title, $this->schemaVersion);
408+
$this->applyId($objectSchema);
375409
$required = $this->getArray('required') ?? [];
376410

377411
if (($properties = $this->getArray('properties')) !== null) {
@@ -434,6 +468,7 @@ private function createObjectSchema(?string $title): ObjectSchema
434468
private function createNullSchema(?string $title): NullSchema
435469
{
436470
$nullSchema = new NullSchema($title, $this->schemaVersion);
471+
$this->applyId($nullSchema);
437472

438473
if (($description = $this->getString('description')) !== null) {
439474
$nullSchema->description($description);
@@ -461,6 +496,8 @@ private function createUnionSchema(?string $title): UnionSchema
461496
$schema = new UnionSchema(SchemaType::cases(), $title, $this->schemaVersion);
462497
}
463498

499+
$this->applyId($schema);
500+
464501
if (($enum = $this->getArray('enum')) !== null && $enum !== []) {
465502
/** @var non-empty-array<bool|float|int|string|null> $enum */
466503
$schema->enum($enum);

src/Types/AbstractSchema.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Cortex\JsonSchema\Enums\SchemaType;
88
use Cortex\JsonSchema\Enums\SchemaVersion;
99
use Cortex\JsonSchema\Contracts\JsonSchema;
10+
use Cortex\JsonSchema\Types\Concerns\HasId;
1011
use Cortex\JsonSchema\Types\Concerns\HasRef;
1112
use Cortex\JsonSchema\Types\Concerns\HasEnum;
1213
use Cortex\JsonSchema\Types\Concerns\HasConst;
@@ -25,6 +26,7 @@
2526
abstract class AbstractSchema implements JsonSchema
2627
{
2728
use HasRef;
29+
use HasId;
2830
use HasEnum;
2931
use HasConst;
3032
use HasTitle;
@@ -113,6 +115,7 @@ public function toArray(bool $includeSchemaRef = true, bool $includeTitle = true
113115
$schema['$schema'] = $this->schemaVersion->value;
114116
}
115117

118+
$schema = $this->addIdToSchema($schema);
116119
$schema = $this->addTitleToSchema($schema, $includeTitle);
117120
$schema = $this->addFormatToSchema($schema);
118121
$schema = $this->addDescriptionToSchema($schema);

src/Types/Concerns/HasId.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cortex\JsonSchema\Types\Concerns;
6+
7+
/** @mixin \Cortex\JsonSchema\Contracts\JsonSchema */
8+
trait HasId
9+
{
10+
protected ?string $id = null;
11+
12+
/**
13+
* Set the $id value for this schema.
14+
*/
15+
public function id(string $id): static
16+
{
17+
$this->id = $id;
18+
19+
return $this;
20+
}
21+
22+
/**
23+
* Get the $id value for this schema.
24+
*/
25+
public function getId(): ?string
26+
{
27+
return $this->id;
28+
}
29+
30+
/**
31+
* Add $id to schema array.
32+
*
33+
* @param array<string, mixed> $schema
34+
*
35+
* @return array<string, mixed>
36+
*/
37+
protected function addIdToSchema(array $schema): array
38+
{
39+
if ($this->id !== null) {
40+
$schema['$id'] = $this->id;
41+
}
42+
43+
return $schema;
44+
}
45+
}

src/Types/Concerns/HasValidation.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public function validate(mixed $value): void
2323
{
2424
$validator = new Validator();
2525
$validator->parser()->setOption('defaultDraft', '2020-12');
26+
$validator->parser()->setOption('decodeContent', true);
2627

2728
try {
2829
$result = $validator->validate(

src/Types/StringSchema.php

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
use Override;
88
use Cortex\JsonSchema\Enums\SchemaType;
9+
use Cortex\JsonSchema\Enums\SchemaFeature;
910
use Cortex\JsonSchema\Enums\SchemaVersion;
11+
use Cortex\JsonSchema\Contracts\JsonSchema;
1012
use Cortex\JsonSchema\Exceptions\SchemaException;
1113

1214
final class StringSchema extends AbstractSchema
@@ -17,6 +19,12 @@ final class StringSchema extends AbstractSchema
1719

1820
protected ?string $pattern = null;
1921

22+
protected ?string $contentEncoding = null;
23+
24+
protected ?string $contentMediaType = null;
25+
26+
protected JsonSchema|bool|null $contentSchema = null;
27+
2028
public function __construct(?string $title = null, ?SchemaVersion $schemaVersion = null)
2129
{
2230
parent::__construct(SchemaType::String, $title, $schemaVersion);
@@ -66,6 +74,37 @@ public function pattern(string $pattern): static
6674
return $this;
6775
}
6876

77+
/**
78+
* Set the content encoding.
79+
*/
80+
public function contentEncoding(string $contentEncoding): static
81+
{
82+
$this->contentEncoding = $contentEncoding;
83+
84+
return $this;
85+
}
86+
87+
/**
88+
* Set the content media type.
89+
*/
90+
public function contentMediaType(string $contentMediaType): static
91+
{
92+
$this->contentMediaType = $contentMediaType;
93+
94+
return $this;
95+
}
96+
97+
/**
98+
* Set the content schema.
99+
*/
100+
public function contentSchema(JsonSchema|bool $contentSchema): static
101+
{
102+
$this->validateFeatureSupport(SchemaFeature::ContentSchema);
103+
$this->contentSchema = $contentSchema;
104+
105+
return $this;
106+
}
107+
69108
/**
70109
* Convert to array.
71110
*
@@ -76,7 +115,31 @@ public function toArray(bool $includeSchemaRef = true, bool $includeTitle = true
76115
{
77116
$schema = parent::toArray($includeSchemaRef, $includeTitle);
78117

79-
return $this->addLengthToSchema($schema);
118+
$schema = $this->addLengthToSchema($schema);
119+
120+
return $this->addContentToSchema($schema);
121+
}
122+
123+
/**
124+
* Get features used by this schema from all traits.
125+
*
126+
* @return array<\Cortex\JsonSchema\Enums\SchemaFeature>
127+
*/
128+
#[Override]
129+
protected function getUsedFeatures(): array
130+
{
131+
$features = [
132+
...parent::getUsedFeatures(),
133+
...$this->getContentFeatures(),
134+
];
135+
136+
$uniqueFeatures = [];
137+
138+
foreach ($features as $feature) {
139+
$uniqueFeatures[$feature->value] = $feature;
140+
}
141+
142+
return array_values($uniqueFeatures);
80143
}
81144

82145
/**
@@ -102,4 +165,46 @@ protected function addLengthToSchema(array $schema): array
102165

103166
return $schema;
104167
}
168+
169+
/**
170+
* Add content keywords to schema array.
171+
*
172+
* @param array<string, mixed> $schema
173+
*
174+
* @return array<string, mixed>
175+
*/
176+
protected function addContentToSchema(array $schema): array
177+
{
178+
if ($this->contentEncoding !== null) {
179+
$schema['contentEncoding'] = $this->contentEncoding;
180+
}
181+
182+
if ($this->contentMediaType !== null) {
183+
$schema['contentMediaType'] = $this->contentMediaType;
184+
}
185+
186+
if ($this->contentSchema !== null) {
187+
$schema['contentSchema'] = $this->contentSchema instanceof JsonSchema
188+
? $this->contentSchema->toArray(includeSchemaRef: false, includeTitle: false)
189+
: $this->contentSchema;
190+
}
191+
192+
return $schema;
193+
}
194+
195+
/**
196+
* Get content features used by this schema.
197+
*
198+
* @return array<\Cortex\JsonSchema\Enums\SchemaFeature>
199+
*/
200+
protected function getContentFeatures(): array
201+
{
202+
$features = [];
203+
204+
if ($this->contentSchema !== null) {
205+
$features[] = SchemaFeature::ContentSchema;
206+
}
207+
208+
return $features;
209+
}
105210
}

0 commit comments

Comments
 (0)