From 5c197f8bb7ab2cfdd1a1716cff9cf79b617fc2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 26 Mar 2016 15:02:04 +0100 Subject: [PATCH 1/3] Explicitly depend on rize/uri-template to build URIs --- composer.json | 3 +- src/Client.php | 78 ++++++++++++++++++++++++-------------------- tests/ClientTest.php | 2 +- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/composer.json b/composer.json index 3f12402..642d5a8 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "react/event-loop": "~0.3.0|~0.4.0", "clue/buzz-react": "~0.4.0", "react/promise": "~2.0|~1.1", - "clue/json-stream": "~0.1.0" + "clue/json-stream": "~0.1.0", + "rize/uri-template": "^0.3" }, "require-dev": { "clue/tar-react": "~0.1.0", diff --git a/src/Client.php b/src/Client.php index e0288b8..0997f56 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,6 +8,7 @@ use React\Promise\PromiseInterface as Promise; use Clue\React\Docker\Io\StreamingParser; use React\Stream\ReadableStreamInterface; +use Rize\UriTemplate; /** * Docker Remote API client @@ -29,6 +30,7 @@ class Client private $browser; private $parser; private $streamingParser; + private $uri; /** * Instantiate new Client @@ -38,9 +40,10 @@ class Client * @param Browser $browser Browser instance to use, requires correct Sender and base URI * @param ResponseParser|null $parser (optional) ResponseParser instance to use * @param StreamingParser|null $streamingParser (optional) StreamingParser instance to use + * @param UriTemplate|null $uri (optional) UriTemplate instance to use * @see Factory::createClient() */ - public function __construct(Browser $browser, ResponseParser $parser = null, StreamingParser $streamingParser = null) + public function __construct(Browser $browser, ResponseParser $parser = null, StreamingParser $streamingParser = null, UriTemplate $uri = null) { if ($parser === null) { $parser = new ResponseParser(); @@ -50,9 +53,14 @@ public function __construct(Browser $browser, ResponseParser $parser = null, Str $streamingParser = new StreamingParser(); } + if ($uri === null) { + $uri = new UriTemplate(); + } + $this->browser = $browser; $this->parser = $parser; $this->streamingParser = $streamingParser; + $this->uri = $uri; } /** @@ -63,7 +71,7 @@ public function __construct(Browser $browser, ResponseParser $parser = null, Str */ public function ping() { - return $this->browser->get($this->browser->resolve('/_ping'))->then(array($this->parser, 'expectPlain')); + return $this->browser->get('/_ping')->then(array($this->parser, 'expectPlain')); } /** @@ -74,7 +82,7 @@ public function ping() */ public function info() { - return $this->browser->get($this->browser->resolve('/info'))->then(array($this->parser, 'expectJson')); + return $this->browser->get('/info')->then(array($this->parser, 'expectJson')); } /** @@ -85,7 +93,7 @@ public function info() */ public function version() { - return $this->browser->get($this->browser->resolve('/version'))->then(array($this->parser, 'expectJson')); + return $this->browser->get('/version')->then(array($this->parser, 'expectJson')); } /** @@ -99,7 +107,7 @@ public function version() public function containerList($all = false, $size = false) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/containers/json{?all,size}', array( 'all' => $this->boolArg($all), @@ -120,7 +128,7 @@ public function containerList($all = false, $size = false) public function containerCreate($config, $name = null) { return $this->postJson( - $this->browser->resolve( + $this->uri->expand( '/containers/create{?name}', array( 'name' => $name @@ -140,7 +148,7 @@ public function containerCreate($config, $name = null) public function containerInspect($container) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/json', array( 'container' => $container @@ -160,7 +168,7 @@ public function containerInspect($container) public function containerTop($container, $ps_args = null) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/top{?ps_args}', array( 'container' => $container, @@ -180,7 +188,7 @@ public function containerTop($container, $ps_args = null) public function containerChanges($container) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/changes', array( 'container' => $container @@ -212,7 +220,7 @@ public function containerChanges($container) public function containerExport($container) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/export', array( 'container' => $container @@ -247,7 +255,7 @@ public function containerExportStream($container) { return $this->streamingParser->parsePlainStream( $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/export', array( 'container' => $container @@ -269,7 +277,7 @@ public function containerExportStream($container) public function containerResize($container, $w, $h) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/resize{?w,h}', array( 'container' => $container, @@ -291,7 +299,7 @@ public function containerResize($container, $w, $h) public function containerStart($container, $config = array()) { return $this->postJson( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/start', array( 'container' => $container @@ -312,7 +320,7 @@ public function containerStart($container, $config = array()) public function containerStop($container, $t) { return $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/stop{?t}', array( 'container' => $container, @@ -333,7 +341,7 @@ public function containerStop($container, $t) public function containerRestart($container, $t) { return $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/restart{?t}', array( 'container' => $container, @@ -354,7 +362,7 @@ public function containerRestart($container, $t) public function containerKill($container, $signal = null) { return $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/kill{?signal}', array( 'container' => $container, @@ -374,7 +382,7 @@ public function containerKill($container, $signal = null) public function containerPause($container) { return $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/pause', array( 'container' => $container @@ -393,7 +401,7 @@ public function containerPause($container) public function containerUnpause($container) { return $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/unpause', array( 'container' => $container @@ -412,7 +420,7 @@ public function containerUnpause($container) public function containerWait($container) { return $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/wait', array( 'container' => $container @@ -433,7 +441,7 @@ public function containerWait($container) public function containerRemove($container, $v = false, $force = false) { return $this->browser->delete( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}{?v,force}', array( 'container' => $container, @@ -468,7 +476,7 @@ public function containerRemove($container, $v = false, $force = false) public function containerCopy($container, $config) { return $this->postJson( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/copy', array( 'container' => $container @@ -505,7 +513,7 @@ public function containerCopyStream($container, $config) { return $this->streamingParser->parsePlainStream( $this->postJson( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/copy', array( 'container' => $container @@ -527,7 +535,7 @@ public function containerCopyStream($container, $config) public function imageList($all = false) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/images/json{?all}', array( 'all' => $this->boolArg($all) @@ -598,7 +606,7 @@ public function imageCreateStream($fromImage = null, $fromSrc = null, $repo = nu { return $this->streamingParser->parseJsonStream( $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/images/create{?fromImage,fromSrc,repo,tag,registry}', array( 'fromImage' => $fromImage, @@ -623,7 +631,7 @@ public function imageCreateStream($fromImage = null, $fromSrc = null, $repo = nu public function imageInspect($image) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/images/{image}/json', array( 'image' => $image @@ -642,7 +650,7 @@ public function imageInspect($image) public function imageHistory($image) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/images/{image}/history', array( 'image' => $image @@ -708,10 +716,10 @@ public function imagePushStream($image, $tag = null, $registry = null, $registry { return $this->streamingParser->parseJsonStream( $this->browser->post( - $this->browser->resolve( - '/images{+registry}/{image}/push{?tag}', + $this->uri->expand( + '/images{/registry}/{image}/push{?tag}', array( - 'registry' => ($registry === null ? '' : ('/' . $registry)), + 'registry' => $registry, 'image' => $image, 'tag' => $tag ) @@ -734,7 +742,7 @@ public function imagePushStream($image, $tag = null, $registry = null, $registry public function imageTag($image, $repo, $tag = null, $force = false) { return $this->browser->post( - $this->browser->resolve( + $this->uri->expand( '/images/{image}/tag{?repo,tag,force}', array( 'image' => $image, @@ -758,7 +766,7 @@ public function imageTag($image, $repo, $tag = null, $force = false) public function imageRemove($image, $force = false, $noprune = false) { return $this->browser->delete( - $this->browser->resolve( + $this->uri->expand( '/images/{image}{?force,noprune}', array( 'image' => $image, @@ -779,7 +787,7 @@ public function imageRemove($image, $force = false, $noprune = false) public function imageSearch($term) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/images/search{?term}', array( 'term' => $term @@ -799,7 +807,7 @@ public function imageSearch($term) public function execCreate($container, $config) { return $this->postJson( - $this->browser->resolve( + $this->uri->expand( '/containers/{container}/exec', array( 'container' => $container @@ -823,7 +831,7 @@ public function execCreate($container, $config) public function execStart($exec, $config) { return $this->postJson( - $this->browser->resolve( + $this->uri->expand( '/exec/{exec}/start', array( 'exec' => $exec @@ -847,7 +855,7 @@ public function execStart($exec, $config) public function execResize($exec, $w, $h) { return $this->browser->get( - $this->browser->resolve( + $this->uri->expand( '/exec/{exec}/resize{?w,h}', array( 'exec' => $exec, diff --git a/tests/ClientTest.php b/tests/ClientTest.php index e389df6..67150f2 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -293,7 +293,7 @@ public function testImagePushCustomRegistry() $json = array(); $stream = $this->getMock('React\Stream\ReadableStreamInterface'); - $this->expectRequest('post', '/images/demo.acme.com:5000/123/push?tag=test', $this->createResponseJsonStream($json)); + $this->expectRequest('post', '/images/demo.acme.com%3A5000/123/push?tag=test', $this->createResponseJsonStream($json)); $this->streamingParser->expects($this->once())->method('parseJsonStream')->will($this->returnValue($stream)); $this->streamingParser->expects($this->once())->method('deferredStream')->with($this->equalTo($stream), $this->equalTo('progress'))->will($this->returnPromise($json)); From 792faffc8cd1396a281a4e6302a7a062302c7bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 19 Apr 2016 17:16:58 +0200 Subject: [PATCH 2/3] Update dependencies for more reliable streaming APIs Update to clue/buzz-react:0.5 to support PSR-7 messages and take advantange of its new streaming API. Use clue/promise-stream-react for more reliable promise unwrapping, better error reporting and advanced back pressure support --- composer.json | 5 ++-- examples/export.php | 1 + src/Client.php | 14 +++++++----- src/Io/ResponseParser.php | 8 +++---- src/Io/StreamingParser.php | 39 ++++---------------------------- tests/ClientTest.php | 13 ++++++----- tests/Io/ResponseParserTest.php | 5 ++-- tests/Io/StreamingParserTest.php | 6 ++--- 8 files changed, 33 insertions(+), 58 deletions(-) diff --git a/composer.json b/composer.json index 642d5a8..a1f5940 100644 --- a/composer.json +++ b/composer.json @@ -16,10 +16,11 @@ "require": { "php": ">=5.3", "react/event-loop": "~0.3.0|~0.4.0", - "clue/buzz-react": "~0.4.0", + "clue/buzz-react": "^0.5", "react/promise": "~2.0|~1.1", "clue/json-stream": "~0.1.0", - "rize/uri-template": "^0.3" + "rize/uri-template": "^0.3", + "clue/promise-stream-react": "^0.1" }, "require-dev": { "clue/tar-react": "~0.1.0", diff --git a/examples/export.php b/examples/export.php index 2abce0a..d69a6ec 100644 --- a/examples/export.php +++ b/examples/export.php @@ -25,6 +25,7 @@ }); $out = new Stream(fopen($target, 'w'), $loop); +$out->pause(); $stream->pipe($out); $loop->run(); diff --git a/src/Client.php b/src/Client.php index 0997f56..40aebe3 100644 --- a/src/Client.php +++ b/src/Client.php @@ -3,7 +3,6 @@ namespace Clue\React\Docker; use Clue\React\Buzz\Browser; -use Clue\React\Buzz\Message\Response; use Clue\React\Docker\Io\ResponseParser; use React\Promise\PromiseInterface as Promise; use Clue\React\Docker\Io\StreamingParser; @@ -254,7 +253,7 @@ public function containerExport($container) public function containerExportStream($container) { return $this->streamingParser->parsePlainStream( - $this->browser->get( + $this->browser->withOptions(array('streaming' => true))->get( $this->uri->expand( '/containers/{container}/export', array( @@ -512,14 +511,17 @@ public function containerCopy($container, $config) public function containerCopyStream($container, $config) { return $this->streamingParser->parsePlainStream( - $this->postJson( + $this->browser->withOptions(array('streaming' => true))->post( $this->uri->expand( '/containers/{container}/copy', array( 'container' => $container ) ), - $config + array( + 'Content-Type' => 'application/json' + ), + $this->json($config) ) ); } @@ -605,7 +607,7 @@ public function imageCreate($fromImage = null, $fromSrc = null, $repo = null, $t public function imageCreateStream($fromImage = null, $fromSrc = null, $repo = null, $tag = null, $registry = null, $registryAuth = null) { return $this->streamingParser->parseJsonStream( - $this->browser->post( + $this->browser->withOptions(array('streaming' => true))->post( $this->uri->expand( '/images/create{?fromImage,fromSrc,repo,tag,registry}', array( @@ -715,7 +717,7 @@ public function imagePush($image, $tag = null, $registry = null, $registryAuth = public function imagePushStream($image, $tag = null, $registry = null, $registryAuth = null) { return $this->streamingParser->parseJsonStream( - $this->browser->post( + $this->browser->withOptions(array('streaming' => true))->post( $this->uri->expand( '/images{/registry}/{image}/push{?tag}', array( diff --git a/src/Io/ResponseParser.php b/src/Io/ResponseParser.php index 529ce2b..7c3039e 100644 --- a/src/Io/ResponseParser.php +++ b/src/Io/ResponseParser.php @@ -2,25 +2,25 @@ namespace Clue\React\Docker\Io; -use Clue\React\Buzz\Message\Response; +use Psr\Http\Message\ResponseInterface; class ResponseParser { - public function expectPlain(Response $response) + public function expectPlain(ResponseInterface $response) { // text/plain return (string)$response->getBody(); } - public function expectJson(Response $response) + public function expectJson(ResponseInterface $response) { // application/json return json_decode((string)$response->getBody(), true); } - public function expectEmpty(Response $response) + public function expectEmpty(ResponseInterface $response) { // 204 No Content // no content-type diff --git a/src/Io/StreamingParser.php b/src/Io/StreamingParser.php index c375c71..99e6635 100644 --- a/src/Io/StreamingParser.php +++ b/src/Io/StreamingParser.php @@ -9,6 +9,8 @@ use React\Stream\ReadableStreamInterface; use RuntimeException; use React\Promise\CancellablePromiseInterface; +use Clue\React\Promise\Stream; +use Psr\Http\Message\ResponseInterface; class StreamingParser { @@ -58,40 +60,9 @@ public function parsePlainStream(PromiseInterface $promise) { // text/plain - $out = new ReadableStream(); - - // try to cancel promise once the stream closes - if ($promise instanceof CancellablePromiseInterface) { - $out->on('close', function() use ($promise) { - $promise->cancel(); - }); - } - - $promise->then( - function ($response) use ($out) { - $out->close(); - }, - function ($error) use ($out) { - $out->emit('error', array($error, $out)); - $out->close(); - }, - function ($progress) use ($out) { - if (is_array($progress) && isset($progress['responseStream'])) { - $stream = $progress['responseStream']; - /* @var $stream React\Stream\Stream */ - - // hack to do not buffer stream contents in body - $stream->removeAllListeners('data'); - - // got a streaming HTTP response => forward each data chunk to the resulting output stream - $stream->on('data', function ($data) use ($out) { - $out->emit('data', array($data, $out)); - }); - } - } - ); - - return $out; + return Stream\unwrapReadable($promise->then(function (ResponseInterface $response) { + return $response->getBody(); + })); } public function deferredStream(ReadableStreamInterface $stream, $progressEventName) diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 67150f2..192e029 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -1,10 +1,11 @@ expectPromiseResolveWith('', $this->client->execResize(123, 800, 600)); } - private function expectRequestFlow($method, $url, Response $response, $parser) + private function expectRequestFlow($method, $url, ResponseInterface $response, $parser) { $return = (string)$response->getBody(); if ($parser === 'expectJson') { @@ -372,10 +373,10 @@ private function expectRequestFlow($method, $url, Response $response, $parser) $this->parser->expects($this->once())->method($parser)->with($this->equalTo($response))->will($this->returnValue($return)); } - private function expectRequest($method, $url, Response $response) + private function expectRequest($method, $url, ResponseInterface $response) { $that = $this; - $this->sender->expects($this->once())->method('send')->with($this->callback(function ($request) use ($that, $method, $url) { + $this->sender->expects($this->once())->method('send')->with($this->callback(function (RequestInterface $request) use ($that, $method, $url) { $that->assertEquals(strtoupper($method), $request->getMethod()); $that->assertEquals('http://x' . $url, (string)$request->getUri()); @@ -385,7 +386,7 @@ private function expectRequest($method, $url, Response $response) private function createResponse($body = '') { - return new Response('HTTP/1.0', 200, 'OK', array(), new Body($body)); + return new Response(200, array(), $body); } private function createResponseJson($json) diff --git a/tests/Io/ResponseParserTest.php b/tests/Io/ResponseParserTest.php index ca745a3..a366ab4 100644 --- a/tests/Io/ResponseParserTest.php +++ b/tests/Io/ResponseParserTest.php @@ -1,8 +1,7 @@ assertFalse($stream->isReadable()); } - public function testJsonResolvingPromiseWillEmitCloseEvent() + public function testJsonResolvingPromiseWithWrongValueWillEmitErrorAndCloseEvent() { $deferred = new Deferred(); @@ -49,10 +49,10 @@ public function testJsonResolvingPromiseWillEmitCloseEvent() $this->assertTrue($stream->isReadable()); - $stream->on('error', $this->expectCallableNever()); + $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); - $deferred->resolve('data'); + $deferred->resolve('not a stream'); $this->assertFalse($stream->isReadable()); } From e5d9e3840059a5a83e0870cd6e8fd02b9c31b94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 19 Apr 2016 18:07:55 +0200 Subject: [PATCH 3/3] First class support for PHP 5.3 through PHP 7 and HHVM --- .travis.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 57bbe78..3464291 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,17 @@ language: php + php: - 5.3 + - 5.4 + - 5.5 - 5.6 + - 7 - hhvm -matrix: - allow_failures: - - php: 5.3 # works locally? + +sudo: false + install: - - composer install --prefer-source --no-interaction + - composer install --no-interaction + script: - phpunit --coverage-text