Skip to content

Commit bd7dea7

Browse files
committed
Improve OrderedMap interface
1 parent aaadd11 commit bd7dea7

File tree

9 files changed

+462
-40
lines changed

9 files changed

+462
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ All Notable changes to `bakame/http-strucured-fields` will be documented in this
66

77
### Added
88

9-
- `Item::parameterByOffset` and `InnerList::parameterByOffset` tu returns the parameter as a tuple.
9+
- Public API around accessing parameters using their index instead of their key.
1010

1111
### Fixed
1212

13-
- `Parser` class is no longer internal
13+
- `Parameters::remove` also removes parameters per indexes
1414

1515
### Deprecated
1616

README.md

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ composer require bakame/http-structured-fields
5757

5858
### Foreword
5959

60-
**While this package parses and serializes the header value, it does not validate its content.
60+
**⚠️WARNING: While this package parses and serializes the header value, it does not validate its content.
6161
It is still required to validate the parsed data against the constraints of the corresponding
6262
header. Content validation is out of scope for this library.**
6363

@@ -195,7 +195,7 @@ $token->type(); // returns Type::Token enum
195195
$byte->type(); // returns Type::ByteSequence
196196
```
197197

198-
**Both classes DO NOT expose the `Stringable` interface to distinguish them
198+
**⚠️WARNING: Both classes DO NOT expose the `Stringable` interface to distinguish them
199199
from a string or a string like object**
200200

201201
#### Item
@@ -310,9 +310,9 @@ use Bakame\Http\StructuredFields\Item;
310310
Item::withValue(DateTimeInterface|ByteSequence|Token|string|int|float|bool $value): static
311311
```
312312

313-
#### Dictionaries
313+
#### Ordered Maps
314314

315-
The `Dictionary` and `Parameters` instances can be build with an associative iterable structure as shown below
315+
The `Dictionary` and `Parameters` are ordered map instances. They can be built using their keys with an associative iterable structure as shown below
316316

317317
```php
318318
use Bakame\Http\StructuredFields\Dictionary;
@@ -327,7 +327,7 @@ echo $value->toHttpValue(); //"b=?0, a=bar, c=@1671800423"
327327
echo $value; //"b=?0, a=bar, c=@1671800423"
328328
```
329329

330-
or with an iterable structure of pairs (tuple) as defined in the RFC:
330+
or using their indexes with an iterable structure of pairs (tuple) as defined in the RFC:
331331

332332
```php
333333
use Bakame\Http\StructuredFields\Parameters;
@@ -358,6 +358,7 @@ $map->mergeAssociative(...$others): static;
358358
$map->mergePairs(...$others): static;
359359
$map->remove(string ...$key): static;
360360
```
361+
361362
As shown below:
362363
`
363364
```php
@@ -380,6 +381,49 @@ echo $value->toHttpValue(); //b=?0, a=(bar "42" 42 42.0), c=@1671800423
380381
echo $value; //b=?0, a=(bar "42" 42 42.0), c=@1671800423
381382
```
382383

384+
Since version `1.1.0` it is possible to also build `Dictionary` and `Parameters` instances
385+
using indexes and pair as per described in the RFC.
386+
387+
The `$pair` parameter is a tuple (ie: an array as list with exactly two members) where:
388+
389+
- the first array member is the parameter `$key`
390+
- the second array member is the parameter `$value`
391+
392+
```php
393+
// since version 1.1
394+
$map->unshift(array ...$pairs): static;
395+
$map->push(array ...$pairs): static;
396+
$map->insert(int $key, array ...$pairs): static;
397+
$map->replace(int $key, array $pair): static;
398+
```
399+
400+
We can rewrite the previous example
401+
402+
```php
403+
use Bakame\Http\StructuredFields\Dictionary;
404+
use Bakame\Http\StructuredFields\Item;
405+
use Bakame\Http\StructuredFields\Token;
406+
407+
$value = Dictionary::new()
408+
->push(
409+
['a', InnerList::new(
410+
Item::fromToken('bar'),
411+
Item::fromString('42'),
412+
Item::fromInteger(42),
413+
Item::fromDecimal(42)
414+
)],
415+
['c', true]
416+
)
417+
->unshift(['b', Item::false()])
418+
->replace(2, ['c', Item::fromDateString('2022-12-23 13:00:23')])
419+
;
420+
421+
echo $value->toHttpValue(); //b=?0, a=(bar "42" 42 42.0), c=@1671800423
422+
echo $value; //b=?0, a=(bar "42" 42 42.0), c=@1671800423
423+
```
424+
425+
**⚠️WARNING: on duplication parameters with the same `keys` are merged as per RFC logic.**
426+
383427
#### Automatic conversion
384428

385429
For all containers, to ease instantiation the following automatic conversion are applied on
@@ -501,17 +545,29 @@ Both objects provide additional modifying methods to help deal with parameters.
501545
You can attach and update the associated `Parameters` instance using the following methods.
502546

503547
```php
504-
use Bakame\Http\StructuredFields\Parameters;
505-
506548
$field->addParameter(string $key, mixed $value): static;
507549
$field->appendParameter(string $key, mixed $value): static;
508550
$field->prependParameter(string $key, mixed $value): static;
509551
$field->withoutParameters(string ...$keys): static;
510552
$field->withoutAnyParameter(): static;
511553
$field->withParameters(Parameters $parameters): static;
512554
```
555+
Since verrsion `1.1` it is also possible to use the index of each member to perform additional
556+
modifications.
557+
558+
```php
559+
$field->pushParameters(array ...$pairs): static
560+
$field->unshiftParameters(array ...$pairs): static
561+
$field->insertParameters(int $index, array ...$pairs): static
562+
$field->replaceParameter(int $index, array $pair): static
563+
```
564+
565+
The `$pair` parameter is a tuple (ie: an array as list with exactly two members) where:
566+
567+
- the first array member is the parameter `$key`
568+
- the second array member is the parameter `$value`
513569

514-
**The return value will be the parent class an NOT a `Parameters` instance**
570+
**⚠️WARNING: The return value will be the parent class an NOT a `Parameters` instance**
515571

516572
```php
517573
use Bakame\Http\StructuredFields\InnerList;
@@ -523,7 +579,15 @@ echo InnerList::new('foo', 'bar')
523579
->addParameter('max-age', 2500)
524580
->toHttpValue();
525581

526-
// return the InnerList HTTP value
582+
echo InnerList::new('foo', 'bar')
583+
->pushParameter(
584+
['expire', Item::fromDateString('+30 minutes')],
585+
['path', '/'],
586+
['max-age', 2500],
587+
)
588+
->toHttpValue();
589+
590+
// both flow return the InnerList HTTP value
527591
// ("foo" "bar");expire=@1681538756;path="/";max-age=2500
528592
```
529593

src/Dictionary.php

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use function is_array;
1717
use function is_iterable;
1818
use function is_string;
19-
use const ARRAY_FILTER_USE_KEY;
2019

2120
/**
2221
* @see https://www.rfc-editor.org/rfc/rfc8941.html#section-3.2
@@ -210,9 +209,7 @@ public function get(string|int $key): StructuredField
210209
public function hasPair(int ...$indexes): bool
211210
{
212211
foreach ($indexes as $index) {
213-
try {
214-
$this->filterIndex($index);
215-
} catch (InvalidOffset) {
212+
if (null === $this->filterIndex($index)) {
216213
return false;
217214
}
218215
}
@@ -223,12 +220,12 @@ public function hasPair(int ...$indexes): bool
223220
/**
224221
* Filters and format instance index.
225222
*/
226-
private function filterIndex(int $index): int
223+
private function filterIndex(int $index): int|null
227224
{
228225
$max = count($this->members);
229226

230227
return match (true) {
231-
[] === $this->members, 0 > $max + $index, 0 > $max - $index - 1 => throw InvalidOffset::dueToIndexNotFound($index),
228+
[] === $this->members, 0 > $max + $index, 0 > $max - $index - 1 => null,
232229
0 > $index => $max + $index,
233230
default => $index,
234231
};
@@ -241,7 +238,12 @@ private function filterIndex(int $index): int
241238
*/
242239
public function pair(int $index): array
243240
{
244-
return [...$this->toPairs()][$this->filterIndex($index)];
241+
$offset = $this->filterIndex($index);
242+
if (null === $offset) {
243+
throw InvalidOffset::dueToIndexNotFound($index);
244+
}
245+
246+
return [...$this->toPairs()][$offset];
245247
}
246248

247249
/**
@@ -269,13 +271,20 @@ private function newInstance(array $members): self
269271

270272
public function remove(string|int ...$keys): static
271273
{
272-
$members = array_filter(
273-
$this->members,
274-
fn (string|int $key): bool => !in_array($key, $keys, true),
275-
ARRAY_FILTER_USE_KEY
276-
);
274+
/** @var array<array-key, true> $indexes */
275+
$indexes = array_fill_keys($keys, true);
276+
$pairs = [];
277+
foreach ($this->toPairs() as $index => $pair) {
278+
if (!isset($indexes[$index]) && !isset($indexes[$pair[0]])) {
279+
$pairs[] = $pair;
280+
}
281+
}
277282

278-
return $this->newInstance($members);
283+
if (count($this->members) === count($pairs)) {
284+
return $this;
285+
}
286+
287+
return self::fromPairs($pairs);
279288
}
280289

281290
/**
@@ -302,6 +311,79 @@ public function prepend(string $key, iterable|StructuredField|Token|ByteSequence
302311
return $this->newInstance($members);
303312
}
304313

314+
/**
315+
* @param array{0:string, 1:SfMember|SfMemberInput} ...$pairs
316+
*/
317+
public function push(array ...$pairs): self
318+
{
319+
if ([] === $pairs) {
320+
return $this;
321+
}
322+
323+
$newPairs = iterator_to_array($this->toPairs());
324+
foreach ($pairs as $pair) {
325+
$newPairs[] = $pair;
326+
}
327+
328+
return self::fromPairs($newPairs);
329+
}
330+
331+
/**
332+
* @param array{0:string, 1:SfMember|SfMemberInput} ...$pairs
333+
*/
334+
public function unshift(array ...$pairs): self
335+
{
336+
if ([] === $pairs) {
337+
return $this;
338+
}
339+
340+
foreach ($this->members as $key => $member) {
341+
$pairs[] = [$key, $member];
342+
}
343+
344+
return self::fromPairs($pairs);
345+
}
346+
347+
/**
348+
* @param array{0:string, 1:SfMember|SfMemberInput} ...$members
349+
*/
350+
public function insert(int $index, array ...$members): static
351+
{
352+
$offset = $this->filterIndex($index);
353+
354+
return match (true) {
355+
null === $offset => throw InvalidOffset::dueToIndexNotFound($index),
356+
[] === $members => $this,
357+
0 === $offset => $this->unshift(...$members),
358+
count($this->members) === $offset => $this->push(...$members),
359+
default => (function (Iterator $newMembers) use ($offset, $members) {
360+
$newMembers = iterator_to_array($newMembers);
361+
array_splice($newMembers, $offset, 0, $members);
362+
363+
return self::fromPairs($newMembers);
364+
})($this->toPairs()),
365+
};
366+
}
367+
368+
/**
369+
* @param array{0:string, 1:SfMember|SfMemberInput} $member
370+
*/
371+
public function replace(int $index, array $member): static
372+
{
373+
$offset = $this->filterIndex($index);
374+
if (null === $offset) {
375+
throw InvalidOffset::dueToIndexNotFound($index);
376+
}
377+
378+
$member[1] = self::filterMember($member[1]);
379+
$pairs = iterator_to_array($this->toPairs());
380+
if ($pairs[$offset] == $member) {
381+
return $this;
382+
}
383+
384+
return self::fromPairs(array_replace($pairs, [$offset => $member]));
385+
}
386+
305387
/**
306388
* @param iterable<string, SfMember|SfMemberInput> ...$others
307389
*/

src/DictionaryTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,63 @@ public function it_forbids_adding_members_using_the_array_access_interface(): vo
316316

317317
Dictionary::fromPairs([['foobar', 'foobar'], ['zero', 0]])['foobar'] = Item::false();
318318
}
319+
320+
#[Test]
321+
public function it_can_returns_the_container_member_keys_with_pairs(): void
322+
{
323+
$instance = Dictionary::new();
324+
325+
self::assertSame([], $instance->keys());
326+
self::assertSame(['a', 'b'], $instance->push(['a', false], ['b', true])->keys());
327+
328+
$container = Dictionary::new()
329+
->unshift(['a', '42'])
330+
->push(['b', 42])
331+
->insert(1, ['c', 42.0])
332+
->replace(0, ['d', 'forty-two']);
333+
334+
self::assertSame(['d', 'c', 'b'], $container->keys());
335+
self::assertSame('d="forty-two", c=42.0, b=42', $container->toHttpValue());
336+
}
337+
338+
#[Test]
339+
public function it_can_push_and_unshift_new_pair(): void
340+
{
341+
$instance = Dictionary::new()
342+
->push(['a', false])
343+
->unshift(['b', true]);
344+
345+
self::assertSame('b, a=?0', $instance->toHttpValue());
346+
self::assertSame('b, a=?0', (string) $instance);
347+
}
348+
349+
#[Test]
350+
public function it_fails_to_insert_at_an_invalid_index(): void
351+
{
352+
$this->expectException(InvalidOffset::class);
353+
354+
Dictionary::new()->insert(3, ['a', 1]);
355+
}
356+
357+
#[Test]
358+
public function it_can_push_nothing(): void
359+
{
360+
self::assertEquals(Dictionary::new()->push()->unshift(), Dictionary::new());
361+
}
362+
363+
#[Test]
364+
public function it_fails_to_replace_unknown_index(): void
365+
{
366+
$this->expectException(InvalidOffset::class);
367+
368+
Dictionary::new()->replace(0, ['a', true]);
369+
}
370+
371+
#[Test]
372+
public function it_returns_the_same_instance_if_nothing_is_replaced(): void
373+
{
374+
$field = Dictionary::new()->push(['a', true]);
375+
376+
self::assertSame($field->replace(0, ['a', true]), $field);
377+
}
319378
}

0 commit comments

Comments
 (0)