diff --git a/README.md b/README.md index adbe57d..1e7efa9 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ The **SchemaContextBundle** provides a lightweight way to manage dynamic schema ## Features - Extracts tenant schema param from baggage request header. -- Stores schema context in a global `SchemaResolver`. -- Injects schema info into Messenger messages via a middleware. -- Rehydrates schema on message consumption via a middleware. -- Provide decorator for Http clients to propagate schema header +- Stores schema and baggage context in a global `BaggageSchemaResolver`. +- Injects schema and baggage info into Messenger messages via a middleware. +- Rehydrates schema and baggage on message consumption via a middleware. +- Provide decorator for Http clients to propagate baggage header --- @@ -49,34 +49,35 @@ APP_NAME=develop ## Usage ```php -use Macpaw\SchemaContextBundle\Service\SchemaResolver; +use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver; -public function index(SchemaResolver $schemaResolver) +public function index(BaggageSchemaResolver $schemaResolver) { $schema = $schemaResolver->getSchema(); + $baggage = $schemaResolver->getBaggage(); // Use schema in logic } ``` -## Schema-Aware HTTP Client +## Baggage-Aware HTTP Client Decorate your http client in your service configuration: ```yaml services: - schema_aware_payment_http_client: - class: Macpaw\SchemaContextBundle\HttpClient\SchemaAwareHttpClient + baggage_aware_payment_http_client: + class: Macpaw\SchemaContextBundle\HttpClient\BaggageAwareHttpClient decorates: payment_http_client #http client to decorate arguments: - - '@schema_aware_payment_http_client.inner' - - '@Macpaw\SchemaContextBundle\Service\SchemaResolver' - - '%schema_context.header_name%' + - '@baggage_aware_payment_http_client.inner' + - '@Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver' + - '@Macpaw\SchemaContextBundle\Service\BaggageCodec' ``` ## Messenger Integration The bundle provides a middleware that automatically: -* Adds a SchemaStamp to dispatched messages +* Adds a BaggageSchemaStamp to dispatched messages -* Restores the schema context on message handling +* Restores the schema and baggage context on message handling Enable the middleware in your `messenger.yaml`: @@ -86,7 +87,7 @@ framework: buses: messenger.bus.default: middleware: - - Macpaw\SchemaContextBundle\Messenger\Middleware\SchemaMiddleware + - Macpaw\SchemaContextBundle\Messenger\Middleware\BaggageMiddleware ``` ## Testing @@ -100,4 +101,3 @@ Feel free to open issues and submit pull requests. ## License This bundle is released under the MIT license. - diff --git a/config/services.yaml b/config/services.yaml index 2acb1ef..2e4c682 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,13 +4,13 @@ services: autoconfigure: true public: false - Macpaw\SchemaContextBundle\Service\SchemaResolver: + Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver: public: true shared: true - Macpaw\SchemaContextBundle\EventListener\SchemaRequestListener: + Macpaw\SchemaContextBundle\EventListener\BaggageRequestListener: arguments: - $schemaResolver: '@Macpaw\SchemaContextBundle\Service\SchemaResolver' + $baggageSchemaResolver: '@Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver' $schemaRequestHeader: '%schema_context.header_name%' $defaultSchema: '%schema_context.default_schema%' $appName: '%schema_context.app_name%' @@ -18,6 +18,6 @@ services: tags: - { name: kernel.event_subscriber } - Macpaw\SchemaContextBundle\Messenger\Middleware\SchemaMiddleware: + Macpaw\SchemaContextBundle\Messenger\Middleware\BaggageMiddleware: tags: - { name: messenger.middleware } diff --git a/src/EventListener/SchemaRequestListener.php b/src/EventListener/BaggageRequestListener.php similarity index 61% rename from src/EventListener/SchemaRequestListener.php rename to src/EventListener/BaggageRequestListener.php index 02cfdf2..7ff35c4 100644 --- a/src/EventListener/SchemaRequestListener.php +++ b/src/EventListener/BaggageRequestListener.php @@ -4,15 +4,17 @@ namespace Macpaw\SchemaContextBundle\EventListener; -use Macpaw\SchemaContextBundle\Service\SchemaResolver; +use Macpaw\SchemaContextBundle\Service\BaggageCodec; +use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class SchemaRequestListener implements EventSubscriberInterface +class BaggageRequestListener implements EventSubscriberInterface { public function __construct( - private SchemaResolver $schemaResolver, + private BaggageSchemaResolver $baggageSchemaResolver, + private BaggageCodec $baggageCodec, private string $schemaRequestHeader, private string $defaultSchema, private string $appName, @@ -35,24 +37,18 @@ public function onKernelRequest(RequestEvent $event): void $request = $event->getRequest(); $baggage = $request->headers->get('baggage'); + $schema = null; if ($baggage) { - foreach (explode(',', $baggage) as $part) { - [$key, $value] = array_map( - static fn(?string $v): ?string => $v !== null ? trim($v) : null, - explode('=', $part, 2) + [null, null] - ); - - if ($key === $this->schemaRequestHeader && $value !== null) { - $schema = $value; - break; - } - } - } + $baggage = $this->baggageCodec->decode($baggage); + $this->baggageSchemaResolver->setBaggage($baggage); - $schema ??= $this->defaultSchema; + $schema = $baggage[$this->schemaRequestHeader] ?? null; + } if ($schema !== null && $schema !== '') { - $this->schemaResolver->setSchema($schema); + $this->baggageSchemaResolver->setSchema($schema); + } else { + $this->baggageSchemaResolver->setSchema($this->defaultSchema); } } diff --git a/src/HttpClient/SchemaAwareHttpClient.php b/src/HttpClient/BaggageAwareHttpClient.php similarity index 62% rename from src/HttpClient/SchemaAwareHttpClient.php rename to src/HttpClient/BaggageAwareHttpClient.php index a820419..f4d4c45 100644 --- a/src/HttpClient/SchemaAwareHttpClient.php +++ b/src/HttpClient/BaggageAwareHttpClient.php @@ -4,17 +4,18 @@ namespace Macpaw\SchemaContextBundle\HttpClient; +use Macpaw\SchemaContextBundle\Service\BaggageCodec; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; -use Macpaw\SchemaContextBundle\Service\SchemaResolver; +use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver; use Symfony\Contracts\HttpClient\ResponseStreamInterface; -class SchemaAwareHttpClient implements HttpClientInterface +class BaggageAwareHttpClient implements HttpClientInterface { public function __construct( private HttpClientInterface $inner, - private SchemaResolver $schemaResolver, - private string $schemaRequestHeader + private BaggageSchemaResolver $baggageSchemaResolver, + private BaggageCodec $baggageCodec, ) { } @@ -23,18 +24,20 @@ public function __construct( */ public function request(string $method, string $url, array $options = []): ResponseInterface { - $schema = $this->schemaResolver->getSchema(); - $baggageHeader = $this->schemaRequestHeader . '=' . $schema; $headers = isset($options['headers']) && is_array($options['headers']) ? $options['headers'] : []; - if (isset($headers['baggage'])) { - $headers['baggage'] .= ',' . $baggageHeader; - } else { - $headers['baggage'] = $baggageHeader; - } + $baggage = isset($headers['baggage']) + ? $this->baggageCodec->decode($headers['baggage']) + : []; + + $baggage = [ + ...$baggage, + ...($this->baggageSchemaResolver->getBaggage() ?? []) + ]; + $headers['baggage'] = $this->baggageCodec->encode($baggage); $options['headers'] = $headers; return $this->inner->request($method, $url, $options); @@ -53,6 +56,6 @@ public function withOptions(array $options): static $wrapped = $this->inner->withOptions($options); /** @phpstan-ignore-next-line */ - return new self($wrapped, $this->schemaResolver, $this->schemaRequestHeader); + return new self($wrapped, $this->baggageSchemaResolver, $this->baggageCodec); } } diff --git a/src/Messenger/Middleware/BaggageSchemaMiddleware.php b/src/Messenger/Middleware/BaggageSchemaMiddleware.php new file mode 100644 index 0000000..5a064d2 --- /dev/null +++ b/src/Messenger/Middleware/BaggageSchemaMiddleware.php @@ -0,0 +1,43 @@ +last(BaggageSchemaStamp::class); + + if ($stamp instanceof BaggageSchemaStamp) { + $this->baggageSchemaResolver + ->setSchema($stamp->schema) + ->setBaggage($this->baggageCodec->decode($stamp->baggage)); + + return $stack->next()->handle($envelope, $stack); + } + + $schema = $this->baggageSchemaResolver->getSchema(); + $baggage = $this->baggageCodec->encode($this->baggageSchemaResolver->getBaggage() ?? []); + + if ($schema !== null && $schema !== '') { + $envelope = $envelope->with(new BaggageSchemaStamp($schema, $baggage)); + } + + return $stack->next()->handle($envelope, $stack); + } +} diff --git a/src/Messenger/Middleware/SchemaMiddleware.php b/src/Messenger/Middleware/SchemaMiddleware.php deleted file mode 100644 index 78fe890..0000000 --- a/src/Messenger/Middleware/SchemaMiddleware.php +++ /dev/null @@ -1,37 +0,0 @@ -last(SchemaStamp::class); - - if ($stamp instanceof SchemaStamp) { - $this->schemaResolver->setSchema($stamp->schema); - - return $stack->next()->handle($envelope, $stack); - } - - $schema = $this->schemaResolver->getSchema(); - - if ($schema !== null && $schema !== '') { - $envelope = $envelope->with(new SchemaStamp($schema)); - } - - return $stack->next()->handle($envelope, $stack); - } -} diff --git a/src/Messenger/Stamp/SchemaStamp.php b/src/Messenger/Stamp/BaggageSchemaStamp.php similarity index 55% rename from src/Messenger/Stamp/SchemaStamp.php rename to src/Messenger/Stamp/BaggageSchemaStamp.php index ac5fd3c..8f64aa6 100644 --- a/src/Messenger/Stamp/SchemaStamp.php +++ b/src/Messenger/Stamp/BaggageSchemaStamp.php @@ -6,9 +6,9 @@ use Symfony\Component\Messenger\Stamp\StampInterface; -class SchemaStamp implements StampInterface +class BaggageSchemaStamp implements StampInterface { - public function __construct(public string $schema) + public function __construct(public string $schema, public string $baggage) { } } diff --git a/src/Service/BaggageCodec.php b/src/Service/BaggageCodec.php new file mode 100644 index 0000000..989d5aa --- /dev/null +++ b/src/Service/BaggageCodec.php @@ -0,0 +1,49 @@ + $baggage + */ + public function encode(array $baggage): string + { + $parts = []; + + foreach ($baggage as $key => $value) { + if ($value === null) { + $parts[] = trim($key); + } else { + $parts[] = trim($key) . '=' . trim($value); + } + } + + return implode(',', $parts); + } + + /** + * @return array + */ + public function decode(string $baggage): array + { + $result = []; + foreach (explode(',', $baggage) as $part) { + $part = trim($part); + if ($part === '') { + continue; + } + + if (str_contains($part, '=')) { + [$key, $value] = explode('=', $part, 2); + $result[trim($key)] = trim($value); + } else { + $result[$part] = null; + } + } + + return $result; + } +} diff --git a/src/Service/BaggageSchemaResolver.php b/src/Service/BaggageSchemaResolver.php new file mode 100644 index 0000000..28108f8 --- /dev/null +++ b/src/Service/BaggageSchemaResolver.php @@ -0,0 +1,44 @@ +|null + */ + private ?array $baggage = null; + private ?string $schema = null; + + /** + * @return array|null + */ + public function getBaggage(): ?array + { + return $this->baggage; + } + + /** + * @param array $baggage + */ + public function setBaggage(array $baggage): self + { + $this->baggage = $baggage; + + return $this; + } + + public function setSchema(string $schema): self + { + $this->schema = $schema; + + return $this; + } + + public function getSchema(): ?string + { + return $this->schema; + } +} diff --git a/src/Service/SchemaResolver.php b/src/Service/SchemaResolver.php deleted file mode 100644 index fbf538f..0000000 --- a/src/Service/SchemaResolver.php +++ /dev/null @@ -1,20 +0,0 @@ -schema = $schema; - } - - public function getSchema(): ?string - { - return $this->schema; - } -} diff --git a/tests/EventListener/BaggageRequestListenerTest.php b/tests/EventListener/BaggageRequestListenerTest.php new file mode 100644 index 0000000..4f6ac3b --- /dev/null +++ b/tests/EventListener/BaggageRequestListenerTest.php @@ -0,0 +1,91 @@ + 'X-Schema=tenant1']); + $kernel = $this->createMock(HttpKernelInterface::class); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener->onKernelRequest($event); + + self::assertSame('tenant1', $resolver->getSchema()); + self::assertSame([ + 'X-Schema' => 'tenant1', + ], $resolver->getBaggage()); + } + + public function testBaggageFromHeaderIsSetWithMultiplyParameters(): void + { + $resolver = new BaggageSchemaResolver(); + $baggageCodec = new BaggageCodec(); + $listener = new BaggageRequestListener( + $resolver, + $baggageCodec, + 'X-Schema', + 'default', + 'test-app', + ['test-app'], + ); + + $request = new Request([], [], [], [], [], ['HTTP_BAGGAGE' => 'X-Schema= tenant1 ,test , foo=bar']); + $kernel = $this->createMock(HttpKernelInterface::class); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener->onKernelRequest($event); + + self::assertSame('tenant1', $resolver->getSchema()); + self::assertSame([ + 'X-Schema' => 'tenant1', + 'test' => null, + 'foo' => 'bar', + ], $resolver->getBaggage()); + } + + public function testDefaultSchemaIsUsedIfHeaderMissing(): void + { + $resolver = new BaggageSchemaResolver(); + $baggageCodec = new BaggageCodec(); + $listener = new BaggageRequestListener( + $resolver, + $baggageCodec, + 'X-Schema', + 'fallback', + 'test-app', + ['test-app'], + ); + + $request = new Request(); + $kernel = $this->createMock(HttpKernelInterface::class); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener->onKernelRequest($event); + + self::assertSame('fallback', $resolver->getSchema()); + self::assertNull($resolver->getBaggage()); + } +} diff --git a/tests/EventListener/SchemaRequestListenerTest.php b/tests/EventListener/SchemaRequestListenerTest.php deleted file mode 100644 index 45024ed..0000000 --- a/tests/EventListener/SchemaRequestListenerTest.php +++ /dev/null @@ -1,55 +0,0 @@ - 'X-Schema=tenant1']); - $kernel = $this->createMock(HttpKernelInterface::class); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - - $listener->onKernelRequest($event); - - self::assertSame('tenant1', $resolver->getSchema()); - } - - public function testDefaultSchemaIsUsedIfHeaderMissing(): void - { - $resolver = new SchemaResolver(); - $listener = new SchemaRequestListener( - $resolver, - 'X-Schema', - 'fallback', - 'test-app', - ['test-app'], - ); - - $request = new Request(); - $kernel = $this->createMock(HttpKernelInterface::class); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - - $listener->onKernelRequest($event); - - self::assertSame('fallback', $resolver->getSchema()); - } -} diff --git a/tests/HttpClient/BaggageAwareHttpClientTest.php b/tests/HttpClient/BaggageAwareHttpClientTest.php new file mode 100644 index 0000000..a10b48c --- /dev/null +++ b/tests/HttpClient/BaggageAwareHttpClientTest.php @@ -0,0 +1,100 @@ + $arrayBaggage + */ + #[DataProvider('baggageHeaderDataProvider')] + public function testItInjectsSchemaIntoBaggageHeader( + string $requestBaggage, + array $arrayBaggage, + string $expectedSentBaggage + ): void { + $mockClient = $this->createMock(HttpClientInterface::class); + $mockClient + ->expects($this->once()) + ->method('request') + ->with( + 'GET', + 'https://api.example.com/test', + $this->callback(function (array $options) use ($expectedSentBaggage) { + $headers = $options['headers'] ?? []; + $baggage = $headers['baggage'] ?? null; + + return $baggage === $expectedSentBaggage; + }) + ) + ->willReturn(new MockResponse('OK')); + + $baggageSchemaResolver = (new BaggageSchemaResolver())->setBaggage($arrayBaggage); + $baggageCodec = new BaggageCodec(); + + $client = new BaggageAwareHttpClient( + $mockClient, + $baggageSchemaResolver, + $baggageCodec, + ); + + $response = $client->request('GET', 'https://api.example.com/test', [ + 'headers' => [ + 'baggage' => $requestBaggage, + ], + ]); + + self::assertInstanceOf(ResponseInterface::class, $response); + } + + /** + * @return iterable, string}> + */ + public static function baggageHeaderDataProvider(): iterable + { + yield [ + '', + [], + '', + ]; + + yield [ + '', + [ + 'X-Schema' => 'tenant_42', + ], + 'X-Schema=tenant_42', + ]; + + yield [ + '', + [ + 'X-Schema' => 'tenant_42', + 'foo' => null, + 'bar' => 'baz', + ], + 'X-Schema=tenant_42,foo,bar=baz', + ]; + + yield [ + 'test = 123', + [ + 'X-Schema' => 'tenant_42', + 'foo' => null, + 'bar' => 'baz', + ], + 'test=123,X-Schema=tenant_42,foo,bar=baz', + ]; + } +} diff --git a/tests/HttpClient/SchemaAwareHttpClientTest.php b/tests/HttpClient/SchemaAwareHttpClientTest.php deleted file mode 100644 index e8439e3..0000000 --- a/tests/HttpClient/SchemaAwareHttpClientTest.php +++ /dev/null @@ -1,55 +0,0 @@ -createMock(HttpClientInterface::class); - $mockClient - ->expects($this->once()) - ->method('request') - ->with( - 'GET', - 'https://api.example.com/test', - $this->callback(function (array $options) use ($expectedSchema, $schemaRequestHeader) { - $headers = $options['headers'] ?? []; - $baggage = $headers['baggage'] ?? null; - - if (is_array($baggage)) { - $baggage = implode(',', $baggage); - } - - return is_string($baggage) && str_contains($baggage, $schemaRequestHeader . '=' . $expectedSchema); - }) - ) - ->willReturn($mockResponse); - - $schemaResolver = $this->createMock(SchemaResolver::class); - $schemaResolver->method('getSchema')->willReturn($expectedSchema); - - $client = new SchemaAwareHttpClient( - $mockClient, - $schemaResolver, - $schemaRequestHeader, - ); - - $response = $client->request('GET', 'https://api.example.com/test'); - - self::assertInstanceOf(ResponseInterface::class, $response); - } -} diff --git a/tests/Messenger/Middleware/SchemaMiddlewareTest.php b/tests/Messenger/Middleware/BaggageSchemaMiddlewareTest.php similarity index 54% rename from tests/Messenger/Middleware/SchemaMiddlewareTest.php rename to tests/Messenger/Middleware/BaggageSchemaMiddlewareTest.php index 0b6fcd4..d4a6944 100644 --- a/tests/Messenger/Middleware/SchemaMiddlewareTest.php +++ b/tests/Messenger/Middleware/BaggageSchemaMiddlewareTest.php @@ -4,21 +4,29 @@ namespace Macpaw\SchemaContextBundle\Tests\Messenger\Middleware; -use Macpaw\SchemaContextBundle\Messenger\Middleware\SchemaMiddleware; -use Macpaw\SchemaContextBundle\Messenger\Stamp\SchemaStamp; -use Macpaw\SchemaContextBundle\Service\SchemaResolver; +use Macpaw\SchemaContextBundle\Messenger\Middleware\BaggageSchemaMiddleware; +use Macpaw\SchemaContextBundle\Messenger\Stamp\BaggageSchemaStamp; +use Macpaw\SchemaContextBundle\Service\BaggageCodec; +use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; use Symfony\Component\Messenger\Middleware\StackInterface; -class SchemaMiddlewareTest extends TestCase +class BaggageSchemaMiddlewareTest extends TestCase { public function testSchemaIsSetFromStamp(): void { - $resolver = new SchemaResolver(); - $middleware = new SchemaMiddleware($resolver); - $stamp = new SchemaStamp('tenant1'); + $schema = 'tenant1'; + $rawBaggage = 'X-Schema=tenant1'; + $baggage = [ + 'X-Schema' => 'tenant1', + ]; + + $resolver = new BaggageSchemaResolver(); + $baggageCodec = new BaggageCodec(); + $middleware = new BaggageSchemaMiddleware($resolver, $baggageCodec); + $stamp = new BaggageSchemaStamp($schema, $rawBaggage); $envelope = (new Envelope(new \stdClass()))->with($stamp); $stack = $this->createMock(StackInterface::class); $nextMiddleware = new class implements MiddlewareInterface { @@ -34,15 +42,23 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope $middleware->handle($envelope, $stack); - $this->assertSame('tenant1', $resolver->getSchema()); + $this->assertSame($schema, $resolver->getSchema()); + $this->assertSame($baggage, $resolver->getBaggage()); } public function testSchemaStampIsInjectedIfMissing(): void { $schema = 'tenant1'; - $resolver = new SchemaResolver(); - $resolver->setSchema($schema); - $middleware = new SchemaMiddleware($resolver); + $rawBaggage = 'X-Schema=tenant1'; + $baggage = [ + 'X-Schema' => 'tenant1', + ]; + $resolver = new BaggageSchemaResolver(); + $resolver + ->setSchema($schema) + ->setBaggage($baggage); + $baggageCodec = new BaggageCodec(); + $middleware = new BaggageSchemaMiddleware($resolver, $baggageCodec); $originalEnvelope = new Envelope(new \stdClass()); $stack = $this->createMock(StackInterface::class); @@ -59,9 +75,10 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope $resultEnvelope = $middleware->handle($originalEnvelope, $stack); - $stamp = $resultEnvelope->last(SchemaStamp::class); + $stamp = $resultEnvelope->last(BaggageSchemaStamp::class); - $this->assertInstanceOf(SchemaStamp::class, $stamp); + $this->assertInstanceOf(BaggageSchemaStamp::class, $stamp); $this->assertSame($schema, $stamp->schema); + $this->assertSame($rawBaggage, $stamp->baggage); } } diff --git a/tests/Service/BaggageCodecTest.php b/tests/Service/BaggageCodecTest.php new file mode 100644 index 0000000..47ba305 --- /dev/null +++ b/tests/Service/BaggageCodecTest.php @@ -0,0 +1,157 @@ + $arrayBaggage + */ + #[DataProvider('decodeDataProvider')] + public function testDecode(array $arrayBaggage, string $expectedResult): void + { + $resolver = new BaggageCodec(); + $result = $resolver->encode($arrayBaggage); + + self::assertSame($expectedResult, $result); + } + + /** + * @param array{string, ?string} $expectedResult + */ + #[DataProvider('encodeDataProvider')] + public function testEncode(string $rawBaggage, array $expectedResult): void + { + $resolver = new BaggageCodec(); + $result = $resolver->decode($rawBaggage); + + self::assertSame($expectedResult, $result); + } + + /** + * @param array $arrayBaggage + * @param array $expectedResult + */ + #[DataProvider('encodeAndDecodeDataProvider')] + public function testEncodeAndDecode(array $arrayBaggage, array $expectedResult): void + { + $resolver = new BaggageCodec(); + $result = $resolver->decode($resolver->encode($arrayBaggage)); + + self::assertSame($expectedResult, $result); + } + + /** + * @return iterable, string}> + */ + public static function decodeDataProvider(): iterable + { + yield [ + [], + '', + ]; + + yield [ + ['foo' => null], + 'foo', + ]; + + yield [ + [' foo ' => null], + 'foo', + ]; + + yield [ + ['foo' => null, 'bar' => 'baz'], + 'foo,bar=baz', + ]; + + yield [ + [' foo ' => null, ' bar ' => ' baz '], + 'foo,bar=baz', + ]; + + yield [ + ['foo' => null, 'bar' => 'baz', 'X-Schema' => '123'], + 'foo,bar=baz,X-Schema=123', + ]; + } + + /** + * @return iterable}> + */ + public static function encodeDataProvider(): iterable + { + yield [ + '', + [], + ]; + + yield [ + 'foo', + ['foo' => null], + ]; + + yield [ + ' foo ', + ['foo' => null], + ]; + + yield [ + 'foo,bar=baz', + ['foo' => null, 'bar' => 'baz'], + ]; + + yield [ + ' foo , bar = baz ', + ['foo' => null, 'bar' => 'baz'], + ]; + + yield [ + 'foo,bar=baz,X-Schema=123', + ['foo' => null, 'bar' => 'baz', 'X-Schema' => '123'], + ]; + } + + /** + * @return iterable, array}> + */ + public static function encodeAndDecodeDataProvider(): iterable + { + yield [ + [], + [], + ]; + + yield [ + ['foo' => null], + ['foo' => null], + ]; + + yield [ + [' foo ' => null], + ['foo' => null], + ]; + + yield [ + ['foo' => null, 'bar' => 'baz'], + ['foo' => null, 'bar' => 'baz'], + ]; + + yield [ + [' foo ' => null, ' bar ' => ' baz '], + ['foo' => null, 'bar' => 'baz'], + ]; + + yield [ + ['foo' => null, 'bar' => 'baz', 'X-Schema' => '123'], + ['foo' => null, 'bar' => 'baz', 'X-Schema' => '123'], + ]; + } +}