Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 3.6 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Data\Types; 4 5use SocialDept\AtpSchema\Data\TypeDefinition; 6use SocialDept\AtpSchema\Exceptions\RecordValidationException; 7 8class ObjectType extends TypeDefinition 9{ 10 /** 11 * Object properties. 12 * 13 * @var array<string, TypeDefinition> 14 */ 15 public readonly array $properties; 16 17 /** 18 * Required property names. 19 * 20 * @var array<string> 21 */ 22 public readonly array $required; 23 24 /** 25 * Whether nullable properties are allowed. 26 */ 27 public readonly bool $nullable; 28 29 /** 30 * Create a new ObjectType. 31 * 32 * @param array<string, TypeDefinition> $properties 33 * @param array<string> $required 34 */ 35 public function __construct( 36 array $properties = [], 37 array $required = [], 38 bool $nullable = false, 39 ?string $description = null 40 ) { 41 parent::__construct('object', $description); 42 43 $this->properties = $properties; 44 $this->required = $required; 45 $this->nullable = $nullable; 46 } 47 48 /** 49 * Create from array data. 50 */ 51 public static function fromArray(array $data): self 52 { 53 // Properties will be parsed by TypeParser, this is just a placeholder 54 return new self( 55 properties: [], 56 required: $data['required'] ?? [], 57 nullable: $data['nullable'] ?? false, 58 description: $data['description'] ?? null 59 ); 60 } 61 62 /** 63 * Convert to array. 64 */ 65 public function toArray(): array 66 { 67 $array = ['type' => $this->type]; 68 69 if ($this->description !== null) { 70 $array['description'] = $this->description; 71 } 72 73 if (! empty($this->properties)) { 74 $array['properties'] = array_map( 75 fn (TypeDefinition $type) => $type->toArray(), 76 $this->properties 77 ); 78 } 79 80 if (! empty($this->required)) { 81 $array['required'] = $this->required; 82 } 83 84 if ($this->nullable) { 85 $array['nullable'] = $this->nullable; 86 } 87 88 return $array; 89 } 90 91 /** 92 * Validate a value against this type definition. 93 */ 94 public function validate(mixed $value, string $path = ''): void 95 { 96 if (! is_array($value)) { 97 throw RecordValidationException::invalidType($path, 'object', gettype($value)); 98 } 99 100 // Validate required properties 101 foreach ($this->required as $requiredKey) { 102 if (! array_key_exists($requiredKey, $value)) { 103 throw RecordValidationException::invalidValue($path, "missing required property '{$requiredKey}'"); 104 } 105 } 106 107 // Validate each property 108 foreach ($this->properties as $key => $propertyType) { 109 if (! array_key_exists($key, $value)) { 110 continue; 111 } 112 113 $propertyPath = $path ? "{$path}.{$key}" : $key; 114 $propertyValue = $value[$key]; 115 116 // Handle nullable 117 if ($propertyValue === null && $this->nullable) { 118 continue; 119 } 120 121 $propertyType->validate($propertyValue, $propertyPath); 122 } 123 } 124 125 /** 126 * Set properties after construction. 127 * 128 * @param array<string, TypeDefinition> $properties 129 */ 130 public function withProperties(array $properties): self 131 { 132 return new self( 133 properties: $properties, 134 required: $this->required, 135 nullable: $this->nullable, 136 description: $this->description 137 ); 138 } 139}