Skip to content

Commit

Permalink
Psalm 1 + Improve HeaderValueHelper methods annotations (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
vjik authored Jun 8, 2023
1 parent 2023fd8 commit 662b3c7
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 17 deletions.
11 changes: 5 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
# Yii HTTP Change Log


## 1.2.1 under development

- no changes in this release.
- Enh #45: Improve `HeaderValueHelper` methods' annotations (@vjik)

## 1.2.0 November 09, 2021

- New #26: Add `HeaderValueHelper` that has static methods to parse header value parameters (devanych)
- New #26: Add `HeaderValueHelper` that has static methods to parse header value parameters (@devanych)

## 1.1.1 February 10, 2021

- Chg: Update yiisoft/strings dependency (samdark)
- Chg: Update yiisoft/strings dependency (@samdark)

## 1.1.0 December 28, 2020

- Enh #12: Add `Method::ALL` and deprecated `Method::ANY` (samdark)
- Enh #20: Add `ContentDispositionHeader` that generate `Content-Disposition` header name and value (vjik)
- Enh #12: Add `Method::ALL` and deprecated `Method::ANY` (@samdark)
- Enh #20: Add `ContentDispositionHeader` that generate `Content-Disposition` header name and value (@vjik)

## 1.0.0 September 1, 2020

Expand Down
5 changes: 4 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<psalm
errorLevel="2"
errorLevel="1"
findUnusedBaselineEntry="true"
findUnusedCode="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
Expand All @@ -13,4 +13,7 @@
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
</issueHandlers>
</psalm>
50 changes: 40 additions & 10 deletions src/HeaderValueHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
use function usort;

/**
* HeaderValueHelper parses the header value parameters.
* `HeaderValueHelper` parses the header value parameters.
*
* @psalm-type QFactorHeader = array{q: float}&non-empty-array<array-key, string>
*/
final class HeaderValueHelper
{
Expand Down Expand Up @@ -70,7 +72,7 @@ final class HeaderValueHelper
* @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
* @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
*
* @return array First element is the value, and key-value are the parameters.
* @return string[] First element is the value, and key-value are the parameters.
*/
public static function getValueAndParameters(
string $headerValue,
Expand Down Expand Up @@ -104,7 +106,9 @@ public static function getValueAndParameters(
* @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
* @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
*
* @return array Key-value are the parameters.
* @return string[] Key-value are the parameters.
*
* @psalm-return array<string,string>
*/
public static function getParameters(
string $headerValueParameters,
Expand Down Expand Up @@ -142,6 +146,7 @@ static function (array $matches) use (&$output, $lowerCaseParameter, $lowerCaseP
return;
}

/** @psalm-suppress MixedArrayAssignment False-positive error */
$output[$key] = $lowerCaseParameterValue ? mb_strtolower($value) : $value;
},
$headerValueParameters,
Expand All @@ -153,49 +158,65 @@ static function (array $matches) use (&$output, $lowerCaseParameter, $lowerCaseP
throw new InvalidArgumentException('Invalid input: ' . $headerValueParameters);
}
} while ($headerValueParameters !== '');
/** @var array<string,string> $output */

return $output;
}

/**
* Returns a header value as "q" factor sorted list.
*
* @param mixed $values Header value as a comma-separated string or already exploded string array.
* @link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
* @link https://www.ietf.org/rfc/rfc2045.html#section-2
* @see getValueAndParameters
*
* @param string|string[] $values Header value as a comma-separated string or already exploded string array.
* @param bool $lowerCaseValue Whether should cast header value to lowercase.
* @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
* @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
*
* @return array The q factor sorted list.
* @return array[] The q factor sorted list.
*
* @link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
* @link https://www.ietf.org/rfc/rfc2045.html#section-2
* @see getValueAndParameters
* @psalm-return list<QFactorHeader>
* @psalm-suppress MoreSpecificReturnType, LessSpecificReturnStatement Need for Psalm 4.30
*/
public static function getSortedValueAndParameters(
$values,
bool $lowerCaseValue = true,
bool $lowerCaseParameter = true,
bool $lowerCaseParameterValue = true
): array {
/** @var mixed $values Don't trust to annotations. */

if (!is_array($values) && !is_string($values)) {
throw new InvalidArgumentException('Values are neither array nor string.');
}

$list = [];

foreach ((array) $values as $headerValue) {
if (!is_string($headerValue)) {
throw new InvalidArgumentException('Values must be array of strings.');
}

/** @psalm-suppress InvalidOperand Presume that `preg_split` never returns false here. */
$list = [...$list, ...preg_split('/\s*,\s*/', trim($headerValue), -1, PREG_SPLIT_NO_EMPTY)];
}

/** @var string[] $list */

if (count($list) === 0) {
return [];
}

$output = [];

foreach ($list as $value) {
$parse = self::getValueAndParameters($value, $lowerCaseValue, $lowerCaseParameter, $lowerCaseParameterValue);
$parse = self::getValueAndParameters(
$value,
$lowerCaseValue,
$lowerCaseParameter,
$lowerCaseParameterValue
);
// case-insensitive "q" parameter
$q = $parse['q'] ?? $parse['Q'] ?? 1.0;

Expand Down Expand Up @@ -237,12 +258,20 @@ public static function getSortedAcceptTypes($values): array
$output = self::getSortedValueAndParameters($values);

usort($output, static function (array $a, array $b) {
/**
* @psalm-var QFactorHeader $a
* @psalm-var QFactorHeader $b
*/

if ($a['q'] !== $b['q']) {
// The higher q value wins
return $a['q'] > $b['q'] ? -1 : 1;
}

/** @var string $typeA */
$typeA = reset($a);

/** @var string $typeB */
$typeB = reset($b);

if (strpos($typeA, '*') === false && strpos($typeB, '*') === false) {
Expand Down Expand Up @@ -285,6 +314,7 @@ public static function getSortedAcceptTypes($values): array
asort($value, SORT_STRING);
$output[$key] = $type . ';' . implode(';', $value);
}
/** @var string[] $output */

return $output;
}
Expand Down
24 changes: 24 additions & 0 deletions tests/HeaderValueHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use stdClass;
use Yiisoft\Http\HeaderValueHelper;

final class HeaderValueHelperTest extends TestCase
Expand Down Expand Up @@ -65,6 +66,29 @@ public function testSortedValuesAndParameters($input, array $expected): void
$this->assertSame($expected, HeaderValueHelper::getSortedValueAndParameters($input));
}

public function dataSortedValuesAndParametersInvalidValue(): array
{
$notArrayAndNotString = 'Values are neither array nor string.';
$notArrayOfStrings = 'Values must be array of strings.';

return [
'object' => [new stdClass(), $notArrayAndNotString],
'int' => [7, $notArrayAndNotString],
'bool' => [true, $notArrayAndNotString],
'array-of-int' => [[1, 2, 3], $notArrayOfStrings],
];
}

/**
* @dataProvider dataSortedValuesAndParametersInvalidValue
*/
public function testSortedValuesAndParametersInvalidValue($value, string $message): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage($message);
HeaderValueHelper::getSortedValueAndParameters($value);
}

public function qFactorSortFailDataProvider(): array
{
return [
Expand Down

0 comments on commit 662b3c7

Please sign in to comment.