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 custom Laravel validation rules

+1390
+116
src/SchemaServiceProvider.php
··· 54 54 */ 55 55 public function boot(): void 56 56 { 57 + $this->bootValidationRules(); 58 + 57 59 if ($this->app->runningInConsole()) { 58 60 $this->bootForConsole(); 59 61 } 62 + } 63 + 64 + /** 65 + * Register custom validation rules. 66 + */ 67 + protected function bootValidationRules(): void 68 + { 69 + $validator = $this->app->make('validator'); 70 + 71 + // Register AT Protocol validation rules 72 + $validator->extend('nsid', function ($attribute, $value) { 73 + $rule = new Validation\Rules\Nsid(); 74 + $failed = false; 75 + $rule->validate($attribute, $value, function () use (&$failed) { 76 + $failed = true; 77 + }); 78 + 79 + return ! $failed; 80 + }, 'The :attribute is not a valid NSID.'); 81 + 82 + $validator->extend('did', function ($attribute, $value) { 83 + $rule = new Validation\Rules\Did(); 84 + $failed = false; 85 + $rule->validate($attribute, $value, function () use (&$failed) { 86 + $failed = true; 87 + }); 88 + 89 + return ! $failed; 90 + }, 'The :attribute is not a valid DID.'); 91 + 92 + $validator->extend('handle', function ($attribute, $value) { 93 + $rule = new Validation\Rules\Handle(); 94 + $failed = false; 95 + $rule->validate($attribute, $value, function () use (&$failed) { 96 + $failed = true; 97 + }); 98 + 99 + return ! $failed; 100 + }, 'The :attribute is not a valid handle.'); 101 + 102 + $validator->extend('at_uri', function ($attribute, $value) { 103 + $rule = new Validation\Rules\AtUri(); 104 + $failed = false; 105 + $rule->validate($attribute, $value, function () use (&$failed) { 106 + $failed = true; 107 + }); 108 + 109 + return ! $failed; 110 + }, 'The :attribute is not a valid AT URI.'); 111 + 112 + $validator->extend('at_datetime', function ($attribute, $value) { 113 + $rule = new Validation\Rules\AtDatetime(); 114 + $failed = false; 115 + $rule->validate($attribute, $value, function () use (&$failed) { 116 + $failed = true; 117 + }); 118 + 119 + return ! $failed; 120 + }, 'The :attribute is not a valid AT Protocol datetime.'); 121 + 122 + $validator->extend('cid', function ($attribute, $value) { 123 + $rule = new Validation\Rules\Cid(); 124 + $failed = false; 125 + $rule->validate($attribute, $value, function () use (&$failed) { 126 + $failed = true; 127 + }); 128 + 129 + return ! $failed; 130 + }, 'The :attribute is not a valid CID.'); 131 + 132 + $validator->extend('max_graphemes', function ($attribute, $value, $parameters) { 133 + if (empty($parameters)) { 134 + return false; 135 + } 136 + $rule = new Validation\Rules\MaxGraphemes((int) $parameters[0]); 137 + $failed = false; 138 + $rule->validate($attribute, $value, function () use (&$failed) { 139 + $failed = true; 140 + }); 141 + 142 + return ! $failed; 143 + }, 'The :attribute may not be greater than :max_graphemes graphemes.'); 144 + 145 + $validator->extend('min_graphemes', function ($attribute, $value, $parameters) { 146 + if (empty($parameters)) { 147 + return false; 148 + } 149 + $rule = new Validation\Rules\MinGraphemes((int) $parameters[0]); 150 + $failed = false; 151 + $rule->validate($attribute, $value, function () use (&$failed) { 152 + $failed = true; 153 + }); 154 + 155 + return ! $failed; 156 + }, 'The :attribute must be at least :min_graphemes graphemes.'); 157 + 158 + $validator->extend('language', function ($attribute, $value) { 159 + $rule = new Validation\Rules\Language(); 160 + $failed = false; 161 + $rule->validate($attribute, $value, function () use (&$failed) { 162 + $failed = true; 163 + }); 164 + 165 + return ! $failed; 166 + }, 'The :attribute is not a valid BCP 47 language code.'); 167 + 168 + // Register replacements for parameterized rules 169 + $validator->replacer('max_graphemes', function ($message, $attribute, $rule, $parameters) { 170 + return str_replace(':max_graphemes', $parameters[0], $message); 171 + }); 172 + 173 + $validator->replacer('min_graphemes', function ($message, $attribute, $rule, $parameters) { 174 + return str_replace(':min_graphemes', $parameters[0], $message); 175 + }); 60 176 } 61 177 62 178 /**
+54
src/Validation/Rules/AtDatetime.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use DateTime; 7 + use Illuminate\Contracts\Validation\ValidationRule; 8 + 9 + class AtDatetime implements ValidationRule 10 + { 11 + /** 12 + * Run the validation rule. 13 + */ 14 + public function validate(string $attribute, mixed $value, Closure $fail): void 15 + { 16 + if (! is_string($value)) { 17 + $fail("The {$attribute} must be a string."); 18 + 19 + return; 20 + } 21 + 22 + if (! $this->isValidAtDatetime($value)) { 23 + $fail("The {$attribute} is not a valid AT Protocol datetime."); 24 + } 25 + } 26 + 27 + /** 28 + * Validate AT Protocol datetime format. 29 + * 30 + * Must be ISO 8601 format with timezone (typically UTC) 31 + * Example: 2024-01-01T00:00:00Z or 2024-01-01T00:00:00.000Z 32 + */ 33 + protected function isValidAtDatetime(string $value): bool 34 + { 35 + // Try to parse as DateTime with ISO 8601 format 36 + $datetime = DateTime::createFromFormat(DateTime::ATOM, $value); 37 + 38 + if ($datetime !== false) { 39 + return true; 40 + } 41 + 42 + // Also try with milliseconds 43 + $datetime = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $value); 44 + 45 + if ($datetime !== false) { 46 + return true; 47 + } 48 + 49 + // Try standard ISO 8601 with Z 50 + $datetime = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $value); 51 + 52 + return $datetime !== false; 53 + } 54 + }
+98
src/Validation/Rules/AtUri.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + 8 + class AtUri implements ValidationRule 9 + { 10 + /** 11 + * Run the validation rule. 12 + */ 13 + public function validate(string $attribute, mixed $value, Closure $fail): void 14 + { 15 + if (! is_string($value)) { 16 + $fail("The {$attribute} must be a string."); 17 + 18 + return; 19 + } 20 + 21 + if (! $this->isValidAtUri($value)) { 22 + $fail("The {$attribute} is not a valid AT URI."); 23 + } 24 + } 25 + 26 + /** 27 + * Validate AT URI format. 28 + * 29 + * Format: at://did:plc:xyz/collection/rkey 30 + * or: at://handle.domain/collection/rkey 31 + */ 32 + protected function isValidAtUri(string $value): bool 33 + { 34 + // Must start with at:// 35 + if (! str_starts_with($value, 'at://')) { 36 + return false; 37 + } 38 + 39 + // Remove at:// prefix 40 + $remainder = substr($value, 5); 41 + 42 + // Must have at least authority part 43 + if (empty($remainder)) { 44 + return false; 45 + } 46 + 47 + // Split into authority and path 48 + $parts = explode('/', $remainder, 2); 49 + $authority = $parts[0]; 50 + 51 + // Validate authority (DID or handle) 52 + $didRule = new Did(); 53 + $handleRule = new Handle(); 54 + 55 + $isValidDid = true; 56 + $isValidHandle = true; 57 + 58 + $didRule->validate('authority', $authority, function () use (&$isValidDid) { 59 + $isValidDid = false; 60 + }); 61 + 62 + $handleRule->validate('authority', $authority, function () use (&$isValidHandle) { 63 + $isValidHandle = false; 64 + }); 65 + 66 + if (! $isValidDid && ! $isValidHandle) { 67 + return false; 68 + } 69 + 70 + // If there's a path, validate it 71 + if (isset($parts[1]) && ! empty($parts[1])) { 72 + // Path should be collection/rkey format 73 + $pathParts = explode('/', $parts[1]); 74 + 75 + if (count($pathParts) < 1) { 76 + return false; 77 + } 78 + 79 + // Each path segment should be valid 80 + foreach ($pathParts as $segment) { 81 + if (empty($segment) || ! $this->isValidPathSegment($segment)) { 82 + return false; 83 + } 84 + } 85 + } 86 + 87 + return true; 88 + } 89 + 90 + /** 91 + * Check if path segment is valid. 92 + */ 93 + protected function isValidPathSegment(string $segment): bool 94 + { 95 + // Path segments should be alphanumeric with dots, hyphens, underscores 96 + return (bool) preg_match('/^[a-zA-Z0-9._-]+$/', $segment); 97 + } 98 + }
+76
src/Validation/Rules/Cid.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + 8 + class Cid implements ValidationRule 9 + { 10 + /** 11 + * Run the validation rule. 12 + */ 13 + public function validate(string $attribute, mixed $value, Closure $fail): void 14 + { 15 + if (! is_string($value)) { 16 + $fail("The {$attribute} must be a string."); 17 + 18 + return; 19 + } 20 + 21 + if (! $this->isValidCid($value)) { 22 + $fail("The {$attribute} is not a valid CID."); 23 + } 24 + } 25 + 26 + /** 27 + * Validate CID format. 28 + * 29 + * CID (Content Identifier) is typically base58 or base32 encoded 30 + * CIDv0: Qm... (base58, 46 characters) 31 + * CIDv1: b... (base32) or z... (base58) 32 + */ 33 + protected function isValidCid(string $value): bool 34 + { 35 + $length = strlen($value); 36 + 37 + // CIDv0: Starts with Qm and is 46 characters 38 + if (str_starts_with($value, 'Qm') && $length === 46) { 39 + return $this->isBase58($value); 40 + } 41 + 42 + // CIDv1: Starts with 'b' (base32) or 'z' (base58) 43 + if (str_starts_with($value, 'b') && $length > 10) { 44 + return $this->isBase32($value); 45 + } 46 + 47 + if (str_starts_with($value, 'z') && $length > 10) { 48 + return $this->isBase58($value); 49 + } 50 + 51 + // Also accept bafy... (base32 CIDv1) 52 + if (str_starts_with($value, 'bafy') && $length > 10) { 53 + return $this->isBase32($value); 54 + } 55 + 56 + return false; 57 + } 58 + 59 + /** 60 + * Check if string is valid base58. 61 + */ 62 + protected function isBase58(string $value): bool 63 + { 64 + // Base58 alphabet (no 0, O, I, l) 65 + return (bool) preg_match('/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/', $value); 66 + } 67 + 68 + /** 69 + * Check if string is valid base32. 70 + */ 71 + protected function isBase32(string $value): bool 72 + { 73 + // Base32 lowercase alphabet 74 + return (bool) preg_match('/^[a-z2-7]+$/', $value); 75 + } 76 + }
+44
src/Validation/Rules/Did.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + 8 + class Did implements ValidationRule 9 + { 10 + /** 11 + * Run the validation rule. 12 + */ 13 + public function validate(string $attribute, mixed $value, Closure $fail): void 14 + { 15 + if (! is_string($value)) { 16 + $fail("The {$attribute} must be a string."); 17 + 18 + return; 19 + } 20 + 21 + // Check if Beacon package is available 22 + if (class_exists('SocialDept\Beacon\Support\Identity')) { 23 + if (! \SocialDept\Beacon\Support\Identity::isDid($value)) { 24 + $fail("The {$attribute} is not a valid DID."); 25 + } 26 + 27 + return; 28 + } 29 + 30 + // Fallback validation if Beacon is not available 31 + if (! $this->isValidDid($value)) { 32 + $fail("The {$attribute} is not a valid DID."); 33 + } 34 + } 35 + 36 + /** 37 + * Fallback DID validation. 38 + */ 39 + protected function isValidDid(string $value): bool 40 + { 41 + // DID format: did:method:method-specific-id 42 + return (bool) preg_match('/^did:[a-z]+:[a-zA-Z0-9._:%-]+$/', $value); 43 + } 44 + }
+50
src/Validation/Rules/Handle.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + 8 + class Handle implements ValidationRule 9 + { 10 + /** 11 + * Run the validation rule. 12 + */ 13 + public function validate(string $attribute, mixed $value, Closure $fail): void 14 + { 15 + if (! is_string($value)) { 16 + $fail("The {$attribute} must be a string."); 17 + 18 + return; 19 + } 20 + 21 + // Check if Beacon package is available 22 + if (class_exists('SocialDept\Beacon\Support\Identity')) { 23 + if (! \SocialDept\Beacon\Support\Identity::isHandle($value)) { 24 + $fail("The {$attribute} is not a valid handle."); 25 + } 26 + 27 + return; 28 + } 29 + 30 + // Fallback validation if Beacon is not available 31 + if (! $this->isValidHandle($value)) { 32 + $fail("The {$attribute} is not a valid handle."); 33 + } 34 + } 35 + 36 + /** 37 + * Fallback handle validation. 38 + */ 39 + protected function isValidHandle(string $value): bool 40 + { 41 + // Handle format: domain.tld (DNS name) 42 + // Must be at least 3 chars, no spaces, valid DNS characters 43 + if (strlen($value) < 3 || strlen($value) > 253) { 44 + return false; 45 + } 46 + 47 + // Check for valid DNS hostname format 48 + return (bool) preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/', $value); 49 + } 50 + }
+61
src/Validation/Rules/Language.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + 8 + class Language implements ValidationRule 9 + { 10 + /** 11 + * Run the validation rule. 12 + */ 13 + public function validate(string $attribute, mixed $value, Closure $fail): void 14 + { 15 + if (! is_string($value)) { 16 + $fail("The {$attribute} must be a string."); 17 + 18 + return; 19 + } 20 + 21 + if (! $this->isValidBcp47($value)) { 22 + $fail("The {$attribute} is not a valid BCP 47 language code."); 23 + } 24 + } 25 + 26 + /** 27 + * Validate BCP 47 language code. 28 + * 29 + * Format: language[-script][-region][-variant] 30 + * Examples: en, en-US, zh-Hans, en-GB-oed 31 + */ 32 + protected function isValidBcp47(string $value): bool 33 + { 34 + // BCP 47 regex pattern 35 + // Primary language: 2-3 letter code or 4-8 letter code 36 + // Script: 4 letters (optional) 37 + // Region: 2 letters or 3 digits (optional) 38 + // Variant: 5-8 alphanumeric or digit followed by 3 alphanumeric (optional, repeatable) 39 + $pattern = '/^ 40 + ([a-z]{2,3}|[a-z]{4}|[a-z]{5,8}) # Primary language 41 + (-[A-Z][a-z]{3})? # Script (optional) 42 + (-([A-Z]{2}|[0-9]{3}))? # Region (optional) 43 + (-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))* # Variant (optional, repeatable) 44 + (-[a-z]-[a-z0-9]{2,8})* # Extension (optional) 45 + (-x-[a-z0-9]{1,8})? # Private use (optional) 46 + $/xi'; 47 + 48 + if (! preg_match($pattern, $value)) { 49 + return false; 50 + } 51 + 52 + // Additional validation: Check if primary language is valid 53 + $parts = explode('-', $value); 54 + $primaryLanguage = strtolower($parts[0]); 55 + 56 + // Language code should be 2-3 characters (ISO 639-1 or 639-2) 57 + $length = strlen($primaryLanguage); 58 + 59 + return $length >= 2 && $length <= 8; 60 + } 61 + }
+40
src/Validation/Rules/MaxGraphemes.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + 8 + class MaxGraphemes implements ValidationRule 9 + { 10 + /** 11 + * Maximum grapheme count. 12 + */ 13 + protected int $max; 14 + 15 + /** 16 + * Create a new rule instance. 17 + */ 18 + public function __construct(int $max) 19 + { 20 + $this->max = $max; 21 + } 22 + 23 + /** 24 + * Run the validation rule. 25 + */ 26 + public function validate(string $attribute, mixed $value, Closure $fail): void 27 + { 28 + if (! is_string($value)) { 29 + $fail("The {$attribute} must be a string."); 30 + 31 + return; 32 + } 33 + 34 + $count = grapheme_strlen($value); 35 + 36 + if ($count > $this->max) { 37 + $fail("The {$attribute} may not be greater than {$this->max} graphemes."); 38 + } 39 + } 40 + }
+40
src/Validation/Rules/MinGraphemes.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + 8 + class MinGraphemes implements ValidationRule 9 + { 10 + /** 11 + * Minimum grapheme count. 12 + */ 13 + protected int $min; 14 + 15 + /** 16 + * Create a new rule instance. 17 + */ 18 + public function __construct(int $min) 19 + { 20 + $this->min = $min; 21 + } 22 + 23 + /** 24 + * Run the validation rule. 25 + */ 26 + public function validate(string $attribute, mixed $value, Closure $fail): void 27 + { 28 + if (! is_string($value)) { 29 + $fail("The {$attribute} must be a string."); 30 + 31 + return; 32 + } 33 + 34 + $count = grapheme_strlen($value); 35 + 36 + if ($count < $this->min) { 37 + $fail("The {$attribute} must be at least {$this->min} graphemes."); 38 + } 39 + } 40 + }
+28
src/Validation/Rules/Nsid.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation\Rules; 4 + 5 + use Closure; 6 + use Illuminate\Contracts\Validation\ValidationRule; 7 + use SocialDept\Schema\Parser\Nsid as NsidParser; 8 + 9 + class Nsid implements ValidationRule 10 + { 11 + /** 12 + * Run the validation rule. 13 + */ 14 + public function validate(string $attribute, mixed $value, Closure $fail): void 15 + { 16 + if (! is_string($value)) { 17 + $fail("The {$attribute} must be a string."); 18 + 19 + return; 20 + } 21 + 22 + try { 23 + NsidParser::parse($value); 24 + } catch (\Exception) { 25 + $fail("The {$attribute} is not a valid NSID."); 26 + } 27 + } 28 + }
+94
tests/Unit/Validation/Rules/AtDatetimeTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\AtDatetime; 7 + 8 + class AtDatetimeTest extends TestCase 9 + { 10 + protected AtDatetime $rule; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->rule = new AtDatetime(); 17 + } 18 + 19 + public function test_it_validates_valid_datetime_with_z(): void 20 + { 21 + $valid = '2024-01-01T00:00:00Z'; 22 + 23 + $failed = false; 24 + $this->rule->validate('datetime', $valid, function () use (&$failed) { 25 + $failed = true; 26 + }); 27 + 28 + $this->assertFalse($failed); 29 + } 30 + 31 + public function test_it_validates_datetime_with_milliseconds(): void 32 + { 33 + $valid = '2024-01-01T12:34:56.789Z'; 34 + 35 + $failed = false; 36 + $this->rule->validate('datetime', $valid, function () use (&$failed) { 37 + $failed = true; 38 + }); 39 + 40 + $this->assertFalse($failed); 41 + } 42 + 43 + public function test_it_validates_various_datetime_formats(): void 44 + { 45 + $validDatetimes = [ 46 + '2024-01-01T00:00:00Z', 47 + '2024-12-31T23:59:59Z', 48 + '2024-06-15T12:30:45Z', 49 + ]; 50 + 51 + foreach ($validDatetimes as $datetime) { 52 + $failed = false; 53 + $this->rule->validate('datetime', $datetime, function () use (&$failed) { 54 + $failed = true; 55 + }); 56 + 57 + $this->assertFalse($failed, "Expected {$datetime} to be valid"); 58 + } 59 + } 60 + 61 + public function test_it_rejects_invalid_datetime(): void 62 + { 63 + $invalid = 'not-a-datetime'; 64 + 65 + $failed = false; 66 + $this->rule->validate('datetime', $invalid, function () use (&$failed) { 67 + $failed = true; 68 + }); 69 + 70 + $this->assertTrue($failed); 71 + } 72 + 73 + public function test_it_rejects_datetime_without_timezone(): void 74 + { 75 + $invalid = '2024-01-01T00:00:00'; 76 + 77 + $failed = false; 78 + $this->rule->validate('datetime', $invalid, function () use (&$failed) { 79 + $failed = true; 80 + }); 81 + 82 + $this->assertTrue($failed); 83 + } 84 + 85 + public function test_it_rejects_non_string(): void 86 + { 87 + $failed = false; 88 + $this->rule->validate('datetime', 123, function () use (&$failed) { 89 + $failed = true; 90 + }); 91 + 92 + $this->assertTrue($failed); 93 + } 94 + }
+100
tests/Unit/Validation/Rules/AtUriTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\AtUri; 7 + 8 + class AtUriTest extends TestCase 9 + { 10 + protected AtUri $rule; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->rule = new AtUri(); 17 + } 18 + 19 + public function test_it_validates_valid_at_uri_with_did(): void 20 + { 21 + $valid = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jwlwj2ctlk26'; 22 + 23 + $failed = false; 24 + $this->rule->validate('uri', $valid, function () use (&$failed) { 25 + $failed = true; 26 + }); 27 + 28 + $this->assertFalse($failed); 29 + } 30 + 31 + public function test_it_validates_valid_at_uri_with_handle(): void 32 + { 33 + $valid = 'at://user.bsky.social/app.bsky.feed.post/3jwlwj2ctlk26'; 34 + 35 + $failed = false; 36 + $this->rule->validate('uri', $valid, function () use (&$failed) { 37 + $failed = true; 38 + }); 39 + 40 + $this->assertFalse($failed); 41 + } 42 + 43 + public function test_it_validates_at_uri_without_path(): void 44 + { 45 + $valid = 'at://did:plc:z72i7hdynmk6r22z27h6tvur'; 46 + 47 + $failed = false; 48 + $this->rule->validate('uri', $valid, function () use (&$failed) { 49 + $failed = true; 50 + }); 51 + 52 + $this->assertFalse($failed); 53 + } 54 + 55 + public function test_it_rejects_uri_without_at_protocol(): void 56 + { 57 + $invalid = 'https://example.com/path'; 58 + 59 + $failed = false; 60 + $this->rule->validate('uri', $invalid, function () use (&$failed) { 61 + $failed = true; 62 + }); 63 + 64 + $this->assertTrue($failed); 65 + } 66 + 67 + public function test_it_rejects_invalid_authority(): void 68 + { 69 + $invalid = 'at://not a valid authority/path'; 70 + 71 + $failed = false; 72 + $this->rule->validate('uri', $invalid, function () use (&$failed) { 73 + $failed = true; 74 + }); 75 + 76 + $this->assertTrue($failed); 77 + } 78 + 79 + public function test_it_rejects_empty_uri(): void 80 + { 81 + $invalid = 'at://'; 82 + 83 + $failed = false; 84 + $this->rule->validate('uri', $invalid, function () use (&$failed) { 85 + $failed = true; 86 + }); 87 + 88 + $this->assertTrue($failed); 89 + } 90 + 91 + public function test_it_rejects_non_string(): void 92 + { 93 + $failed = false; 94 + $this->rule->validate('uri', 123, function () use (&$failed) { 95 + $failed = true; 96 + }); 97 + 98 + $this->assertTrue($failed); 99 + } 100 + }
+88
tests/Unit/Validation/Rules/CidTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\Cid; 7 + 8 + class CidTest extends TestCase 9 + { 10 + protected Cid $rule; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->rule = new Cid(); 17 + } 18 + 19 + public function test_it_validates_cidv0(): void 20 + { 21 + $valid = 'QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V'; 22 + 23 + $failed = false; 24 + $this->rule->validate('cid', $valid, function () use (&$failed) { 25 + $failed = true; 26 + }); 27 + 28 + $this->assertFalse($failed); 29 + } 30 + 31 + public function test_it_validates_cidv1_with_base32(): void 32 + { 33 + $valid = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi'; 34 + 35 + $failed = false; 36 + $this->rule->validate('cid', $valid, function () use (&$failed) { 37 + $failed = true; 38 + }); 39 + 40 + $this->assertFalse($failed); 41 + } 42 + 43 + public function test_it_validates_cidv1_with_base58(): void 44 + { 45 + $valid = 'zdj7WhuEjrB52m1BisYCtmjH1hSKa7yZ3jEZ9JcXaFRD51wVz'; 46 + 47 + $failed = false; 48 + $this->rule->validate('cid', $valid, function () use (&$failed) { 49 + $failed = true; 50 + }); 51 + 52 + $this->assertFalse($failed); 53 + } 54 + 55 + public function test_it_rejects_invalid_cid(): void 56 + { 57 + $invalid = 'not-a-cid'; 58 + 59 + $failed = false; 60 + $this->rule->validate('cid', $invalid, function () use (&$failed) { 61 + $failed = true; 62 + }); 63 + 64 + $this->assertTrue($failed); 65 + } 66 + 67 + public function test_it_rejects_cidv0_with_wrong_length(): void 68 + { 69 + $invalid = 'QmShortCid'; 70 + 71 + $failed = false; 72 + $this->rule->validate('cid', $invalid, function () use (&$failed) { 73 + $failed = true; 74 + }); 75 + 76 + $this->assertTrue($failed); 77 + } 78 + 79 + public function test_it_rejects_non_string(): void 80 + { 81 + $failed = false; 82 + $this->rule->validate('cid', 123, function () use (&$failed) { 83 + $failed = true; 84 + }); 85 + 86 + $this->assertTrue($failed); 87 + } 88 + }
+82
tests/Unit/Validation/Rules/DidTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\Did; 7 + 8 + class DidTest extends TestCase 9 + { 10 + protected Did $rule; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->rule = new Did(); 17 + } 18 + 19 + public function test_it_validates_valid_did(): void 20 + { 21 + $valid = 'did:plc:abcdef123456'; 22 + 23 + $failed = false; 24 + $this->rule->validate('did', $valid, function () use (&$failed) { 25 + $failed = true; 26 + }); 27 + 28 + $this->assertFalse($failed); 29 + } 30 + 31 + public function test_it_validates_various_did_methods(): void 32 + { 33 + $validDids = [ 34 + 'did:plc:z72i7hdynmk6r22z27h6tvur', 35 + 'did:web:example.com', 36 + 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK', 37 + ]; 38 + 39 + foreach ($validDids as $did) { 40 + $failed = false; 41 + $this->rule->validate('did', $did, function () use (&$failed) { 42 + $failed = true; 43 + }); 44 + 45 + $this->assertFalse($failed, "Expected {$did} to be valid"); 46 + } 47 + } 48 + 49 + public function test_it_rejects_invalid_did(): void 50 + { 51 + $invalid = 'not-a-did'; 52 + 53 + $failed = false; 54 + $this->rule->validate('did', $invalid, function () use (&$failed) { 55 + $failed = true; 56 + }); 57 + 58 + $this->assertTrue($failed); 59 + } 60 + 61 + public function test_it_rejects_did_without_method(): void 62 + { 63 + $invalid = 'did:'; 64 + 65 + $failed = false; 66 + $this->rule->validate('did', $invalid, function () use (&$failed) { 67 + $failed = true; 68 + }); 69 + 70 + $this->assertTrue($failed); 71 + } 72 + 73 + public function test_it_rejects_non_string(): void 74 + { 75 + $failed = false; 76 + $this->rule->validate('did', 123, function () use (&$failed) { 77 + $failed = true; 78 + }); 79 + 80 + $this->assertTrue($failed); 81 + } 82 + }
+83
tests/Unit/Validation/Rules/HandleTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\Handle; 7 + 8 + class HandleTest extends TestCase 9 + { 10 + protected Handle $rule; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->rule = new Handle(); 17 + } 18 + 19 + public function test_it_validates_valid_handle(): void 20 + { 21 + $valid = 'user.bsky.social'; 22 + 23 + $failed = false; 24 + $this->rule->validate('handle', $valid, function () use (&$failed) { 25 + $failed = true; 26 + }); 27 + 28 + $this->assertFalse($failed); 29 + } 30 + 31 + public function test_it_validates_various_handles(): void 32 + { 33 + $validHandles = [ 34 + 'example.com', 35 + 'user.bsky.social', 36 + 'my-handle.example.io', 37 + 'test123.domain.org', 38 + ]; 39 + 40 + foreach ($validHandles as $handle) { 41 + $failed = false; 42 + $this->rule->validate('handle', $handle, function () use (&$failed) { 43 + $failed = true; 44 + }); 45 + 46 + $this->assertFalse($failed, "Expected {$handle} to be valid"); 47 + } 48 + } 49 + 50 + public function test_it_rejects_invalid_handle(): void 51 + { 52 + $invalid = 'invalid handle'; 53 + 54 + $failed = false; 55 + $this->rule->validate('handle', $invalid, function () use (&$failed) { 56 + $failed = true; 57 + }); 58 + 59 + $this->assertTrue($failed); 60 + } 61 + 62 + public function test_it_rejects_too_short_handle(): void 63 + { 64 + $invalid = 'ab'; 65 + 66 + $failed = false; 67 + $this->rule->validate('handle', $invalid, function () use (&$failed) { 68 + $failed = true; 69 + }); 70 + 71 + $this->assertTrue($failed); 72 + } 73 + 74 + public function test_it_rejects_non_string(): void 75 + { 76 + $failed = false; 77 + $this->rule->validate('handle', 123, function () use (&$failed) { 78 + $failed = true; 79 + }); 80 + 81 + $this->assertTrue($failed); 82 + } 83 + }
+110
tests/Unit/Validation/Rules/LanguageTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\Language; 7 + 8 + class LanguageTest extends TestCase 9 + { 10 + protected Language $rule; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->rule = new Language(); 17 + } 18 + 19 + public function test_it_validates_simple_language_code(): void 20 + { 21 + $valid = 'en'; 22 + 23 + $failed = false; 24 + $this->rule->validate('language', $valid, function () use (&$failed) { 25 + $failed = true; 26 + }); 27 + 28 + $this->assertFalse($failed); 29 + } 30 + 31 + public function test_it_validates_language_with_region(): void 32 + { 33 + $valid = 'en-US'; 34 + 35 + $failed = false; 36 + $this->rule->validate('language', $valid, function () use (&$failed) { 37 + $failed = true; 38 + }); 39 + 40 + $this->assertFalse($failed); 41 + } 42 + 43 + public function test_it_validates_language_with_script(): void 44 + { 45 + $valid = 'zh-Hans'; 46 + 47 + $failed = false; 48 + $this->rule->validate('language', $valid, function () use (&$failed) { 49 + $failed = true; 50 + }); 51 + 52 + $this->assertFalse($failed); 53 + } 54 + 55 + public function test_it_validates_complex_language_tags(): void 56 + { 57 + $validLanguages = [ 58 + 'en', 59 + 'en-US', 60 + 'zh-Hans', 61 + 'zh-Hans-CN', 62 + 'en-GB', 63 + 'es-419', 64 + 'fr-CA', 65 + ]; 66 + 67 + foreach ($validLanguages as $language) { 68 + $failed = false; 69 + $this->rule->validate('language', $language, function () use (&$failed) { 70 + $failed = true; 71 + }); 72 + 73 + $this->assertFalse($failed, "Expected {$language} to be valid"); 74 + } 75 + } 76 + 77 + public function test_it_rejects_invalid_language_code(): void 78 + { 79 + $invalid = 'not-a-language-123'; 80 + 81 + $failed = false; 82 + $this->rule->validate('language', $invalid, function () use (&$failed) { 83 + $failed = true; 84 + }); 85 + 86 + $this->assertTrue($failed); 87 + } 88 + 89 + public function test_it_rejects_too_short_code(): void 90 + { 91 + $invalid = 'e'; 92 + 93 + $failed = false; 94 + $this->rule->validate('language', $invalid, function () use (&$failed) { 95 + $failed = true; 96 + }); 97 + 98 + $this->assertTrue($failed); 99 + } 100 + 101 + public function test_it_rejects_non_string(): void 102 + { 103 + $failed = false; 104 + $this->rule->validate('language', 123, function () use (&$failed) { 105 + $failed = true; 106 + }); 107 + 108 + $this->assertTrue($failed); 109 + } 110 + }
+78
tests/Unit/Validation/Rules/MaxGraphemesTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\MaxGraphemes; 7 + 8 + class MaxGraphemesTest extends TestCase 9 + { 10 + public function test_it_validates_string_within_limit(): void 11 + { 12 + $rule = new MaxGraphemes(10); 13 + 14 + $valid = 'Hello'; 15 + 16 + $failed = false; 17 + $rule->validate('text', $valid, function () use (&$failed) { 18 + $failed = true; 19 + }); 20 + 21 + $this->assertFalse($failed); 22 + } 23 + 24 + public function test_it_rejects_string_exceeding_limit(): void 25 + { 26 + $rule = new MaxGraphemes(5); 27 + 28 + $invalid = 'This is too long'; 29 + 30 + $failed = false; 31 + $rule->validate('text', $invalid, function () use (&$failed) { 32 + $failed = true; 33 + }); 34 + 35 + $this->assertTrue($failed); 36 + } 37 + 38 + public function test_it_counts_graphemes_correctly(): void 39 + { 40 + $rule = new MaxGraphemes(5); 41 + 42 + // 6 emoji graphemes 43 + $invalid = '😀😁😂😃😄😅'; 44 + 45 + $failed = false; 46 + $rule->validate('text', $invalid, function () use (&$failed) { 47 + $failed = true; 48 + }); 49 + 50 + $this->assertTrue($failed); 51 + } 52 + 53 + public function test_it_allows_exact_limit(): void 54 + { 55 + $rule = new MaxGraphemes(5); 56 + 57 + $valid = '😀😁😂😃😄'; 58 + 59 + $failed = false; 60 + $rule->validate('text', $valid, function () use (&$failed) { 61 + $failed = true; 62 + }); 63 + 64 + $this->assertFalse($failed); 65 + } 66 + 67 + public function test_it_rejects_non_string(): void 68 + { 69 + $rule = new MaxGraphemes(10); 70 + 71 + $failed = false; 72 + $rule->validate('text', 123, function () use (&$failed) { 73 + $failed = true; 74 + }); 75 + 76 + $this->assertTrue($failed); 77 + } 78 + }
+78
tests/Unit/Validation/Rules/MinGraphemesTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\MinGraphemes; 7 + 8 + class MinGraphemesTest extends TestCase 9 + { 10 + public function test_it_validates_string_meeting_minimum(): void 11 + { 12 + $rule = new MinGraphemes(5); 13 + 14 + $valid = 'Hello World'; 15 + 16 + $failed = false; 17 + $rule->validate('text', $valid, function () use (&$failed) { 18 + $failed = true; 19 + }); 20 + 21 + $this->assertFalse($failed); 22 + } 23 + 24 + public function test_it_rejects_string_below_minimum(): void 25 + { 26 + $rule = new MinGraphemes(10); 27 + 28 + $invalid = 'Short'; 29 + 30 + $failed = false; 31 + $rule->validate('text', $invalid, function () use (&$failed) { 32 + $failed = true; 33 + }); 34 + 35 + $this->assertTrue($failed); 36 + } 37 + 38 + public function test_it_counts_graphemes_correctly(): void 39 + { 40 + $rule = new MinGraphemes(5); 41 + 42 + // 3 emoji graphemes 43 + $invalid = '😀😁😂'; 44 + 45 + $failed = false; 46 + $rule->validate('text', $invalid, function () use (&$failed) { 47 + $failed = true; 48 + }); 49 + 50 + $this->assertTrue($failed); 51 + } 52 + 53 + public function test_it_allows_exact_minimum(): void 54 + { 55 + $rule = new MinGraphemes(5); 56 + 57 + $valid = '😀😁😂😃😄'; 58 + 59 + $failed = false; 60 + $rule->validate('text', $valid, function () use (&$failed) { 61 + $failed = true; 62 + }); 63 + 64 + $this->assertFalse($failed); 65 + } 66 + 67 + public function test_it_rejects_non_string(): void 68 + { 69 + $rule = new MinGraphemes(5); 70 + 71 + $failed = false; 72 + $rule->validate('text', 123, function () use (&$failed) { 73 + $failed = true; 74 + }); 75 + 76 + $this->assertTrue($failed); 77 + } 78 + }
+70
tests/Unit/Validation/Rules/NsidTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation\Rules; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\Rules\Nsid; 7 + 8 + class NsidTest extends TestCase 9 + { 10 + protected Nsid $rule; 11 + 12 + protected function setUp(): void 13 + { 14 + parent::setUp(); 15 + 16 + $this->rule = new Nsid(); 17 + } 18 + 19 + public function test_it_validates_valid_nsid(): void 20 + { 21 + $valid = 'app.bsky.feed.post'; 22 + 23 + $failed = false; 24 + $this->rule->validate('nsid', $valid, function () use (&$failed) { 25 + $failed = true; 26 + }); 27 + 28 + $this->assertFalse($failed); 29 + } 30 + 31 + public function test_it_rejects_invalid_nsid(): void 32 + { 33 + $invalid = 'invalid-nsid'; 34 + 35 + $failed = false; 36 + $this->rule->validate('nsid', $invalid, function () use (&$failed) { 37 + $failed = true; 38 + }); 39 + 40 + $this->assertTrue($failed); 41 + } 42 + 43 + public function test_it_rejects_non_string(): void 44 + { 45 + $failed = false; 46 + $this->rule->validate('nsid', 123, function () use (&$failed) { 47 + $failed = true; 48 + }); 49 + 50 + $this->assertTrue($failed); 51 + } 52 + 53 + public function test_it_validates_various_nsids(): void 54 + { 55 + $validNsids = [ 56 + 'com.example.test', 57 + 'app.bsky.feed.post', 58 + 'io.github.user.action', 59 + ]; 60 + 61 + foreach ($validNsids as $nsid) { 62 + $failed = false; 63 + $this->rule->validate('nsid', $nsid, function () use (&$failed) { 64 + $failed = true; 65 + }); 66 + 67 + $this->assertFalse($failed, "Expected {$nsid} to be valid"); 68 + } 69 + } 70 + }