Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at main 7.3 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Tests\Integration; 4 5use Orchestra\Testbench\TestCase; 6use SocialDept\AtpSchema\Contracts\Transformer; 7use SocialDept\AtpSchema\Data\LexiconDocument; 8use SocialDept\AtpSchema\Parser\SchemaLoader; 9use SocialDept\AtpSchema\Services\ModelMapper; 10use SocialDept\AtpSchema\Validation\Validator; 11 12class ModelMappingIntegrationTest extends TestCase 13{ 14 protected ModelMapper $mapper; 15 16 protected Validator $validator; 17 18 protected SchemaLoader $schemaLoader; 19 20 protected function setUp(): void 21 { 22 parent::setUp(); 23 24 $this->mapper = new ModelMapper(); 25 $this->schemaLoader = new SchemaLoader([]); 26 $this->validator = new Validator($this->schemaLoader); 27 } 28 29 public function test_it_transforms_and_validates_complete_workflow(): void 30 { 31 // Register transformer 32 $this->mapper->register('app.bsky.feed.post', new PostTransformer()); 33 34 // Load schema 35 $schema = LexiconDocument::fromArray([ 36 'lexicon' => 1, 37 'id' => 'app.bsky.feed.post', 38 'defs' => [ 39 'main' => [ 40 'type' => 'record', 41 'key' => 'tid', 42 'record' => [ 43 'type' => 'object', 44 'required' => ['text', 'createdAt'], 45 'properties' => [ 46 'text' => ['type' => 'string', 'maxLength' => 300], 47 'createdAt' => ['type' => 'string', 'format' => 'datetime'], 48 ], 49 ], 50 ], 51 ], 52 ]); 53 54 // Transform to model 55 $model = $this->mapper->fromArray('app.bsky.feed.post', [ 56 'text' => 'Hello, world!', 57 'createdAt' => '2024-01-01T00:00:00Z', 58 ]); 59 60 // Verify model 61 $this->assertInstanceOf(Post::class, $model); 62 $this->assertEquals('Hello, world!', $model->text); 63 64 // Transform back to array 65 $data = $this->mapper->toArray('app.bsky.feed.post', $model); 66 67 // Validate transformed data 68 $result = $this->validator->validate($data, $schema); 69 70 $this->assertTrue($result); 71 } 72 73 public function test_it_handles_multiple_model_transformations(): void 74 { 75 $this->mapper->registerMany([ 76 'app.bsky.feed.post' => new PostTransformer(), 77 'app.bsky.feed.repost' => new RepostTransformer(), 78 ]); 79 80 $posts = [ 81 ['text' => 'First post', 'createdAt' => '2024-01-01T00:00:00Z'], 82 ['text' => 'Second post', 'createdAt' => '2024-01-02T00:00:00Z'], 83 ]; 84 85 $models = $this->mapper->fromArrayMany('app.bsky.feed.post', $posts); 86 87 $this->assertCount(2, $models); 88 $this->assertContainsOnlyInstancesOf(Post::class, $models); 89 90 $arrays = $this->mapper->toArrayMany('app.bsky.feed.post', $models); 91 92 $this->assertEquals($posts, $arrays); 93 } 94 95 public function test_it_extends_mapper_with_macros(): void 96 { 97 ModelMapper::macro('validateAndTransform', function ($type, $data, $schema) { 98 // Validate first 99 if (! $this->validator->validate($data, $schema)) { 100 return null; 101 } 102 103 // Then transform 104 return $this->fromArray($type, $data); 105 }); 106 107 $this->mapper->validator = $this->validator; 108 $this->mapper->register('app.bsky.feed.post', new PostTransformer()); 109 110 $schema = LexiconDocument::fromArray([ 111 'lexicon' => 1, 112 'id' => 'app.bsky.feed.post', 113 'defs' => [ 114 'main' => [ 115 'type' => 'record', 116 'key' => 'tid', 117 'record' => [ 118 'type' => 'object', 119 'required' => ['text'], 120 'properties' => [ 121 'text' => ['type' => 'string'], 122 'createdAt' => ['type' => 'string'], 123 ], 124 ], 125 ], 126 ], 127 ]); 128 129 $validData = ['text' => 'Hello', 'createdAt' => '2024-01-01T00:00:00Z']; 130 131 $result = $this->mapper->validateAndTransform('app.bsky.feed.post', $validData, $schema); 132 133 $this->assertInstanceOf(Post::class, $result); 134 135 ModelMapper::flushMacros(); 136 } 137 138 public function test_it_handles_nested_transformations(): void 139 { 140 $this->mapper->register('app.bsky.actor.profile', new ProfileTransformer()); 141 142 $data = [ 143 'displayName' => 'Alice', 144 'description' => 'Developer', 145 'avatar' => [ 146 'url' => 'https://example.com/avatar.jpg', 147 'size' => 12345, 148 ], 149 ]; 150 151 $model = $this->mapper->fromArray('app.bsky.actor.profile', $data); 152 153 $this->assertInstanceOf(Profile::class, $model); 154 $this->assertEquals('Alice', $model->displayName); 155 $this->assertIsArray($model->avatar); 156 157 $transformed = $this->mapper->toArray('app.bsky.actor.profile', $model); 158 159 $this->assertEquals($data, $transformed); 160 } 161} 162 163// Test models and transformers 164class Post 165{ 166 public function __construct( 167 public string $text, 168 public string $createdAt 169 ) { 170 } 171} 172 173class PostTransformer implements Transformer 174{ 175 public function fromArray(array $data): Post 176 { 177 return new Post( 178 text: $data['text'], 179 createdAt: $data['createdAt'] 180 ); 181 } 182 183 public function toArray(mixed $model): array 184 { 185 return [ 186 'text' => $model->text, 187 'createdAt' => $model->createdAt, 188 ]; 189 } 190 191 public function supports(string $type): bool 192 { 193 return $type === 'app.bsky.feed.post'; 194 } 195} 196 197class Repost 198{ 199 public function __construct( 200 public string $uri, 201 public string $createdAt 202 ) { 203 } 204} 205 206class RepostTransformer implements Transformer 207{ 208 public function fromArray(array $data): Repost 209 { 210 return new Repost( 211 uri: $data['uri'], 212 createdAt: $data['createdAt'] 213 ); 214 } 215 216 public function toArray(mixed $model): array 217 { 218 return [ 219 'uri' => $model->uri, 220 'createdAt' => $model->createdAt, 221 ]; 222 } 223 224 public function supports(string $type): bool 225 { 226 return $type === 'app.bsky.feed.repost'; 227 } 228} 229 230class Profile 231{ 232 public function __construct( 233 public string $displayName, 234 public string $description, 235 public array $avatar 236 ) { 237 } 238} 239 240class ProfileTransformer implements Transformer 241{ 242 public function fromArray(array $data): Profile 243 { 244 return new Profile( 245 displayName: $data['displayName'], 246 description: $data['description'], 247 avatar: $data['avatar'] 248 ); 249 } 250 251 public function toArray(mixed $model): array 252 { 253 return [ 254 'displayName' => $model->displayName, 255 'description' => $model->description, 256 'avatar' => $model->avatar, 257 ]; 258 } 259 260 public function supports(string $type): bool 261 { 262 return $type === 'app.bsky.actor.profile'; 263 } 264}