Compare commits

...

2 Commits

Author SHA1 Message Date
cb03c811ef Allow deeper nesting of hostnames 2026-06-02 13:19:25 +00:00
6f3c7338c7 Changed indentation and added type hints 2026-06-02 13:14:16 +00:00
2 changed files with 246 additions and 228 deletions

View File

@@ -13,140 +13,140 @@ use ML\IRI\IRI;
*/ */
class COIDParser { class COIDParser {
const COID_INVALID = 0; const COID_INVALID = 0;
const COID_ROOT = 1; const COID_ROOT = 1;
const COID_UNVERSIONED = 2; const COID_UNVERSIONED = 2;
const COID_VERSIONED = 3; const COID_VERSIONED = 3;
const COID_VERSION_WILDCARD = 4; const COID_VERSION_WILDCARD = 4;
const REGEX_HOSTNAME = "/^([a-z0-9-]+\.)?[a-z0-9-]+\.[a-z]+$/"; const REGEX_HOSTNAME = "/^([a-z0-9-]+\.)*[a-z0-9-]+\.[a-z]+$/";
const REGEX_SEGMENT = "/^[A-Za-z-_0-9\.]+$/"; const REGEX_SEGMENT = "/^[A-Za-z-_0-9\.]+$/";
const REGEX_VERSION_WILDCARD = "/^((\^|~)(\d+\.)?\d|(\d+\.){1,2}\*)$/"; const REGEX_VERSION_WILDCARD = "/^((\^|~)(\d+\.)?\d|(\d+\.){1,2}\*)$/";
/** /**
* Creates a new IRI object representing a COID from a string. * Creates a new IRI object representing a COID from a string.
* Adds the "coid://" prefix if necessary and normalizes case. * Adds the "coid://" prefix if necessary and normalizes case.
* *
* @param string $coidString A COID string. * @param string $coidString A COID string.
* @return IRI * @return IRI
*/ */
public static function fromString($coidString) { public static function fromString($coidString) : IRI {
$coidPre = new IRI( $coidPre = new IRI(
(strtolower(substr($coidString, 0, 7))=='coid://') ? $coidString : 'coid://'.$coidString (strtolower(substr($coidString, 0, 7)) == 'coid://') ? $coidString : 'coid://'.$coidString
); );
// Normalize scheme and host segments to lower case // Normalize scheme and host segments to lower case
return new IRI('coid://'.strtolower($coidPre->getHost()).$coidPre->getPath()); return new IRI('coid://'.strtolower($coidPre->getHost()).$coidPre->getPath());
}
/**
* Get the type of a COID.
*
* @param IRI $coid
* @return int|null
*/
public static function getType(IRI $coid) {
if ($coid->getScheme()!='coid' || $coid->getHost()==''
|| preg_match(self::REGEX_HOSTNAME, $coid->getHost()) != 1)
return self::COID_INVALID;
if ($coid->getPath()=='' || $coid->getPath()=='/')
return self::COID_ROOT;
$segments = explode('/', $coid->getPath());
switch (count($segments)) {
case 2:
return (preg_match(self::REGEX_SEGMENT, $segments[1]) == 1)
? self::COID_UNVERSIONED
: self::COID_INVALID;
case 3:
if (preg_match(self::REGEX_SEGMENT, $segments[1]) != 1)
return self::COID_INVALID;
if (preg_match(self::REGEX_SEGMENT, $segments[2]) == 1)
return self::COID_VERSIONED;
else
if (preg_match(self::REGEX_VERSION_WILDCARD, $segments[2]) == 1)
return self::COID_VERSION_WILDCARD;
else
return self::COID_INVALID;
default:
return self::COID_INVALID;
} }
}
/** /**
* Checks whether the given IRI object is a valid COID. * Get the type of a COID.
* *
* @param IRI $coid * @param IRI $coid
* @return boolean * @return int|null
*/ */
public static function isValidCOID(IRI $coid) { public static function getType(IRI $coid) : ?int {
return (self::getType($coid)!=self::COID_INVALID); if ($coid->getScheme()!='coid' || $coid->getHost()==''
} || preg_match(self::REGEX_HOSTNAME, $coid->getHost()) != 1)
return self::COID_INVALID;
/** if ($coid->getPath()=='' || $coid->getPath()=='/')
* Get the name segment of a valid COID or null if not available. return self::COID_ROOT;
*
* @param IRI $coid
* @return string|null
*/
public static function getName(IRI $coid) {
if (self::getType($coid)!=self::COID_INVALID
&& self::getType($coid)!=self::COID_ROOT) {
$segments = explode('/', $coid->getPath());
return $segments[1];
} else
return null;
}
/** $segments = explode('/', $coid->getPath());
* Get the version segment of a valid, versioned COID or null if not available. switch (count($segments)) {
* case 2:
* @param IRI $coid return (preg_match(self::REGEX_SEGMENT, $segments[1]) == 1)
* @return string|null ? self::COID_UNVERSIONED
*/ : self::COID_INVALID;
public static function getVersion(IRI $coid) { case 3:
if (self::getType($coid)==self::COID_VERSIONED) { if (preg_match(self::REGEX_SEGMENT, $segments[1]) != 1)
$segments = explode('/', $coid->getPath()); return self::COID_INVALID;
return $segments[2];
} else
return null;
}
/** if (preg_match(self::REGEX_SEGMENT, $segments[2]) == 1)
* Get the version segment of a versioned or version wildcard COID or return self::COID_VERSIONED;
* null if not available. else
* if (preg_match(self::REGEX_VERSION_WILDCARD, $segments[2]) == 1)
* @param IRI $coid return self::COID_VERSION_WILDCARD;
* @return string|null else
*/ return self::COID_INVALID;
public static function getVersionWildcard(IRI $coid) { default:
if (self::getType($coid)==self::COID_VERSION_WILDCARD) { return self::COID_INVALID;
$segments = explode('/', $coid->getPath()); }
return $segments[2]; }
} else
return null; /**
} * Checks whether the given IRI object is a valid COID.
*
/** * @param IRI $coid
* Returns the COID itself if it is a root COID or a new IRI object * @return bool
* representing the namespace underlying the given COID. */
* public static function isValidCOID(IRI $coid) : bool {
* @param IRI $coid return (self::getType($coid)!=self::COID_INVALID);
* @return IRI|null }
*/
public static function getNamespaceCOID(IRI $coid) { /**
switch (self::getType($coid)) { * Get the name segment of a valid COID or null if not available.
case self::COID_ROOT: *
return $coid; * @param IRI $coid
case self::COID_UNVERSIONED: * @return string|null
case self::COID_VERSIONED: */
case self::COID_VERSION_WILDCARD: public static function getName(IRI $coid) : ?string {
return new IRI('coid://'.$coid->getHost()); if (self::getType($coid)!=self::COID_INVALID
default: && self::getType($coid)!=self::COID_ROOT) {
return null; $segments = explode('/', $coid->getPath());
return $segments[1];
} else
return null;
}
/**
* Get the version segment of a valid, versioned COID or null if not available.
*
* @param IRI $coid
* @return string|null
*/
public static function getVersion(IRI $coid) : ?string {
if (self::getType($coid)==self::COID_VERSIONED) {
$segments = explode('/', $coid->getPath());
return $segments[2];
} else
return null;
}
/**
* Get the version segment of a versioned or version wildcard COID or
* null if not available.
*
* @param IRI $coid
* @return string|null
*/
public static function getVersionWildcard(IRI $coid) : ?string {
if (self::getType($coid)==self::COID_VERSION_WILDCARD) {
$segments = explode('/', $coid->getPath());
return $segments[2];
} else
return null;
}
/**
* Returns the COID itself if it is a root COID or a new IRI object
* representing the namespace underlying the given COID.
*
* @param IRI $coid
* @return IRI|null
*/
public static function getNamespaceCOID(IRI $coid) : ?IRI {
switch (self::getType($coid)) {
case self::COID_ROOT:
return $coid;
case self::COID_UNVERSIONED:
case self::COID_VERSIONED:
case self::COID_VERSION_WILDCARD:
return new IRI('coid://'.$coid->getHost());
default:
return null;
}
} }
}
} }

View File

@@ -10,121 +10,139 @@ use ML\IRI\IRI;
class COIDParserTest extends \PHPUnit\Framework\TestCase { class COIDParserTest extends \PHPUnit\Framework\TestCase {
public function testRootCOID() { public function testRootCOID() {
$coid = new IRI('coid://example.com'); $coid = new IRI('coid://example.com');
$this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid)); $this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid));
}
public function testInvalidRootCOID() { $coid = new IRI('coid://subdomain.example.com');
$coid = new IRI('coid://example'); $this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://exämple.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://ex&mple.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testInvalidCOID() { $coid = new IRI('coid://anotherlevel.subdomain.example.com');
$coid = new IRI('http://example.com'); $this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid)); }
$coid = new IRI('example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('COID://example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('Coid://example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://EXAMPLE.COM');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://exAMPle.CoM');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testUnversionedCOID() { public function testInvalidRootCOID() {
$coid = new IRI('coid://example.com/Example'); $coid = new IRI('coid://example');
$this->assertEquals(COIDParser::COID_UNVERSIONED, COIDParser::getType($coid)); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testInvalidUnversionedCOID() { $coid = new IRI('coid://exämple.com');
$coid = new IRI('coid://example.com/Exümple'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Examp%e');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionedCOID() { $coid = new IRI('coid://ex&mple.com');
$coid = new IRI('coid://example.com/Example/1.0'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid)); }
$coid = new IRI('coid://example.com/Example/alpha');
$this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
}
public function testInvalidVersionedCOID() { public function testInvalidCOID() {
$coid = new IRI('coid://example.com/Example/1.$'); $coid = new IRI('http://example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid)); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionWildcardCOID() { $coid = new IRI('example.com');
$coid = new IRI('coid://example.com/Example/^1.0'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/~1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.*');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
}
public function testInvalidVersionWildcardCOID() { $coid = new IRI('COID://example.com');
$coid = new IRI('coid://example.com/Example/^1.*'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.a.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testIRICaseSensitivity() { $coid = new IRI('Coid://example.com');
$coid1 = new IRI('coid://example.com/example/1.0'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid2 = new IRI('coid://example.com/Example/1.0');
$this->assertFalse($coid1->equals($coid2));
}
public function testRootFromString() { $coid = new IRI('coid://EXAMPLE.COM');
$coid1 = new IRI('coid://example.com'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid2 = COIDParser::fromString('coid://example.com');
$coid3 = COIDParser::fromString('example.com');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testUnversionedFromString() { $coid = new IRI('coid://exAMPle.CoM');
$coid1 = new IRI('coid://example.com/Example'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid2 = COIDParser::fromString('coid://example.com/Example'); }
$coid3 = COIDParser::fromString('example.com/Example');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testVersionedFromString() { public function testUnversionedCOID() {
$coid1 = new IRI('coid://example.com/Example/1.0'); $coid = new IRI('coid://subdomain.example.com/Example');
$coid2 = COIDParser::fromString('coid://example.com/Example/1.0'); $this->assertEquals(COIDParser::COID_UNVERSIONED, COIDParser::getType($coid));
$coid3 = COIDParser::fromString('example.com/Example/1.0'); }
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeRootFromString() { public function testInvalidUnversionedCOID() {
$coid1 = new IRI('coid://example.com'); $coid = new IRI('coid://example.com/Exümple');
$coid2 = COIDParser::fromString('COID://example.com'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid3 = COIDParser::fromString('ExAmple.COM');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeNonRootFromString() { $coid = new IRI('coid://example.com/Examp%e');
$coid1 = new IRI('coid://example.com/Example'); $this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid2 = COIDParser::fromString('COID://example.com/Example'); }
$coid3 = COIDParser::fromString('ExAmple.COM/Example');
$coid4 = COIDParser::fromString('ExAmple.COM/EXample'); public function testVersionedCOID() {
$this->assertTrue($coid1->equals($coid2)); $coid = new IRI('coid://anotherlevel.subdomain.example.com/Example/1.0');
$this->assertTrue($coid1->equals($coid3)); $this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
$this->assertFalse($coid1->equals($coid4));
} $coid = new IRI('coid://subdomain.example.com/Example/alpha');
$this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
}
public function testInvalidVersionedCOID() {
$coid = new IRI('coid://example.com/Example/1.$');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionWildcardCOID() {
$coid = new IRI('coid://example.com/Example/^1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/~1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.*');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
}
public function testInvalidVersionWildcardCOID() {
$coid = new IRI('coid://example.com/Example/^1.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.a.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testIRICaseSensitivity() {
$coid1 = new IRI('coid://example.com/example/1.0');
$coid2 = new IRI('coid://example.com/Example/1.0');
$this->assertFalse($coid1->equals($coid2));
}
public function testRootFromString() {
$coid1 = new IRI('coid://example.com');
$coid2 = COIDParser::fromString('coid://example.com');
$coid3 = COIDParser::fromString('example.com');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testUnversionedFromString() {
$coid1 = new IRI('coid://example.com/Example');
$coid2 = COIDParser::fromString('coid://example.com/Example');
$coid3 = COIDParser::fromString('example.com/Example');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testVersionedFromString() {
$coid1 = new IRI('coid://example.com/Example/1.0');
$coid2 = COIDParser::fromString('coid://example.com/Example/1.0');
$coid3 = COIDParser::fromString('example.com/Example/1.0');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeRootFromString() {
$coid1 = new IRI('coid://example.com');
$coid2 = COIDParser::fromString('COID://example.com');
$coid3 = COIDParser::fromString('ExAmple.COM');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeNonRootFromString() {
$coid1 = new IRI('coid://example.com/Example');
$coid2 = COIDParser::fromString('COID://example.com/Example');
$coid3 = COIDParser::fromString('ExAmple.COM/Example');
$coid4 = COIDParser::fromString('ExAmple.COM/EXample');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
$this->assertFalse($coid1->equals($coid4));
}
} }