Skip to content

Commit c06744d

Browse files
committed
Merge branch 'psr-7'
2 parents ec86a7f + d7c65db commit c06744d

File tree

16 files changed

+830
-181
lines changed

16 files changed

+830
-181
lines changed

src/Cli/CliCommand.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ abstract protected function run(string ...$args);
9090
/**
9191
* Override to modify the command's JSON Schema before it is returned
9292
*
93-
* @param array{'$schema':string,title?:string,required?:string[],properties?:array<string,mixed>} $schema
94-
* @return array<string,mixed>
93+
* @param array{'$schema':string,title:string|null,type:string,required:string[],properties:array<string,mixed>} $schema
94+
* @return array{'$schema':string,title:string|null,type:string,required:string[],properties:array<string,mixed>,...}
9595
*/
9696
protected function filterJsonSchema(array $schema): array
9797
{
@@ -690,18 +690,17 @@ private function prepareUsage(?string $description, Formatter $formatter, ?int $
690690
}
691691

692692
/**
693-
* @return array<string,mixed>
693+
* @return array{'$schema':string,title?:string,type:string,required?:string[],properties?:array<string,mixed>,...}
694694
*/
695695
final public function getJsonSchema(?string $title = null): array
696696
{
697697
$schema = [
698698
'$schema' => 'http://json-schema.org/draft-04/schema#',
699699
];
700-
if ($title !== null) {
701-
$schema['title'] = $title;
702-
}
700+
$schema['title'] = $title;
703701
$schema['type'] = 'object';
704702
$schema['required'] = [];
703+
$schema['properties'] = [];
705704

706705
foreach ($this->getOptions() as $option) {
707706
if (!($option->Visibility & CliOptionVisibility::SCHEMA)) {
@@ -720,11 +719,17 @@ final public function getJsonSchema(?string $title = null): array
720719
}
721720
}
722721

723-
if (!$schema['required']) {
724-
unset($schema['required']);
722+
// Preserve essential properties in their original order
723+
$schema = array_merge($schema, $this->filterJsonSchema($schema));
724+
725+
foreach (['title', 'required', 'properties'] as $property) {
726+
$value = $schema[$property];
727+
if ($value === null || $value === []) {
728+
unset($schema[$property]);
729+
}
725730
}
726731

727-
return $this->filterJsonSchema($schema);
732+
return $schema;
728733
}
729734

730735
/**

src/Concern/TReadableCollection.php

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -297,16 +297,28 @@ public function count(): int
297297
*/
298298
protected function getItems($items): array
299299
{
300-
if ($items instanceof self) {
300+
if ($items instanceof static) {
301301
return $items->Items;
302302
}
303-
if ($items instanceof Arrayable) {
304-
return $items->toArray();
305-
}
306-
if (is_array($items)) {
307-
return $items;
303+
if ($items instanceof self) {
304+
$items = $items->Items;
305+
} elseif ($items instanceof Arrayable) {
306+
$items = $items->toArray();
307+
} elseif (!is_array($items)) {
308+
$items = iterator_to_array($items);
308309
}
309-
return iterator_to_array($items);
310+
return $this->filterItems($items);
311+
}
312+
313+
/**
314+
* Override to normalise items applied to the collection
315+
*
316+
* @param array<TKey,TValue> $items
317+
* @return array<TKey,TValue>
318+
*/
319+
protected function filterItems(array $items): array
320+
{
321+
return $items;
310322
}
311323

312324
/**

src/Http/Catalog/HttpHeader.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ final class HttpHeader extends Dictionary
1717

1818
public const CONTENT_TYPE = 'Content-Type';
1919

20+
public const HOST = 'Host';
21+
2022
public const PREFER = 'Prefer';
2123

2224
public const PROXY_AUTHORIZATION = 'Proxy-Authorization';

src/Http/HttpHeaders.php

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
use Lkrms\Http\Catalog\HttpHeader;
1313
use Lkrms\Http\Contract\AccessTokenInterface;
1414
use Lkrms\Http\Contract\HttpHeadersInterface;
15-
use Lkrms\Support\Catalog\RegularExpression as Regex;
1615
use Lkrms\Utility\Arr;
1716
use Lkrms\Utility\Pcre;
1817
use Generator;
1918
use LogicException;
2019

2120
/**
22-
* A collection of HTTP headers
21+
* A collection of [RFC7230]-compliant HTTP headers
2322
*/
2423
class HttpHeaders implements HttpHeadersInterface, IImmutable
2524
{
@@ -31,6 +30,25 @@ class HttpHeaders implements HttpHeadersInterface, IImmutable
3130
withPropertyValue as with;
3231
}
3332

33+
private const HTTP_HEADER_FIELD_NAME = '/^[-0-9a-z!#$%&\'*+.^_`|~]++$/iD';
34+
35+
private const HTTP_HEADER_FIELD_VALUE = '/^([\x21-\x7e\x80-\xff]++(?:\h++[\x21-\x7e\x80-\xff]++)*+)?$/D';
36+
37+
private const HTTP_HEADER_FIELD = <<<'REGEX'
38+
/ ^
39+
(?(DEFINE)
40+
(?<token> [-0-9a-z!#$%&'*+.^_`|~]++ )
41+
(?<field_vchar> [\x21-\x7e\x80-\xff]++ )
42+
(?<field_content> (?&field_vchar) (?: \h++ (?&field_vchar) )*+ )
43+
)
44+
(?:
45+
(?<name> (?&token) ) (?<bad_whitespace> \h++ )?+ : \h*+ (?<value> (?&field_content)? ) |
46+
\h++ (?<extended> (?&field_content)? )
47+
)
48+
(?<carry> \h++ )?
49+
$ /xiD
50+
REGEX;
51+
3452
/**
3553
* [ [ Name => value ], ... ]
3654
*
@@ -61,6 +79,31 @@ class HttpHeaders implements HttpHeadersInterface, IImmutable
6179
*/
6280
protected ?string $Carry = null;
6381

82+
/**
83+
* @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
84+
*/
85+
public function __construct($items = [])
86+
{
87+
$headers = [];
88+
$index = [];
89+
$i = -1;
90+
foreach ($items as $key => $value) {
91+
$values = (array) $value;
92+
if (!$values) {
93+
continue;
94+
}
95+
$lower = strtolower($key);
96+
$key = $this->filterName($key);
97+
foreach ($values as $value) {
98+
$headers[++$i] = [$key => $this->filterValue($value)];
99+
$index[$lower][] = $i;
100+
}
101+
}
102+
$this->Headers = $headers;
103+
$this->Index = $this->filterIndex($index);
104+
$this->Items = $this->doGetHeaders();
105+
}
106+
64107
/**
65108
* @inheritDoc
66109
*/
@@ -82,8 +125,7 @@ public function addLine(string $line, bool $strict = false)
82125
$value = null;
83126
if ($strict) {
84127
$line = substr($line, 0, -2);
85-
$regex = Regex::anchorAndDelimit(Regex::HTTP_HEADER_FIELD);
86-
if (!Pcre::match($regex, $line, $matches, \PREG_UNMATCHED_AS_NULL) ||
128+
if (!Pcre::match(self::HTTP_HEADER_FIELD, $line, $matches, \PREG_UNMATCHED_AS_NULL) ||
87129
$matches['bad_whitespace'] !== null) {
88130
throw new InvalidArgumentException(sprintf('Invalid HTTP header field: %s', $line));
89131
}
@@ -134,9 +176,9 @@ public function add($key, $value)
134176
$lower = strtolower($key);
135177
$headers = $this->Headers;
136178
$index = $this->Index;
137-
$key = $this->normaliseName($key);
179+
$key = $this->filterName($key);
138180
foreach ($values as $value) {
139-
$headers[] = [$key => $this->normaliseValue($value)];
181+
$headers[] = [$key => $this->filterValue($value)];
140182
$index[$lower][] = array_key_last($headers);
141183
}
142184
return $this->replaceHeaders($headers, $index);
@@ -172,9 +214,9 @@ public function set($key, $value)
172214
}
173215
unset($index[$lower]);
174216
}
175-
$key = $this->normaliseName($key);
217+
$key = $this->filterName($key);
176218
foreach ($values as $value) {
177-
$headers[] = [$key => $this->normaliseValue($value)];
219+
$headers[] = [$key => $this->filterValue($value)];
178220
$index[$lower][] = array_key_last($headers);
179221
}
180222
return $this->replaceHeaders($headers, $index);
@@ -233,10 +275,10 @@ public function merge($items, bool $preserveExisting = false)
233275
// Maintain the order of $index for comparison
234276
$index[$lower] = [];
235277
}
236-
$key = $this->normaliseName($key);
278+
$key = $this->filterName($key);
237279
foreach ($values as $value) {
238280
$applied = true;
239-
$headers[] = [$key => $this->normaliseValue($value)];
281+
$headers[] = [$key => $this->filterValue($value)];
240282
$index[$lower][] = array_key_last($headers);
241283
}
242284
}
@@ -486,18 +528,18 @@ protected function compareItems($a, $b): int
486528
return $a <=> $b;
487529
}
488530

489-
protected function normaliseName(string $name): string
531+
protected function filterName(string $name): string
490532
{
491-
if (!Pcre::match(Regex::anchorAndDelimit(Regex::HTTP_HEADER_FIELD_NAME), $name)) {
533+
if (!Pcre::match(self::HTTP_HEADER_FIELD_NAME, $name)) {
492534
throw new InvalidArgumentException(sprintf('Invalid header name: %s', $name));
493535
}
494536
return $name;
495537
}
496538

497-
protected function normaliseValue(string $value): string
539+
protected function filterValue(string $value): string
498540
{
499-
$value = Pcre::replace('/\r\n\h+/', ' ', trim($value));
500-
if (!Pcre::match(Regex::anchorAndDelimit(Regex::HTTP_HEADER_FIELD_VALUE), $value)) {
541+
$value = Pcre::replace('/\r\n\h+/', ' ', trim($value, " \t"));
542+
if (!Pcre::match(self::HTTP_HEADER_FIELD_VALUE, $value)) {
501543
throw new InvalidArgumentException(sprintf('Invalid header value: %s', $value));
502544
}
503545
return $value;
@@ -534,7 +576,7 @@ protected function replaceHeaders(?array $headers, array $index)
534576

535577
$clone = $this->clone();
536578
$clone->Headers = $headers;
537-
$clone->Index = $index;
579+
$clone->Index = $clone->filterIndex($index);
538580
$clone->Items = $clone->doGetHeaders();
539581
return $clone;
540582
}
@@ -574,6 +616,20 @@ protected function getIndexHeaders(array $index): array
574616
return array_intersect_key($this->Headers, $headers ?? []);
575617
}
576618

619+
/**
620+
* @param array<string,int[]> $index
621+
* @return array<string,int[]>
622+
*/
623+
private function filterIndex(array $index): array
624+
{
625+
// According to [RFC7230] Section 5.4, "a user agent SHOULD generate
626+
// Host as the first header field following the request-line"
627+
if (isset($index['host'])) {
628+
$index = ['host' => $index['host']] + $index;
629+
}
630+
return $index;
631+
}
632+
577633
/**
578634
* @param array<array<string,string>> $headers
579635
* @param array<string,int[]> $index

src/Http/HttpMessage.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ abstract class HttpMessage implements MessageInterface, IImmutable
2828

2929
/**
3030
* @param StreamInterface|resource|string|null $body
31-
* @param HttpHeadersInterface|array<string,string[]>|null $headers
31+
* @param HttpHeadersInterface|array<string,string[]|string>|null $headers
3232
*/
3333
public function __construct(
3434
$body = null,
@@ -91,39 +91,39 @@ public function getBody(): StreamInterface
9191
/**
9292
* @inheritDoc
9393
*/
94-
public function withProtocolVersion(string $version): self
94+
public function withProtocolVersion(string $version): MessageInterface
9595
{
9696
return $this->with('ProtocolVersion', $this->filterProtocolVersion($version));
9797
}
9898

9999
/**
100100
* @inheritDoc
101101
*/
102-
public function withHeader(string $name, $value): self
102+
public function withHeader(string $name, $value): MessageInterface
103103
{
104104
return $this->with('Headers', $this->Headers->set($name, $value));
105105
}
106106

107107
/**
108108
* @inheritDoc
109109
*/
110-
public function withAddedHeader(string $name, $value): self
110+
public function withAddedHeader(string $name, $value): MessageInterface
111111
{
112112
return $this->with('Headers', $this->Headers->add($name, $value));
113113
}
114114

115115
/**
116116
* @inheritDoc
117117
*/
118-
public function withoutHeader(string $name): self
118+
public function withoutHeader(string $name): MessageInterface
119119
{
120120
return $this->with('Headers', $this->Headers->unset($name));
121121
}
122122

123123
/**
124124
* @inheritDoc
125125
*/
126-
public function withBody(StreamInterface $body): self
126+
public function withBody(StreamInterface $body): MessageInterface
127127
{
128128
return $this->with('Body', $body);
129129
}

0 commit comments

Comments
 (0)