Skip to content

Commit a5bc4fe

Browse files
committed
Normalized parsing result to using pairs
1 parent d6e1a8a commit a5bc4fe

File tree

8 files changed

+41
-117
lines changed

8 files changed

+41
-117
lines changed

src/Dictionary.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
use function array_key_exists;
1818
use function array_keys;
19-
use function array_map;
2019
use function count;
2120
use function implode;
2221
use function is_array;
@@ -194,15 +193,7 @@ public static function fromRfc8941(Stringable|string $httpValue): self
194193
*/
195194
public static function fromHttpValue(Stringable|string $httpValue, ?Ietf $rfc = null): self
196195
{
197-
$converter = fn (array $member): InnerList|Item => match (true) {
198-
is_array($member[0]) => InnerList::fromAssociative(
199-
array_map(fn (array $item) => Item::fromAssociative(...$item), $member[0]),
200-
$member[1]
201-
),
202-
default => Item::fromAssociative(...$member),
203-
};
204-
205-
return new self(array_map($converter, Parser::new($rfc)->parseDictionary($httpValue)));
196+
return self::fromPairs(Parser::new($rfc)->parseDictionary($httpValue)); /* @phpstan-ignore-line */
206197
}
207198

208199
public function toRfc9651(): string

src/InnerList.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,7 @@ private function filterMember(mixed $member): Item
7474
*/
7575
public static function fromHttpValue(Stringable|string $httpValue, ?Ietf $rfc = null): self
7676
{
77-
[$members, $parameters] = Parser::new($rfc)->parseInnerList($httpValue);
78-
79-
return new self(
80-
array_map(fn (array $member): Item => Item::fromAssociative(...$member), $members),
81-
Parameters::fromAssociative($parameters)
82-
);
77+
return self::fromPair(Parser::new($rfc)->parseInnerList($httpValue));
8378
}
8479

8580
/**

src/Item.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static function fromRfc8941(Stringable|string $httpValue): self
4949
*/
5050
public static function fromHttpValue(Stringable|string $httpValue, ?Ietf $rfc = null): self
5151
{
52-
return self::fromAssociative(...Parser::new($rfc)->parseItem($httpValue));
52+
return self::fromPair(Parser::new($rfc)->parseItem($httpValue));
5353
}
5454

5555
/**

src/OuterList.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,7 @@ private function filterMember(mixed $member): InnerList|Item
7777
*/
7878
public static function fromHttpValue(Stringable|string $httpValue, ?Ietf $rfc = null): self
7979
{
80-
$converter = fn (array $member): InnerList|Item => match (true) {
81-
is_array($member[0]) => InnerList::fromAssociative(
82-
array_map(fn (array $item) => Item::fromAssociative(...$item), $member[0]),
83-
$member[1]
84-
),
85-
default => Item::fromAssociative(...$member),
86-
};
87-
88-
return new self(...array_map($converter, Parser::new($rfc)->parseList($httpValue)));
80+
return self::fromPairs(Parser::new($rfc)->parseList($httpValue)); /* @phpstan-ignore-line */
8981
}
9082

9183
/**

src/Parameters.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public static function fromPairs(StructuredFieldProvider|iterable $pairs): self
136136
*/
137137
public static function fromHttpValue(Stringable|string $httpValue, ?Ietf $rfc = null): self
138138
{
139-
return new self(Parser::new($rfc)->parseParameters($httpValue));
139+
return self::fromPairs(Parser::new($rfc)->parseParameters($httpValue)); /* @phpstan-ignore-line */
140140
}
141141

142142
public static function fromRfc9651(Stringable|string $httpValue): self

src/Parser.php

Lines changed: 32 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@
3333
* @internal Do not use directly this class as it's behaviour and return type
3434
* MAY change significantly even during a major release cycle.
3535
*
36-
* @phpstan-import-type SfType from StructuredFieldProvider
36+
* @phpstan-type SfValue Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool
37+
* @phpstan-type SfParameter array<array{0:string, 1:SfValue}>
38+
* @phpstan-type SfItem array{0:SfValue, 1: SfParameter}
39+
* @phpstan-type SfInnerList array{0:array<SfItem>, 1: SfParameter}
3740
*/
3841
final class Parser
3942
{
40-
private const REGEXP_BYTE_SEQUENCE = '/^(?<sequence>:(?<byte>[a-z\d+\/=]*):)/i';
43+
private const REGEXP_BYTES = '/^(?<sequence>:(?<byte>[a-z\d+\/=]*):)/i';
4144
private const REGEXP_BOOLEAN = '/^\?[01]/';
4245
private const REGEXP_DATE = '/^@(?<date>-?\d{1,15})(?:[^\d.]|$)/';
4346
private const REGEXP_DECIMAL = '/^-?\d{1,12}\.\d{1,3}$/';
@@ -59,36 +62,14 @@ public function __construct(private readonly Ietf $rfc)
5962
}
6063

6164
/**
62-
* Returns the data type value represented as a PHP type from an HTTP textual representation.
65+
* Returns an Item as a PHP list array from an HTTP textual representation.
6366
*
64-
* @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.4
65-
* @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.5
66-
* @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.6
67-
* @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.7
68-
* @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.8
67+
* @see https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-item
6968
*
70-
* @throws Exception|SyntaxError
71-
*/
72-
public function parseValue(Stringable|string $httpValue): Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool
73-
{
74-
$remainder = trim((string) $httpValue, ' ');
75-
if ('' === $remainder || 1 === preg_match(self::REGEXP_INVALID_CHARACTERS, $remainder)) {
76-
throw new SyntaxError("The HTTP textual representation \"$httpValue\" for an item value contains invalid characters.");
77-
}
78-
79-
[$value, $offset] = $this->extractValue($remainder);
80-
if ('' !== substr($remainder, $offset)) {
81-
throw new SyntaxError("The HTTP textual representation \"$httpValue\" for an item value contains invalid characters.");
82-
}
83-
84-
return $value;
85-
}
86-
87-
/**
8869
*
8970
* @throws Exception|SyntaxError
9071
*
91-
* @return array{0:SfType, 1:array<string, SfType>}
72+
* @return SfItem
9273
*/
9374
public function parseItem(Stringable|string $httpValue): array
9475
{
@@ -103,17 +84,17 @@ public function parseItem(Stringable|string $httpValue): array
10384
throw new SyntaxError("The HTTP textual representation \"$httpValue\" for an item contains invalid characters.");
10485
}
10586

106-
return [$value, $this->parseParameters($remainder)];
87+
return [$value, $this->parseParameters($remainder)]; /* @phpstan-ignore-line */
10788
}
10889

10990
/**
110-
* Returns an instance from an HTTP textual representation.
91+
* Returns a Parameters ordered map container as a PHP list array from an HTTP textual representation.
11192
*
11293
* @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1.2
11394
*
11495
* @throws SyntaxError|Exception
11596
*
116-
* @return array<string, SfType>
97+
* @return array<SfParameter>
11798
*/
11899
public function parseParameters(Stringable|string $httpValue): array
119100
{
@@ -123,7 +104,7 @@ public function parseParameters(Stringable|string $httpValue): array
123104
throw new SyntaxError("The HTTP textual representation \"$httpValue\" for Parameters contains invalid characters.");
124105
}
125106

126-
return $parameters;
107+
return $parameters; /* @phpstan-ignore-line */
127108
}
128109

129110
/**
@@ -133,7 +114,7 @@ public function parseParameters(Stringable|string $httpValue): array
133114
*
134115
* @throws SyntaxError|Exception
135116
*
136-
* @return array<array{0:SfType|array<array{0:SfType, 1:array<string, SfType>}>, 1:array<string, SfType>}>
117+
* @return array<SfInnerList|SfItem>
137118
*/
138119
public function parseList(Stringable|string $httpValue): array
139120
{
@@ -148,13 +129,13 @@ public function parseList(Stringable|string $httpValue): array
148129
}
149130

150131
/**
151-
* Returns an ordered map represented as a PHP associative array from an HTTP textual representation.
132+
* Returns a Dictionary represented as a PHP list array from an HTTP textual representation.
152133
*
153134
* @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.2
154135
*
155136
* @throws SyntaxError|Exception
156137
*
157-
* @return array<string, array{0:SfType|array<array{0:SfType, 1:array<string, SfType>}>, 1:array<string, SfType>}>
138+
* @return array<array{0:string, 1:SfInnerList|SfItem}>
158139
*/
159140
public function parseDictionary(Stringable|string $httpValue): array
160141
{
@@ -166,9 +147,11 @@ public function parseDictionary(Stringable|string $httpValue): array
166147
if ('' === $remainder || '=' !== $remainder[0]) {
167148
$remainder = '=?1'.$remainder;
168149
}
150+
$member = [$name];
169151

170-
[$map[$name], $offset] = $this->extractItemOrInnerList(substr($remainder, 1));
152+
[$member[1], $offset] = $this->extractItemOrInnerList(substr($remainder, 1));
171153
$remainder = self::removeCommaSeparatedWhiteSpaces($remainder, ++$offset);
154+
$map[] = $member;
172155
}
173156

174157
return $map;
@@ -181,7 +164,7 @@ public function parseDictionary(Stringable|string $httpValue): array
181164
*
182165
* @throws SyntaxError|Exception
183166
*
184-
* @return array{0:array<array{0:SfType, 1:array<string, SfType>}>, 1:array<string, SfType>}
167+
* @return SfInnerList
185168
*/
186169
public function parseInnerList(Stringable|string $httpValue): array
187170
{
@@ -241,7 +224,7 @@ private static function removeOptionalWhiteSpaces(string $httpValue): string
241224
*
242225
* @throws SyntaxError|Exception
243226
*
244-
* @return array{0:array{0:SfType|array<array{0:SfType, 1:array<string, SfType>}>, 1:array<string, SfType>}, 1:int}
227+
* @return array{0: SfInnerList|SfItem, 1:int}
245228
*/
246229
private function extractItemOrInnerList(string $httpValue): array
247230
{
@@ -261,7 +244,7 @@ private function extractItemOrInnerList(string $httpValue): array
261244
*
262245
* @throws SyntaxError|Exception
263246
*
264-
* @return array{0:array{0:array<array{0:SfType, 1:array<string, SfType>}>, 1:array<string, SfType>}, 1:int}
247+
* @return array{0: SfInnerList, 1 :int}
265248
*/
266249
private function extractInnerList(string $httpValue): array
267250
{
@@ -293,7 +276,7 @@ private function extractInnerList(string $httpValue): array
293276
*
294277
* @throws SyntaxError|Exception
295278
*
296-
* @return array{0:array{0:SfType, 1:array<string, SfType>}, 1:string}
279+
* @return array{0:SfItem, 1:string}
297280
*/
298281
private function extractItem(string $remainder): array
299282
{
@@ -311,13 +294,13 @@ private function extractItem(string $remainder): array
311294
*
312295
* @throws SyntaxError|Exception
313296
*
314-
* @return array{0:SfType, 1:int}
297+
* @return array{0:SfValue, 1:int}
315298
*/
316299
private function extractValue(string $httpValue): array
317300
{
318301
return match (true) {
319302
'"' === $httpValue[0] => self::extractString($httpValue),
320-
':' === $httpValue[0] => self::extractByteSequence($httpValue),
303+
':' === $httpValue[0] => self::extractBytes($httpValue),
321304
'?' === $httpValue[0] => self::extractBoolean($httpValue),
322305
'@' === $httpValue[0] => self::extractDate($httpValue, $this->rfc),
323306
str_starts_with($httpValue, '%"') => self::extractDisplayString($httpValue, $this->rfc),
@@ -334,7 +317,7 @@ private function extractValue(string $httpValue): array
334317
*
335318
* @throws SyntaxError|Exception
336319
*
337-
* @return array{0:array<string, SfType>, 1:int}
320+
* @return array{0:SfParameter, 1:int}
338321
*/
339322
private function extractParametersValues(Stringable|string $httpValue): array
340323
{
@@ -344,13 +327,15 @@ private function extractParametersValues(Stringable|string $httpValue): array
344327
while ('' !== $remainder && ';' === $remainder[0]) {
345328
$remainder = ltrim(substr($remainder, 1), ' ');
346329
$name = MapKey::fromStringBeginning($remainder)->value;
347-
$map[$name] = true;
330+
$member = [$name, true];
348331
$remainder = substr($remainder, strlen($name));
349332
if ('' !== $remainder && '=' === $remainder[0]) {
350333
$remainder = substr($remainder, 1);
351-
[$map[$name], $offset] = $this->extractValue($remainder);
334+
[$member[1], $offset] = $this->extractValue($remainder);
352335
$remainder = substr($remainder, $offset);
353336
}
337+
338+
$map[] = $member;
354339
}
355340

356341
return [$map, strlen($httpValue) - strlen($remainder)];
@@ -519,7 +504,7 @@ private static function extractToken(string $httpValue): array
519504
{
520505
preg_match(self::REGEXP_TOKEN, $httpValue, $found);
521506

522-
$token = $found['token'] ?? '';
507+
$token = $found['token'] ?? throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Token contains invalid characters.");
523508

524509
return [Token::fromString($token), strlen($token)];
525510
}
@@ -531,9 +516,9 @@ private static function extractToken(string $httpValue): array
531516
*
532517
* @return array{0:Bytes, 1:int}
533518
*/
534-
private static function extractByteSequence(string $httpValue): array
519+
private static function extractBytes(string $httpValue): array
535520
{
536-
if (1 !== preg_match(self::REGEXP_BYTE_SEQUENCE, $httpValue, $matches)) {
521+
if (1 !== preg_match(self::REGEXP_BYTES, $httpValue, $matches)) {
537522
throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Byte Sequence contains invalid characters.");
538523
}
539524

tests/ParserBench.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function __construct()
2020
#[Bench\Assert('mode(variant.mem.peak) < 2097152'), Bench\Assert('mode(variant.time.avg) < 10000000')]
2121
public function benchParsingAList(): void
2222
{
23-
$httpValue = '("lang" "en-US"); expires=@1623233894; samesite=Strict; secure';
23+
$httpValue = '("lang" "en-US" token); expires=@1623233894; samesite=Strict; secure';
2424
for ($i = 0; $i < 100_000; $i++) {
2525
$this->parser->parseList($httpValue);
2626
}
@@ -42,7 +42,7 @@ public function benchParsingAnItem(): void
4242
#[Bench\Assert('mode(variant.mem.peak) < 2097152'), Bench\Assert('mode(variant.time.avg) < 10000000')]
4343
public function benchParsingAnDictionary(): void
4444
{
45-
$httpValue = 'lang="en-US"; samesite=Strict; secure, type=42.0; expires=@1623233894';
45+
$httpValue = 'code=token, lang="en-US"; samesite=Strict; secure, type=42.0; expires=@1623233894';
4646
for ($i = 0; $i < 100_000; $i++) {
4747
$this->parser->parseDictionary($httpValue);
4848
}

tests/ParserTest.php

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Bakame\Http\StructuredFields;
66

77
use DateTimeImmutable;
8-
use PHPUnit\Framework\Attributes\DataProvider;
98
use PHPUnit\Framework\Attributes\Test;
109

1110
/**
@@ -57,8 +56,8 @@ public function it_parse_a_date_item(): void
5756
{
5857
$field = $this->parser->parseDictionary('a=@12345678;key=1');
5958

60-
self::assertInstanceOf(DateTimeImmutable::class, $field['a'][0]);
61-
self::assertSame(1, $field['a'][1]['key']);
59+
self::assertInstanceOf(DateTimeImmutable::class, $field[0][1][0]);
60+
self::assertSame('key', $field[0][1][1][0][0]);
6261
}
6362

6463
#[Test]
@@ -141,18 +140,6 @@ public function it_fails_to_parse_an_invalid_http_field_2(): void
141140
$this->parser->parseInnerList('"hello)world" 42 42.0;john=doe);foo="bar("');
142141
}
143142

144-
#[Test]
145-
#[DataProvider('provideHttpValueForDataType')]
146-
public function it_parses_basic_data_type(string $httpValue, Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool $expected): void
147-
{
148-
$field = $this->parser->parseValue($httpValue);
149-
if (is_scalar($expected)) {
150-
self::assertSame($expected, $field);
151-
} else {
152-
self::assertEquals($expected, $field);
153-
}
154-
}
155-
156143
/**
157144
* @return iterable<array{httpValue:string, expected:SfType}>
158145
*/
@@ -203,30 +190,4 @@ public static function provideHttpValueForDataType(): iterable
203190
'expected' => DisplayString::fromDecoded('bébé'),
204191
];
205192
}
206-
207-
#[Test]
208-
#[DataProvider('provideInvalidHttpValueForDataType')]
209-
public function it_fails_to_parse_basic_data_type(string $httpValue): void
210-
{
211-
$this->expectException(SyntaxError::class);
212-
213-
$this->parser->parseValue($httpValue);
214-
}
215-
216-
/**
217-
* @return array<array<string>>
218-
*/
219-
public static function provideInvalidHttpValueForDataType(): array
220-
{
221-
return [
222-
['!invalid'],
223-
['"inva'."\n".'lid"'],
224-
['@1_000_000_000_000.0'],
225-
['-1_000_000_000_000.0'],
226-
[' '],
227-
['%"b%c3%a9b%c3%a9'],
228-
['%b%c3%a9b%c3%a9"'],
229-
['%"b%C3%A9b%C3%A9"'],
230-
];
231-
}
232193
}

0 commit comments

Comments
 (0)