Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Data\Types;
4
5use SocialDept\AtpSchema\Data\TypeDefinition;
6use SocialDept\AtpSchema\Exceptions\RecordValidationException;
7
8class BlobType extends TypeDefinition
9{
10 /**
11 * Accepted MIME types.
12 *
13 * @var array<string>|null
14 */
15 public readonly ?array $accept;
16
17 /**
18 * Maximum blob size in bytes.
19 */
20 public readonly ?int $maxSize;
21
22 /**
23 * Create a new BlobType.
24 *
25 * @param array<string>|null $accept
26 */
27 public function __construct(
28 ?array $accept = null,
29 ?int $maxSize = null,
30 ?string $description = null
31 ) {
32 parent::__construct('blob', $description);
33
34 $this->accept = $accept;
35 $this->maxSize = $maxSize;
36 }
37
38 /**
39 * Create from array data.
40 */
41 public static function fromArray(array $data): self
42 {
43 return new self(
44 accept: $data['accept'] ?? null,
45 maxSize: $data['maxSize'] ?? null,
46 description: $data['description'] ?? null
47 );
48 }
49
50 /**
51 * Convert to array.
52 */
53 public function toArray(): array
54 {
55 $array = ['type' => $this->type];
56
57 if ($this->description !== null) {
58 $array['description'] = $this->description;
59 }
60
61 if ($this->accept !== null) {
62 $array['accept'] = $this->accept;
63 }
64
65 if ($this->maxSize !== null) {
66 $array['maxSize'] = $this->maxSize;
67 }
68
69 return $array;
70 }
71
72 /**
73 * Validate a value against this type definition.
74 */
75 public function validate(mixed $value, string $path = ''): void
76 {
77 if (! is_array($value)) {
78 throw RecordValidationException::invalidType($path, 'blob (object)', gettype($value));
79 }
80
81 // Blob must have $type property
82 if (! isset($value['$type']) || $value['$type'] !== 'blob') {
83 throw RecordValidationException::invalidValue($path, 'must have $type property set to "blob"');
84 }
85
86 // Blob must have ref (CID reference)
87 if (! isset($value['ref'])) {
88 throw RecordValidationException::invalidValue($path, 'must have ref property');
89 }
90
91 // Blob must have mimeType
92 if (! isset($value['mimeType'])) {
93 throw RecordValidationException::invalidValue($path, 'must have mimeType property');
94 }
95
96 // Blob must have size
97 if (! isset($value['size'])) {
98 throw RecordValidationException::invalidValue($path, 'must have size property');
99 }
100
101 // Validate MIME type if accept is specified
102 if ($this->accept !== null) {
103 $mimeType = $value['mimeType'];
104
105 if (! $this->isMimeTypeAccepted($mimeType)) {
106 $accepted = implode(', ', $this->accept);
107
108 throw RecordValidationException::invalidValue($path, "MIME type must be one of: {$accepted}");
109 }
110 }
111
112 // Validate size if maxSize is specified
113 if ($this->maxSize !== null) {
114 $size = $value['size'];
115
116 if (! is_int($size)) {
117 throw RecordValidationException::invalidValue($path, 'size must be an integer');
118 }
119
120 if ($size > $this->maxSize) {
121 throw RecordValidationException::invalidValue($path, "size must not exceed {$this->maxSize} bytes");
122 }
123 }
124 }
125
126 /**
127 * Check if a MIME type is accepted.
128 */
129 protected function isMimeTypeAccepted(string $mimeType): bool
130 {
131 if ($this->accept === null) {
132 return true;
133 }
134
135 foreach ($this->accept as $accepted) {
136 // Exact match
137 if ($accepted === $mimeType) {
138 return true;
139 }
140
141 // Wildcard match (e.g., image/*)
142 if (str_ends_with($accepted, '/*')) {
143 $prefix = substr($accepted, 0, -1);
144 if (str_starts_with($mimeType, $prefix)) {
145 return true;
146 }
147 }
148 }
149
150 return false;
151 }
152}