Skip to content

Commit

Permalink
Can parse, sort, and retrieve preferred media-type
Browse files Browse the repository at this point in the history
  • Loading branch information
pattisahusiwa committed Aug 26, 2020
1 parent cb90b25 commit 7b359a7
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 0 deletions.
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ parameters:
-
message: '#[\w\\]+::(set[uU]p|test\w+)\(\) has no return typehint specified#'
path: tests/
-
message: '#Cannot call method [\w\(\)]+ on [\w\\]+\\MediaType\|null#'
path: tests/


# Miscellaneous parameters
inferPrivatePropertyTypeFromConstructor: true
61 changes: 61 additions & 0 deletions src/AcceptParser.php
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);
}
}
48 changes: 48 additions & 0 deletions src/Entity/MediaList.php
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;
}
}
107 changes: 107 additions & 0 deletions src/Entity/MediaType.php
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;
}
}
169 changes: 169 additions & 0 deletions tests/Units/AcceptParserTest.php
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());
}
}

0 comments on commit 7b359a7

Please sign in to comment.