Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 4.1 kB view raw
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}