Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 3.8 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Parser; 4 5use SocialDept\AtpSchema\Exceptions\SchemaException; 6use Stringable; 7 8class Nsid implements Stringable 9{ 10 /** 11 * NSID pattern: authority.name (reversed domain notation) 12 */ 13 protected const NSID_REGEX = '/^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/'; 14 15 /** 16 * Maximum NSID length 17 */ 18 protected const MAX_LENGTH = 317; 19 20 /** 21 * Minimum NSID segments 22 */ 23 protected const MIN_SEGMENTS = 3; 24 25 /** 26 * Create a new NSID instance. 27 */ 28 public function __construct( 29 protected string $nsid 30 ) { 31 $this->validate(); 32 } 33 34 /** 35 * Parse NSID from string. 36 */ 37 public static function parse(string $nsid): self 38 { 39 return new self($nsid); 40 } 41 42 /** 43 * Validate NSID format. 44 */ 45 protected function validate(): void 46 { 47 if (empty($this->nsid)) { 48 throw SchemaException::withContext('NSID cannot be empty', ['nsid' => $this->nsid]); 49 } 50 51 if (strlen($this->nsid) > self::MAX_LENGTH) { 52 throw SchemaException::withContext( 53 'NSID exceeds maximum length of '.self::MAX_LENGTH.' characters', 54 ['nsid' => $this->nsid, 'length' => strlen($this->nsid)] 55 ); 56 } 57 58 if (! preg_match(self::NSID_REGEX, $this->nsid)) { 59 throw SchemaException::withContext( 60 'Invalid NSID format. Expected reversed domain notation (e.g., app.bsky.feed.post)', 61 ['nsid' => $this->nsid] 62 ); 63 } 64 65 $segments = explode('.', $this->nsid); 66 if (count($segments) < self::MIN_SEGMENTS) { 67 throw SchemaException::withContext( 68 'NSID must have at least '.self::MIN_SEGMENTS.' segments', 69 ['nsid' => $this->nsid, 'segments' => count($segments)] 70 ); 71 } 72 } 73 74 /** 75 * Get the authority (all segments except the last). 76 */ 77 public function getAuthority(): string 78 { 79 $segments = explode('.', $this->nsid); 80 array_pop($segments); 81 82 return implode('.', $segments); 83 } 84 85 /** 86 * Get the name (last segment). 87 */ 88 public function getName(): string 89 { 90 $segments = explode('.', $this->nsid); 91 92 return end($segments); 93 } 94 95 /** 96 * Get all segments. 97 * 98 * @return array<string> 99 */ 100 public function getSegments(): array 101 { 102 return explode('.', $this->nsid); 103 } 104 105 /** 106 * Convert to standard domain format (reverse segments). 107 */ 108 public function toDomain(): string 109 { 110 $segments = $this->getSegments(); 111 112 return implode('.', array_reverse($segments)); 113 } 114 115 /** 116 * Get the NSID string. 117 */ 118 public function toString(): string 119 { 120 return $this->nsid; 121 } 122 123 /** 124 * Convert to string. 125 */ 126 public function __toString(): string 127 { 128 return $this->toString(); 129 } 130 131 /** 132 * Check if NSID is valid (static method). 133 */ 134 public static function isValid(string $nsid): bool 135 { 136 try { 137 new self($nsid); 138 139 return true; 140 } catch (SchemaException) { 141 return false; 142 } 143 } 144 145 /** 146 * Check equality with another NSID. 147 */ 148 public function equals(self $other): bool 149 { 150 return $this->nsid === $other->nsid; 151 } 152 153 /** 154 * Get the authority domain for DNS lookup. 155 * Returns the authority segments in DNS order (reversed). 156 */ 157 public function getAuthorityDomain(): string 158 { 159 $authoritySegments = explode('.', $this->getAuthority()); 160 161 return implode('.', array_reverse($authoritySegments)); 162 } 163}