diff --git a/README.md b/README.md index 28cad06d..57853793 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Features - Subject/Observer signal slot system for unobtrusively modifying request behavior - Supports all of the features of libcurl including authentication, redirects, SSL, proxies, etc - Web service client framework for building future-proof interfaces to web services +- Full support for [URI templates](http://tools.ietf.org/html/draft-gregorio-uritemplate-08) HTTP basics ----------- @@ -118,6 +119,44 @@ try { } ``` +URI templates +------------- + +Guzzle supports the entire URI templates RFC. + +```php + '/path/to', + 'a' => 'hi', + 'data' => array( + 'foo' => 'bar', + 'mesa' => 'jarjar' + ) +)); + +$request = $client->get('http://www.test.com{+path}{?a,data*}'); +``` + +The generated request URL would become: ``http://www.test.com/path/to?a=hi&foo=bar&mesa=jarajar`` + +You can specify URI templates and an array of additional template variables to use when creating requests: + +```php + 'hi' +)); + +$request = $client->get(array('/{?a,b}', array( + 'b' => 'there' +)); +``` + +The resulting URL would become ``http://test.com?a=hi&b=there`` + Testing Guzzle -------------- diff --git a/src/Guzzle/Guzzle.php b/src/Guzzle/Guzzle.php index 14187d53..6924a551 100644 --- a/src/Guzzle/Guzzle.php +++ b/src/Guzzle/Guzzle.php @@ -91,12 +91,7 @@ class Guzzle */ public static function inject($input, Collection $config) { - // Skip expensive regular expressions if it isn't needed - if (strpos($input, '{{') === false) { - return $input; - } - - return preg_replace_callback('/{{\s*([A-Za-z_\-\.0-9]+)\s*}}/', function($matches) use ($config) { + return preg_replace_callback('/{{1,2}\s*([A-Za-z_\-\.0-9]+)\s*}{1,2}/', function($matches) use ($config) { return $config->get(trim($matches[1])); }, $input ); diff --git a/src/Guzzle/Http/Client.php b/src/Guzzle/Http/Client.php index 1254458f..ddf9e809 100644 --- a/src/Guzzle/Http/Client.php +++ b/src/Guzzle/Http/Client.php @@ -7,6 +7,7 @@ use Guzzle\Common\AbstractHasDispatcher; use Guzzle\Common\ExceptionCollection; use Guzzle\Common\Collection; use Guzzle\Http\Url; +use Guzzle\Http\UriTemplate; use Guzzle\Http\EntityBody; use Guzzle\Http\Message\RequestInterface; use Guzzle\Http\Message\RequestFactory; @@ -39,6 +40,11 @@ class Client extends AbstractHasDispatcher implements ClientInterface */ private $curlMulti; + /** + * @var UriTemplate URI template owned by the client + */ + private $uriTemplate; + /** * {@inheritdoc} */ @@ -107,41 +113,91 @@ class Client extends AbstractHasDispatcher implements ClientInterface } /** - * Inject configuration values into a formatted string with {{param}} as a - * parameter delimiter (replace param with the configuration value name) + * Expand a URI template using client configuration data * - * @param string $string String to inject config values into + * @param string $template URI template to expand + * @param array $variables (optional) Additional variables to use in the expansion * * @return string */ - public final function inject($string) + public function expandTemplate($template, array $variables = null) { - return Guzzle::inject($string, $this->getConfig()); + $expansionVars = $this->getConfig()->getAll(); + if ($variables) { + $expansionVars = array_merge($expansionVars, $variables); + } + + return $this->getUriTemplate() + ->setTemplate($template) + ->expand($expansionVars); + } + + /** + * Set the URI template expander to use with the client + * + * @param UriTemplate $uriTemplate + * + * @return Client + */ + public function setUriTemplate(UriTemplate $uriTemplate) + { + $this->uriTemplate = $uriTemplate; + + return $this; + } + + /** + * Get the URI template expander used by the client. A default UriTemplate + * object will be created if one does not exist. + * + * @return UriTemplate + */ + public function getUriTemplate() + { + if (!$this->uriTemplate) { + $this->uriTemplate = new UriTemplate(); + } + + return $this->uriTemplate; } /** * Create and return a new {@see RequestInterface} configured for the client * * @param string $method (optional) HTTP method. Defaults to GET - * @param string $uri (optional) Resource URI. Use an absolute path to - * override the base path of the client, or a relative path to append - * to the base path of the client. The URI can contain the - * querystring as well. + * @param string|array $uri (optional) Resource URI. Use an absolute path + * to override the base path of the client, or a relative path to + * append to the base path of the client. The URI can contain the + * querystring as well. Use an array to provide a URI template and + * additional variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param string|resource|array|EntityBody $body (optional) Entity body of * request (POST/PUT) or response (GET) * * @return RequestInterface + * @throws InvalidArgumentException if a URI array is passed that does not + * contain exactly two elements: the URI followed by template variables */ public function createRequest($method = RequestInterface::GET, $uri = null, $headers = null, $body = null) { + if (!is_array($uri)) { + $templateVars = null; + } else { + if (count($uri) != 2 || !is_array($uri[1])) { + throw new \InvalidArgumentException('You must provide a URI' + . ' template followed by an array of template variables' + . ' when using an array for a URI template'); + } + list($uri, $templateVars) = $uri; + } + if (!$uri) { $url = $this->getBaseUrl(); } else if (strpos($uri, 'http') === 0) { // Use absolute URLs as-is - $url = $this->inject($uri); + $url = $this->expandTemplate($uri, $templateVars); } else { - $url = Url::factory($this->getBaseUrl())->combine($this->inject($uri)); + $url = Url::factory($this->getBaseUrl())->combine($this->expandTemplate($uri, $templateVars)); } return $this->prepareRequest( @@ -193,20 +249,20 @@ class Client extends AbstractHasDispatcher implements ClientInterface } /** - * Get the base service endpoint URL with configuration options injected - * into the configuration setting. + * Get the client's base URL as either an expanded or raw URI template * - * @param bool $inject (optional) Set to FALSE to get the raw base URL + * @param bool $expand (optional) Set to FALSE to get the raw base URL + * without URI template expansion * * @return string|null */ - public function getBaseUrl($inject = true) + public function getBaseUrl($expand = true) { - return $inject ? $this->inject($this->baseUrl) : $this->baseUrl; + return $expand ? $this->expandTemplate($this->baseUrl) : $this->baseUrl; } /** - * Set the base service endpoint URL + * Set the base URL of the client * * @param string $url The base service endpoint URL of the webservice * @@ -242,24 +298,28 @@ class Client extends AbstractHasDispatcher implements ClientInterface /** * Create a GET request for the client * - * @param string $path (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param string|resource|array|EntityBody $body (optional) Where to store * the response entity body * * @return Request */ - public final function get($path = null, $headers = null, $body = null) + public final function get($uri = null, $headers = null, $body = null) { - return $this->createRequest('GET', $path, $headers, $body); + return $this->createRequest('GET', $uri, $headers, $body); } /** * Create a HEAD request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * * @return Request @@ -272,8 +332,10 @@ class Client extends AbstractHasDispatcher implements ClientInterface /** * Create a DELETE request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * * @return Request @@ -286,8 +348,10 @@ class Client extends AbstractHasDispatcher implements ClientInterface /** * Create a PUT request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param string|resource|array|EntityBody $body Body to send in the request * @@ -301,8 +365,10 @@ class Client extends AbstractHasDispatcher implements ClientInterface /** * Create a POST request for the client * - * @param string $uri (optional) Resource URI of the request. Use an absolute path to - * override the base path, or a relative path to append it. + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param array|Collection|string|EntityBody $postBody (optional) POST * body. Can be a string, EntityBody, or associative array of POST @@ -319,8 +385,10 @@ class Client extends AbstractHasDispatcher implements ClientInterface /** * Create an OPTIONS request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * * @return Request */ diff --git a/src/Guzzle/Http/ClientInterface.php b/src/Guzzle/Http/ClientInterface.php index c6bdfe6d..8505e98c 100644 --- a/src/Guzzle/Http/ClientInterface.php +++ b/src/Guzzle/Http/ClientInterface.php @@ -36,23 +36,39 @@ interface ClientInterface extends HasDispatcherInterface function getConfig($key = false); /** - * Inject configuration values into a formatted string with {{param}} as a - * parameter delimiter (replace param with the configuration value name) + * Set the URI template expander to use with the client * - * @param string $string String to inject config values into + * @param UriTemplate $uriTemplate + * + * @return ClientInterface + */ + function setUriTemplate(UriTemplate $uriTemplate); + + /** + * Get the URI template expander used by the client + * + * @return UriTemplate + */ + function getUriTemplate(); + + /** + * Expand a URI template using client configuration data + * + * @param string $template URI template to expand + * @param array $variables (optional) Additional variables to use in the expansion * * @return string */ - function inject($string); + function expandTemplate($template, array $variables = null); /** * Create and return a new {@see RequestInterface} configured for the client * * @param string $method (optional) HTTP method. Defaults to GET - * @param string $uri (optional) Resource URI. Use an absolute path to - * override the base path of the client, or a relative path to append - * to the base path of the client. The URI can contain the - * querystring as well. + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param string|resource|array|EntityBody $body (optional) Entity body of * request (POST/PUT) or response (GET) @@ -76,18 +92,17 @@ interface ClientInterface extends HasDispatcherInterface function prepareRequest(RequestInterface $request); /** - * Get the base service endpoint URL with configuration options injected - * into the configuration setting. + * Get the client's base URL as either an expanded or raw URI template * - * @param bool $inject (optional) Set to FALSE to get the raw base URL + * @param bool $expand (optional) Set to FALSE to get the raw base URL + * without URI template expansion * * @return string - * @throws RuntimeException if a base URL has not been set */ - function getBaseUrl($inject = true); + function getBaseUrl($expand = true); /** - * Set the base service endpoint URL + * Set the base URL of the client * * @param string $url The base service endpoint URL of the webservice * @@ -110,8 +125,10 @@ interface ClientInterface extends HasDispatcherInterface /** * Create a GET request for the client * - * @param string $path (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param string|resource|array|EntityBody $body (optional) Where to store * the response entity body @@ -123,8 +140,10 @@ interface ClientInterface extends HasDispatcherInterface /** * Create a HEAD request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * * @return RequestInterface @@ -134,8 +153,10 @@ interface ClientInterface extends HasDispatcherInterface /** * Create a DELETE request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * * @return RequestInterface @@ -145,8 +166,10 @@ interface ClientInterface extends HasDispatcherInterface /** * Create a PUT request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or a relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param string|resource|array|EntityBody $body Body to send in the request * @@ -157,8 +180,10 @@ interface ClientInterface extends HasDispatcherInterface /** * Create a POST request for the client * - * @param string $uri (optional) Resource URI of the request. Use an absolute path to - * override the base path, or a relative path to append it. + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * @param array|Collection $headers (optional) HTTP headers * @param array|Collection|string|EntityBody $postBody (optional) POST * body. Can be a string, EntityBody, or associative array of POST @@ -172,8 +197,10 @@ interface ClientInterface extends HasDispatcherInterface /** * Create an OPTIONS request for the client * - * @param string $uri (optional) Resource URI of the request. Use an - * absolute path to override the base path, or relative path to append + * @param string|array $uri (optional) Resource URI of the request. Use an + * absolute path to override the base path, or a relative path to + * append. Use an array to provide a URI template and additional + * variables to use in the URI template expansion. * * @return RequestInterface */ diff --git a/src/Guzzle/Http/UriTemplate.php b/src/Guzzle/Http/UriTemplate.php new file mode 100644 index 00000000..44748201 --- /dev/null +++ b/src/Guzzle/Http/UriTemplate.php @@ -0,0 +1,252 @@ +template = $template; + } + + /** + * Get the URI template + * + * @return string + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Set the URI template + * + * @param string $template URI template to expand + * + * @return UriTemplate + */ + public function setTemplate($template) + { + $this->template = $template; + + return $this; + } + + /** + * Expand the URI template using the supplied variables + * + * @param array $variables Variables to use with the expansion + * + * @return string Returns the expanded template + */ + public function expand(array $variables) + { + $this->variables = $variables; + + return preg_replace_callback($this->regex, array($this, 'expandMatch'), $this->template); + } + + /** + * Set the regular expression used to identify URI templates + * + * @param string $regex Regular expression + * + * @return UriTemplate + */ + public function setRegex($regex) + { + $this->regex = $regex; + + return $this; + } + + /** + * Parse an expression into parts + * + * @param string $expression Expression to parse + * + * @return array Returns an associative array of parts + */ + private function parseExpression($expression) + { + // Check for URI operators + $operator = ''; + if (in_array($expression[0], self::$operators)) { + $operator = $expression[0]; + $expression = substr($expression, 1); + } + + return array( + 'operator' => $operator, + 'values' => array_map(function($value) { + $value = trim($value); + $varspec = array(); + $substrPos = strpos($value, ':'); + if ($substrPos) { + $varspec['value'] = substr($value, 0, $substrPos); + $varspec['modifier'] = ':'; + $varspec['position'] = (int) substr($value, $substrPos + 1); + } else if (substr($value, -1) == '*') { + $varspec['modifier'] = '*'; + $varspec['value'] = substr($value, 0, -1); + } else { + $varspec['value'] = (string) $value; + $varspec['modifier'] = ''; + } + return $varspec; + }, explode(',', $expression)) + ); + } + + /** + * Process an expansion + * + * @param array $matches Matches met in the preg_replace_callback + * + * @return string Returns the replacement string + */ + private function expandMatch(array $matches) + { + $parsed = self::parseExpression($matches[1]); + $replacements = array(); + + $prefix = $parsed['operator']; + $joiner = $parsed['operator']; + $useQueryString = false; + if ($parsed['operator'] == '?') { + $joiner = '&'; + $useQueryString = true; + } else if ($parsed['operator'] == '&') { + $useQueryString = true; + } else if ($parsed['operator'] == '#') { + $joiner = ','; + } else if ($parsed['operator'] == ';') { + $useQueryString = true; + } else if ($parsed['operator'] == '' || $parsed['operator'] == '+') { + $joiner = ','; + $prefix = ''; + } + + foreach ($parsed['values'] as $value) { + + if (!array_key_exists($value['value'], $this->variables)) { + continue; + } + + $variable = $this->variables[$value['value']]; + $actuallyUseQueryString = $useQueryString; + $expanded = ''; + + if (is_array($variable)) { + + $isAssoc = $this->isAssoc($variable); + $kvp = array(); + foreach ($variable as $key => $var) { + if ($isAssoc) { + $key = rawurlencode($key); + } + $var = rawurlencode($var); + if ($parsed['operator'] == '+' || $parsed['operator'] == '#') { + $var = $this->decodeReserved($var); + } + + if ($value['modifier'] == '*') { + if ($isAssoc) { + $var = $key . '=' . $var; + } else if ($key > 0 && $actuallyUseQueryString) { + $var = $value['value'] . '=' . $var; + } + } + + $kvp[$key] = $var; + } + + if ($value['modifier'] == '*') { + $expanded = implode($joiner, $kvp); + if ($isAssoc) { + // Don't prepend the value name when using the explode + // modifier with an associative array + $actuallyUseQueryString = false; + } + } else { + if ($isAssoc) { + // When an associative array is encountered and the + // explode modifier is not set, then the result must + // be a comma separated list of keys followed by their + // respective values. + foreach ($kvp as $k => &$v) { + $v = $k . ',' . $v; + } + } + $expanded = implode(',', $kvp); + } + + } else { + if ($value['modifier'] == ':') { + $variable = substr($variable, 0, $value['position']); + } + $expanded = rawurlencode($variable); + if ($parsed['operator'] == '+' || $parsed['operator'] == '#') { + $expanded = $this->decodeReserved($expanded); + } + } + + if ($actuallyUseQueryString) { + if (!$expanded && $joiner != '&') { + $expanded = $value['value']; + } else { + $expanded = $value['value'] . '=' . $expanded; + } + } + + $replacements[] = $expanded; + } + + $ret = implode($joiner, $replacements); + if ($ret && $prefix) { + return $prefix . $ret; + } + + return $ret; + } + + /** + * Determines if an array is associative + * + * @param array $array Array to check + * + * @return bool + */ + private function isAssoc(array $array) + { + return (bool) count(array_filter(array_keys($array), 'is_string')); + } + + /** + * Removes percent encoding on reserved characters (used with + and # modifiers) + * + * @param string $string String to fix + * + * @return string + */ + private function decodeReserved($string) + { + return str_replace(self::$delimsPct, self::$delims, $string); + } +} \ No newline at end of file diff --git a/src/Guzzle/Service/Command/DynamicCommand.php b/src/Guzzle/Service/Command/DynamicCommand.php index 620e4dcb..3b0fd398 100644 --- a/src/Guzzle/Service/Command/DynamicCommand.php +++ b/src/Guzzle/Service/Command/DynamicCommand.php @@ -5,6 +5,7 @@ namespace Guzzle\Service\Command; use Guzzle\Guzzle; use Guzzle\Http\EntityBody; use Guzzle\Http\Url; +use Guzzle\Http\UriTemplate; use Guzzle\Service\Inspector; /** @@ -27,28 +28,25 @@ class DynamicCommand extends AbstractCommand */ protected function build() { - // Get the path values and use the client config settings - $pathValues = $this->getClient()->getConfig(); - $foundPath = false; - foreach ($this->apiCommand->getParams() as $name => $arg) { - if ($arg->get('location') == 'path') { - $pathValues->set($name, $arg->get('prepend') . $this->get($name) . $arg->get('append')); - $foundPath = true; - } - } - - // Build a custom URL if there are path values - if ($foundPath) { - $path = str_replace('//', '', Guzzle::inject($this->apiCommand->getPath(), $pathValues)); - } else { - $path = $this->apiCommand->getPath(); - } - - if (!$path) { + if (!$this->apiCommand->getUri()) { $url = $this->getClient()->getBaseUrl(); } else { + + // Get the path values and use the client config settings + $variables = $this->getClient()->getConfig()->getAll(); + foreach ($this->apiCommand->getParams() as $name => $arg) { + if (is_scalar($this->get($name))) { + $variables[$name] = $arg->get('prepend') . $this->get($name) . $arg->get('append'); + } + } + + // Expand the URI template using the URI values + $template = new UriTemplate($this->apiCommand->getUri()); + $uri = $template->expand($variables); + + // Merge the client's base URL with the URI template $url = Url::factory($this->getClient()->getBaseUrl()); - $url->combine($path); + $url->combine($uri); $url = (string) $url; } @@ -61,11 +59,7 @@ class DynamicCommand extends AbstractCommand if ($this->get($name)) { // Check that a location is set - $location = $arg->get('location') ?: 'query'; - - if ($location == 'path' || $location == 'data') { - continue; - } + $location = $arg->get('location'); if ($location) { diff --git a/src/Guzzle/Service/Description/ApiCommand.php b/src/Guzzle/Service/Description/ApiCommand.php index 5228767e..f472c787 100644 --- a/src/Guzzle/Service/Description/ApiCommand.php +++ b/src/Guzzle/Service/Description/ApiCommand.php @@ -26,7 +26,7 @@ class ApiCommand * string name Name of the command * string doc Method documentation * string method HTTP method of the command - * string path (optional) Path routing information of the command to include in the path + * string uri (optional) URI routing information of the command * string class (optional) Concrete class that implements this command * array params Associative array of parameters for the command with each * parameter containing the following keys: @@ -51,7 +51,12 @@ class ApiCommand $this->config['name'] = isset($config['name']) ? trim($config['name']) : ''; $this->config['doc'] = isset($config['doc']) ? trim($config['doc']) : ''; $this->config['method'] = isset($config['method']) ? trim($config['method']) : ''; - $this->config['path'] = isset($config['path']) ? trim($config['path']) : ''; + $this->config['uri'] = isset($config['uri']) ? trim($config['uri']) : ''; + if (!$this->config['uri']) { + // Add backwards compatibility with the path attribute + $this->config['uri'] = isset($config['path']) ? trim($config['path']) : ''; + } + $this->config['class'] = isset($config['class']) ? trim($config['class']) : 'Guzzle\\Service\\Command\\DynamicCommand'; if (isset($config['params']) && is_array($config['params'])) { @@ -136,13 +141,12 @@ class ApiCommand } /** - * Get the path routing information to append to the path of the generated - * request + * Get the URI that will be merged into the generated request * * @return string */ - public function getPath() + public function getUri() { - return $this->config['path']; + return $this->config['uri']; } } \ No newline at end of file diff --git a/src/Guzzle/Service/Inspector.php b/src/Guzzle/Service/Inspector.php index 4a933e6f..95380e27 100644 --- a/src/Guzzle/Service/Inspector.php +++ b/src/Guzzle/Service/Inspector.php @@ -279,7 +279,7 @@ class Inspector } // Inject configuration information into the config value - if (is_scalar($config->get($name)) && strpos($config->get($name), '{{') !== false) { + if (is_scalar($config->get($name)) && strpos($config->get($name), '{') !== false) { $config->set($name, Guzzle::inject($config->get($name), $config)); } diff --git a/src/Guzzle/Service/ServiceBuilder.php b/src/Guzzle/Service/ServiceBuilder.php index b8da50fe..c5c287a3 100644 --- a/src/Guzzle/Service/ServiceBuilder.php +++ b/src/Guzzle/Service/ServiceBuilder.php @@ -133,8 +133,8 @@ class ServiceBuilder implements \ArrayAccess // Convert references to the actual client foreach ($this->builderConfig[$name]['params'] as $k => &$v) { - if (0 === strpos($v, '{{') && strlen($v) - 2 == strpos($v, '}}')) { - $v = $this->get(trim(substr($v, 2, -2))); + if (0 === strpos($v, '{') && strlen($v) - 1 == strrpos($v, '}')) { + $v = $this->get(trim(str_replace(array('{', '}'), '', $v))); } } diff --git a/tests/Guzzle/Tests/GuzzleTest.php b/tests/Guzzle/Tests/GuzzleTest.php index 813a5edd..0545be15 100644 --- a/tests/Guzzle/Tests/GuzzleTest.php +++ b/tests/Guzzle/Tests/GuzzleTest.php @@ -50,6 +50,7 @@ class GuzzleTest extends GuzzleTestCase 'abc' => 'this' )), array('_is_a_', '{{ abc }}_is_{{ not_found }}a_{{ 0 }}', array()), + array('_is_a_', '{abc}_is_{not_found}a_{{0}}', array()), ); } diff --git a/tests/Guzzle/Tests/Http/ClientTest.php b/tests/Guzzle/Tests/Http/ClientTest.php index 1227a82f..541a702b 100644 --- a/tests/Guzzle/Tests/Http/ClientTest.php +++ b/tests/Guzzle/Tests/Http/ClientTest.php @@ -5,6 +5,7 @@ namespace Guzzle\Tests\Http; use Guzzle\Guzzle; use Guzzle\Common\Collection; use Guzzle\Common\Log\ClosureLogAdapter; +use Guzzle\Http\UriTemplate; use Guzzle\Http\Message\Response; use Guzzle\Http\Message\RequestFactory; use Guzzle\Http\Plugin\ExponentialBackoffPlugin; @@ -101,7 +102,7 @@ class ClientTest extends \Guzzle\Tests\GuzzleTestCase /** * @covers Guzzle\Http\Client */ - public function testInjectConfig() + public function testExpandsUriTemplatesUsingConfig() { $client = new Client('http://www.google.com/'); $client->setConfig(array( @@ -109,7 +110,7 @@ class ClientTest extends \Guzzle\Tests\GuzzleTestCase 'key' => 'value', 'foo' => 'bar' )); - $this->assertEquals('Testing...api/v1/key/value', $client->inject('Testing...api/{{api}}/key/{{key}}')); + $this->assertEquals('Testing...api/v1/key/value', $client->expandTemplate('Testing...api/{api}/key/{{key}}')); // Make sure that the client properly validates and injects config $this->assertEquals('bar', $client->getConfig('foo')); @@ -429,4 +430,114 @@ class ClientTest extends \Guzzle\Tests\GuzzleTestCase $client->getEventDispatcher()->addSubscriber($mock); $client->send(array($client->get(), $client->head())); } + + /** + * @covers Guzzle\Http\Client + */ + public function testQueryStringsAreNotDoubleEncoded() + { + $client = new Client('http://test.com', array( + 'path' => array('foo', 'bar'), + 'query' => 'hi there', + 'data' => array( + 'test' => 'a&b' + ) + )); + + $request = $client->get('{/path*}{?query,data*}'); + $this->assertEquals('http://test.com/foo/bar?query=hi%20there&test=a%26b', $request->getUrl()); + $this->assertEquals('hi there', $request->getQuery()->get('query')); + $this->assertEquals('a&b', $request->getQuery()->get('test')); + } + + /** + * @covers Guzzle\Http\Client + */ + public function testQueryStringsAreNotDoubleEncodedUsingAbsolutePaths() + { + $client = new Client('http://test.com', array( + 'path' => array('foo', 'bar'), + 'query' => 'hi there', + )); + $request = $client->get('http://test.com{?query}'); + $this->assertEquals('http://test.com/?query=hi%20there', $request->getUrl()); + $this->assertEquals('hi there', $request->getQuery()->get('query')); + } + + /** + * @covers Guzzle\Http\Client::setUriTemplate + * @covers Guzzle\Http\Client::getUriTemplate + */ + public function testAllowsUriTemplateInjection() + { + $client = new Client('http://test.com', array( + 'path' => array('foo', 'bar'), + 'query' => 'hi there', + )); + + $a = $client->getUriTemplate(); + $this->assertSame($a, $client->getUriTemplate()); + $client->setUriTemplate(new UriTemplate()); + $this->assertNotSame($a, $client->getUriTemplate()); + } + + /** + * @covers Guzzle\Http\Client::expandTemplate + */ + public function testAllowsCustomVariablesWhenExpandingTemplates() + { + $client = new Client('http://test.com', array( + 'test' => 'hi', + )); + + $uri = $client->expandTemplate('http://{test}{?query*}', array( + 'query' => array( + 'han' => 'solo' + ) + )); + + $this->assertEquals('http://hi?han=solo', $uri); + } + + /** + * @covers Guzzle\Http\Client::createRequest + * @expectedException InvalidArgumentException + */ + public function testUriArrayMustContainExactlyTwoElements() + { + $client = new Client(); + $client->createRequest('GET', array('haha!')); + } + + /** + * @covers Guzzle\Http\Client::createRequest + * @expectedException InvalidArgumentException + */ + public function testUriArrayMustContainAnArray() + { + $client = new Client(); + $client->createRequest('GET', array('haha!', 'test')); + } + + /** + * @covers Guzzle\Http\Client::createRequest + * @covers Guzzle\Http\Client::get + * @covers Guzzle\Http\Client::put + * @covers Guzzle\Http\Client::post + * @covers Guzzle\Http\Client::head + * @covers Guzzle\Http\Client::options + */ + public function testUriArrayAllowsCustomTemplateVariables() + { + $client = new Client(); + $vars = array( + 'var' => 'hi' + ); + $this->assertEquals('/hi', (string) $client->createRequest('GET', array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->get(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->put(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->post(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->head(array('/{var}', $vars))->getUrl()); + $this->assertEquals('/hi', (string) $client->options(array('/{var}', $vars))->getUrl()); + } } \ No newline at end of file diff --git a/tests/Guzzle/Tests/Http/UriTemplateTest.php b/tests/Guzzle/Tests/Http/UriTemplateTest.php new file mode 100644 index 00000000..75a8ef4f --- /dev/null +++ b/tests/Guzzle/Tests/Http/UriTemplateTest.php @@ -0,0 +1,188 @@ + 'value', + 'hello' => 'Hello World!', + 'empty' => '', + 'path' => '/foo/bar', + 'x' => '1024', + 'y' => '768', + 'list' => array('red', 'green', 'blue'), + 'keys' => array( + "semi" => ';', + "dot" => '.', + "comma" => ',' + ) + ); + + return array_map(function($t) use ($params) { + $t[] = $params; + return $t; + }, array( + array('{var}', 'value'), + array('{hello}', 'Hello%20World%21'), + array('{+var}', 'value'), + array('{+hello}', 'Hello%20World!'), + array('{+path}/here', '/foo/bar/here'), + array('here?ref={+path}', 'here?ref=/foo/bar'), + array('X{#var}', 'X#value'), + array('X{#hello}', 'X#Hello%20World!'), + array('map?{x,y}', 'map?1024,768'), + array('{x,hello,y}', '1024,Hello%20World%21,768'), + array('{+x,hello,y}', '1024,Hello%20World!,768'), + array('{+path,x}/here', '/foo/bar,1024/here'), + array('{#x,hello,y}', '#1024,Hello%20World!,768'), + array('{#path,x}/here', '#/foo/bar,1024/here'), + array('X{.var}', 'X.value'), + array('X{.x,y}', 'X.1024.768'), + array('{/var}', '/value'), + array('{/var,x}/here', '/value/1024/here'), + array('{;x,y}', ';x=1024;y=768'), + array('{;x,y,empty}', ';x=1024;y=768;empty'), + array('{?x,y}', '?x=1024&y=768'), + array('{?x,y,empty}', '?x=1024&y=768&empty='), + array('?fixed=yes{&x}', '?fixed=yes&x=1024'), + array('{&x,y,empty}', '&x=1024&y=768&empty='), + array('{var:3}', 'val'), + array('{var:30}', 'value'), + array('{list}', 'red,green,blue'), + array('{list*}', 'red,green,blue'), + array('{keys}', 'semi,%3B,dot,.,comma,%2C'), + array('{keys*}', 'semi=%3B,dot=.,comma=%2C'), + array('{+path:6}/here', '/foo/b/here'), + array('{+list}', 'red,green,blue'), + array('{+list*}', 'red,green,blue'), + array('{+keys}', 'semi,;,dot,.,comma,,'), + array('{+keys*}', 'semi=;,dot=.,comma=,'), + array('{#path:6}/here', '#/foo/b/here'), + array('{#list}', '#red,green,blue'), + array('{#list*}', '#red,green,blue'), + array('{#keys}', '#semi,;,dot,.,comma,,'), + array('{#keys*}', '#semi=;,dot=.,comma=,'), + array('X{.var:3}', 'X.val'), + array('X{.list}', 'X.red,green,blue'), + array('X{.list*}', 'X.red.green.blue'), + array('X{.keys}', 'X.semi,%3B,dot,.,comma,%2C'), + array('X{.keys*}', 'X.semi=%3B.dot=..comma=%2C'), + array('{/var:1,var}', '/v/value'), + array('{/list}', '/red,green,blue'), + array('{/list*}', '/red/green/blue'), + array('{/list*,path:4}', '/red/green/blue/%2Ffoo'), + array('{/keys}', '/semi,%3B,dot,.,comma,%2C'), + array('{/keys*}', '/semi=%3B/dot=./comma=%2C'), + array('{;hello:5}', ';hello=Hello'), + array('{;list}', ';list=red,green,blue'), + array('{;list*}', ';list=red;list=green;list=blue'), + array('{;keys}', ';keys=semi,%3B,dot,.,comma,%2C'), + array('{;keys*}', ';semi=%3B;dot=.;comma=%2C'), + array('{?var:3}', '?var=val'), + array('{?list}', '?list=red,green,blue'), + array('{?list*}', '?list=red&list=green&list=blue'), + array('{?keys}', '?keys=semi,%3B,dot,.,comma,%2C'), + array('{?keys*}', '?semi=%3B&dot=.&comma=%2C'), + array('{&var:3}', '&var=val'), + array('{&list}', '&list=red,green,blue'), + array('{&list*}', '&list=red&list=green&list=blue'), + array('{&keys}', '&keys=semi,%3B,dot,.,comma,%2C'), + array('{&keys*}', '&semi=%3B&dot=.&comma=%2C'), + // Test that missing expansions are skipped + array('test{&missing*}', 'test'), + // Test that multiple expansions can be set + array('http://{var}/{var:2}{?keys*}', 'http://value/va?semi=%3B&dot=.&comma=%2C'), + // Test that it is backwards compatible with {{ }} syntax + array('{{var}}|{{var:3}}', 'value|val'), + // Test more complex query string stuff + array('http://www.test.com{+path}{?var,keys*}', 'http://www.test.com/foo/bar?var=value&semi=%3B&dot=.&comma=%2C') + )); + } + + /** + * @dataProvider templateProvider + */ + public function testExpandsUriTemplates($template, $expansion, $params) + { + $uri = new UriTemplate($template); + $this->assertEquals($template, $uri->getTemplate()); + $result = $uri->expand($params); + $this->assertEquals($expansion, $result); + } + + public function expressionProvider() + { + return array( + array( + '{+var*}', array( + 'operator' => '+', + 'values' => array( + array('value' => 'var', 'modifier' => '*') + ) + ), + ), + array( + '{?keys,var,val}', array( + 'operator' => '?', + 'values' => array( + array('value' => 'keys', 'modifier' => ''), + array('value' => 'var', 'modifier' => ''), + array('value' => 'val', 'modifier' => '') + ) + ), + ), + array( + '{+x,hello,y}', array( + 'operator' => '+', + 'values' => array( + array('value' => 'x', 'modifier' => ''), + array('value' => 'hello', 'modifier' => ''), + array('value' => 'y', 'modifier' => '') + ) + ) + ) + ); + } + + /** + * @dataProvider expressionProvider + */ + public function testParsesExpressions($exp, $data) + { + $template = new UriTemplate($exp); + + // Access the config object + $class = new \ReflectionClass($template); + $method = $class->getMethod('parseExpression'); + $method->setAccessible(true); + + $exp = substr($exp, 1, -1); + $this->assertEquals($data, $method->invokeArgs($template, array($exp))); + } + + /** + * @covers Guzzle\Http\UriTemplate::setRegex + */ + public function testAllowsCustomUriTemplateRegex() + { + $template = new UriTemplate('abc_<$var>'); + $template->setRegex('/\<\$(.+)\>/'); + $this->assertEquals('abc_hi', $template->expand(array( + 'var' => 'hi' + ))); + } +} \ No newline at end of file diff --git a/tests/Guzzle/Tests/Service/Command/DynamicCommandTest.php b/tests/Guzzle/Tests/Service/Command/DynamicCommandTest.php index 17a490a9..2ac674e1 100644 --- a/tests/Guzzle/Tests/Service/Command/DynamicCommandTest.php +++ b/tests/Guzzle/Tests/Service/Command/DynamicCommandTest.php @@ -27,16 +27,14 @@ class DynamicCommandTest extends \Guzzle\Tests\GuzzleTestCase 'test_command' => new ApiCommand(array( 'doc' => 'documentationForCommand', 'method' => 'HEAD', - 'path' => '/{{key}}', + 'uri' => '{/key}', 'params' => array( 'bucket' => array( 'required' => true, - 'append' => '.', - 'location' => 'path' + 'append' => '.' ), 'key' => array( - 'location' => 'path', - 'prepend' => '/' + 'prepend' => 'hi_' ), 'acl' => array( 'location' => 'query' @@ -106,12 +104,12 @@ class DynamicCommandTest extends \Guzzle\Tests\GuzzleTestCase $request = $command->setClient($client)->prepare(); // Ensure that the path values were injected into the path and base_url - $this->assertEquals('/key', $request->getPath()); + $this->assertEquals('/hi_key', $request->getPath()); $this->assertEquals('www.example.com', $request->getHost()); // Check the complete request $this->assertEquals( - "HEAD /key HTTP/1.1\r\n" . + "HEAD /hi_key HTTP/1.1\r\n" . "Host: www.example.com\r\n" . "User-Agent: " . Guzzle::getDefaultUserAgent() . "\r\n" . "\r\n", (string) $request); diff --git a/tests/Guzzle/Tests/Service/Description/ApiCommandTest.php b/tests/Guzzle/Tests/Service/Description/ApiCommandTest.php index 7c3cc028..780856c8 100644 --- a/tests/Guzzle/Tests/Service/Description/ApiCommandTest.php +++ b/tests/Guzzle/Tests/Service/Description/ApiCommandTest.php @@ -35,7 +35,7 @@ class ApiCommandTest extends \Guzzle\Tests\GuzzleTestCase $this->assertEquals('test', $c->getName()); $this->assertEquals('doc', $c->getDoc()); $this->assertEquals('POST', $c->getMethod()); - $this->assertEquals('/api/v1', $c->getPath()); + $this->assertEquals('/api/v1', $c->getUri()); $this->assertEquals('Guzzle\\Service\\Command\\DynamicCommand', $c->getConcreteClass()); $this->assertEquals(array( 'key' => new Collection(array( @@ -86,7 +86,7 @@ class ApiCommandTest extends \Guzzle\Tests\GuzzleTestCase 'class' => 'Guzzle\\Service\\Command\ClosureCommand', 'doc' => 'test', 'method' => 'PUT', - 'path' => '/', + 'uri' => '/', 'params' => array( 'p' => new Collection(array( 'name' => 'foo' diff --git a/tests/Guzzle/Tests/Service/Description/JsonDescriptionBuilderTest.php b/tests/Guzzle/Tests/Service/Description/JsonDescriptionBuilderTest.php index 8f86a0bc..816fad18 100644 --- a/tests/Guzzle/Tests/Service/Description/JsonDescriptionBuilderTest.php +++ b/tests/Guzzle/Tests/Service/Description/JsonDescriptionBuilderTest.php @@ -22,8 +22,8 @@ class JsonDescriptionBuilderTest extends \Guzzle\Tests\GuzzleTestCase $description = JsonDescriptionBuilder::build(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'TestData' . DIRECTORY_SEPARATOR . 'test_service.json'); $this->assertTrue($description->hasCommand('test')); $test = $description->getCommand('test'); - $this->assertEquals('/path', $test->getPath()); + $this->assertEquals('/path', $test->getUri()); $test = $description->getCommand('concrete'); - $this->assertEquals('/abstract', $test->getPath()); + $this->assertEquals('/abstract', $test->getUri()); } } \ No newline at end of file diff --git a/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php b/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php index 15a0abed..34db602f 100644 --- a/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php +++ b/tests/Guzzle/Tests/Service/Description/ServiceDescriptionTest.php @@ -37,7 +37,7 @@ class ServiceDescriptionTest extends \Guzzle\Tests\GuzzleTestCase )); $c = $d->getCommand('concrete'); - $this->assertEquals('/test', $c->getPath()); + $this->assertEquals('/test', $c->getUri()); $this->assertEquals('GET', $c->getMethod()); $params = $c->getParams(); $param = $params['test']; diff --git a/tests/Guzzle/Tests/Service/Description/XmlDescriptionBuilderTest.php b/tests/Guzzle/Tests/Service/Description/XmlDescriptionBuilderTest.php index c449950e..257286c4 100644 --- a/tests/Guzzle/Tests/Service/Description/XmlDescriptionBuilderTest.php +++ b/tests/Guzzle/Tests/Service/Description/XmlDescriptionBuilderTest.php @@ -46,7 +46,7 @@ class XmlDescriptionBuilderTest extends \Guzzle\Tests\GuzzleTestCase ), $command->getParam('bucket')->getAll()); $this->assertEquals('DELETE', $command->getMethod()); - $this->assertEquals('{{ bucket }}/{{ key }}{{ format }}', $command->getPath()); + $this->assertEquals('{{ bucket }}/{{ key }}{{ format }}', $command->getUri()); $this->assertEquals('Documentation', $command->getDoc()); $this->assertArrayHasKey('custom_filter', Inspector::getInstance()->getRegisteredConstraints()); @@ -59,6 +59,6 @@ class XmlDescriptionBuilderTest extends \Guzzle\Tests\GuzzleTestCase { $service = XmlDescriptionBuilder::build(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'TestData' . DIRECTORY_SEPARATOR . 'test_service.xml'); $command = $service->getCommand('concrete'); - $this->assertEquals('/test', $command->getPath()); + $this->assertEquals('/test', $command->getUri()); } } \ No newline at end of file diff --git a/tests/Guzzle/Tests/TestData/test_service.xml b/tests/Guzzle/Tests/TestData/test_service.xml index ec219013..65ec669b 100644 --- a/tests/Guzzle/Tests/TestData/test_service.xml +++ b/tests/Guzzle/Tests/TestData/test_service.xml @@ -16,7 +16,7 @@ - + Documentation @@ -36,18 +36,18 @@ - + - + - +