Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add extension points and macros

+637
+2
src/Services/BlobHandler.php
··· 5 5 use Illuminate\Contracts\Filesystem\Filesystem; 6 6 use Illuminate\Http\UploadedFile; 7 7 use Illuminate\Support\Facades\Storage; 8 + use Illuminate\Support\Traits\Macroable; 8 9 use SocialDept\Schema\Data\BlobReference; 9 10 use SocialDept\Schema\Exceptions\RecordValidationException; 10 11 11 12 class BlobHandler 12 13 { 14 + use Macroable; 13 15 /** 14 16 * Storage disk name. 15 17 */
+2
src/Services/ModelMapper.php
··· 2 2 3 3 namespace SocialDept\Schema\Services; 4 4 5 + use Illuminate\Support\Traits\Macroable; 5 6 use SocialDept\Schema\Contracts\Transformer; 6 7 use SocialDept\Schema\Exceptions\SchemaException; 7 8 8 9 class ModelMapper 9 10 { 11 + use Macroable; 10 12 /** 11 13 * Registered transformers. 12 14 *
+2
src/Services/UnionResolver.php
··· 2 2 3 3 namespace SocialDept\Schema\Services; 4 4 5 + use Illuminate\Support\Traits\Macroable; 5 6 use SocialDept\Schema\Contracts\LexiconRegistry; 6 7 use SocialDept\Schema\Data\LexiconDocument; 7 8 use SocialDept\Schema\Exceptions\RecordValidationException; 8 9 9 10 class UnionResolver 10 11 { 12 + use Macroable; 11 13 /** 12 14 * Create a new UnionResolver. 13 15 */
+141
src/Support/ExtensionManager.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Support; 4 + 5 + use Closure; 6 + 7 + class ExtensionManager 8 + { 9 + /** 10 + * Registered hooks. 11 + * 12 + * @var array<string, array<Closure>> 13 + */ 14 + protected array $hooks = []; 15 + 16 + /** 17 + * Register a hook callback. 18 + */ 19 + public function hook(string $name, Closure $callback): self 20 + { 21 + if (! isset($this->hooks[$name])) { 22 + $this->hooks[$name] = []; 23 + } 24 + 25 + $this->hooks[$name][] = $callback; 26 + 27 + return $this; 28 + } 29 + 30 + /** 31 + * Execute all hooks for a given name. 32 + * 33 + * @return array<mixed> 34 + */ 35 + public function execute(string $name, mixed ...$args): array 36 + { 37 + if (! isset($this->hooks[$name])) { 38 + return []; 39 + } 40 + 41 + $results = []; 42 + 43 + foreach ($this->hooks[$name] as $callback) { 44 + $results[] = $callback(...$args); 45 + } 46 + 47 + return $results; 48 + } 49 + 50 + /** 51 + * Execute hooks and return the first non-null result. 52 + */ 53 + public function executeUntil(string $name, mixed ...$args): mixed 54 + { 55 + if (! isset($this->hooks[$name])) { 56 + return null; 57 + } 58 + 59 + foreach ($this->hooks[$name] as $callback) { 60 + $result = $callback(...$args); 61 + 62 + if ($result !== null) { 63 + return $result; 64 + } 65 + } 66 + 67 + return null; 68 + } 69 + 70 + /** 71 + * Execute hooks with a value that can be modified by each hook. 72 + */ 73 + public function filter(string $name, mixed $value, mixed ...$args): mixed 74 + { 75 + if (! isset($this->hooks[$name])) { 76 + return $value; 77 + } 78 + 79 + foreach ($this->hooks[$name] as $callback) { 80 + $value = $callback($value, ...$args); 81 + } 82 + 83 + return $value; 84 + } 85 + 86 + /** 87 + * Check if a hook has any callbacks registered. 88 + */ 89 + public function has(string $name): bool 90 + { 91 + return isset($this->hooks[$name]) && count($this->hooks[$name]) > 0; 92 + } 93 + 94 + /** 95 + * Get all callbacks for a hook. 96 + * 97 + * @return array<Closure> 98 + */ 99 + public function get(string $name): array 100 + { 101 + return $this->hooks[$name] ?? []; 102 + } 103 + 104 + /** 105 + * Remove all callbacks for a hook. 106 + */ 107 + public function remove(string $name): self 108 + { 109 + unset($this->hooks[$name]); 110 + 111 + return $this; 112 + } 113 + 114 + /** 115 + * Clear all hooks. 116 + */ 117 + public function clear(): self 118 + { 119 + $this->hooks = []; 120 + 121 + return $this; 122 + } 123 + 124 + /** 125 + * Get count of callbacks for a hook. 126 + */ 127 + public function count(string $name): int 128 + { 129 + return count($this->hooks[$name] ?? []); 130 + } 131 + 132 + /** 133 + * Get all registered hook names. 134 + * 135 + * @return array<string> 136 + */ 137 + public function names(): array 138 + { 139 + return array_keys($this->hooks); 140 + } 141 + }
+2
src/Validation/Validator.php
··· 2 2 3 3 namespace SocialDept\Schema\Validation; 4 4 5 + use Illuminate\Support\Traits\Macroable; 5 6 use SocialDept\Schema\Contracts\LexiconValidator as LexiconValidatorContract; 6 7 use SocialDept\Schema\Data\LexiconDocument; 7 8 use SocialDept\Schema\Exceptions\RecordValidationException; ··· 11 12 12 13 class Validator implements LexiconValidatorContract 13 14 { 15 + use Macroable; 14 16 /** 15 17 * Validation mode constants. 16 18 */
+267
tests/Unit/Support/ExtensionManagerTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Support; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Support\ExtensionManager; 7 + 8 + class ExtensionManagerTest extends TestCase 9 + { 10 + protected ExtensionManager $manager; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->manager = new ExtensionManager(); 17 + } 18 + 19 + public function test_it_registers_hook(): void 20 + { 21 + $this->manager->hook('test', fn () => 'result'); 22 + 23 + $this->assertTrue($this->manager->has('test')); 24 + } 25 + 26 + public function test_it_executes_hook(): void 27 + { 28 + $this->manager->hook('test', fn () => 'result1'); 29 + $this->manager->hook('test', fn () => 'result2'); 30 + 31 + $results = $this->manager->execute('test'); 32 + 33 + $this->assertEquals(['result1', 'result2'], $results); 34 + } 35 + 36 + public function test_it_executes_hook_with_arguments(): void 37 + { 38 + $this->manager->hook('test', fn ($a, $b) => $a + $b); 39 + 40 + $results = $this->manager->execute('test', 5, 3); 41 + 42 + $this->assertEquals([8], $results); 43 + } 44 + 45 + public function test_it_returns_empty_array_for_nonexistent_hook(): void 46 + { 47 + $results = $this->manager->execute('nonexistent'); 48 + 49 + $this->assertEquals([], $results); 50 + } 51 + 52 + public function test_it_executes_until_first_non_null(): void 53 + { 54 + $this->manager->hook('test', fn () => null); 55 + $this->manager->hook('test', fn () => 'found'); 56 + $this->manager->hook('test', fn () => 'not reached'); 57 + 58 + $result = $this->manager->executeUntil('test'); 59 + 60 + $this->assertEquals('found', $result); 61 + } 62 + 63 + public function test_it_returns_null_when_all_hooks_return_null(): void 64 + { 65 + $this->manager->hook('test', fn () => null); 66 + $this->manager->hook('test', fn () => null); 67 + 68 + $result = $this->manager->executeUntil('test'); 69 + 70 + $this->assertNull($result); 71 + } 72 + 73 + public function test_it_returns_null_for_nonexistent_hook_until(): void 74 + { 75 + $result = $this->manager->executeUntil('nonexistent'); 76 + 77 + $this->assertNull($result); 78 + } 79 + 80 + public function test_it_filters_value_through_hooks(): void 81 + { 82 + $this->manager->hook('test', fn ($value) => $value * 2); 83 + $this->manager->hook('test', fn ($value) => $value + 10); 84 + 85 + $result = $this->manager->filter('test', 5); 86 + 87 + $this->assertEquals(20, $result); // (5 * 2) + 10 88 + } 89 + 90 + public function test_it_returns_original_value_when_no_hooks(): void 91 + { 92 + $result = $this->manager->filter('nonexistent', 'original'); 93 + 94 + $this->assertEquals('original', $result); 95 + } 96 + 97 + public function test_it_filters_with_additional_arguments(): void 98 + { 99 + $this->manager->hook('test', fn ($value, $multiplier) => $value * $multiplier); 100 + 101 + $result = $this->manager->filter('test', 5, 3); 102 + 103 + $this->assertEquals(15, $result); 104 + } 105 + 106 + public function test_it_checks_if_has_hook(): void 107 + { 108 + $this->assertFalse($this->manager->has('test')); 109 + 110 + $this->manager->hook('test', fn () => 'result'); 111 + 112 + $this->assertTrue($this->manager->has('test')); 113 + } 114 + 115 + public function test_it_gets_hook_callbacks(): void 116 + { 117 + $callback1 = fn () => 'result1'; 118 + $callback2 = fn () => 'result2'; 119 + 120 + $this->manager->hook('test', $callback1); 121 + $this->manager->hook('test', $callback2); 122 + 123 + $callbacks = $this->manager->get('test'); 124 + 125 + $this->assertCount(2, $callbacks); 126 + $this->assertSame($callback1, $callbacks[0]); 127 + $this->assertSame($callback2, $callbacks[1]); 128 + } 129 + 130 + public function test_it_returns_empty_array_for_nonexistent_hook_get(): void 131 + { 132 + $callbacks = $this->manager->get('nonexistent'); 133 + 134 + $this->assertEquals([], $callbacks); 135 + } 136 + 137 + public function test_it_removes_hook(): void 138 + { 139 + $this->manager->hook('test', fn () => 'result'); 140 + 141 + $this->assertTrue($this->manager->has('test')); 142 + 143 + $this->manager->remove('test'); 144 + 145 + $this->assertFalse($this->manager->has('test')); 146 + } 147 + 148 + public function test_it_clears_all_hooks(): void 149 + { 150 + $this->manager->hook('test1', fn () => 'result1'); 151 + $this->manager->hook('test2', fn () => 'result2'); 152 + 153 + $this->assertTrue($this->manager->has('test1')); 154 + $this->assertTrue($this->manager->has('test2')); 155 + 156 + $this->manager->clear(); 157 + 158 + $this->assertFalse($this->manager->has('test1')); 159 + $this->assertFalse($this->manager->has('test2')); 160 + } 161 + 162 + public function test_it_counts_callbacks(): void 163 + { 164 + $this->assertEquals(0, $this->manager->count('test')); 165 + 166 + $this->manager->hook('test', fn () => 'result1'); 167 + 168 + $this->assertEquals(1, $this->manager->count('test')); 169 + 170 + $this->manager->hook('test', fn () => 'result2'); 171 + 172 + $this->assertEquals(2, $this->manager->count('test')); 173 + } 174 + 175 + public function test_it_gets_hook_names(): void 176 + { 177 + $this->manager->hook('test1', fn () => 'result1'); 178 + $this->manager->hook('test2', fn () => 'result2'); 179 + $this->manager->hook('test3', fn () => 'result3'); 180 + 181 + $names = $this->manager->names(); 182 + 183 + $this->assertEquals(['test1', 'test2', 'test3'], $names); 184 + } 185 + 186 + public function test_it_returns_empty_array_when_no_hooks(): void 187 + { 188 + $names = $this->manager->names(); 189 + 190 + $this->assertEquals([], $names); 191 + } 192 + 193 + public function test_it_chains_hook_calls(): void 194 + { 195 + $result = $this->manager 196 + ->hook('test1', fn () => 'result1') 197 + ->hook('test2', fn () => 'result2'); 198 + 199 + $this->assertSame($this->manager, $result); 200 + $this->assertTrue($this->manager->has('test1')); 201 + $this->assertTrue($this->manager->has('test2')); 202 + } 203 + 204 + public function test_it_chains_remove_calls(): void 205 + { 206 + $this->manager->hook('test', fn () => 'result'); 207 + 208 + $result = $this->manager->remove('test'); 209 + 210 + $this->assertSame($this->manager, $result); 211 + } 212 + 213 + public function test_it_chains_clear_calls(): void 214 + { 215 + $result = $this->manager->clear(); 216 + 217 + $this->assertSame($this->manager, $result); 218 + } 219 + 220 + public function test_it_handles_multiple_callbacks_for_same_hook(): void 221 + { 222 + $executed = []; 223 + 224 + $this->manager->hook('test', function () use (&$executed) { 225 + $executed[] = 'first'; 226 + }); 227 + 228 + $this->manager->hook('test', function () use (&$executed) { 229 + $executed[] = 'second'; 230 + }); 231 + 232 + $this->manager->hook('test', function () use (&$executed) { 233 + $executed[] = 'third'; 234 + }); 235 + 236 + $this->manager->execute('test'); 237 + 238 + $this->assertEquals(['first', 'second', 'third'], $executed); 239 + } 240 + 241 + public function test_it_handles_complex_filter_chain(): void 242 + { 243 + $this->manager->hook('transform', fn ($value) => strtoupper($value)); 244 + $this->manager->hook('transform', fn ($value) => str_replace(' ', '_', $value)); 245 + $this->manager->hook('transform', fn ($value) => $value.'_SUFFIX'); 246 + 247 + $result = $this->manager->filter('transform', 'hello world'); 248 + 249 + $this->assertEquals('HELLO_WORLD_SUFFIX', $result); 250 + } 251 + 252 + public function test_it_handles_execute_until_with_arguments(): void 253 + { 254 + $this->manager->hook('search', fn ($needle, $haystack) => in_array($needle, $haystack) ? $needle : null); 255 + 256 + $result = $this->manager->executeUntil('search', 'b', ['a', 'b', 'c']); 257 + 258 + $this->assertEquals('b', $result); 259 + } 260 + 261 + public function test_it_removes_nonexistent_hook_safely(): void 262 + { 263 + $result = $this->manager->remove('nonexistent'); 264 + 265 + $this->assertSame($this->manager, $result); 266 + } 267 + }
+221
tests/Unit/Support/MacroableTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Support; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Services\BlobHandler; 7 + use SocialDept\Schema\Services\ModelMapper; 8 + use SocialDept\Schema\Services\UnionResolver; 9 + use SocialDept\Schema\Validation\Validator; 10 + 11 + class MacroableTest extends TestCase 12 + { 13 + protected function setUp(): void 14 + { 15 + parent::setUp(); 16 + 17 + // Clear any existing macros 18 + ModelMapper::flushMacros(); 19 + BlobHandler::flushMacros(); 20 + UnionResolver::flushMacros(); 21 + Validator::flushMacros(); 22 + } 23 + 24 + public function test_model_mapper_supports_macros(): void 25 + { 26 + ModelMapper::macro('customMethod', fn () => 'custom result'); 27 + 28 + $mapper = new ModelMapper(); 29 + 30 + $this->assertEquals('custom result', $mapper->customMethod()); 31 + } 32 + 33 + public function test_model_mapper_macro_can_access_instance(): void 34 + { 35 + ModelMapper::macro('getCount', function () { 36 + return $this->count(); 37 + }); 38 + 39 + $mapper = new ModelMapper(); 40 + 41 + $this->assertEquals(0, $mapper->getCount()); 42 + } 43 + 44 + public function test_blob_handler_supports_macros(): void 45 + { 46 + BlobHandler::macro('customMethod', fn () => 'blob custom'); 47 + 48 + $handler = new BlobHandler(); 49 + 50 + $this->assertEquals('blob custom', $handler->customMethod()); 51 + } 52 + 53 + public function test_blob_handler_macro_can_access_instance(): void 54 + { 55 + BlobHandler::macro('getCurrentDisk', function () { 56 + return $this->getDisk(); 57 + }); 58 + 59 + $handler = new BlobHandler('local'); 60 + 61 + $this->assertEquals('local', $handler->getCurrentDisk()); 62 + } 63 + 64 + public function test_union_resolver_supports_macros(): void 65 + { 66 + UnionResolver::macro('customMethod', fn () => 'union custom'); 67 + 68 + $resolver = new UnionResolver(); 69 + 70 + $this->assertEquals('union custom', $resolver->customMethod()); 71 + } 72 + 73 + public function test_validator_supports_macros(): void 74 + { 75 + Validator::macro('customMethod', fn () => 'validator custom'); 76 + 77 + $schemaLoader = $this->createMock(\SocialDept\Schema\Parser\SchemaLoader::class); 78 + $validator = new Validator($schemaLoader); 79 + 80 + $this->assertEquals('validator custom', $validator->customMethod()); 81 + } 82 + 83 + public function test_macros_can_accept_parameters(): void 84 + { 85 + ModelMapper::macro('multiply', fn ($a, $b) => $a * $b); 86 + 87 + $mapper = new ModelMapper(); 88 + 89 + $this->assertEquals(15, $mapper->multiply(3, 5)); 90 + } 91 + 92 + public function test_macros_work_across_multiple_instances(): void 93 + { 94 + ModelMapper::macro('greet', fn ($name) => "Hello, {$name}!"); 95 + 96 + $mapper1 = new ModelMapper(); 97 + $mapper2 = new ModelMapper(); 98 + 99 + $this->assertEquals('Hello, Alice!', $mapper1->greet('Alice')); 100 + $this->assertEquals('Hello, Bob!', $mapper2->greet('Bob')); 101 + } 102 + 103 + public function test_macro_can_return_instance_for_chaining(): void 104 + { 105 + ModelMapper::macro('chainable', function () { 106 + return $this; 107 + }); 108 + 109 + $mapper = new ModelMapper(); 110 + 111 + $result = $mapper->chainable()->chainable(); 112 + 113 + $this->assertSame($mapper, $result); 114 + } 115 + 116 + public function test_has_macro_checks_existence(): void 117 + { 118 + ModelMapper::macro('exists', fn () => true); 119 + 120 + $this->assertTrue(ModelMapper::hasMacro('exists')); 121 + $this->assertFalse(ModelMapper::hasMacro('doesNotExist')); 122 + } 123 + 124 + public function test_flush_macros_removes_all(): void 125 + { 126 + ModelMapper::macro('test1', fn () => 'result1'); 127 + ModelMapper::macro('test2', fn () => 'result2'); 128 + 129 + $this->assertTrue(ModelMapper::hasMacro('test1')); 130 + $this->assertTrue(ModelMapper::hasMacro('test2')); 131 + 132 + ModelMapper::flushMacros(); 133 + 134 + $this->assertFalse(ModelMapper::hasMacro('test1')); 135 + $this->assertFalse(ModelMapper::hasMacro('test2')); 136 + } 137 + 138 + public function test_macros_are_independent_between_classes(): void 139 + { 140 + ModelMapper::macro('sharedName', fn () => 'mapper'); 141 + BlobHandler::macro('sharedName', fn () => 'handler'); 142 + 143 + $mapper = new ModelMapper(); 144 + $handler = new BlobHandler(); 145 + 146 + $this->assertEquals('mapper', $mapper->sharedName()); 147 + $this->assertEquals('handler', $handler->sharedName()); 148 + } 149 + 150 + public function test_macro_can_use_closure_variables(): void 151 + { 152 + $prefix = 'PREFIX'; 153 + 154 + ModelMapper::macro('withPrefix', fn ($value) => "{$prefix}: {$value}"); 155 + 156 + $mapper = new ModelMapper(); 157 + 158 + $this->assertEquals('PREFIX: test', $mapper->withPrefix('test')); 159 + } 160 + 161 + public function test_macro_can_modify_instance_state(): void 162 + { 163 + BlobHandler::macro('switchDisk', function ($disk) { 164 + $this->setDisk($disk); 165 + 166 + return $this; 167 + }); 168 + 169 + $handler = new BlobHandler('local'); 170 + 171 + $this->assertEquals('local', $handler->getDisk()); 172 + 173 + $handler->switchDisk('s3'); 174 + 175 + $this->assertEquals('s3', $handler->getDisk()); 176 + } 177 + 178 + public function test_mixin_adds_multiple_methods(): void 179 + { 180 + $mixin = new class () { 181 + public function method1() 182 + { 183 + return fn () => 'method1'; 184 + } 185 + 186 + public function method2() 187 + { 188 + return fn () => 'method2'; 189 + } 190 + }; 191 + 192 + ModelMapper::mixin($mixin); 193 + 194 + $mapper = new ModelMapper(); 195 + 196 + $this->assertEquals('method1', $mapper->method1()); 197 + $this->assertEquals('method2', $mapper->method2()); 198 + } 199 + 200 + public function test_complex_macro_with_dependencies(): void 201 + { 202 + ModelMapper::macro('complexOperation', function ($data) { 203 + // Use existing methods 204 + $count = $this->count(); 205 + 206 + return [ 207 + 'count' => $count, 208 + 'data' => strtoupper($data), 209 + ]; 210 + }); 211 + 212 + $mapper = new ModelMapper(); 213 + 214 + $result = $mapper->complexOperation('test'); 215 + 216 + $this->assertEquals([ 217 + 'count' => 0, 218 + 'data' => 'TEST', 219 + ], $result); 220 + } 221 + }