-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Can parse, sort, and retrieve preferred media-type
- Loading branch information
1 parent
cb90b25
commit 7b359a7
Showing
5 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Xynha\HttpAccept; | ||
|
||
use InvalidArgumentException; | ||
use Xynha\HttpAccept\Entity\MediaList; | ||
use Xynha\HttpAccept\Entity\MediaType; | ||
|
||
final class AcceptParser | ||
{ | ||
|
||
public function parse(string $source) : MediaList | ||
{ | ||
if (empty($source)) { | ||
throw new InvalidArgumentException('Accept data is empty'); | ||
} | ||
|
||
$list = new MediaList(); | ||
$parts = explode(',', $source); | ||
|
||
foreach ($parts as $key) { | ||
$key = trim($key); | ||
$media = $this->parseMediaType($key); | ||
$list = $list->addMedia($media); | ||
} | ||
|
||
return $list; | ||
} | ||
|
||
private function parseMediaType(string $source) : MediaType | ||
{ | ||
if ($source === '*') { | ||
$source = '*/*'; | ||
} | ||
$parts = explode(';', $source); | ||
$mime = trim((string)array_shift($parts)); | ||
|
||
if ($mime === '' || strpos($mime, '/') === false) { | ||
throw new InvalidArgumentException('Invalid media-type format'); | ||
} | ||
|
||
$quality = null; | ||
$params = []; | ||
foreach ($parts as $item) { | ||
$tparams = explode('=', $item); | ||
$key = trim($tparams[0] ?? ''); | ||
$value = trim($tparams[1] ?? ''); | ||
|
||
switch (true) { | ||
case $key === 'q': | ||
$quality = !empty($value) ? (float)$value : null; | ||
break; | ||
case !empty($key) && !empty($value): | ||
$params[] = $key . '=' . $value; | ||
break; | ||
} | ||
} | ||
|
||
return new MediaType($mime, $quality, $params); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Xynha\HttpAccept\Entity; | ||
|
||
final class MediaList | ||
{ | ||
|
||
/** @var array<string,MediaType> */ | ||
private $media = []; | ||
|
||
/** @var array<string,float> */ | ||
private $score = []; | ||
|
||
/** @var string[] */ | ||
private $order; | ||
|
||
public function addMedia(MediaType $media) : self | ||
{ | ||
$new = clone $this; | ||
$new->media[$media->name()] = $media; | ||
$new->score[$media->name()] = $media->score(); | ||
return $new; | ||
} | ||
|
||
public function count() : int | ||
{ | ||
return count($this->media); | ||
} | ||
|
||
public function preferedMedia(int $pos) : ?MediaType | ||
{ | ||
if (empty($this->order)) { | ||
uasort($this->score, [$this, 'uasort']); | ||
$this->order = array_keys($this->score); | ||
} | ||
|
||
$key = $this->order[$pos] ?? ''; | ||
return $this->media[$key] ?? null; | ||
} | ||
|
||
private function uasort(float $valA, float $valB) : int | ||
{ | ||
if ($valA === $valB) { | ||
return 0; | ||
} | ||
return ($valA < $valB) ? 1 : -1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Xynha\HttpAccept\Entity; | ||
|
||
use InvalidArgumentException; | ||
|
||
final class MediaType | ||
{ | ||
|
||
/** @var string */ | ||
private $name; | ||
|
||
/** @var string */ | ||
private $type; | ||
|
||
/** @var string */ | ||
private $subtype; | ||
|
||
/** @var float|null */ | ||
private $quality; | ||
|
||
/** @var string[] */ | ||
private $params; | ||
|
||
/** @var float */ | ||
private $score; | ||
|
||
/** @param string[] $params */ | ||
public function __construct(string $mime, ?float $quality, array $params) | ||
{ | ||
list($type, $subtype) = explode('/', $mime); | ||
|
||
if (trim($type) === '' || trim($subtype) === '') { | ||
throw new InvalidArgumentException('Invalid media-type format'); | ||
} | ||
|
||
$this->type = trim($type); | ||
$this->subtype = trim($subtype); | ||
$this->quality = $quality; | ||
$this->params = $params; | ||
|
||
$this->score = $this->calculateScore($this->type(), $this->subtype(), $this->quality(), $this->parameters()); | ||
|
||
$this->name = $this->type() . '/' . $this->subtype(); | ||
if (!empty($this->parameters())) { | ||
$this->name .= ';' . implode(';', $this->parameters()); | ||
} | ||
} | ||
|
||
public function type() : string | ||
{ | ||
return $this->type; | ||
} | ||
|
||
public function subtype() : string | ||
{ | ||
return $this->subtype; | ||
} | ||
|
||
public function mimetype() : string | ||
{ | ||
return $this->type . '/' . $this->subtype; | ||
} | ||
|
||
public function quality() : float | ||
{ | ||
if ($this->quality === null) { | ||
return 1.0; | ||
} | ||
|
||
return $this->quality; | ||
} | ||
|
||
/** @return array<string,string> */ | ||
public function parameters() : array | ||
{ | ||
return $this->params; | ||
} | ||
|
||
public function score() : float | ||
{ | ||
return $this->score; | ||
} | ||
|
||
public function name() : string | ||
{ | ||
return $this->name; | ||
} | ||
|
||
/** @param string[] $params */ | ||
private function calculateScore(string $type, string $subtype, float $quality, array $params) : float | ||
{ | ||
$score = 0.0; | ||
if (!empty($type) && $type !== '*') { | ||
$score = 1000.0; | ||
} | ||
|
||
if (!empty($subtype) && $subtype !== '*') { | ||
$score += 100.0; | ||
} | ||
|
||
$score += count($params) * 10.0; | ||
$score += $quality * 1.0; | ||
|
||
return $score; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
<?php declare(strict_types=1); | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Xynha\HttpAccept\AcceptParser; | ||
|
||
final class AcceptParserTest extends TestCase | ||
{ | ||
|
||
/** @var AcceptParser */ | ||
private $parser; | ||
|
||
protected function setUp() | ||
{ | ||
$this->parser = new AcceptParser(); | ||
} | ||
|
||
public function testEmptySource() | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('Accept data is empty'); | ||
|
||
$this->parser->parse(''); | ||
} | ||
|
||
public function testEmptyMimetype() | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('Invalid media-type format'); | ||
|
||
$this->parser->parse(';q=1'); | ||
} | ||
|
||
public function testInvalidMimetype() | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('Invalid media-type format'); | ||
|
||
$this->parser->parse('mime'); | ||
} | ||
|
||
public function testMissingSubtype() | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('Invalid media-type format'); | ||
|
||
$this->parser->parse('mime/'); | ||
} | ||
|
||
public function testInvalidParameterFormat() | ||
{ | ||
$list = $this->parser->parse('type/subtype;level'); | ||
$media = $list->preferedMedia(0); | ||
|
||
$this->assertSame('type/subtype', $media->name()); | ||
$this->assertSame('type/subtype', $media->mimetype()); | ||
$this->assertSame([], $media->parameters()); | ||
$this->assertSame(1.0, $media->quality()); | ||
$this->assertSame(1101.0, $media->score()); | ||
} | ||
|
||
public function testAsteriskOnly() | ||
{ | ||
$list = $this->parser->parse('*'); | ||
$media = $list->preferedMedia(0); | ||
|
||
$this->assertSame('*/*', $media->name()); | ||
$this->assertSame('*/*', $media->mimetype()); | ||
$this->assertSame([], $media->parameters()); | ||
$this->assertSame(1.0, $media->quality()); | ||
$this->assertSame(1.0, $media->score()); | ||
} | ||
|
||
public function testParseEmptyQuality() | ||
{ | ||
$list = $this->parser->parse('type/subtype;q='); | ||
$media = $list->preferedMedia(0); | ||
|
||
$this->assertSame('type/subtype', $media->name()); | ||
$this->assertSame('type/subtype', $media->mimetype()); | ||
$this->assertSame([], $media->parameters()); | ||
$this->assertSame(1.0, $media->quality()); | ||
$this->assertSame(1101.0, $media->score()); | ||
} | ||
|
||
public function testParseFloatQuality() | ||
{ | ||
$list = $this->parser->parse('type/subtype;q=0.5'); | ||
$media = $list->preferedMedia(0); | ||
|
||
$this->assertSame('type/subtype', $media->name()); | ||
$this->assertSame('type/subtype', $media->mimetype()); | ||
$this->assertSame([], $media->parameters()); | ||
$this->assertSame(0.5, $media->quality()); | ||
$this->assertSame(1100.5, $media->score()); | ||
} | ||
|
||
public function testParseIntegerQuality() | ||
{ | ||
$list = $this->parser->parse('type/subtype;q=1'); | ||
$media = $list->preferedMedia(0); | ||
|
||
$this->assertSame('type/subtype', $media->name()); | ||
$this->assertSame('type/subtype', $media->mimetype()); | ||
$this->assertSame([], $media->parameters()); | ||
$this->assertSame(1.0, $media->quality()); | ||
$this->assertSame(1101.0, $media->score()); | ||
} | ||
|
||
public function testParseExtension() | ||
{ | ||
$list = $this->parser->parse(' type / subtype ; level = 1 ; level = 2'); | ||
$media = $list->preferedMedia(0); | ||
|
||
$this->assertSame('type/subtype;level=1;level=2', $media->name()); | ||
$this->assertSame('type/subtype', $media->mimetype()); | ||
$this->assertSame(['level=1', 'level=2'], $media->parameters()); | ||
$this->assertSame(1.0, $media->quality()); | ||
$this->assertSame(1121.0, $media->score()); | ||
} | ||
|
||
public function testSimilarMediatype() | ||
{ | ||
$list = $this->parser->parse('type/subtype, type / subtype'); | ||
$this->assertSame(1, $list->count()); | ||
|
||
$list = $this->parser->parse('type/subtype;level=1, type / subtype ; level = 1 '); | ||
$this->assertSame(1, $list->count()); | ||
} | ||
|
||
public function testGetMediatype() | ||
{ | ||
$list = $this->parser->parse('type/subtype, type / subtype'); | ||
|
||
$this->assertNull($list->preferedMedia(-1)); | ||
$this->assertNotNull($list->preferedMedia(0)); | ||
$this->assertNull($list->preferedMedia(1)); | ||
} | ||
|
||
public function testSortWithoutQuality() | ||
{ | ||
$list = $this->parser->parse('text/html, text/html;level=1, */*, text/html;level=1;level=2, text/*'); | ||
|
||
$this->assertSame('text/html;level=1;level=2', $list->preferedMedia(0)->name()); | ||
$this->assertSame('text/html;level=1', $list->preferedMedia(1)->name()); | ||
$this->assertSame('text/html', $list->preferedMedia(2)->name()); | ||
$this->assertSame('text/*', $list->preferedMedia(3)->name()); | ||
$this->assertSame('*/*', $list->preferedMedia(4)->name()); | ||
} | ||
|
||
public function testSortSimilarScore() | ||
{ | ||
$list = $this->parser->parse('*/*, text/html;level=1;level=2 , text/*, text/css;level=1;level=2'); | ||
|
||
$this->assertSame('text/html;level=1;level=2', $list->preferedMedia(0)->name()); | ||
$this->assertSame('text/css;level=1;level=2', $list->preferedMedia(1)->name()); | ||
$this->assertSame('text/*', $list->preferedMedia(2)->name()); | ||
$this->assertSame('*/*', $list->preferedMedia(3)->name()); | ||
} | ||
|
||
public function testSortWithQuality() | ||
{ | ||
$list = $this->parser->parse('*/*;q=1, text/html;q=0.25 , text/*;q=0.75, text/css;q=0.5'); | ||
|
||
$this->assertSame('text/css', $list->preferedMedia(0)->name()); | ||
$this->assertSame('text/html', $list->preferedMedia(1)->name()); | ||
$this->assertSame('text/*', $list->preferedMedia(2)->name()); | ||
$this->assertSame('*/*', $list->preferedMedia(3)->name()); | ||
} | ||
} |