Skip to content

Commit 408300c

Browse files
committed
Properly use the expected field from the httpwg test suite
1 parent 68c520b commit 408300c

File tree

5 files changed

+110
-20
lines changed

5 files changed

+110
-20
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"phpstan/phpstan-deprecation-rules": "^1.1.4",
3737
"phpunit/phpunit": "^10.5.5",
3838
"phpbench/phpbench": "^1.2.15",
39-
"symfony/var-dumper": "^6.4.0"
39+
"symfony/var-dumper": "^6.4.0",
40+
"bakame/aide-base32": "dev-main"
4041
},
4142
"autoload": {
4243
"psr-4": {

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ parameters:
1313
path: tests/ItemTest.php
1414
- message: '#Method Bakame\\Http\\StructuredFields\\DataType::build\(\) has parameter \$data with no value type specified in iterable type iterable.#'
1515
path: src/DataType.php
16+
excludePaths:
17+
- tests/Record.php
1618
reportUnmatchedIgnoredErrors: true

src/DisplayString.php

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ public static function fromEncoded(Stringable|string $encodedValue): self
5050
throw new SyntaxError('The string '.$value.' contains invalid utf-8 encoded sequence.');
5151
}
5252

53-
$value = (string) preg_replace_callback(',%[A-Fa-f0-9]{2},', fn (array $matches) => rawurldecode($matches[0]), $value);
53+
$value = (string) preg_replace_callback(
54+
',%[A-Fa-f0-9]{2},',
55+
fn (array $matches): string => rawurldecode($matches[0]),
56+
$value
57+
);
58+
5459
if (1 !== preg_match('//u', $value)) {
5560
throw new SyntaxError('The string contains invalid characters.'.$value);
5661
}
@@ -79,21 +84,11 @@ public function decoded(): string
7984
*/
8085
public function encoded(): string
8186
{
82-
$value = $this->value;
83-
84-
$encodeMatches = static fn (array $matches): string => match (1) {
85-
preg_match('/[^A-Za-z\d_\-.~]/', rawurldecode($matches[0])) => strtolower(rawurlencode($matches[0])),
86-
default => $matches[0],
87-
};
88-
89-
return match (true) {
90-
'' === $value => $value,
91-
default => (string) preg_replace_callback(
92-
'/[^A-Za-z\d_\-.~\!\$&\'\(\)\*\+,;\=%\\\\:@\/? ]+|%(?![A-Fa-f\d]{2})/',
93-
$encodeMatches(...),
94-
$value
95-
),
96-
};
87+
return (string) preg_replace_callback(
88+
'/[%"\x00-\x1F\x7F-\xFF]/',
89+
static fn (array $matches): string => strtolower(rawurlencode($matches[0])),
90+
$this->value
91+
);
9792
}
9893

9994
public function equals(mixed $other): bool

tests/Record.php

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@
44

55
namespace Bakame\Http\StructuredFields;
66

7+
use DateTimeImmutable;
8+
use ValueError;
9+
710
/**
811
* @phpstan-type RecordData array{
912
* name: string,
1013
* header_type: 'dictionary'|'list'|'item',
1114
* raw: array<string>,
1215
* canonical?: array<string>,
1316
* must_fail?: bool,
14-
* can_fail?: bool
17+
* can_fail?: bool,
18+
* expected?: array,
1519
* }
20+
* @phpstan-type itemValue array{__type:string, value:int|string}|string|bool|int|float|null
1621
*/
1722
final class Record
1823
{
@@ -26,6 +31,7 @@ private function __construct(
2631
public readonly array $canonical,
2732
public readonly bool $mustFail,
2833
public readonly bool $canFail,
34+
public readonly ?StructuredField $expected
2935
) {
3036
}
3137

@@ -34,7 +40,7 @@ private function __construct(
3440
*/
3541
public static function fromDecoded(array $data): self
3642
{
37-
$data += ['canonical' => $data['raw'], 'must_fail' => false, 'can_fail' => false];
43+
$data += ['canonical' => $data['raw'], 'must_fail' => false, 'can_fail' => false, 'expected' => []];
3844

3945
return new self(
4046
$data['name'],
@@ -43,6 +49,89 @@ public static function fromDecoded(array $data): self
4349
$data['canonical'],
4450
$data['must_fail'],
4551
$data['can_fail'],
52+
self::parseExpected($data['header_type'], $data['expected'])
4653
);
4754
}
55+
56+
private static function parseExpected(string $dataTypeValue, array $expected): ?StructuredField
57+
{
58+
return match (DataType::tryFrom($dataTypeValue)) {
59+
DataType::Dictionary => self::parseDictionary($expected),
60+
DataType::List => self::parseList($expected),
61+
DataType::Item => self::parseItem($expected),
62+
default => null,
63+
};
64+
}
65+
66+
/**
67+
* @param itemValue $data
68+
*/
69+
private static function parseValue(array|string|bool|int|float|null $data): Token|DateTimeImmutable|ByteSequence|DisplayString|string|bool|int|float|null
70+
{
71+
return match (true) {
72+
!is_array($data) => $data,
73+
2 !== count($data),
74+
!isset($data['__type'], $data['value']) => throw new ValueError('Unknown or unsupported type: '.json_encode($data)),
75+
default => match (Type::tryFrom($data['__type'])) {
76+
Type::Token => Token::fromString($data['value']),
77+
Type::Date => (new DateTimeImmutable())->setTimestamp((int) $data['value']),
78+
Type::DisplayString => DisplayString::fromDecoded($data['value']),
79+
Type::ByteSequence => ByteSequence::fromDecoded(base32_decode(encoded: $data['value'], strict: true)),
80+
default => throw new ValueError('Unknown or unsupported type: '.json_encode($data)),
81+
},
82+
};
83+
}
84+
85+
/**
86+
* @param array<array{0:string, 1:itemValue> $parameters
87+
*/
88+
private static function parseParameters(array $parameters): Parameters
89+
{
90+
return Parameters::fromPairs(array_map(
91+
fn ($value) => [$value[0], self::parseValue($value[1])],
92+
$parameters
93+
));
94+
}
95+
96+
/**
97+
* @param array{0:itemValue, 1:array<array{0:string, 1:itemValue}>}|itemValue $value
98+
*/
99+
private static function parseItem(mixed $value): ?Item
100+
{
101+
return match (true) {
102+
!is_array($value) => Item::new($value),
103+
[] === $value => null,
104+
default => Item::new([
105+
self::parseValue($value[0]),
106+
self::parseParameters($value[1]),
107+
]),
108+
};
109+
}
110+
111+
private static function parseInnerList(array $innerListPair): InnerList
112+
{
113+
return InnerList::fromPair([
114+
array_map(fn ($value) => self::parseItem($value), $innerListPair[0]),
115+
self::parseParameters($innerListPair[1]),
116+
]);
117+
}
118+
119+
private static function parseList(array $listPairs): OuterList
120+
{
121+
return OuterList::fromPairs(array_map(
122+
fn ($value) => is_array($value[0]) && array_is_list($value[0]) ? self::parseInnerList($value) : self::parseItem($value),
123+
$listPairs
124+
));
125+
}
126+
127+
private static function parseDictionary(array $dictionaryPair): Dictionary
128+
{
129+
return Dictionary::fromPairs(array_map(
130+
fn ($value) => [
131+
$value[0],
132+
is_array($value[1][0]) && array_is_list($value[1][0]) ? self::parseInnerList($value[1]) : self::parseItem($value[1]),
133+
],
134+
$dictionaryPair
135+
));
136+
}
48137
}

tests/StructuredFieldTestCase.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ public function it_can_pass_http_wg_tests(Record $test): void
3030
$structuredField = DataType::from($test->type)->parse(implode(',', $test->raw));
3131

3232
if (!$test->mustFail) {
33-
self::assertSame(implode(',', $test->canonical), $structuredField->toHttpValue());
33+
self::assertSame(
34+
$test->expected?->toHttpValue() ?? implode(',', $test->canonical),
35+
$structuredField->toHttpValue()
36+
);
3437
}
3538
}
3639

0 commit comments

Comments
 (0)