From 60b309a3d14a8b2d415e8e1286f6506c2e2fa9c4 Mon Sep 17 00:00:00 2001 From: PJ Dietz Date: Sun, 22 Mar 2015 14:03:31 -0400 Subject: [PATCH] Add Message\Message --- src/Message/Message.php | 246 ++++++++++++++++++ test/tests/unit/Message/MessageTest.php | 332 ++++++++++++++++++++++++ 2 files changed, 578 insertions(+) create mode 100644 src/Message/Message.php create mode 100644 test/tests/unit/Message/MessageTest.php diff --git a/src/Message/Message.php b/src/Message/Message.php new file mode 100644 index 0000000..53c1400 --- /dev/null +++ b/src/Message/Message.php @@ -0,0 +1,246 @@ +headers = new HeaderCollection(); + } + + /** + * Retrieves the HTTP protocol version as a string. + * + * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). + * + * @return string HTTP protocol version. + */ + public function getProtocolVersion() + { + return $this->protcolVersion; + } + + /** + * Create a new instance with the specified HTTP protocol version. + * + * The version string MUST contain only the HTTP version number (e.g., + * "1.1", "1.0"). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new protocol version. + * + * @param string $version HTTP protocol version + * @return self + */ + public function withProtocolVersion($version) + { + $message = clone $this; + $message->protcolVersion = $version; + return $message; + } + + /** + * Retrieves all message headers. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return array Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings. + */ + public function getHeaders() + { + $headers = []; + foreach ($this->headers as $key => $value) { + $headers[$key] = $value; + } + return $headers; + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name) + { + return isset($this->headers[$name]); + } + + /** + * Retrieve a header by the given case-insensitive name, as a string. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeaderLines() instead + * and supply your own delimiter when concatenating. + * + * If the header did not appear in the message, this method should return + * a null value. + * + * @param string $name Case-insensitive header field name. + * @return string|null + */ + public function getHeader($name) + { + if (isset($this->headers[$name])) { + return join(", ", $this->headers[$name]); + } else { + return null; + } + } + + /** + * Retrieves a header by the given case-insensitive name as an array of strings. + * + * If the header did not appear in the message, this method should return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] + */ + public function getHeaderLines($name) + { + if (isset($this->headers[$name])) { + return $this->headers[$name]; + } else { + return []; + } + } + + /** + * Create a new instance with the provided header, replacing any existing + * values of any headers with the same case-insensitive name. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return self + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value) + { + $message = clone $this; + unset($message->headers[$name]); + $message->headers[$name] = $value; + return $message; + } + + /** + * Creates a new instance, with the specified header appended with the + * given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return self + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value) + { + $message = clone $this; + $message->headers[$name] = $value; + return $message; + } + + /** + * Creates a new instance, without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return self + */ + public function withoutHeader($name) + { + $message = clone $this; + unset($message->headers[$name]); + return $message; + } + + /** + * Gets the body of the message. + * + * @return StreamableInterface Returns the body as a stream. + */ + public function getBody() + { + return $this->body; + } + + /** + * Create a new instance, with the specified message body. + * + * The body MUST be a StreamableInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamableInterface $body Body. + * @return self + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamableInterface $body) + { + $message = clone $this; + $message->body = $body; + return $message; + } + + // ----------------------------------------------------------------------------------------------------------------- + + public function __clone() + { + $this->headers = clone $this->headers; + } +} + diff --git a/test/tests/unit/Message/MessageTest.php b/test/tests/unit/Message/MessageTest.php new file mode 100644 index 0000000..40d5c37 --- /dev/null +++ b/test/tests/unit/Message/MessageTest.php @@ -0,0 +1,332 @@ +getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $this->assertNotNull($message); + } + + /** + * @covers WellRESTed\Message\Message::getProtocolVersion + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\HeaderCollection + */ + public function testReturnsProtocolVersion11ByDefault() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $this->assertEquals("1.1", $message->getProtocolVersion()); + } + + /** + * @covers WellRESTed\Message\Message::getProtocolVersion + * @uses WellRESTed\Message\Message::withProtocolVersion + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testReturnsProtocolVersion() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withProtocolVersion("1.0"); + $this->assertEquals("1.0", $message->getProtocolVersion()); + } + + /** + * @covers WellRESTed\Message\Message::withProtocolVersion + * @uses WellRESTed\Message\Message::getProtocolVersion + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testReplacesProtocolVersion() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withProtocolVersion("1.0"); + $this->assertEquals("1.0", $message->getProtocolVersion()); + } + + /** + * @covers WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::getHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testWithHeaderSetsHeader() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withHeader("Content-type", "application/json"); + $this->assertEquals("application/json", $message->getHeader("Content-type")); + } + + /** + * @covers WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::getHeaderLines + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testWithHeaderReplacesValue() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withHeader("Set-Cookie", "cat=Molly"); + $message = $message->withHeader("Set-Cookie", "dog=Bear"); + $cookies = $message->getHeaderLines("Set-Cookie"); + $this->assertNotContains("cat=Molly", $cookies); + $this->assertContains("dog=Bear", $cookies); + } + + /** + * @covers WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::getHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testWithAddedHeaderSetsHeader() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withAddedHeader("Content-type", "application/json"); + $this->assertEquals("application/json", $message->getHeader("Content-type")); + } + + /** + * @covers WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::getHeaderLines + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testWithAddedHeaderAppendsValue() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withAddedHeader("Set-Cookie", "cat=Molly"); + $message = $message->withAddedHeader("Set-Cookie", "dog=Bear"); + $cookies = $message->getHeaderLines("Set-Cookie"); + $this->assertContains("cat=Molly", $cookies); + $this->assertContains("dog=Bear", $cookies); + } + + /** + * @covers WellRESTed\Message\Message::withoutHeader + * @uses WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::hasHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testWithoutHeaderRemovesHeader() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withHeader("Content-type", "application/json"); + $message = $message->withoutHeader("Content-type"); + $this->assertFalse($message->hasHeader("Content-type")); + } + + /** + * @covers WellRESTed\Message\Message::getHeader + * @uses WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetHeaderReturnsSingleHeader() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withAddedHeader("Content-type", "application/json"); + $this->assertEquals("application/json", $message->getHeader("Content-type")); + } + + /** + * @covers WellRESTed\Message\Message::getHeader + * @uses WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetHeaderReturnsMultipleHeadersJoinedByCommas() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withAddedHeader("X-name", "cat=Molly"); + $message = $message->withAddedHeader("X-name", "dog=Bear"); + $this->assertEquals("cat=Molly, dog=Bear", $message->getHeader("X-name")); + } + + /** + * @covers WellRESTed\Message\Message::getHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetHeaderReturnsNullForUnsetHeader() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $this->assertNull($message->getHeader("X-not-set")); + } + + /** + * @covers WellRESTed\Message\Message::getHeaderLines + * @uses WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetHeaderLinesReturnsMultipleValuesForHeader() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withAddedHeader("X-name", "cat=Molly"); + $message = $message->withAddedHeader("X-name", "dog=Bear"); + $this->assertEquals(["cat=Molly", "dog=Bear"], $message->getHeaderLines("X-name")); + } + + /** + * @covers WellRESTed\Message\Message::getHeaderLines + * @uses WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetHeaderLinesReturnsEmptyArrayForUnsetHeader() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $this->assertEquals([], $message->getHeaderLines("X-name")); + } + + /** + * @covers WellRESTed\Message\Message::hasHeader + * @uses WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testHasHeaderReturnsTrueWhenHeaderIsSet() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withHeader("Content-type", "application/json"); + $this->assertTrue($message->hasHeader("Content-type")); + } + + /** + * @covers WellRESTed\Message\Message::hasHeader + * @uses WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testHasHeaderReturnsFalseWhenHeaderIsNotSet() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $this->assertFalse($message->hasHeader("Content-type")); + } + + /** + * @covers WellRESTed\Message\Message::getHeaders + * @uses WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetHeadersReturnOriginalHeaderNamesAsKeys() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withHeader("Set-Cookie", "cat=Molly"); + $message = $message->withAddedHeader("Set-Cookie", "dog=Bear"); + $message = $message->withHeader("Content-type", "application/json"); + + $headers = []; + foreach ($message->getHeaders() as $key => $values) { + $headers[] = $key; + } + + $expected = ["Content-type", "Set-Cookie"]; + $this->assertEquals(0, count(array_diff($expected, $headers))); + $this->assertEquals(0, count(array_diff($headers, $expected))); + } + + /** + * @covers WellRESTed\Message\Message::getHeaders + * @uses WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::withAddedHeader + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetHeadersReturnOriginalHeaderNamesAndValues() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withHeader("Set-Cookie", "cat=Molly"); + $message = $message->withAddedHeader("Set-Cookie", "dog=Bear"); + $message = $message->withHeader("Content-type", "application/json"); + + $headers = []; + + foreach ($message->getHeaders() as $key => $values) { + foreach ($values as $value) { + if (isset($headers[$key])) { + $headers[$key][] = $value; + } else { + $headers[$key] = [$value]; + } + } + } + + $expected = [ + "Set-Cookie" => ["cat=Molly", "dog=Bear"], + "Content-type" => ["application/json"] + ]; + + $this->assertEquals($expected, $headers); + } + + /** + * @covers WellRESTed\Message\Message::getBody + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetBodyReturnsNullByDefalt() + { + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $this->assertNull($message->getBody()); + } + + /** + * @covers WellRESTed\Message\Message::getBody + * @covers WellRESTed\Message\Message::withBody + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\HeaderCollection + */ + public function testGetBodyReturnsAttachedStream() + { + $stream = $this->prophesize("\\Psr\\Http\\Message\\StreamableInterface"); + $stream = $stream->reveal(); + + $message = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message = $message->withBody($stream); + $this->assertSame($stream, $message->getBody()); + } + + /** + * @covers WellRESTed\Message\Message::__clone + * @uses WellRESTed\Message\Message::__construct + * @uses WellRESTed\Message\Message::withHeader + * @uses WellRESTed\Message\Message::getHeader + * @uses WellRESTed\Message\HeaderCollection + */ + public function testCloneMakesDeepCopyOfHeaders() + { + $message1 = $this->getMockForAbstractClass("\\WellRESTed\\Message\\Message"); + $message1 = $message1->withHeader("Content-type", "text/plain"); + $message2 = $message1->withHeader("Content-type", "application/json"); + $this->assertEquals("text/plain", $message1->getHeader("Content-type")); + $this->assertEquals("application/json", $message2->getHeader("Content-type")); + } +}