From a22bf303775d68b3959e2e0784695823104f7980 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sat, 23 Dec 2023 09:18:09 +0100 Subject: [PATCH] Re-introducing the DataType enum --- README.md | 113 ++++++++++++++++++++++++++++++++++++++-------- phpstan.neon | 2 + src/DataType.php | 44 ++++++++++++++++++ src/Type.php | 4 +- src/functions.php | 58 +++++++++++------------- 5 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 src/DataType.php diff --git a/README.md b/README.md index c4cc88e..f6d3b63 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ build and update HTTP Structured Fields in PHP according to the [RFC8941](https: Once installed you will be able to do the following: ```php -use Bakame\Http\StructuredFields\InnerList; -use Bakame\Http\StructuredFields\OuterList; use Bakame\Http\StructuredFields\Token; //1 - parsing an Accept Header @@ -30,15 +28,15 @@ echo http_build_structured_field('list', [ [ ['foo', 'bar'], [ - 'expire' => $expire, - 'path' => '/', - 'max-age' => 2500, - 'secure' => true, - 'httponly' => true, - 'samesite' => Token::fromString('lax'), + ['expire', $expire], + ['path', '/'], + [ 'max-age', 2500], + ['secure', true], + ['httponly', true], + ['samesite', Token::fromString('lax')], ] - ] -]); + ], +]), // returns ("foo" "bar");expire=@1681504328;path="/";max-age=2500;secure;httponly=?0;samesite=lax ``` @@ -65,7 +63,53 @@ header. Content validation is out of scope for this library. ### Parsing and Serializing Structured Fields -#### Basic usage +#### Functional API + +> [!NOTE] +> New in version 1.2.0 + +Two (2) functions are added to allow parsing and building http structured fields. +`http_parse_structured_field` and `http_build_structured_field` both take as +their first argument the type of data type you either want to parse or build +and as a second parameter the data that needs to be converted. +In case of the `http_parse_structured_field`, the second value is either a string +or a stringable object and on success it will return a +`Bakame\Http\StruncturedFields\StruncturedField` implementing object otherwise an +exception will be thrown. + +```php +$headerLine = 'bar;baz=42'; //the raw header line is a structured field item +$field = http_parse_structured_field('item', $headerLine); +$field->value(); // returns Token::fromString('bar); the found token value +$field->parameter('baz'); // returns 42; the value of the parameter or null if the parameter is not defined. +``` + +On the other hand, `http_build_structured_field` expects an iterable structure composed +of pair values that matches each data type and returns the structured field text representation +of the header. + +```php +use Bakame\Http\StructuredFields\Item; +use Bakame\Http\StructuredFields\DataType; + +$expire = Item::fromDateString('+30 minutes'); +$data = [ + [ + 'dumela lefatshe', + [['a', false]] + ], + [ + ['a', 'b', $expire], + [['a', true]] + ], +]; +echo http_build_structured_field(DataType::List, $data); +// display "dumela lefatshe";a=?0, ("a" "b" @1703319068);a +``` + +The data type can be given as a string or using the `DataType` enum. + +#### OOP usage Parsing the header value is done via the `fromHttpValue` named constructor. The method is attached to each library's structured fields representation @@ -74,13 +118,15 @@ as shown below: ```php declare(strict_types=1); +use Bakame\Http\StructuredFields\DataType; + require 'vendor/autoload.php'; // the raw HTTP field value is given by your application // via any given framework, package or super global. $headerLine = 'bar;baz=42'; //the raw header line is a structured field item -$field = parse($headerLine, 'item'); +$field = DataType::Item->parse($headerLine); $field->value(); // returns Token::fromString('bar); the found token value $field->parameter('baz'); // returns 42; the value of the parameter or null if the parameter is not defined. ``` @@ -91,10 +137,7 @@ compliant HTTP field string value. To ease integration, the `__toString` method implemented as an alias to the `toHttpValue` method. ````php -use function Bakame\Http\StructuredFields\http_sf_parse; -use function Bakame\Http\StructuredFields\http_sf_build; - -$field = http_sf_parse('bar; baz=42; secure=?1', 'item'); +$field = DataType::Item->parse('bar; baz=42; secure=?1'); echo $field->toHttpValue(); // return 'bar;baz=42;secure' // on serialization the field has been normalized @@ -104,8 +147,6 @@ echo $field->toHttpValue(); // return 'bar;baz=42;secure' header('foo: '. $field->toHttpValue()); //or header('foo: '. $field); -//or -header('foo: '. http_sf_build($field)); ```` All five (5) structured data type as defined in the RFC are provided inside the @@ -211,9 +252,10 @@ To ease validation a `Type::equals` method is exposed to check if the `Item` has the expected type. It can also be used to compare types. ```php +use Bakame\Http\StructuredFields\DataType; use Bakame\Http\StructuredFields\Type; -$field = Item::fromHttpValue('"foo"'); +$field = DataType::Item->parse('"foo"'); Type::Date->equals($field); // returns false Type::String->equals($field); // returns true; Type::Boolean->equals(Type::String); // returns false @@ -302,7 +344,7 @@ if you try to use them on any container object: ```php use Bakame\Http\StructuredFields\Parameters; -$value = Parameters::fromHttpValue(';a=foobar']); +$value = Parameters::fromHttpValue(';a=foobar'); $value->has('b'); // return false $value['a']->value(); // return 'foobar' $value['b']; // triggers a InvalidOffset exception, the index does not exist @@ -613,6 +655,37 @@ echo $list->toHttpValue(); //'(:SGVsbG8gV29ybGQ=: 42.0 42)' echo $list; //'(:SGVsbG8gV29ybGQ=: 42.0 42)' ``` +> [!NOTE] +> New in version 1.2.0 + +It is also possible to create an `OuterList` based on an iterable structure +of pairs. + +```php +use Bakame\Http\StructuredFields\OuterList; + +$list = OuterList::fromPairs([ + [ + ['foo', 'bar'], + [ + ['expire', $expire], + ['path', '/'], + [ 'max-age', 2500], + ['secure', true], + ['httponly', true], + ['samesite', Token::fromString('lax')], + ] + ], + [ + 'coucoulesamis', + [['a', false]], + ] + ]); +``` + +The pairs definitions are the same as for creating either a `InnerList` or an `Item` using +their respective `fromPair` method. + #### Adding and updating parameters To ease working with instances that have a `Parameters` object attached to, the following diff --git a/phpstan.neon b/phpstan.neon index da48cc2..83e9838 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,6 +11,8 @@ parameters: ignoreErrors: - message: '#it_fails_to_create_an_item_from_an_array_of_pairs\(\)#' path: tests/ItemTest.php + - message: '#Method Bakame\\Http\\StructuredFields\\DataType::build\(\) has parameter \$data with no value type specified in iterable type iterable.#' + path: src/DataType.php - message: '#Function http_build_structured_field\(\) has parameter \$data with no value type specified in iterable type iterable.#' path: src/functions.php reportUnmatchedIgnoredErrors: true diff --git a/src/DataType.php b/src/DataType.php new file mode 100644 index 0000000..8b03311 --- /dev/null +++ b/src/DataType.php @@ -0,0 +1,44 @@ + Dictionary::fromHttpValue($httpValue), + self::Parameters => Parameters::fromHttpValue($httpValue), + self::List => OuterList::fromHttpValue($httpValue), + self::InnerList => InnerList::fromHttpValue($httpValue), + self::Item => Item::fromHttpValue($httpValue), + }; + } + + /** + * @throws StructuredFieldError + */ + public function build(iterable $data): string + { + return match ($this) { + self::Dictionary => Dictionary::fromPairs($data)->toHttpValue(), + self::Parameters => Parameters::fromPairs($data)->toHttpValue(), + self::List => OuterList::fromPairs($data)->toHttpValue(), + self::InnerList => InnerList::fromPair([...$data])->toHttpValue(), /* @phpstan-ignore-line */ + self::Item => Item::fromPair([...$data])->toHttpValue(), /* @phpstan-ignore-line */ + }; + } +} diff --git a/src/Type.php b/src/Type.php index 4af9901..cfcb184 100644 --- a/src/Type.php +++ b/src/Type.php @@ -48,9 +48,9 @@ public static function tryFromValue(mixed $value): self|null is_float($value) => Type::Decimal, is_bool($value) => Type::Boolean, is_string($value) => match (true) { - 1 === preg_match('/[^\x20-\x7f]/', $value) => Type::DisplayString, - 1 === preg_match("/^([a-z*][a-z\d:\/!#\$%&'*+\-.^_`|~]*)$/i", $value) => Type::Token, + null !== Token::tryFromString($value) => Type::Token, null !== ByteSequence::tryFromEncoded($value) => Type::ByteSequence, + 1 === preg_match('/[^\x20-\x7f]/', $value) => Type::DisplayString, default => Type::String, }, default => null, diff --git a/src/functions.php b/src/functions.php index 5064479..131b649 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,31 +2,30 @@ declare(strict_types=1); -use Bakame\Http\StructuredFields\Dictionary; -use Bakame\Http\StructuredFields\InnerList; -use Bakame\Http\StructuredFields\Item; -use Bakame\Http\StructuredFields\OuterList; -use Bakame\Http\StructuredFields\Parameters; +use Bakame\Http\StructuredFields\DataType; use Bakame\Http\StructuredFields\StructuredField; +use Bakame\Http\StructuredFields\StructuredFieldError; if (!function_exists('http_parse_structured_field')) { /** * Parse a header conform to the HTTP Structured Field RFCs. * - * @param 'dictionary'|'parameters'|'list'|'innerlist'|'item' $type + * @throws OutOfBoundsException If the type is unknown or unsupported + * @throws StructuredFieldError If parsing the structured field fails * - * @throws OutOfRangeException If the value is unknown or undefined + * @see DataType::parse() */ - function http_parse_structured_field(string $type, string $httpValue): StructuredField + function http_parse_structured_field(DataType|string $dataType, string $httpValue): StructuredField { - return match ($type) { - 'dictionary' => Dictionary::fromHttpValue($httpValue), - 'parameters' => Parameters::fromHttpValue($httpValue), - 'list' => OuterList::fromHttpValue($httpValue), - 'innerlist' => InnerList::fromHttpValue($httpValue), - 'item' => Item::fromHttpValue($httpValue), /* @phpstan-ignore-line */ - default => throw new OutOfBoundsException('The submitted type "'.$type.'" is unknown or not supported,'), /* @phpstan-ignore-line */ - }; + if (!$dataType instanceof DataType) { + $dataType = DataType::tryFrom($dataType); + } + + if (null === $dataType) { + throw new OutOfBoundsException('The submitted type "'.$dataType.'" is unknown or not supported,'); + } + + return $dataType->parse($httpValue); } } @@ -34,26 +33,23 @@ function http_parse_structured_field(string $type, string $httpValue): Structure /** * Build an HTTP Structured Field Text representation fron an iterable PHP structure. * - * @param 'dictionary'|'parameters'|'list'|'innerlist'|'item' $type * @param iterable $data the iterable data used to generate the structured field * * @throws OutOfBoundsException If the type is unknown or unsupported + * @throws StructuredFieldError If building the structured field fails * - * @see Dictionary::fromPairs() - * @see Parameters::fromPairs() - * @see OuterList::fromPairs() - * @see InnerList::fromPair() - * @see Item::fromPair() + * @see DataType::build() */ - function http_build_structured_field(string $type, iterable $data): string /* @phptan-ignore-line */ + function http_build_structured_field(DataType|string $dataType, iterable $data): string /* @phptan-ignore-line */ { - return match ($type) { - 'dictionary' => Dictionary::fromPairs($data)->toHttpValue(), - 'parameters' => Parameters::fromPairs($data)->toHttpValue(), - 'list' => OuterList::fromPairs($data)->toHttpValue(), - 'innerlist' => InnerList::fromPair([...$data])->toHttpValue(), /* @phpstan-ignore-line */ - 'item' => Item::fromPair([...$data])->toHttpValue(), /* @phpstan-ignore-line */ - default => throw new OutOfBoundsException('The submitted type "'.$type.'" is unknown or not supported,'), /* @phpstan-ignore-line */ - }; + if (!$dataType instanceof DataType) { + $dataType = DataType::tryFrom($dataType); + } + + if (null === $dataType) { + throw new OutOfBoundsException('The submitted type "'.$dataType.'" is unknown or not supported,'); + } + + return $dataType->build([...$data]); } }