CHANGELOG.md000064400000045042144760114130006363 0ustar00# Change Log ## [1.2.0](https://github.com/guzzle/guzzle-services/tree/1.2.0) (2020-11-13) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.2.0...HEAD) **Closed issues:** - Fix weird "equal equal not" operator [\#154](https://github.com/guzzle/guzzle-services/issues/154) **Merged pull requests:** - Support Guzzle 7 [\#176](https://github.com/guzzle/guzzle-services/pull/176) ([ptlevi](https://github.com/ptlevi)) ## [1.1.3](https://github.com/guzzle/guzzle-services/tree/1.1.3) (2017-10-06) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.2...HEAD) **Closed issues:** - Parameter type configuration causes issue when filters change input type [\#147](https://github.com/guzzle/guzzle-services/issues/147) **Merged pull requests:** - Use wire name when visiting array [\#152](https://github.com/guzzle/guzzle-services/pull/152) ([my2ter](https://github.com/my2ter)) - Adding descriptive error message on parameter failure [\#144](https://github.com/guzzle/guzzle-services/pull/144) ([igorsantos07](https://github.com/igorsantos07)) ## [1.1.2](https://github.com/guzzle/guzzle-services/tree/1.1.2) (2017-05-19) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.1...1.1.2) **Closed issues:** - Default values ignored in 1.1 [\#146](https://github.com/guzzle/guzzle-services/issues/146) - Operations extends is broken in 1.1.1 [\#145](https://github.com/guzzle/guzzle-services/issues/145) ## [1.1.1](https://github.com/guzzle/guzzle-services/tree/1.1.1) (2017-05-15) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.0...1.1.1) **Closed issues:** - Filters are applied twice [\#134](https://github.com/guzzle/guzzle-services/issues/134) - Is it possible to NOT urlencode a specific uri parameter value? [\#97](https://github.com/guzzle/guzzle-services/issues/97) **Merged pull requests:** - Fix minor typos in documentation. [\#139](https://github.com/guzzle/guzzle-services/pull/139) ([forevermatt](https://github.com/forevermatt)) - Do not mutate command at validation [\#135](https://github.com/guzzle/guzzle-services/pull/135) ([danizord](https://github.com/danizord)) - Added tests for JSON array of arrays and array of objects [\#131](https://github.com/guzzle/guzzle-services/pull/131) ([selfcatering](https://github.com/selfcatering)) - Allow filters on response model [\#138](https://github.com/guzzle/guzzle-services/pull/138) ([danizord](https://github.com/danizord)) - Exposing properties to a parent class [\#136](https://github.com/guzzle/guzzle-services/pull/136) ([Napas](https://github.com/Napas)) ## [1.1.0](https://github.com/guzzle/guzzle-services/tree/1.1.0) (2017-01-31) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.0.1...1.1.0) **Closed issues:** - Grab a list of objects when they are not located at top level of a json response \(HATEOAS\) [\#90](https://github.com/guzzle/guzzle-services/issues/90) - Regression of Issue \#51 - XmlLocation response not handling multiple tags of the same name correctly [\#82](https://github.com/guzzle/guzzle-services/issues/82) - PUT requests with parameters with location of "postField" result in Exception [\#78](https://github.com/guzzle/guzzle-services/issues/78) - Allow to provide Post Body as an Array [\#77](https://github.com/guzzle/guzzle-services/issues/77) **Merged pull requests:** - Bring more flexibility to query params serialization [\#132](https://github.com/guzzle/guzzle-services/pull/132) ([bakura10](https://github.com/bakura10)) - Allow to fix validation for parameters with a format [\#130](https://github.com/guzzle/guzzle-services/pull/130) ([bakura10](https://github.com/bakura10)) ## [1.0.1](https://github.com/guzzle/guzzle-services/tree/1.0.1) (2017-01-13) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.0.0...1.0.1) **Implemented enhancements:** - Set a name when pushing ValidatedDescriptionHandler to stack [\#127](https://github.com/guzzle/guzzle-services/issues/127) **Fixed bugs:** - combine method in Uri [\#101](https://github.com/guzzle/guzzle-services/issues/101) - Undefined Variable [\#88](https://github.com/guzzle/guzzle-services/issues/88) - Regression in array parameter serialization [\#128](https://github.com/guzzle/guzzle-services/issues/128) - Unable to POST multiple multipart parameters [\#123](https://github.com/guzzle/guzzle-services/issues/123) **Closed issues:** - Tag pre 1.0.0 release [\#121](https://github.com/guzzle/guzzle-services/issues/121) - Adjust inline documentation of Parameter [\#120](https://github.com/guzzle/guzzle-services/issues/120) - postField location not recognized after upgrading to 1.0 [\#119](https://github.com/guzzle/guzzle-services/issues/119) - Create a new release for the guzzle6 branch [\#118](https://github.com/guzzle/guzzle-services/issues/118) - Compatibility problem with PHP7.0 ? [\#116](https://github.com/guzzle/guzzle-services/issues/116) - What is the correct type of Parameter static option [\#113](https://github.com/guzzle/guzzle-services/issues/113) - Improve the construction of baseUri in Description [\#112](https://github.com/guzzle/guzzle-services/issues/112) - Please create version tag for current master branch [\#110](https://github.com/guzzle/guzzle-services/issues/110) - Problems with postField params [\#98](https://github.com/guzzle/guzzle-services/issues/98) **Merged pull requests:** - Fix serialization of query params [\#129](https://github.com/guzzle/guzzle-services/pull/129) ([bakura10](https://github.com/bakura10)) ## [1.0.0](https://github.com/guzzle/guzzle-services/tree/1.0.0) (2016-11-24) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.6.0...1.0.0) **Closed issues:** - AbstractClient' not found [\#117](https://github.com/guzzle/guzzle-services/issues/117) **Merged pull requests:** - Make Guzzle Services compatible with Guzzle6 [\#109](https://github.com/guzzle/guzzle-services/pull/109) ([Konafets](https://github.com/Konafets)) ## [0.6.0](https://github.com/guzzle/guzzle-services/tree/0.6.0) (2016-10-21) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.5.0...0.6.0) **Closed issues:** - Broken composer install [\#111](https://github.com/guzzle/guzzle-services/issues/111) - The visit\(\) method is expected to return a RequestInterface but it doesn't in JsonLocation [\#106](https://github.com/guzzle/guzzle-services/issues/106) - Allow parameters in baseUrl [\#102](https://github.com/guzzle/guzzle-services/issues/102) - Have default params at client construction, gone away? [\#100](https://github.com/guzzle/guzzle-services/issues/100) - Runtime Exception Error is always empty [\#99](https://github.com/guzzle/guzzle-services/issues/99) - PHP Fatal error: Unsupported operand types in guzzlehttp/guzzle-services/src/GuzzleClient.php on line 72 [\#95](https://github.com/guzzle/guzzle-services/issues/95) - Date of next version [\#94](https://github.com/guzzle/guzzle-services/issues/94) - Map null reponse values to defined reponse model properties [\#91](https://github.com/guzzle/guzzle-services/issues/91) - Map a json-array into a Model [\#80](https://github.com/guzzle/guzzle-services/issues/80) - If property specified in json model but empty, notice raised [\#75](https://github.com/guzzle/guzzle-services/issues/75) - Allow primitive response types for operations [\#73](https://github.com/guzzle/guzzle-services/issues/73) - Allow shortened definition of properties in models [\#71](https://github.com/guzzle/guzzle-services/issues/71) - Where's the ServiceDescriptionLoader/AbstractConfigLoader? [\#68](https://github.com/guzzle/guzzle-services/issues/68) - errorResposnes from operation is never used [\#66](https://github.com/guzzle/guzzle-services/issues/66) - Updating the description [\#65](https://github.com/guzzle/guzzle-services/issues/65) - Parameter type validation is too strict [\#7](https://github.com/guzzle/guzzle-services/issues/7) **Merged pull requests:** - fix code example [\#115](https://github.com/guzzle/guzzle-services/pull/115) ([snoek09](https://github.com/snoek09)) - Bug Fix for GuzzleClient constructor [\#96](https://github.com/guzzle/guzzle-services/pull/96) ([peterfox](https://github.com/peterfox)) - add plugin section to readme [\#93](https://github.com/guzzle/guzzle-services/pull/93) ([gimler](https://github.com/gimler)) - Allow mapping null response values to defined response model properties [\#92](https://github.com/guzzle/guzzle-services/pull/92) ([shaun785](https://github.com/shaun785)) - Updated exception message for better debugging [\#85](https://github.com/guzzle/guzzle-services/pull/85) ([stovak](https://github.com/stovak)) - Gracefully handle null return from $this-\>getConfig\('defaults'\) [\#84](https://github.com/guzzle/guzzle-services/pull/84) ([fuhry](https://github.com/fuhry)) - Fixing issue \#82 to address regression for handling elements with the sa... [\#83](https://github.com/guzzle/guzzle-services/pull/83) ([sprak3000](https://github.com/sprak3000)) - Fix for specified property but no value in json \(notice for undefined in... [\#76](https://github.com/guzzle/guzzle-services/pull/76) ([rfink](https://github.com/rfink)) - Add ErrorHandler subscriber [\#67](https://github.com/guzzle/guzzle-services/pull/67) ([bakura10](https://github.com/bakura10)) - Fix combine base url and command uri [\#108](https://github.com/guzzle/guzzle-services/pull/108) ([vlastv](https://github.com/vlastv)) - Fixing JsonLocation::visit\(\) not returning a request \#106 [\#107](https://github.com/guzzle/guzzle-services/pull/107) ([Pinolo](https://github.com/Pinolo)) - Fix call to undefined method "GuzzleHttp\Psr7\Uri::combine" [\#105](https://github.com/guzzle/guzzle-services/pull/105) ([horrorin](https://github.com/horrorin)) - fix description for get request example [\#87](https://github.com/guzzle/guzzle-services/pull/87) ([snoek09](https://github.com/snoek09)) - Allow raw values \(non array/object\) for root model definitions [\#74](https://github.com/guzzle/guzzle-services/pull/74) ([rfink](https://github.com/rfink)) - Allow shortened definition of properties by assigning them directly to a type [\#72](https://github.com/guzzle/guzzle-services/pull/72) ([rfink](https://github.com/rfink)) ## [0.5.0](https://github.com/guzzle/guzzle-services/tree/0.5.0) (2014-12-23) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.4.0...0.5.0) **Closed issues:** - Does it supports custom class instantiate to define an operation using a service description [\#62](https://github.com/guzzle/guzzle-services/issues/62) - Tag version 0.4.0 [\#61](https://github.com/guzzle/guzzle-services/issues/61) - XmlLocation not adding attributes to non-leaf child nodes [\#52](https://github.com/guzzle/guzzle-services/issues/52) - XmlLocation response not handling multiple tags of the same name correctly [\#51](https://github.com/guzzle/guzzle-services/issues/51) - Validation Bug [\#47](https://github.com/guzzle/guzzle-services/issues/47) - CommandException doesn't contain response data [\#44](https://github.com/guzzle/guzzle-services/issues/44) - \[Fix included\] XmlLocation requires text value to have attributes [\#37](https://github.com/guzzle/guzzle-services/issues/37) - Question: Mocking a Response does not throw exception [\#35](https://github.com/guzzle/guzzle-services/issues/35) - allow default 'location' on Model [\#26](https://github.com/guzzle/guzzle-services/issues/26) - create mock subscriber requests from descriptions [\#25](https://github.com/guzzle/guzzle-services/issues/25) **Merged pull requests:** - Documentation: Add 'boolean-string' as a supported "format" value [\#63](https://github.com/guzzle/guzzle-services/pull/63) ([jwcobb](https://github.com/jwcobb)) ## [0.4.0](https://github.com/guzzle/guzzle-services/tree/0.4.0) (2014-11-03) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.3.0...0.4.0) **Closed issues:** - Exceptions Thrown From Subscribers Are Ignored? [\#58](https://github.com/guzzle/guzzle-services/issues/58) - Totally Broken With Guzzle 5 [\#57](https://github.com/guzzle/guzzle-services/issues/57) - GuzzleHTTP/Command Dependency fail [\#50](https://github.com/guzzle/guzzle-services/issues/50) - Request parameter PathLocation [\#46](https://github.com/guzzle/guzzle-services/issues/46) - Requesting a new version tag [\#45](https://github.com/guzzle/guzzle-services/issues/45) - CommandException expects second parameter to be CommandTransaction instance [\#43](https://github.com/guzzle/guzzle-services/issues/43) - Cannot add Autorization header to my requests [\#39](https://github.com/guzzle/guzzle-services/issues/39) - Resouce Itterators [\#36](https://github.com/guzzle/guzzle-services/issues/36) - Question [\#33](https://github.com/guzzle/guzzle-services/issues/33) - query location array can be comma separated [\#31](https://github.com/guzzle/guzzle-services/issues/31) - Automatically returns array from command? [\#30](https://github.com/guzzle/guzzle-services/issues/30) - Arrays nested under objects in JSON response broken? [\#27](https://github.com/guzzle/guzzle-services/issues/27) - Question? [\#23](https://github.com/guzzle/guzzle-services/issues/23) **Merged pull requests:** - Bump the version in the readme [\#60](https://github.com/guzzle/guzzle-services/pull/60) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Bump the next version to 0.4 [\#56](https://github.com/guzzle/guzzle-services/pull/56) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Fixed the guzzlehttp/command version constraint [\#55](https://github.com/guzzle/guzzle-services/pull/55) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Work with latest Guzzle 5 and Command updates [\#54](https://github.com/guzzle/guzzle-services/pull/54) ([mtdowling](https://github.com/mtdowling)) - Addressing Issue \#51 & Issue \#52 [\#53](https://github.com/guzzle/guzzle-services/pull/53) ([sprak3000](https://github.com/sprak3000)) - added description interface to extend it [\#49](https://github.com/guzzle/guzzle-services/pull/49) ([danieledangeli](https://github.com/danieledangeli)) - Update readme to improve documentation \(\#46\) [\#48](https://github.com/guzzle/guzzle-services/pull/48) ([bonndan](https://github.com/bonndan)) - Fixed the readme version constraint [\#42](https://github.com/guzzle/guzzle-services/pull/42) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Update .travis.yml [\#41](https://github.com/guzzle/guzzle-services/pull/41) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Added a branch alias [\#40](https://github.com/guzzle/guzzle-services/pull/40) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Fixes Response\XmlLocation requires text value [\#38](https://github.com/guzzle/guzzle-services/pull/38) ([magnetik](https://github.com/magnetik)) - Removing unnecessary \(\) from docblock [\#32](https://github.com/guzzle/guzzle-services/pull/32) ([jamiehannaford](https://github.com/jamiehannaford)) - Fix JSON response location so that both is supported: arrays nested unde... [\#28](https://github.com/guzzle/guzzle-services/pull/28) ([ukautz](https://github.com/ukautz)) - Throw Any Exceptions On Process [\#59](https://github.com/guzzle/guzzle-services/pull/59) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Allow extension to work recursively over models [\#34](https://github.com/guzzle/guzzle-services/pull/34) ([jamiehannaford](https://github.com/jamiehannaford)) - A custom class can be configured for command instances. [\#29](https://github.com/guzzle/guzzle-services/pull/29) ([robinvdvleuten](https://github.com/robinvdvleuten)) - \[WIP\] doing some experimentation [\#24](https://github.com/guzzle/guzzle-services/pull/24) ([cordoval](https://github.com/cordoval)) ## [0.3.0](https://github.com/guzzle/guzzle-services/tree/0.3.0) (2014-06-01) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.2.0...0.3.0) **Closed issues:** - Testing Guzzle Services doesn't work [\#19](https://github.com/guzzle/guzzle-services/issues/19) - Description factory [\#18](https://github.com/guzzle/guzzle-services/issues/18) - support to load service description from file [\#15](https://github.com/guzzle/guzzle-services/issues/15) - Update dependency on guzzlehttp/command [\#11](https://github.com/guzzle/guzzle-services/issues/11) **Merged pull requests:** - Add license file [\#22](https://github.com/guzzle/guzzle-services/pull/22) ([siwinski](https://github.com/siwinski)) - Fix 'Invalid argument supplied for foreach\(\)' [\#21](https://github.com/guzzle/guzzle-services/pull/21) ([Olden](https://github.com/Olden)) - Fixed string zero \('0'\) values not being filtered in XML. [\#20](https://github.com/guzzle/guzzle-services/pull/20) ([dragonwize](https://github.com/dragonwize)) - baseUrl can be a string or an uri template [\#16](https://github.com/guzzle/guzzle-services/pull/16) ([robinvdvleuten](https://github.com/robinvdvleuten)) ## [0.2.0](https://github.com/guzzle/guzzle-services/tree/0.2.0) (2014-03-30) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.1.0...0.2.0) **Closed issues:** - please remove wiki [\#13](https://github.com/guzzle/guzzle-services/issues/13) - Parameter validation fails for union types [\#12](https://github.com/guzzle/guzzle-services/issues/12) - question on integration with Guzzle4 [\#8](https://github.com/guzzle/guzzle-services/issues/8) - typehints for operations property [\#6](https://github.com/guzzle/guzzle-services/issues/6) - improve exception message [\#5](https://github.com/guzzle/guzzle-services/issues/5) **Merged pull requests:** - Update composer.json [\#14](https://github.com/guzzle/guzzle-services/pull/14) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Update composer.json [\#9](https://github.com/guzzle/guzzle-services/pull/9) ([GrahamCampbell](https://github.com/GrahamCampbell)) - some fixes [\#4](https://github.com/guzzle/guzzle-services/pull/4) ([cordoval](https://github.com/cordoval)) - Fix the CommandException path used in ValidateInput [\#2](https://github.com/guzzle/guzzle-services/pull/2) ([mookle](https://github.com/mookle)) - Minor improvements [\#1](https://github.com/guzzle/guzzle-services/pull/1) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Use latest guzzlehttp/command to fix dependencies [\#10](https://github.com/guzzle/guzzle-services/pull/10) ([sbward](https://github.com/sbward)) - some collaboration using Gush :\) [\#3](https://github.com/guzzle/guzzle-services/pull/3) ([cordoval](https://github.com/cordoval)) ## [0.1.0](https://github.com/guzzle/guzzle-services/tree/0.1.0) (2014-03-15) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* LICENSE000064400000002127144760114130005554 0ustar00Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Makefile000064400000000042144760114130006201 0ustar00test: vendor/bin/phpunit $(TEST) README.md000064400000006732144760114130006034 0ustar00# Guzzle Services Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures. ```php use GuzzleHttp\Client; use GuzzleHttp\Command\Guzzle\GuzzleClient; use GuzzleHttp\Command\Guzzle\Description; $client = new Client(); $description = new Description([ 'baseUri' => 'http://httpbin.org/', 'operations' => [ 'testing' => [ 'httpMethod' => 'GET', 'uri' => '/get{?foo}', 'responseModel' => 'getResponse', 'parameters' => [ 'foo' => [ 'type' => 'string', 'location' => 'uri' ], 'bar' => [ 'type' => 'string', 'location' => 'query' ] ] ] ], 'models' => [ 'getResponse' => [ 'type' => 'object', 'additionalProperties' => [ 'location' => 'json' ] ] ] ]); $guzzleClient = new GuzzleClient($client, $description); $result = $guzzleClient->testing(['foo' => 'bar']); echo $result['args']['foo']; // bar ``` ## Installing This project can be installed using Composer: ``composer require guzzlehttp/guzzle-services`` For **Guzzle 5**, use ``composer require guzzlehttp/guzzle-services:0.6``. **Note:** If Composer is not installed [globally](https://getcomposer.org/doc/00-intro.md#globally) then you may need to run the preceding Composer commands using ``php composer.phar`` (where ``composer.phar`` is the path to your copy of Composer), instead of just ``composer``. ## Plugins * Load Service description from file [https://github.com/gimler/guzzle-description-loader] ## Transition guide from Guzzle 5.0 to 6.0 ### Change regarding PostField and PostFile The request locations `postField` and `postFile` were removed in favor of `formParam` and `multipart`. If your description looks like ```php [ 'baseUri' => 'http://httpbin.org/', 'operations' => [ 'testing' => [ 'httpMethod' => 'GET', 'uri' => '/get{?foo}', 'responseModel' => 'getResponse', 'parameters' => [ 'foo' => [ 'type' => 'string', 'location' => 'postField' ], 'bar' => [ 'type' => 'string', 'location' => 'postFile' ] ] ] ], ] ``` you need to change `postField` to `formParam` and `postFile` to `multipart`. More documentation coming soon. ## Cookbook ### Changing the way query params are serialized By default, query params are serialized using strict RFC3986 rules, using `http_build_query` method. With this, array params are serialized this way: ```php $client->myMethod(['foo' => ['bar', 'baz']]); // Query params will be foo[0]=bar&foo[1]=baz ``` However, a lot of APIs in the wild require the numeric indices to be removed, so that the query params end up being `foo[]=bar&foo[]=baz`. You can easily change the behaviour by creating your own serializer and overriding the "query" request location: ```php use GuzzleHttp\Command\Guzzle\GuzzleClient; use GuzzleHttp\Command\Guzzle\RequestLocation\QueryLocation; use GuzzleHttp\Command\Guzzle\QuerySerializer\Rfc3986Serializer; use GuzzleHttp\Command\Guzzle\Serializer; $queryLocation = new QueryLocation('query', new Rfc3986Serializer(true)); $serializer = new Serializer($description, ['query' => $queryLocation]); $guzzleClient = new GuzzleClient($client, $description, $serializer); ``` You can also create your own serializer if you have specific needs. composer.json000064400000002737144760114130007300 0ustar00{ "name": "guzzlehttp/guzzle-services", "description": "Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures.", "type": "library", "license": "MIT", "authors": [ { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, { "name": "Jeremy Lindblom", "email": "jeremeamia@gmail.com", "homepage": "https://github.com/jeremeamia" }, { "name": "Stefano Kowalke", "email": "blueduck@mail.org", "homepage": "https://github.com/konafets" } ], "require": { "php": "^7.2.5 || ^8.0", "guzzlehttp/guzzle": "^7.3", "guzzlehttp/command": "^1.2", "guzzlehttp/psr7": "^1.7 || ^2.0", "guzzlehttp/uri-template": "^0.2 || ^1.0" }, "require-dev": { "phpunit/phpunit": "^8.5.19 || ^9.5.8" }, "autoload": { "psr-4": { "GuzzleHttp\\Command\\Guzzle\\": "src/" } }, "autoload-dev": { "psr-4": { "GuzzleHttp\\Tests\\Command\\Guzzle\\": "tests/" } }, "suggest": { "gimler/guzzle-description-loader": "^0.0.4" }, "extra": { "branch-alias": { "dev-master": "1.3-dev" } } } phpunit.xml.dist000064400000000725144760114130007724 0ustar00 tests src src/Description.php000064400000015537144760114130010343 0ustar00{$key} = $config[$key]; } } // Set the baseUri // Account for the old style of using baseUrl if (isset($config['baseUrl'])) { $config['baseUri'] = $config['baseUrl']; } $this->baseUri = isset($config['baseUri']) ? new Uri($config['baseUri']) : new Uri(); // Ensure that the models and operations properties are always arrays $this->models = (array) $this->models; $this->operations = (array) $this->operations; // We want to add operations differently than adding the other properties $defaultKeys[] = 'operations'; // Create operations for each operation if (isset($config['operations'])) { foreach ($config['operations'] as $name => $operation) { if (!is_array($operation)) { throw new \InvalidArgumentException('Operations must be arrays'); } $this->operations[$name] = $operation; } } // Get all of the additional properties of the service description and // store them in a data array foreach (array_diff(array_keys($config), $defaultKeys) as $key) { $this->extraData[$key] = $config[$key]; } // Configure the schema formatter if (isset($options['formatter'])) { $this->formatter = $options['formatter']; } else { static $defaultFormatter; if (!$defaultFormatter) { $defaultFormatter = new SchemaFormatter(); } $this->formatter = $defaultFormatter; } } /** * Get the basePath/baseUri of the description * * @return Uri */ public function getBaseUri() { return $this->baseUri; } /** * Get the API operations of the service * * @return Operation[] Returns an array of {@see Operation} objects */ public function getOperations() { return $this->operations; } /** * Check if the service has an operation by name * * @param string $name Name of the operation to check * * @return bool */ public function hasOperation($name) { return isset($this->operations[$name]); } /** * Get an API operation by name * * @param string $name Name of the command * * @return Operation * @throws \InvalidArgumentException if the operation is not found */ public function getOperation($name) { if (!$this->hasOperation($name)) { throw new \InvalidArgumentException("No operation found named $name"); } // Lazily create operations as they are retrieved if (!($this->operations[$name] instanceof Operation)) { $this->operations[$name]['name'] = $name; $this->operations[$name] = new Operation($this->operations[$name], $this); } return $this->operations[$name]; } /** * Get a shared definition structure. * * @param string $id ID/name of the model to retrieve * * @return Parameter * @throws \InvalidArgumentException if the model is not found */ public function getModel($id) { if (!$this->hasModel($id)) { throw new \InvalidArgumentException("No model found named $id"); } // Lazily create models as they are retrieved if (!($this->models[$id] instanceof Parameter)) { $this->models[$id] = new Parameter( $this->models[$id], ['description' => $this] ); } return $this->models[$id]; } /** * Get all models of the service description. * * @return array */ public function getModels() { $models = []; foreach ($this->models as $name => $model) { $models[$name] = $this->getModel($name); } return $models; } /** * Check if the service description has a model by name. * * @param string $id Name/ID of the model to check * * @return bool */ public function hasModel($id) { return isset($this->models[$id]); } /** * Get the API version of the service * * @return string */ public function getApiVersion() { return $this->apiVersion; } /** * Get the name of the API * * @return string */ public function getName() { return $this->name; } /** * Get a summary of the purpose of the API * * @return string */ public function getDescription() { return $this->description; } /** * Format a parameter using named formats. * * @param string $format Format to convert it to * @param mixed $input Input string * * @return mixed */ public function format($format, $input) { return $this->formatter->format($format, $input); } /** * Get arbitrary data from the service description that is not part of the * Guzzle service description specification. * * @param string $key Data key to retrieve or null to retrieve all extra * * @return null|mixed */ public function getData($key = null) { if ($key === null) { return $this->extraData; } elseif (isset($this->extraData[$key])) { return $this->extraData[$key]; } else { return null; } } } src/DescriptionInterface.php000064400000004467144760114130012164 0ustar00 new BodyLocation(), 'header' => new HeaderLocation(), 'reasonPhrase' => new ReasonPhraseLocation(), 'statusCode' => new StatusCodeLocation(), 'xml' => new XmlLocation(), 'json' => new JsonLocation(), ]; } $this->responseLocations = $responseLocations + $defaultResponseLocations; $this->description = $description; $this->process = $process; } /** * Deserialize the response into the specified result representation * * @param ResponseInterface $response * @param RequestInterface|null $request * @param CommandInterface $command * @return Result|ResultInterface|void|ResponseInterface */ public function __invoke(ResponseInterface $response, RequestInterface $request, CommandInterface $command) { // If the user don't want to process the result, just return the plain response here if ($this->process === false) { return $response; } $name = $command->getName(); $operation = $this->description->getOperation($name); $this->handleErrorResponses($response, $request, $command, $operation); // Add a default Model as the result if no matching schema was found if (!($modelName = $operation->getResponseModel())) { // Not sure if this should be empty or contains the response. // Decided to do it how it was in the old version for now. return new Result(); } $model = $operation->getServiceDescription()->getModel($modelName); if (!$model) { throw new \RuntimeException("Unknown model: {$modelName}"); } return $this->visit($model, $response); } /** * Handles visit() and after() methods of the Response locations * * @param Parameter $model * @param ResponseInterface $response * @return Result|ResultInterface|void */ protected function visit(Parameter $model, ResponseInterface $response) { $result = new Result(); $context = ['visitors' => []]; if ($model->getType() === 'object') { $result = $this->visitOuterObject($model, $result, $response, $context); } elseif ($model->getType() === 'array') { $result = $this->visitOuterArray($model, $result, $response, $context); } else { throw new \InvalidArgumentException('Invalid response model: ' . $model->getType()); } // Call the after() method of each found visitor /** @var ResponseLocationInterface $visitor */ foreach ($context['visitors'] as $visitor) { $result = $visitor->after($result, $response, $model); } return $result; } /** * Handles the before() method of Response locations * * @param string $location * @param Parameter $model * @param ResultInterface $result * @param ResponseInterface $response * @param array $context * @return ResultInterface */ private function triggerBeforeVisitor( $location, Parameter $model, ResultInterface $result, ResponseInterface $response, array &$context ) { if (!isset($this->responseLocations[$location])) { throw new \RuntimeException("Unknown location: $location"); } $context['visitors'][$location] = $this->responseLocations[$location]; $result = $this->responseLocations[$location]->before( $result, $response, $model ); return $result; } /** * Visits the outer object * * @param Parameter $model * @param ResultInterface $result * @param ResponseInterface $response * @param array $context * @return ResultInterface */ private function visitOuterObject( Parameter $model, ResultInterface $result, ResponseInterface $response, array &$context ) { $parentLocation = $model->getLocation(); // If top-level additionalProperties is a schema, then visit it $additional = $model->getAdditionalProperties(); if ($additional instanceof Parameter) { // Use the model location if none set on additionalProperties. $location = $additional->getLocation() ?: $parentLocation; $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context); } // Use 'location' from all individual defined properties, but fall back // to the model location if no per-property location is set. Collect // the properties that need to be visited into an array. $visitProperties = []; foreach ($model->getProperties() as $schema) { $location = $schema->getLocation() ?: $parentLocation; if ($location) { $visitProperties[] = [$location, $schema]; // Trigger the before method on each unique visitor location if (!isset($context['visitors'][$location])) { $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context); } } } // Actually visit each response element foreach ($visitProperties as $property) { $result = $this->responseLocations[$property[0]]->visit($result, $response, $property[1]); } return $result; } /** * Visits the outer array * * @param Parameter $model * @param ResultInterface $result * @param ResponseInterface $response * @param array $context * @return ResultInterface|void */ private function visitOuterArray( Parameter $model, ResultInterface $result, ResponseInterface $response, array &$context ) { // Use 'location' defined on the top of the model if (!($location = $model->getLocation())) { return; } // Trigger the before method on each unique visitor location if (!isset($context['visitors'][$location])) { $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context); } // Visit each item in the response $result = $this->responseLocations[$location]->visit($result, $response, $model); return $result; } /** * Reads the "errorResponses" from commands, and trigger appropriate exceptions * * In order for the exception to be properly triggered, all your exceptions must be instance * of "GuzzleHttp\Command\Exception\CommandException". If that's not the case, your exceptions will be wrapped * around a CommandException * * @param ResponseInterface $response * @param RequestInterface $request * @param CommandInterface $command * @param Operation $operation */ protected function handleErrorResponses( ResponseInterface $response, RequestInterface $request, CommandInterface $command, Operation $operation ) { $errors = $operation->getErrorResponses(); // We iterate through each errors in service description. If the descriptor contains both a phrase and // status code, there must be an exact match of both. Otherwise, a match of status code is enough $bestException = null; foreach ($errors as $error) { $code = (int) $error['code']; if ($response->getStatusCode() !== $code) { continue; } if (isset($error['phrase']) && ! ($error['phrase'] === $response->getReasonPhrase())) { continue; } $bestException = $error['class']; // If there is an exact match of phrase + code, then we cannot find a more specialized exception in // the array, so we can break early instead of iterating the remaining ones if (isset($error['phrase'])) { break; } } if (null !== $bestException) { throw new $bestException($response->getReasonPhrase(), $command, null, $request, $response); } // If we reach here, no exception could be match from descriptor, and Guzzle exception will propagate if // option "http_errors" is set to true, which is the default setting. } } src/GuzzleClient.php000064400000012571144760114130010472 0ustar00config = $config; $this->description = $description; $serializer = $this->getSerializer($commandToRequestTransformer); $deserializer = $this->getDeserializer($responseToResultTransformer); parent::__construct($client, $serializer, $deserializer, $commandHandlerStack); $this->processConfig($config); } /** * Returns the command if valid; otherwise an Exception * @param string $name * @param array $args * @return CommandInterface * @throws \InvalidArgumentException */ public function getCommand($name, array $args = []) { if (!$this->description->hasOperation($name)) { $name = ucfirst($name); if (!$this->description->hasOperation($name)) { throw new \InvalidArgumentException( "No operation found named {$name}" ); } } // Merge in default command options $args += $this->getConfig('defaults'); return parent::getCommand($name, $args); } /** * Return the description * * @return DescriptionInterface */ public function getDescription() { return $this->description; } /** * Returns the passed Serializer when set, a new instance otherwise * * @param callable|null $commandToRequestTransformer * @return \GuzzleHttp\Command\Guzzle\Serializer */ private function getSerializer($commandToRequestTransformer) { return $commandToRequestTransformer !== null ? $commandToRequestTransformer : new Serializer($this->description); } /** * Returns the passed Deserializer when set, a new instance otherwise * * @param callable|null $responseToResultTransformer * @return \GuzzleHttp\Command\Guzzle\Deserializer */ private function getDeserializer($responseToResultTransformer) { $process = (! isset($this->config['process']) || $this->config['process'] === true); return $responseToResultTransformer !== null ? $responseToResultTransformer : new Deserializer($this->description, $process); } /** * Get the config of the client * * @param array|string $option * @return mixed */ public function getConfig($option = null) { return $option === null ? $this->config : (isset($this->config[$option]) ? $this->config[$option] : []); } /** * @param $option * @param $value */ public function setConfig($option, $value) { $this->config[$option] = $value; } /** * Prepares the client based on the configuration settings of the client. * * @param array $config Constructor config as an array */ protected function processConfig(array $config) { // set defaults as an array if not provided if (!isset($config['defaults'])) { $config['defaults'] = []; } // Add the handlers based on the configuration option $stack = $this->getHandlerStack(); if (!isset($config['validate']) || $config['validate'] === true) { $stack->push(new ValidatedDescriptionHandler($this->description), 'validate_description'); } if (!isset($config['process']) || $config['process'] === true) { // TODO: This belongs to the Deserializer and should be handled there. // Question: What is the result when the Deserializer is bypassed? // Possible answer: The raw response. } } } src/Handler/ValidatedDescriptionHandler.php000064400000005654144760114130015033 0ustar00 */ class ValidatedDescriptionHandler { /** @var SchemaValidator $validator */ private $validator; /** @var DescriptionInterface $description */ private $description; /** * ValidatedDescriptionHandler constructor. * * @param DescriptionInterface $description * @param SchemaValidator|null $schemaValidator */ public function __construct(DescriptionInterface $description, SchemaValidator $schemaValidator = null) { $this->description = $description; $this->validator = $schemaValidator ?: new SchemaValidator(); } /** * @param callable $handler * @return \Closure */ public function __invoke(callable $handler) { return function (CommandInterface $command) use ($handler) { $errors = []; $operation = $this->description->getOperation($command->getName()); foreach ($operation->getParams() as $name => $schema) { $value = $command[$name]; if ($value) { $value = $schema->filter($value); } if (! $this->validator->validate($schema, $value)) { $errors = array_merge($errors, $this->validator->getErrors()); } elseif ($value !== $command[$name]) { // Update the config value if it changed and no validation errors were encountered. // This happen when the user extending an operation // See https://github.com/guzzle/guzzle-services/issues/145 $command[$name] = $value; } } if ($params = $operation->getAdditionalParameters()) { foreach ($command->toArray() as $name => $value) { // It's only additional if it isn't defined in the schema if (! $operation->hasParam($name)) { // Always set the name so that error messages are useful $params->setName($name); if (! $this->validator->validate($params, $value)) { $errors = array_merge($errors, $this->validator->getErrors()); } elseif ($value !== $command[$name]) { $command[$name] = $value; } } } } if ($errors) { throw new CommandException('Validation errors: ' . implode("\n", $errors), $command); } return $handler($command); }; } } src/Operation.php000064400000020416144760114130010010 0ustar00 '', 'httpMethod' => '', 'uri' => '', 'responseModel' => null, 'notes' => '', 'summary' => '', 'documentationUrl' => null, 'deprecated' => false, 'data' => [], 'parameters' => [], 'additionalParameters' => null, 'errorResponses' => [] ]; $this->description = $description === null ? new Description([]) : $description; if (isset($config['extends'])) { $config = $this->resolveExtends($config['extends'], $config); } $this->config = $config + $defaults; // Account for the old style of using responseClass if (isset($config['responseClass'])) { $this->config['responseModel'] = $config['responseClass']; } $this->resolveParameters(); } /** * @return array */ public function toArray() { return $this->config; } /** * Get the service description that the operation belongs to * * @return Description */ public function getServiceDescription() { return $this->description; } /** * Get the params of the operation * * @return Parameter[] */ public function getParams() { return $this->parameters; } /** * Get additionalParameters of the operation * * @return Parameter|null */ public function getAdditionalParameters() { return $this->additionalParameters; } /** * Check if the operation has a specific parameter by name * * @param string $name Name of the param * * @return bool */ public function hasParam($name) { return isset($this->parameters[$name]); } /** * Get a single parameter of the operation * * @param string $name Parameter to retrieve by name * * @return Parameter|null */ public function getParam($name) { return isset($this->parameters[$name]) ? $this->parameters[$name] : null; } /** * Get the HTTP method of the operation * * @return string|null */ public function getHttpMethod() { return $this->config['httpMethod']; } /** * Get the name of the operation * * @return string|null */ public function getName() { return $this->config['name']; } /** * Get a short summary of what the operation does * * @return string|null */ public function getSummary() { return $this->config['summary']; } /** * Get a longer text field to explain the behavior of the operation * * @return string|null */ public function getNotes() { return $this->config['notes']; } /** * Get the documentation URL of the operation * * @return string|null */ public function getDocumentationUrl() { return $this->config['documentationUrl']; } /** * Get the name of the model used for processing the response. * * @return string */ public function getResponseModel() { return $this->config['responseModel']; } /** * Get whether or not the operation is deprecated * * @return bool */ public function getDeprecated() { return $this->config['deprecated']; } /** * Get the URI that will be merged into the generated request * * @return string */ public function getUri() { return $this->config['uri']; } /** * Get the errors that could be encountered when executing the operation * * @return array */ public function getErrorResponses() { return $this->config['errorResponses']; } /** * Get extra data from the operation * * @param string $name Name of the data point to retrieve or null to * retrieve all of the extra data. * * @return mixed|null */ public function getData($name = null) { if ($name === null) { return $this->config['data']; } elseif (isset($this->config['data'][$name])) { return $this->config['data'][$name]; } else { return null; } } /** * @param $name * @param array $config * @return array */ private function resolveExtends($name, array $config) { if (!$this->description->hasOperation($name)) { throw new \InvalidArgumentException('No operation named ' . $name); } // Merge parameters together one level deep $base = $this->description->getOperation($name)->toArray(); $result = $config + $base; if (isset($base['parameters']) && isset($config['parameters'])) { $result['parameters'] = $config['parameters'] + $base['parameters']; } return $result; } /** * Process the description and extract the parameter config * * @return void */ private function resolveParameters() { // Parameters need special handling when adding foreach ($this->config['parameters'] as $name => $param) { if (!is_array($param)) { throw new \InvalidArgumentException( "Parameters must be arrays, {$this->config['name']}.$name is ".gettype($param) ); } $param['name'] = $name; $this->parameters[$name] = new Parameter( $param, ['description' => $this->description] ); } if ($this->config['additionalParameters']) { if (is_array($this->config['additionalParameters'])) { $this->additionalParameters = new Parameter( $this->config['additionalParameters'], ['description' => $this->description] ); } else { $this->additionalParameters = $this->config['additionalParameters']; } } } } src/Parameter.php000064400000043127144760114130007774 0ustar00originalData = $data; if (isset($options['description'])) { $this->serviceDescription = $options['description']; if (!($this->serviceDescription instanceof DescriptionInterface)) { throw new \InvalidArgumentException('description must be a Description'); } if (isset($data['$ref'])) { if ($model = $this->serviceDescription->getModel($data['$ref'])) { $name = isset($data['name']) ? $data['name'] : null; $data = $model->toArray() + $data; if ($name) { $data['name'] = $name; } } } elseif (isset($data['extends'])) { // If this parameter extends from another parameter then start // with the actual data union in the parent's data (e.g. actual // supersedes parent) if ($extends = $this->serviceDescription->getModel($data['extends'])) { $data += $extends->toArray(); } } } // Pull configuration data into the parameter foreach ($data as $key => $value) { $this->{$key} = $value; } $this->required = (bool) $this->required; $this->data = (array) $this->data; if ($this->filters) { $this->setFilters((array) $this->filters); } if ($this->type == 'object' && $this->additionalProperties === null) { $this->additionalProperties = true; } } /** * Convert the object to an array * * @return array */ public function toArray() { return $this->originalData; } /** * Get the default or static value of the command based on a value * * @param string $value Value that is currently set * * @return mixed Returns the value, a static value if one is present, or a default value */ public function getValue($value) { if ($this->static || ($this->default !== null && $value === null)) { return $this->default; } return $value; } /** * Run a value through the filters OR format attribute associated with the * parameter. * * @param mixed $value Value to filter * * @return mixed Returns the filtered value * @throws \RuntimeException when trying to format when no service * description is available. */ public function filter($value) { // Formats are applied exclusively and supersed filters if ($this->format) { if (!$this->serviceDescription) { throw new \RuntimeException('No service description was set so ' . 'the value cannot be formatted.'); } return $this->serviceDescription->format($this->format, $value); } // Convert Boolean values if ($this->type == 'boolean' && !is_bool($value)) { $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); } // Apply filters to the value if ($this->filters) { foreach ($this->filters as $filter) { if (is_array($filter)) { // Convert complex filters that hold value place holders foreach ($filter['args'] as &$data) { if ($data == '@value') { $data = $value; } elseif ($data == '@api') { $data = $this; } } $value = call_user_func_array( $filter['method'], $filter['args'] ); } else { $value = call_user_func($filter, $value); } } } return $value; } /** * Get the name of the parameter * * @return string */ public function getName() { return $this->name; } /** * Set the name of the parameter * * @param string $name Name to set */ public function setName($name) { $this->name = $name; } /** * Get the key of the parameter, where sentAs will supersede name if it is * set. * * @return string */ public function getWireName() { return $this->sentAs ?: $this->name; } /** * Get the type(s) of the parameter * * @return string|array */ public function getType() { return $this->type; } /** * Get if the parameter is required * * @return bool */ public function isRequired() { return $this->required; } /** * Get the default value of the parameter * * @return string|null */ public function getDefault() { return $this->default; } /** * Get the description of the parameter * * @return string|null */ public function getDescription() { return $this->description; } /** * Get the minimum acceptable value for an integer * * @return int|null */ public function getMinimum() { return $this->minimum; } /** * Get the maximum acceptable value for an integer * * @return int|null */ public function getMaximum() { return $this->maximum; } /** * Get the minimum allowed length of a string value * * @return int */ public function getMinLength() { return $this->minLength; } /** * Get the maximum allowed length of a string value * * @return int|null */ public function getMaxLength() { return $this->maxLength; } /** * Get the maximum allowed number of items in an array value * * @return int|null */ public function getMaxItems() { return $this->maxItems; } /** * Get the minimum allowed number of items in an array value * * @return int */ public function getMinItems() { return $this->minItems; } /** * Get the location of the parameter * * @return string|null */ public function getLocation() { return $this->location; } /** * Get the sentAs attribute of the parameter that used with locations to * sentAs an attribute when it is being applied to a location. * * @return string|null */ public function getSentAs() { return $this->sentAs; } /** * Retrieve a known property from the parameter by name or a data property * by name. When no specific name value is passed, all data properties * will be returned. * * @param string|null $name Specify a particular property name to retrieve * * @return array|mixed|null */ public function getData($name = null) { if (!$name) { return $this->data; } elseif (isset($this->data[$name])) { return $this->data[$name]; } elseif (isset($this->{$name})) { return $this->{$name}; } return null; } /** * Get whether or not the default value can be changed * * @return bool */ public function isStatic() { return $this->static; } /** * Get an array of filters used by the parameter * * @return array */ public function getFilters() { return $this->filters ?: []; } /** * Get the properties of the parameter * * @return Parameter[] */ public function getProperties() { if (!$this->propertiesCache) { $this->propertiesCache = []; foreach (array_keys($this->properties) as $name) { $this->propertiesCache[$name] = $this->getProperty($name); } } return $this->propertiesCache; } /** * Get a specific property from the parameter * * @param string $name Name of the property to retrieve * * @return null|Parameter */ public function getProperty($name) { if (!isset($this->properties[$name])) { return null; } if (!($this->properties[$name] instanceof self)) { $this->properties[$name]['name'] = $name; $this->properties[$name] = new static( $this->properties[$name], ['description' => $this->serviceDescription] ); } return $this->properties[$name]; } /** * Get the additionalProperties value of the parameter * * @return bool|Parameter|null */ public function getAdditionalProperties() { if (is_array($this->additionalProperties)) { $this->additionalProperties = new static( $this->additionalProperties, ['description' => $this->serviceDescription] ); } return $this->additionalProperties; } /** * Get the item data of the parameter * * @return Parameter */ public function getItems() { if (is_array($this->items)) { $this->items = new static( $this->items, ['description' => $this->serviceDescription] ); } return $this->items; } /** * Get the enum of strings that are valid for the parameter * * @return array|null */ public function getEnum() { return $this->enum; } /** * Get the regex pattern that must match a value when the value is a string * * @return string */ public function getPattern() { return $this->pattern; } /** * Get the format attribute of the schema * * @return string */ public function getFormat() { return $this->format; } /** * Set the array of filters used by the parameter * * @param array $filters Array of functions to use as filters * * @return self */ private function setFilters(array $filters) { $this->filters = []; foreach ($filters as $filter) { $this->addFilter($filter); } return $this; } /** * Add a filter to the parameter * * @param string|array $filter Method to filter the value through * * @return self * @throws \InvalidArgumentException */ private function addFilter($filter) { if (is_array($filter)) { if (!isset($filter['method'])) { throw new \InvalidArgumentException( 'A [method] value must be specified for each complex filter' ); } } if (!$this->filters) { $this->filters = [$filter]; } else { $this->filters[] = $filter; } return $this; } /** * Check if a parameter has a specific variable and if it set. * * @param string $var * @return bool */ public function has($var) { if (!is_string($var)) { throw new \InvalidArgumentException('Expected a string. Got: ' . (is_object($var) ? get_class($var) : gettype($var))); } return isset($this->{$var}) && !empty($this->{$var}); } } src/QuerySerializer/QuerySerializerInterface.php000064400000000443144760114130016165 0ustar00removeNumericIndices = $removeNumericIndices; } /** * {@inheritDoc} */ public function aggregate(array $queryParams) { $queryString = http_build_query($queryParams, null, '&', PHP_QUERY_RFC3986); if ($this->removeNumericIndices) { $queryString = preg_replace('/%5B[0-9]+%5D/simU', '%5B%5D', $queryString); } return $queryString; } }src/RequestLocation/AbstractLocation.php000064400000005442144760114130014427 0ustar00locationName = $locationName; } /** * @param CommandInterface $command * @param RequestInterface $request * @param Parameter $param * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { return $request; } /** * @param CommandInterface $command * @param RequestInterface $request * @param Operation $operation * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { return $request; } /** * Prepare (filter and set desired name for request item) the value for * request. * * @param mixed $value * @param Parameter $param * * @return array|mixed */ protected function prepareValue($value, Parameter $param) { return is_array($value) ? $this->resolveRecursively($value, $param) : $param->filter($value); } /** * Recursively prepare and filter nested values. * * @param array $value Value to map * @param Parameter $param Parameter related to the current key. * * @return array Returns the mapped array */ protected function resolveRecursively(array $value, Parameter $param) { foreach ($value as $name => &$v) { switch ($param->getType()) { case 'object': if ($subParam = $param->getProperty($name)) { $key = $subParam->getWireName(); $value[$key] = $this->prepareValue($v, $subParam); if ($name != $key) { unset($value[$name]); } } elseif ($param->getAdditionalProperties() instanceof Parameter) { $v = $this->prepareValue($v, $param->getAdditionalProperties()); } break; case 'array': if ($items = $param->getItems()) { $v = $this->prepareValue($v, $items); } break; } } return $param->filter($value); } } src/RequestLocation/BodyLocation.php000064400000002222144760114130013552 0ustar00getBody()->getContents(); $value = $command[$param->getName()]; $value = $param->getName() . '=' . $param->filter($value); if ($oldValue !== '') { $value = $oldValue . '&' . $value; } return $request->withBody(Psr7\Utils::streamFor($value)); } } src/RequestLocation/FormParamLocation.php000064400000004465144760114130014554 0ustar00formParamsData['form_params'][$param->getWireName()] = $this->prepareValue( $command[$param->getName()], $param ); return $request; } /** * @param CommandInterface $command * @param RequestInterface $request * @param Operation $operation * * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $data = $this->formParamsData; $this->formParamsData = []; $modify = []; // Add additional parameters to the form_params array $additional = $operation->getAdditionalParameters(); if ($additional && $additional->getLocation() == $this->locationName) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $data['form_params'][$key] = $this->prepareValue($value, $additional); } } } $body = http_build_query($data['form_params'], '', '&'); $modify['body'] = Psr7\Utils::streamFor($body); $modify['set_headers']['Content-Type'] = $this->contentType; return Psr7\Utils::modifyRequest($request, $modify); } } src/RequestLocation/HeaderLocation.php000064400000003352144760114130014052 0ustar00getName()]; return $request->withHeader($param->getWireName(), $param->filter($value)); } /** * @param CommandInterface $command * @param RequestInterface $request * @param Operation $operation * * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { /** @var Parameter $additional */ $additional = $operation->getAdditionalParameters(); if ($additional && ($additional->getLocation() === $this->locationName)) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $request = $request->withHeader($key, $additional->filter($value)); } } } return $request; } } src/RequestLocation/JsonLocation.php000064400000005123144760114130013571 0ustar00jsonContentType = $contentType; } /** * @param CommandInterface $command * @param RequestInterface $request * @param Parameter $param * * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { $this->jsonData[$param->getWireName()] = $this->prepareValue( $command[$param->getName()], $param ); return $request->withBody(Psr7\Utils::streamFor(Utils::jsonEncode($this->jsonData))); } /** * @param CommandInterface $command * @param RequestInterface $request * @param Operation $operation * * @return MessageInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $data = $this->jsonData; $this->jsonData = []; // Add additional parameters to the JSON document $additional = $operation->getAdditionalParameters(); if ($additional && ($additional->getLocation() === $this->locationName)) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $data[$key] = $this->prepareValue($value, $additional); } } } // Don't overwrite the Content-Type if one is set if ($this->jsonContentType && !$request->hasHeader('Content-Type')) { $request = $request->withHeader('Content-Type', $this->jsonContentType); } return $request->withBody(Psr7\Utils::streamFor(Utils::jsonEncode($data))); } } src/RequestLocation/MultiPartLocation.php000064400000004033144760114130014600 0ustar00multipartData[] = [ 'name' => $param->getWireName(), 'contents' => $this->prepareValue($command[$param->getName()], $param) ]; return $request; } /** * @param CommandInterface $command * @param RequestInterface $request * @param Operation $operation * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $data = $this->multipartData; $this->multipartData = []; $modify = []; $body = new Psr7\MultipartStream($data); $modify['body'] = Psr7\Utils::streamFor($body); $request = Psr7\Utils::modifyRequest($request, $modify); if ($request->getBody() instanceof Psr7\MultipartStream) { // Use a multipart/form-data POST if a Content-Type is not set. $request->withHeader('Content-Type', $this->contentType . $request->getBody()->getBoundary()); } return $request; } } src/RequestLocation/QueryLocation.php000064400000005164144760114130013772 0ustar00querySerializer = $querySerializer ?: new Rfc3986Serializer(); } /** * @param CommandInterface $command * @param RequestInterface $request * @param Parameter $param * * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { $uri = $request->getUri(); $query = Psr7\Query::parse($uri->getQuery()); $query[$param->getWireName()] = $this->prepareValue( $command[$param->getName()], $param ); $uri = $uri->withQuery($this->querySerializer->aggregate($query)); return $request->withUri($uri); } /** * @param CommandInterface $command * @param RequestInterface $request * @param Operation $operation * * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $additional = $operation->getAdditionalParameters(); if ($additional && $additional->getLocation() == $this->locationName) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $uri = $request->getUri(); $query = Psr7\Query::parse($uri->getQuery()); $query[$key] = $this->prepareValue( $value, $additional ); $uri = $uri->withQuery($this->querySerializer->aggregate($query)); $request = $request->withUri($uri); } } } return $request; } } src/RequestLocation/RequestLocationInterface.php000064400000002360144760114130016131 0ustar00contentType = $contentType; } /** * @param CommandInterface $command * @param RequestInterface $request * @param Parameter $param * * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { // Buffer and order the parameters to visit based on if they are // top-level attributes or child nodes. // @link https://github.com/guzzle/guzzle/pull/494 if ($param->getData('xmlAttribute')) { array_unshift($this->buffered, $param); } else { $this->buffered[] = $param; } return $request; } /** * @param CommandInterface $command * @param RequestInterface $request * @param Operation $operation * * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { foreach ($this->buffered as $param) { $this->visitWithValue( $command[$param->getName()], $param, $operation ); } $this->buffered = []; $additional = $operation->getAdditionalParameters(); if ($additional && $additional->getLocation() == $this->locationName) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $additional->setName($key); $this->visitWithValue($value, $additional, $operation); } } $additional->setName(null); } // If data was found that needs to be serialized, then do so $xml = ''; if ($this->writer) { $xml = $this->finishDocument($this->writer); } elseif ($operation->getData('xmlAllowEmpty')) { // Check if XML should always be sent for the command $writer = $this->createRootElement($operation); $xml = $this->finishDocument($writer); } if ($xml !== '') { $request = $request->withBody(Psr7\Utils::streamFor($xml)); // Don't overwrite the Content-Type if one is set if ($this->contentType && !$request->hasHeader('Content-Type')) { $request = $request->withHeader('Content-Type', $this->contentType); } } $this->writer = null; return $request; } /** * Create the root XML element to use with a request * * @param Operation $operation Operation object * * @return \XMLWriter */ protected function createRootElement(Operation $operation) { static $defaultRoot = ['name' => 'Request']; // If no root element was specified, then just wrap the XML in 'Request' $root = $operation->getData('xmlRoot') ?: $defaultRoot; // Allow the XML declaration to be customized with xmlEncoding $encoding = $operation->getData('xmlEncoding'); $writer = $this->startDocument($encoding); $writer->startElement($root['name']); // Create the wrapping element with no namespaces if no namespaces were present if (!empty($root['namespaces'])) { // Create the wrapping element with an array of one or more namespaces foreach ((array) $root['namespaces'] as $prefix => $uri) { $nsLabel = 'xmlns'; if (!is_numeric($prefix)) { $nsLabel .= ':'.$prefix; } $writer->writeAttribute($nsLabel, $uri); } } return $writer; } /** * Recursively build the XML body * * @param \XMLWriter $writer XML to modify * @param Parameter $param API Parameter * @param mixed $value Value to add */ protected function addXml(\XMLWriter $writer, Parameter $param, $value) { $value = $param->filter($value); $type = $param->getType(); $name = $param->getWireName(); $prefix = null; $namespace = $param->getData('xmlNamespace'); if (false !== strpos($name, ':')) { list($prefix, $name) = explode(':', $name, 2); } if ($type == 'object' || $type == 'array') { if (!$param->getData('xmlFlattened')) { if ($namespace) { $writer->startElementNS(null, $name, $namespace); } else { $writer->startElement($name); } } if ($param->getType() == 'array') { $this->addXmlArray($writer, $param, $value); } elseif ($param->getType() == 'object') { $this->addXmlObject($writer, $param, $value); } if (!$param->getData('xmlFlattened')) { $writer->endElement(); } return; } if ($param->getData('xmlAttribute')) { $this->writeAttribute($writer, $prefix, $name, $namespace, $value); } else { $this->writeElement($writer, $prefix, $name, $namespace, $value); } } /** * Write an attribute with namespace if used * * @param \XMLWriter $writer XMLWriter instance * @param string $prefix Namespace prefix if any * @param string $name Attribute name * @param string $namespace The uri of the namespace * @param string $value The attribute content */ protected function writeAttribute($writer, $prefix, $name, $namespace, $value) { if ($namespace) { $writer->writeAttributeNS($prefix, $name, $namespace, $value); } else { $writer->writeAttribute($name, $value); } } /** * Write an element with namespace if used * * @param \XMLWriter $writer XML writer resource * @param string $prefix Namespace prefix if any * @param string $name Element name * @param string $namespace The uri of the namespace * @param string $value The element content */ protected function writeElement(\XMLWriter $writer, $prefix, $name, $namespace, $value) { if ($namespace) { $writer->startElementNS($prefix, $name, $namespace); } else { $writer->startElement($name); } if (strpbrk($value, '<>&')) { $writer->writeCData($value); } else { $writer->writeRaw($value); } $writer->endElement(); } /** * Create a new xml writer and start a document * * @param string $encoding document encoding * * @return \XMLWriter the writer resource * @throws \RuntimeException if the document cannot be started */ protected function startDocument($encoding) { $this->writer = new \XMLWriter(); if (!$this->writer->openMemory()) { throw new \RuntimeException('Unable to open XML document in memory'); } if (!$this->writer->startDocument('1.0', $encoding)) { throw new \RuntimeException('Unable to start XML document'); } return $this->writer; } /** * End the document and return the output * * @param \XMLWriter $writer * * @return string the writer resource */ protected function finishDocument($writer) { $writer->endDocument(); return $writer->outputMemory(); } /** * Add an array to the XML * * @param \XMLWriter $writer * @param Parameter $param * @param $value */ protected function addXmlArray(\XMLWriter $writer, Parameter $param, &$value) { if ($items = $param->getItems()) { foreach ($value as $v) { $this->addXml($writer, $items, $v); } } } /** * Add an object to the XML * * @param \XMLWriter $writer * @param Parameter $param * @param $value */ protected function addXmlObject(\XMLWriter $writer, Parameter $param, &$value) { $noAttributes = []; // add values which have attributes foreach ($value as $name => $v) { if ($property = $param->getProperty($name)) { if ($property->getData('xmlAttribute')) { $this->addXml($writer, $property, $v); } else { $noAttributes[] = ['value' => $v, 'property' => $property]; } } } // now add values with no attributes foreach ($noAttributes as $element) { $this->addXml($writer, $element['property'], $element['value']); } } /** * @param $value * @param Parameter $param * @param Operation $operation */ private function visitWithValue( $value, Parameter $param, Operation $operation ) { if (!$this->writer) { $this->createRootElement($operation); } $this->addXml($this->writer, $param, $value); } } src/ResponseLocation/AbstractLocation.php000064400000003034144760114130014570 0ustar00locationName = $locationName; } /** * @param ResultInterface $result * @param ResponseInterface $response * @param Parameter $model * @return ResultInterface */ public function before( ResultInterface $result, ResponseInterface $response, Parameter $model ) { return $result; } /** * @param ResultInterface $result * @param ResponseInterface $response * @param Parameter $model * @return ResultInterface */ public function after( ResultInterface $result, ResponseInterface $response, Parameter $model ) { return $result; } /** * @param ResultInterface $result * @param ResponseInterface $response * @param Parameter $param * @return ResultInterface */ public function visit( ResultInterface $result, ResponseInterface $response, Parameter $param ) { return $result; } } src/ResponseLocation/BodyLocation.php000064400000001600144760114130013717 0ustar00getName()] = $param->filter($response->getBody()); return $result; } } src/ResponseLocation/HeaderLocation.php000064400000002207144760114130014216 0ustar00getName(); if ($header = $response->getHeader($param->getWireName())) { if (is_array($header)) { $header = array_shift($header); } $result[$name] = $param->filter($header); } return $result; } } src/ResponseLocation/JsonLocation.php000064400000013316144760114130013742 0ustar00getBody(); $body = $body ?: "{}"; $this->json = \GuzzleHttp\json_decode($body, true); // relocate named arrays, so that they have the same structure as // arrays nested in objects and visit can work on them in the same way if ($model->getType() === 'array' && ($name = $model->getName())) { $this->json = [$name => $this->json]; } return $result; } /** * @param ResultInterface $result * @param ResponseInterface $response * @param Parameter $model * @return ResultInterface */ public function after( ResultInterface $result, ResponseInterface $response, Parameter $model ) { // Handle additional, undefined properties $additional = $model->getAdditionalProperties(); if (!($additional instanceof Parameter)) { return $result; } // Use the model location as the default if one is not set on additional $addLocation = $additional->getLocation() ?: $model->getLocation(); if ($addLocation == $this->locationName) { foreach ($this->json as $prop => $val) { if (!isset($result[$prop])) { // Only recurse if there is a type specified $result[$prop] = $additional->getType() ? $this->recurse($additional, $val) : $val; } } } $this->json = []; return $result; } /** * @param ResultInterface $result * @param ResponseInterface $response * @param Parameter $param * @return Result|ResultInterface */ public function visit( ResultInterface $result, ResponseInterface $response, Parameter $param ) { $name = $param->getName(); $key = $param->getWireName(); // Check if the result should be treated as a list if ($param->getType() == 'array') { // Treat as javascript array if ($name) { // name provided, store it under a key in the array $subArray = isset($this->json[$key]) ? $this->json[$key] : null; $result[$name] = $this->recurse($param, $subArray); } else { // top-level `array` or an empty name $result = new Result(array_merge( $result->toArray(), $this->recurse($param, $this->json) )); } } elseif (isset($this->json[$key])) { $result[$name] = $this->recurse($param, $this->json[$key]); } return $result; } /** * Recursively process a parameter while applying filters * * @param Parameter $param API parameter being validated * @param mixed $value Value to process. * @return mixed|null */ private function recurse(Parameter $param, $value) { if (!is_array($value)) { return $param->filter($value); } $result = []; $type = $param->getType(); if ($type == 'array') { $items = $param->getItems(); foreach ($value as $val) { $result[] = $this->recurse($items, $val); } } elseif ($type == 'object' && !isset($value[0])) { // On the above line, we ensure that the array is associative and // not numerically indexed if ($properties = $param->getProperties()) { foreach ($properties as $property) { $key = $property->getWireName(); if (array_key_exists($key, $value)) { $result[$property->getName()] = $this->recurse( $property, $value[$key] ); // Remove from the value so that AP can later be handled unset($value[$key]); } } } // Only check additional properties if everything wasn't already // handled if ($value) { $additional = $param->getAdditionalProperties(); if ($additional === null || $additional === true) { // Merge the JSON under the resulting array $result += $value; } elseif ($additional instanceof Parameter) { // Process all child elements according to the given schema foreach ($value as $prop => $val) { $result[$prop] = $this->recurse($additional, $val); } } } } return $param->filter($result); } } src/ResponseLocation/ReasonPhraseLocation.php000064400000001667144760114130015431 0ustar00getName()] = $param->filter( $response->getReasonPhrase() ); return $result; } } src/ResponseLocation/ResponseLocationInterface.php000064400000003473144760114130016453 0ustar00getName()] = $param->filter($response->getStatusCode()); return $result; } } src/ResponseLocation/XmlLocation.php000064400000023362144760114130013573 0ustar00xml = simplexml_load_string((string) $response->getBody()); return $result; } /** * @param ResultInterface $result * @param ResponseInterface $response * @param Parameter $model * @return Result|ResultInterface */ public function after( ResultInterface $result, ResponseInterface $response, Parameter $model ) { // Handle additional, undefined properties $additional = $model->getAdditionalProperties(); if ($additional instanceof Parameter && $additional->getLocation() == $this->locationName ) { $result = new Result(array_merge( $result->toArray(), self::xmlToArray($this->xml) )); } $this->xml = null; return $result; } /** * @param ResultInterface $result * @param ResponseInterface $response * @param Parameter $param * @return ResultInterface */ public function visit( ResultInterface $result, ResponseInterface $response, Parameter $param ) { $sentAs = $param->getWireName(); $ns = null; if (strstr($sentAs, ':')) { list($ns, $sentAs) = explode(':', $sentAs); } // Process the primary property if (count($this->xml->children($ns, true)->{$sentAs})) { $result[$param->getName()] = $this->recursiveProcess( $param, $this->xml->children($ns, true)->{$sentAs} ); } return $result; } /** * Recursively process a parameter while applying filters * * @param Parameter $param API parameter being processed * @param \SimpleXMLElement $node Node being processed * @return array */ private function recursiveProcess( Parameter $param, \SimpleXMLElement $node ) { $result = []; $type = $param->getType(); if ($type == 'object') { $result = $this->processObject($param, $node); } elseif ($type == 'array') { $result = $this->processArray($param, $node); } else { // We are probably handling a flat data node (i.e. string or // integer), so let's check if it's childless, which indicates a // node containing plain text. if ($node->children()->count() == 0) { // Retrieve text from node $result = (string) $node; } } // Filter out the value if (isset($result)) { $result = $param->filter($result); } return $result; } /** * @param Parameter $param * @param \SimpleXMLElement $node * @return array */ private function processArray(Parameter $param, \SimpleXMLElement $node) { // Cast to an array if the value was a string, but should be an array $items = $param->getItems(); $sentAs = $items->getWireName(); $result = []; $ns = null; if (strstr($sentAs, ':')) { // Get namespace from the wire name list($ns, $sentAs) = explode(':', $sentAs); } else { // Get namespace from data $ns = $items->getData('xmlNs'); } if ($sentAs === null) { // A general collection of nodes foreach ($node as $child) { $result[] = $this->recursiveProcess($items, $child); } } else { // A collection of named, repeating nodes // (i.e. ) $children = $node->children($ns, true)->{$sentAs}; foreach ($children as $child) { $result[] = $this->recursiveProcess($items, $child); } } return $result; } /** * Process an object * * @param Parameter $param API parameter being parsed * @param \SimpleXMLElement $node Value to process * @return array */ private function processObject(Parameter $param, \SimpleXMLElement $node) { $result = $knownProps = $knownAttributes = []; // Handle known properties if ($properties = $param->getProperties()) { foreach ($properties as $property) { $name = $property->getName(); $sentAs = $property->getWireName(); $knownProps[$sentAs] = 1; if (strpos($sentAs, ':')) { list($ns, $sentAs) = explode(':', $sentAs); } else { $ns = $property->getData('xmlNs'); } if ($property->getData('xmlAttribute')) { // Handle XML attributes $result[$name] = (string) $node->attributes($ns, true)->{$sentAs}; $knownAttributes[$sentAs] = 1; } elseif (count($node->children($ns, true)->{$sentAs})) { // Found a child node matching wire name $childNode = $node->children($ns, true)->{$sentAs}; $result[$name] = $this->recursiveProcess( $property, $childNode ); } } } // Handle additional, undefined properties $additional = $param->getAdditionalProperties(); if ($additional instanceof Parameter) { // Process all child elements according to the given schema foreach ($node->children($additional->getData('xmlNs'), true) as $childNode) { $sentAs = $childNode->getName(); if (!isset($knownProps[$sentAs])) { $result[$sentAs] = $this->recursiveProcess( $additional, $childNode ); } } } elseif ($additional === null || $additional === true) { // Blindly transform the XML into an array preserving as much data // as possible. Remove processed, aliased properties. $array = array_diff_key(self::xmlToArray($node), $knownProps); // Remove @attributes that were explicitly plucked from the // attributes list. if (isset($array['@attributes']) && $knownAttributes) { $array['@attributes'] = array_diff_key($array['@attributes'], $knownProps); if (!$array['@attributes']) { unset($array['@attributes']); } } // Merge it together with the original result $result = array_merge($array, $result); } return $result; } /** * Convert an XML document to an array. * * @param \SimpleXMLElement $xml * @param int $nesting * @param null $ns * * @return array */ private static function xmlToArray( \SimpleXMLElement $xml, $ns = null, $nesting = 0 ) { $result = []; $children = $xml->children($ns, true); foreach ($children as $name => $child) { $attributes = (array) $child->attributes($ns, true); if (!isset($result[$name])) { $childArray = self::xmlToArray($child, $ns, $nesting + 1); $result[$name] = $attributes ? array_merge($attributes, $childArray) : $childArray; continue; } // A child element with this name exists so we're assuming // that the node contains a list of elements if (!is_array($result[$name])) { $result[$name] = [$result[$name]]; } elseif (!isset($result[$name][0])) { // Convert the first child into the first element of a numerically indexed array $firstResult = $result[$name]; $result[$name] = []; $result[$name][] = $firstResult; } $childArray = self::xmlToArray($child, $ns, $nesting + 1); if ($attributes) { $result[$name][] = array_merge($attributes, $childArray); } else { $result[$name][] = $childArray; } } // Extract text from node $text = trim((string) $xml); if ($text === '') { $text = null; } // Process attributes $attributes = (array) $xml->attributes($ns, true); if ($attributes) { if ($text !== null) { $result['value'] = $text; } $result = array_merge($attributes, $result); } elseif ($text !== null) { $result = $text; } // Make sure we're always returning an array if ($nesting == 0 && !is_array($result)) { $result = [$result]; } return $result; } } src/SchemaFormatter.php000064400000007217144760114130011140 0ustar00formatDateTime($value); case 'date-time-http': return $this->formatDateTimeHttp($value); case 'date': return $this->formatDate($value); case 'time': return $this->formatTime($value); case 'timestamp': return $this->formatTimestamp($value); case 'boolean-string': return $this->formatBooleanAsString($value); default: return $value; } } /** * Perform the actual DateTime formatting * * @param int|string|\DateTime $dateTime Date time value * @param string $format Format of the result * * @return string * @throws \InvalidArgumentException */ protected function dateFormatter($dateTime, $format) { if (is_numeric($dateTime)) { return gmdate($format, (int) $dateTime); } if (is_string($dateTime)) { $dateTime = new \DateTime($dateTime); } if ($dateTime instanceof \DateTimeInterface) { static $utc; if (!$utc) { $utc = new \DateTimeZone('UTC'); } return $dateTime->setTimezone($utc)->format($format); } throw new \InvalidArgumentException('Date/Time values must be either ' . 'be a string, integer, or DateTime object'); } /** * Create a ISO 8601 (YYYY-MM-DDThh:mm:ssZ) formatted date time value in * UTC time. * * @param string|integer|\DateTime $value Date time value * * @return string */ private function formatDateTime($value) { return $this->dateFormatter($value, 'Y-m-d\TH:i:s\Z'); } /** * Create an HTTP date (RFC 1123 / RFC 822) formatted UTC date-time string * * @param string|integer|\DateTime $value Date time value * * @return string */ private function formatDateTimeHttp($value) { return $this->dateFormatter($value, 'D, d M Y H:i:s \G\M\T'); } /** * Create a YYYY-MM-DD formatted string * * @param string|integer|\DateTime $value Date time value * * @return string */ private function formatDate($value) { return $this->dateFormatter($value, 'Y-m-d'); } /** * Create a hh:mm:ss formatted string * * @param string|integer|\DateTime $value Date time value * * @return string */ private function formatTime($value) { return $this->dateFormatter($value, 'H:i:s'); } /** * Formats a boolean value as a string * * @param string|integer|bool $value Value to convert to a boolean * 'true' / 'false' value * * @return string */ private function formatBooleanAsString($value) { return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false'; } /** * Return a UNIX timestamp in the UTC timezone * * @param string|integer|\DateTime $value Time value * * @return int */ private function formatTimestamp($value) { return (int) $this->dateFormatter($value, 'U'); } } src/SchemaValidator.php000064400000026514144760114130011123 0ustar00castIntegerToStringType = $castIntegerToStringType; } /** * @param Parameter $param * @param $value * @return bool */ public function validate(Parameter $param, &$value) { $this->errors = []; $this->recursiveProcess($param, $value); if (empty($this->errors)) { return true; } else { sort($this->errors); return false; } } /** * Get the errors encountered while validating * * @return array */ public function getErrors() { return $this->errors ?: []; } /** * From the allowable types, determine the type that the variable matches * * @param string|array $type Parameter type * @param mixed $value Value to determine the type * * @return string|false Returns the matching type on */ protected function determineType($type, $value) { foreach ((array) $type as $t) { if ($t == 'string' && (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) ) { return 'string'; } elseif ($t == 'object' && (is_array($value) || is_object($value))) { return 'object'; } elseif ($t == 'array' && is_array($value)) { return 'array'; } elseif ($t == 'integer' && is_integer($value)) { return 'integer'; } elseif ($t == 'boolean' && is_bool($value)) { return 'boolean'; } elseif ($t == 'number' && is_numeric($value)) { return 'number'; } elseif ($t == 'numeric' && is_numeric($value)) { return 'numeric'; } elseif ($t == 'null' && !$value) { return 'null'; } elseif ($t == 'any') { return 'any'; } } return false; } /** * Recursively validate a parameter * * @param Parameter $param API parameter being validated * @param mixed $value Value to validate and validate. The value may * change during this validate. * @param string $path Current validation path (used for error reporting) * @param int $depth Current depth in the validation validate * * @return bool Returns true if valid, or false if invalid */ protected function recursiveProcess( Parameter $param, &$value, $path = '', $depth = 0 ) { // Update the value by adding default or static values $value = $param->getValue($value); $required = $param->isRequired(); // if the value is null and the parameter is not required or is static, // then skip any further recursion if ((null === $value && !$required) || $param->isStatic()) { return true; } $type = $param->getType(); // Attempt to limit the number of times is_array is called by tracking // if the value is an array $valueIsArray = is_array($value); // If a name is set then update the path so that validation messages // are more helpful if ($name = $param->getName()) { $path .= "[{$name}]"; } if ($type == 'object') { // Determine whether or not this "value" has properties and should // be traversed $traverse = $temporaryValue = false; // Convert the value to an array if (!$valueIsArray && $value instanceof ToArrayInterface) { $value = $value->toArray(); } if ($valueIsArray) { // Ensure that the array is associative and not numerically // indexed if (isset($value[0])) { $this->errors[] = "{$path} must be an array of properties. Got a numerically indexed array."; return false; } $traverse = true; } elseif ($value === null) { // Attempt to let the contents be built up by default values if // possible $value = []; $temporaryValue = $valueIsArray = $traverse = true; } if ($traverse) { if ($properties = $param->getProperties()) { // if properties were found, validate each property foreach ($properties as $property) { $name = $property->getName(); if (isset($value[$name])) { $this->recursiveProcess($property, $value[$name], $path, $depth + 1); } else { $current = null; $this->recursiveProcess($property, $current, $path, $depth + 1); // Only set the value if it was populated if (null !== $current) { $value[$name] = $current; } } } } $additional = $param->getAdditionalProperties(); if ($additional !== true) { // If additional properties were found, then validate each // against the additionalProperties attr. $keys = array_keys($value); // Determine the keys that were specified that were not // listed in the properties of the schema $diff = array_diff($keys, array_keys($properties)); if (!empty($diff)) { // Determine which keys are not in the properties if ($additional instanceof Parameter) { foreach ($diff as $key) { $this->recursiveProcess($additional, $value[$key], "{$path}[{$key}]", $depth); } } else { // if additionalProperties is set to false and there // are additionalProperties in the values, then fail foreach ($diff as $prop) { $this->errors[] = sprintf('%s[%s] is not an allowed property', $path, $prop); } } } } // A temporary value will be used to traverse elements that // have no corresponding input value. This allows nested // required parameters with default values to bubble up into the // input. Here we check if we used a temp value and nothing // bubbled up, then we need to remote the value. if ($temporaryValue && empty($value)) { $value = null; $valueIsArray = false; } } } elseif ($type == 'array' && $valueIsArray && $param->getItems()) { foreach ($value as $i => &$item) { // Validate each item in an array against the items attribute of the schema $this->recursiveProcess($param->getItems(), $item, $path . "[{$i}]", $depth + 1); } } // If the value is required and the type is not null, then there is an // error if the value is not set if ($required && $value === null && $type != 'null') { $message = "{$path} is " . ($param->getType() ? ('a required ' . implode(' or ', (array) $param->getType())) : 'required'); if ($param->has('description')) { $message .= ': ' . $param->getDescription(); } $this->errors[] = $message; return false; } // Validate that the type is correct. If the type is string but an // integer was passed, the class can be instructed to cast the integer // to a string to pass validation. This is the default behavior. if ($type && (!$type = $this->determineType($type, $value))) { if ($this->castIntegerToStringType && $param->getType() == 'string' && is_integer($value) ) { $value = (string) $value; } else { $this->errors[] = "{$path} must be of type " . implode(' or ', (array) $param->getType()); } } // Perform type specific validation for strings, arrays, and integers if ($type == 'string') { // Strings can have enums which are a list of predefined values if (($enum = $param->getEnum()) && !in_array($value, $enum)) { $this->errors[] = "{$path} must be one of " . implode(' or ', array_map(function ($s) { return '"' . addslashes($s) . '"'; }, $enum)); } // Strings can have a regex pattern that the value must match if (($pattern = $param->getPattern()) && !preg_match($pattern, $value)) { $this->errors[] = "{$path} must match the following regular expression: {$pattern}"; } $strLen = null; if ($min = $param->getMinLength()) { $strLen = strlen($value); if ($strLen < $min) { $this->errors[] = "{$path} length must be greater than or equal to {$min}"; } } if ($max = $param->getMaxLength()) { if (($strLen ?: strlen($value)) > $max) { $this->errors[] = "{$path} length must be less than or equal to {$max}"; } } } elseif ($type == 'array') { $size = null; if ($min = $param->getMinItems()) { $size = count($value); if ($size < $min) { $this->errors[] = "{$path} must contain {$min} or more elements"; } } if ($max = $param->getMaxItems()) { if (($size ?: count($value)) > $max) { $this->errors[] = "{$path} must contain {$max} or fewer elements"; } } } elseif ($type == 'integer' || $type == 'number' || $type == 'numeric') { if (($min = $param->getMinimum()) && $value < $min) { $this->errors[] = "{$path} must be greater than or equal to {$min}"; } if (($max = $param->getMaximum()) && $value > $max) { $this->errors[] = "{$path} must be less than or equal to {$max}"; } } return empty($this->errors); } } src/Serializer.php000064400000013167144760114130010166 0ustar00 new BodyLocation(), 'query' => new QueryLocation(), 'header' => new HeaderLocation(), 'json' => new JsonLocation(), 'xml' => new XmlLocation(), 'formParam' => new FormParamLocation(), 'multipart' => new MultiPartLocation(), ]; } $this->locations = $requestLocations + $defaultRequestLocations; $this->description = $description; } /** * @param CommandInterface $command * @return RequestInterface */ public function __invoke(CommandInterface $command) { $request = $this->createRequest($command); return $this->prepareRequest($command, $request); } /** * Prepares a request for sending using location visitors * * @param CommandInterface $command * @param RequestInterface $request Request being created * @return RequestInterface * @throws \RuntimeException If a location cannot be handled */ protected function prepareRequest( CommandInterface $command, RequestInterface $request ) { $visitedLocations = []; $operation = $this->description->getOperation($command->getName()); // Visit each actual parameter foreach ($operation->getParams() as $name => $param) { /* @var Parameter $param */ $location = $param->getLocation(); // Skip parameters that have not been set or are URI location if ($location == 'uri' || !$command->hasParam($name)) { continue; } if (!isset($this->locations[$location])) { throw new \RuntimeException("No location registered for $name"); } $visitedLocations[$location] = true; $request = $this->locations[$location]->visit($command, $request, $param); } // Ensure that the after() method is invoked for additionalParameters /** @var Parameter $additional */ if ($additional = $operation->getAdditionalParameters()) { $visitedLocations[$additional->getLocation()] = true; } // Call the after() method for each visited location foreach (array_keys($visitedLocations) as $location) { $request = $this->locations[$location]->after($command, $request, $operation); } return $request; } /** * Create a request for the command and operation * * @param CommandInterface $command * * @return RequestInterface * @throws \RuntimeException */ protected function createRequest(CommandInterface $command) { $operation = $this->description->getOperation($command->getName()); // If command does not specify a template, assume the client's base URL. if (null === $operation->getUri()) { return new Request( $operation->getHttpMethod() ?: 'GET', $this->description->getBaseUri() ); } return $this->createCommandWithUri($operation, $command); } /** * Create a request for an operation with a uri merged onto a base URI * * @param \GuzzleHttp\Command\Guzzle\Operation $operation * @param \GuzzleHttp\Command\CommandInterface $command * * @return \GuzzleHttp\Psr7\Request */ private function createCommandWithUri( Operation $operation, CommandInterface $command ) { // Get the path values and use the client config settings $variables = []; foreach ($operation->getParams() as $name => $arg) { /* @var Parameter $arg */ if ($arg->getLocation() == 'uri') { if (isset($command[$name])) { $variables[$name] = $arg->filter($command[$name]); if (!is_array($variables[$name])) { $variables[$name] = (string) $variables[$name]; } } } } // Expand the URI template. $uri = new Uri(UriTemplate::expand($operation->getUri(), $variables)); return new Request( $operation->getHttpMethod() ?: 'GET', UriResolver::resolve($this->description->getBaseUri(), $uri) ); } }