Generator.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Webapi\Model\Rest\Swagger;
  7. use Magento\Framework\Api\SimpleDataObjectConverter;
  8. use Magento\Framework\App\ProductMetadataInterface;
  9. use Magento\Framework\Reflection\TypeProcessor;
  10. use Magento\Framework\Webapi\Authorization;
  11. use Magento\Framework\Webapi\Exception as WebapiException;
  12. use Magento\Webapi\Controller\Rest;
  13. use Magento\Webapi\Model\AbstractSchemaGenerator;
  14. use Magento\Webapi\Model\Config\Converter;
  15. use Magento\Webapi\Model\Rest\Swagger;
  16. use Magento\Webapi\Model\Rest\SwaggerFactory;
  17. use Magento\Webapi\Model\ServiceMetadata;
  18. /**
  19. * REST Swagger schema generator.
  20. *
  21. * Generate REST API description in a format of JSON document,
  22. * compliant with {@link https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md Swagger specification}
  23. *
  24. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  25. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  26. */
  27. class Generator extends AbstractSchemaGenerator
  28. {
  29. /**
  30. * Error response schema
  31. */
  32. const ERROR_SCHEMA = '#/definitions/error-response';
  33. /**
  34. * Unauthorized description
  35. */
  36. const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized';
  37. /** Array signifier */
  38. const ARRAY_SIGNIFIER = '[0]';
  39. /**
  40. * Swagger factory instance.
  41. *
  42. * @var SwaggerFactory
  43. */
  44. protected $swaggerFactory;
  45. /**
  46. * Magento product metadata
  47. *
  48. * @var ProductMetadataInterface
  49. */
  50. protected $productMetadata;
  51. /**
  52. * A map of Tags
  53. *
  54. * example:
  55. * [
  56. * class1Name => tag information,
  57. * class2Name => tag information,
  58. * ...
  59. * ]
  60. *
  61. * @var array
  62. */
  63. protected $tags = [];
  64. /**
  65. * A map of definition
  66. *
  67. * example:
  68. * [
  69. * definitionName1 => definition,
  70. * definitionName2 => definition,
  71. * ...
  72. * ]
  73. * Note: definitionName is converted from class name
  74. * @var array
  75. */
  76. protected $definitions = [];
  77. /**
  78. * List of simple parameter types not to be processed by the definitions generator
  79. * Contains mapping to the internal swagger simple types
  80. *
  81. * @var string[]
  82. */
  83. protected $simpleTypeList = [
  84. 'bool' => 'boolean',
  85. 'boolean' => 'boolean',
  86. 'int' => 'integer',
  87. 'integer' => 'integer',
  88. 'double' => 'number',
  89. 'float' => 'number',
  90. 'number' => 'number',
  91. 'string' => 'string',
  92. TypeProcessor::ANY_TYPE => 'string',
  93. TypeProcessor::NORMALIZED_ANY_TYPE => 'string',
  94. ];
  95. /**
  96. * Initialize dependencies.
  97. *
  98. * @param \Magento\Webapi\Model\Cache\Type\Webapi $cache
  99. * @param \Magento\Framework\Reflection\TypeProcessor $typeProcessor
  100. * @param \Magento\Framework\Webapi\CustomAttribute\ServiceTypeListInterface $serviceTypeList
  101. * @param \Magento\Webapi\Model\ServiceMetadata $serviceMetadata
  102. * @param Authorization $authorization
  103. * @param SwaggerFactory $swaggerFactory
  104. * @param \Magento\Framework\App\ProductMetadataInterface $productMetadata
  105. */
  106. public function __construct(
  107. \Magento\Webapi\Model\Cache\Type\Webapi $cache,
  108. \Magento\Framework\Reflection\TypeProcessor $typeProcessor,
  109. \Magento\Framework\Webapi\CustomAttribute\ServiceTypeListInterface $serviceTypeList,
  110. \Magento\Webapi\Model\ServiceMetadata $serviceMetadata,
  111. Authorization $authorization,
  112. SwaggerFactory $swaggerFactory,
  113. ProductMetadataInterface $productMetadata
  114. ) {
  115. $this->swaggerFactory = $swaggerFactory;
  116. $this->productMetadata = $productMetadata;
  117. parent::__construct(
  118. $cache,
  119. $typeProcessor,
  120. $serviceTypeList,
  121. $serviceMetadata,
  122. $authorization
  123. );
  124. }
  125. /**
  126. * {@inheritdoc}
  127. */
  128. protected function generateSchema($requestedServiceMetadata, $requestScheme, $requestHost, $endpointUrl)
  129. {
  130. /** @var Swagger $swagger */
  131. $swagger = $this->swaggerFactory->create();
  132. $swagger->setInfo($this->getGeneralInfo());
  133. $this->addCustomAttributeTypes();
  134. $swagger->setHost($requestHost);
  135. $swagger->setBasePath(strstr($endpointUrl, Rest::SCHEMA_PATH, true));
  136. $swagger->setSchemes([$requestScheme]);
  137. foreach ($requestedServiceMetadata as $serviceName => $serviceData) {
  138. if (!isset($this->tags[$serviceName])) {
  139. $this->tags[$serviceName] = $this->generateTagInfo($serviceName, $serviceData);
  140. $swagger->addTag($this->tags[$serviceName]);
  141. }
  142. foreach ($serviceData[Converter::KEY_ROUTES] as $uri => $httpMethods) {
  143. $uri = $this->convertPathParams($uri);
  144. foreach ($httpMethods as $httpOperation => $httpMethodData) {
  145. $httpOperation = strtolower($httpOperation);
  146. $phpMethodData = $serviceData[Converter::KEY_METHODS][$httpMethodData[Converter::KEY_METHOD]];
  147. $httpMethodData[Converter::KEY_METHOD] = $phpMethodData;
  148. $httpMethodData['uri'] = $uri;
  149. $httpMethodData['httpOperation'] = $httpOperation;
  150. $swagger->addPath(
  151. $this->convertPathParams($uri),
  152. $httpOperation,
  153. $this->generatePathInfo($httpOperation, $httpMethodData, $serviceName)
  154. );
  155. }
  156. }
  157. }
  158. $swagger->setDefinitions($this->getDefinitions());
  159. return $swagger->toSchema();
  160. }
  161. /**
  162. * Get the 'Info' section data
  163. *
  164. * @return string[]
  165. */
  166. protected function getGeneralInfo()
  167. {
  168. $versionParts = explode('.', $this->productMetadata->getVersion());
  169. if (!isset($versionParts[0]) || !isset($versionParts[1])) {
  170. return []; // Major and minor version are not set - return empty response
  171. }
  172. $majorMinorVersion = $versionParts[0] . '.' . $versionParts[1];
  173. return [
  174. 'version' => $majorMinorVersion,
  175. 'title' => $this->productMetadata->getName() . ' ' . $this->productMetadata->getEdition(),
  176. ];
  177. }
  178. /**
  179. * Generate path info based on method data
  180. *
  181. * @param string $methodName
  182. * @param array $httpMethodData
  183. * @param string $tagName
  184. * @return array
  185. */
  186. protected function generatePathInfo($methodName, $httpMethodData, $tagName)
  187. {
  188. $methodData = $httpMethodData[Converter::KEY_METHOD];
  189. $operationId = $this->typeProcessor->getOperationName($tagName, $methodData[Converter::KEY_METHOD]);
  190. $operationId .= ucfirst($methodName);
  191. $pathInfo = [
  192. 'tags' => [$tagName],
  193. 'description' => $methodData['documentation'],
  194. 'operationId' => $operationId,
  195. ];
  196. $parameters = $this->generateMethodParameters($httpMethodData, $operationId);
  197. if ($parameters) {
  198. $pathInfo['parameters'] = $parameters;
  199. }
  200. $pathInfo['responses'] = $this->generateMethodResponses($methodData);
  201. return $pathInfo;
  202. }
  203. /**
  204. * Generate response based on method data
  205. *
  206. * @param array $methodData
  207. * @return array
  208. */
  209. protected function generateMethodResponses($methodData)
  210. {
  211. $responses = [];
  212. if (isset($methodData['interface']['out']['parameters'])
  213. && is_array($methodData['interface']['out']['parameters'])
  214. ) {
  215. $parameters = $methodData['interface']['out']['parameters'];
  216. $responses = $this->generateMethodSuccessResponse($parameters, $responses);
  217. }
  218. /** Handle authorization exceptions that may not be documented */
  219. if (isset($methodData['resources'])) {
  220. foreach ($methodData['resources'] as $resourceName) {
  221. if ($resourceName !== 'anonymous') {
  222. $responses[WebapiException::HTTP_UNAUTHORIZED]['description'] = self::UNAUTHORIZED_DESCRIPTION;
  223. $responses[WebapiException::HTTP_UNAUTHORIZED]['schema']['$ref'] = self::ERROR_SCHEMA;
  224. break;
  225. }
  226. }
  227. }
  228. if (isset($methodData['interface']['out']['throws'])
  229. && is_array($methodData['interface']['out']['throws'])
  230. ) {
  231. foreach ($methodData['interface']['out']['throws'] as $exceptionClass) {
  232. $responses = $this->generateMethodExceptionErrorResponses($exceptionClass, $responses);
  233. }
  234. }
  235. $responses['default']['description'] = 'Unexpected error';
  236. $responses['default']['schema']['$ref'] = self::ERROR_SCHEMA;
  237. return $responses;
  238. }
  239. /**
  240. * Generate parameters based on method data
  241. *
  242. * @param array $httpMethodData
  243. * @param string $operationId
  244. * @return array
  245. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  246. * @SuppressWarnings(PHPMD.NPathComplexity)
  247. */
  248. private function generateMethodParameters($httpMethodData, $operationId)
  249. {
  250. $bodySchema = [];
  251. $parameters = [];
  252. $phpMethodData = $httpMethodData[Converter::KEY_METHOD];
  253. /** Return nothing if necessary fields are not set */
  254. if (!isset($phpMethodData['interface']['in']['parameters'])
  255. || !isset($httpMethodData['uri'])
  256. || !isset($httpMethodData['httpOperation'])
  257. ) {
  258. return [];
  259. }
  260. foreach ($phpMethodData['interface']['in']['parameters'] as $parameterName => $parameterInfo) {
  261. /** Omit forced parameters */
  262. if (isset($httpMethodData['parameters'][$parameterName]['force'])
  263. && $httpMethodData['parameters'][$parameterName]['force']
  264. ) {
  265. continue;
  266. }
  267. if (!isset($parameterInfo['type'])) {
  268. return [];
  269. }
  270. $description = isset($parameterInfo['documentation']) ? $parameterInfo['documentation'] : null;
  271. /** Get location of parameter */
  272. if (strpos($httpMethodData['uri'], '{' . $parameterName . '}') !== false) {
  273. $parameters[] = $this->generateMethodPathParameter($parameterName, $parameterInfo, $description);
  274. } elseif (strtoupper($httpMethodData['httpOperation']) === 'GET') {
  275. $parameters = $this->generateMethodQueryParameters(
  276. $parameterName,
  277. $parameterInfo,
  278. $description,
  279. $parameters
  280. );
  281. } else {
  282. $bodySchema = $this->generateBodySchema(
  283. $parameterName,
  284. $parameterInfo,
  285. $description,
  286. $bodySchema
  287. );
  288. }
  289. }
  290. /**
  291. * Add all the path params that don't correspond directly the PHP parameters
  292. */
  293. preg_match_all('#\\{([^\\{\\}]*)\\}#', $httpMethodData['uri'], $allPathParams);
  294. $remainingPathParams = array_diff(
  295. $allPathParams[1],
  296. array_keys($phpMethodData['interface']['in']['parameters'])
  297. );
  298. foreach ($remainingPathParams as $pathParam) {
  299. $parameters[] = [
  300. 'name' => $pathParam,
  301. 'in' => 'path',
  302. 'type' => 'string',
  303. 'required' => true
  304. ];
  305. }
  306. if ($bodySchema) {
  307. $bodyParam = [];
  308. $bodyParam['name'] = $operationId . 'Body';
  309. $bodyParam['in'] = 'body';
  310. $bodyParam['schema'] = $bodySchema;
  311. $parameters[] = $bodyParam;
  312. }
  313. return $parameters;
  314. }
  315. /**
  316. * Creates an array for the given query parameter
  317. *
  318. * @param string $name
  319. * @param string $type
  320. * @param string $description
  321. * @param bool|null $required
  322. * @return array
  323. */
  324. private function createQueryParam($name, $type, $description, $required = null)
  325. {
  326. $param = [
  327. 'name' => $name,
  328. 'in' => 'query',
  329. ];
  330. $param = array_merge($param, $this->getObjectSchema($type, $description));
  331. if (isset($required)) {
  332. $param['required'] = $required;
  333. }
  334. return $param;
  335. }
  336. /**
  337. * Generate Tag Info for given service
  338. *
  339. * @param string $serviceName
  340. * @param array $serviceData
  341. * @return string[]
  342. */
  343. protected function generateTagInfo($serviceName, $serviceData)
  344. {
  345. $tagInfo = [];
  346. $tagInfo['name'] = $serviceName;
  347. if (!empty($serviceData) && is_array($serviceData)) {
  348. $tagInfo['description'] = $serviceData[Converter::KEY_DESCRIPTION];
  349. }
  350. return $tagInfo;
  351. }
  352. /**
  353. * Generate definition for given type
  354. *
  355. * @param string $typeName
  356. * @param string $description
  357. * @return array
  358. */
  359. protected function getObjectSchema($typeName, $description = '')
  360. {
  361. $simpleType = $this->getSimpleType($typeName);
  362. if ($simpleType == false) {
  363. $result = ['type' => 'array'];
  364. if (!empty($description)) {
  365. $result['description'] = $description;
  366. }
  367. $trimedTypeName = rtrim($typeName, '[]');
  368. if ($simpleType = $this->getSimpleType($trimedTypeName)) {
  369. $result['items'] = ['type' => $simpleType];
  370. } else {
  371. if (strpos($typeName, '[]') !== false) {
  372. $result['items'] = ['$ref' => $this->getDefinitionReference($trimedTypeName)];
  373. } else {
  374. $result = ['$ref' => $this->getDefinitionReference($trimedTypeName)];
  375. }
  376. if (!$this->isDefinitionExists($trimedTypeName)) {
  377. $definitionKey = $this->toLowerCaseDashSeparated($trimedTypeName);
  378. $this->definitions[$definitionKey] = [];
  379. $this->definitions[$definitionKey] = $this->generateDefinition($trimedTypeName);
  380. }
  381. }
  382. } else {
  383. $result = ['type' => $simpleType];
  384. if (!empty($description)) {
  385. $result['description'] = $description;
  386. }
  387. }
  388. return $result;
  389. }
  390. /**
  391. * Generate definition for given type
  392. *
  393. * @param string $typeName
  394. * @return array
  395. */
  396. protected function generateDefinition($typeName)
  397. {
  398. $properties = [];
  399. $requiredProperties = [];
  400. $typeData = $this->typeProcessor->getTypeData($typeName);
  401. if (isset($typeData['parameters'])) {
  402. foreach ($typeData['parameters'] as $parameterName => $parameterData) {
  403. $properties[$parameterName] = $this->getObjectSchema(
  404. $parameterData['type'],
  405. $parameterData['documentation']
  406. );
  407. if ($parameterData['required']) {
  408. $requiredProperties[] = $parameterName;
  409. }
  410. }
  411. }
  412. $definition = ['type' => 'object'];
  413. if (isset($typeData['documentation'])) {
  414. $definition['description'] = $typeData['documentation'];
  415. }
  416. if (!empty($properties)) {
  417. $definition['properties'] = $properties;
  418. }
  419. if (!empty($requiredProperties)) {
  420. $definition['required'] = $requiredProperties;
  421. }
  422. return $definition;
  423. }
  424. /**
  425. * Get definitions
  426. *
  427. * @return array
  428. * Todo: create interface for error response
  429. */
  430. protected function getDefinitions()
  431. {
  432. return array_merge(
  433. [
  434. 'error-response' => [
  435. 'type' => 'object',
  436. 'properties' => [
  437. 'message' => [
  438. 'type' => 'string',
  439. 'description' => 'Error message',
  440. ],
  441. 'errors' => [
  442. '$ref' => '#/definitions/error-errors',
  443. ],
  444. 'code' => [
  445. 'type' => 'integer',
  446. 'description' => 'Error code',
  447. ],
  448. 'parameters' => [
  449. '$ref' => '#/definitions/error-parameters',
  450. ],
  451. 'trace' => [
  452. 'type' => 'string',
  453. 'description' => 'Stack trace',
  454. ],
  455. ],
  456. 'required' => ['message'],
  457. ],
  458. 'error-errors' => [
  459. 'type' => 'array',
  460. 'description' => 'Errors list',
  461. 'items' => [
  462. '$ref' => '#/definitions/error-errors-item',
  463. ],
  464. ],
  465. 'error-errors-item' => [
  466. 'type' => 'object',
  467. 'description' => 'Error details',
  468. 'properties' => [
  469. 'message' => [
  470. 'type' => 'string',
  471. 'description' => 'Error message',
  472. ],
  473. 'parameters' => [
  474. '$ref' => '#/definitions/error-parameters',
  475. ],
  476. ],
  477. ],
  478. 'error-parameters' => [
  479. 'type' => 'array',
  480. 'description' => 'Error parameters list',
  481. 'items' => [
  482. '$ref' => '#/definitions/error-parameters-item',
  483. ],
  484. ],
  485. 'error-parameters-item' => [
  486. 'type' => 'object',
  487. 'description' => 'Error parameters item',
  488. 'properties' => [
  489. 'resources' => [
  490. 'type' => 'string',
  491. 'description' => 'ACL resource',
  492. ],
  493. 'fieldName' => [
  494. 'type' => 'string',
  495. 'description' => 'Missing or invalid field name'
  496. ],
  497. 'fieldValue' => [
  498. 'type' => 'string',
  499. 'description' => 'Incorrect field value'
  500. ],
  501. ],
  502. ],
  503. ],
  504. $this->snakeCaseDefinitions($this->definitions)
  505. );
  506. }
  507. /**
  508. * Converts definitions' properties array to snake_case.
  509. *
  510. * @param array $definitions
  511. * @return array
  512. */
  513. private function snakeCaseDefinitions($definitions)
  514. {
  515. foreach ($definitions as $name => $vals) {
  516. if (!empty($vals['properties'])) {
  517. $definitions[$name]['properties'] = $this->convertArrayToSnakeCase($vals['properties']);
  518. }
  519. if (!empty($vals['required'])) {
  520. $snakeCaseRequired = [];
  521. foreach ($vals['required'] as $requiredProperty) {
  522. $snakeCaseRequired[] = SimpleDataObjectConverter::camelCaseToSnakeCase($requiredProperty);
  523. }
  524. $definitions[$name]['required'] = $snakeCaseRequired;
  525. }
  526. }
  527. return $definitions;
  528. }
  529. /**
  530. * Converts associative array's key names from camelCase to snake_case, recursively.
  531. *
  532. * @param array $properties
  533. * @return array
  534. */
  535. private function convertArrayToSnakeCase($properties)
  536. {
  537. foreach ($properties as $name => $value) {
  538. $snakeCaseName = SimpleDataObjectConverter::camelCaseToSnakeCase($name);
  539. if (is_array($value)) {
  540. $value = $this->convertArrayToSnakeCase($value);
  541. }
  542. unset($properties[$name]);
  543. $properties[$snakeCaseName] = $value;
  544. }
  545. return $properties;
  546. }
  547. /**
  548. * Get definition reference
  549. *
  550. * @param string $typeName
  551. * @return string
  552. */
  553. protected function getDefinitionReference($typeName)
  554. {
  555. return '#/definitions/' . $this->toLowerCaseDashSeparated($typeName);
  556. }
  557. /**
  558. * Get the CamelCased type name in 'hyphen-separated-lowercase-words' format
  559. *
  560. * e.g. test-module5-v1-entity-all-soap-and-rest
  561. *
  562. * @param string $typeName
  563. * @return string
  564. */
  565. protected function toLowerCaseDashSeparated($typeName)
  566. {
  567. return strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $typeName));
  568. }
  569. /**
  570. * Check if definition exists
  571. *
  572. * @param string $typeName
  573. * @return bool
  574. */
  575. protected function isDefinitionExists($typeName)
  576. {
  577. return isset($this->definitions[$this->toLowerCaseDashSeparated($typeName)]);
  578. }
  579. /**
  580. * Create and add custom attribute types
  581. *
  582. * @return void
  583. */
  584. protected function addCustomAttributeTypes()
  585. {
  586. foreach ($this->serviceTypeList->getDataTypes() as $customAttributeClass) {
  587. $this->typeProcessor->register($customAttributeClass);
  588. }
  589. }
  590. /**
  591. * Get service metadata
  592. *
  593. * @param string $serviceName
  594. * @return array
  595. */
  596. protected function getServiceMetadata($serviceName)
  597. {
  598. return $this->serviceMetadata->getRouteMetadata($serviceName);
  599. }
  600. /**
  601. * Get the simple type supported by Swagger, or false if type is not simple
  602. *
  603. * @param string $type
  604. * @return bool|string
  605. */
  606. protected function getSimpleType($type)
  607. {
  608. if (array_key_exists($type, $this->simpleTypeList)) {
  609. return $this->simpleTypeList[$type];
  610. } else {
  611. return false;
  612. }
  613. }
  614. /**
  615. * Return the parameter names to describe a given parameter, mapped to the respective type
  616. *
  617. * Query parameters may be complex types, and multiple parameters will be listed in the schema to outline
  618. * the structure of the type.
  619. *
  620. * @param string $name
  621. * @param string $type
  622. * @param string $description
  623. * @param string $prefix
  624. * @return string[]
  625. */
  626. protected function getQueryParamNames($name, $type, $description, $prefix = '')
  627. {
  628. if ($this->typeProcessor->isTypeSimple($type)) {
  629. // Primitive type or array of primitive types
  630. return [
  631. $this->handlePrimitive($name, $prefix) => [
  632. 'type' => substr($type, -2) === '[]' ? $type : $this->getSimpleType($type),
  633. 'description' => $description
  634. ]
  635. ];
  636. }
  637. if ($this->typeProcessor->isArrayType($type)) {
  638. // Array of complex type
  639. $arrayType = substr($type, 0, -2);
  640. return $this->handleComplex($name, $arrayType, $prefix, true);
  641. } else {
  642. // Complex type
  643. return $this->handleComplex($name, $type, $prefix, false);
  644. }
  645. }
  646. /**
  647. * Recursively generate the query param names for a complex type
  648. *
  649. * @param string $name
  650. * @param string $type
  651. * @param string $prefix
  652. * @param bool $isArray
  653. * @return string[]
  654. */
  655. private function handleComplex($name, $type, $prefix, $isArray)
  656. {
  657. $parameters = $this->typeProcessor->getTypeData($type)['parameters'];
  658. $queryNames = [];
  659. foreach ($parameters as $subParameterName => $subParameterInfo) {
  660. $subParameterType = $subParameterInfo['type'];
  661. $subParameterDescription = isset($subParameterInfo['documentation'])
  662. ? $subParameterInfo['documentation']
  663. : null;
  664. $subPrefix = $prefix
  665. ? $prefix . '[' . $name . ']'
  666. : $name;
  667. if ($isArray) {
  668. $subPrefix .= self::ARRAY_SIGNIFIER;
  669. }
  670. $queryNames = array_merge(
  671. $queryNames,
  672. $this->getQueryParamNames($subParameterName, $subParameterType, $subParameterDescription, $subPrefix)
  673. );
  674. }
  675. return $queryNames;
  676. }
  677. /**
  678. * Generate the query param name for a primitive type
  679. *
  680. * @param string $name
  681. * @param string $prefix
  682. * @return string
  683. */
  684. private function handlePrimitive($name, $prefix)
  685. {
  686. return $prefix
  687. ? $prefix . '[' . $name . ']'
  688. : $name;
  689. }
  690. /**
  691. * Convert path parameters from :param to {param}
  692. *
  693. * @param string $uri
  694. * @return string
  695. */
  696. private function convertPathParams($uri)
  697. {
  698. $parts = explode('/', $uri);
  699. $count = count($parts);
  700. for ($i=0; $i < $count; $i++) {
  701. if (strpos($parts[$i], ':') === 0) {
  702. $parts[$i] = '{' . substr($parts[$i], 1) . '}';
  703. }
  704. }
  705. return implode('/', $parts);
  706. }
  707. /**
  708. * Generate method path parameter
  709. *
  710. * @param string $parameterName
  711. * @param array $parameterInfo
  712. * @param string $description
  713. * @return string[]
  714. */
  715. private function generateMethodPathParameter($parameterName, $parameterInfo, $description)
  716. {
  717. $param = [
  718. 'name' => $parameterName,
  719. 'in' => 'path',
  720. 'type' => $this->getSimpleType($parameterInfo['type']),
  721. 'required' => true
  722. ];
  723. if ($description) {
  724. $param['description'] = $description;
  725. return $param;
  726. }
  727. return $param;
  728. }
  729. /**
  730. * Generate method query parameters
  731. *
  732. * @param string $parameterName
  733. * @param array $parameterInfo
  734. * @param string $description
  735. * @param array $parameters
  736. * @return array
  737. */
  738. private function generateMethodQueryParameters($parameterName, $parameterInfo, $description, $parameters)
  739. {
  740. $queryParams = $this->getQueryParamNames($parameterName, $parameterInfo['type'], $description);
  741. if (count($queryParams) === 1) {
  742. // handle simple query parameter (includes the 'required' field)
  743. $parameters[] = $this->createQueryParam(
  744. $parameterName,
  745. $parameterInfo['type'],
  746. $description,
  747. $parameterInfo['required']
  748. );
  749. } else {
  750. /**
  751. * Complex query parameters are represented by a set of names which describes the object's fields.
  752. *
  753. * Omits the 'required' field.
  754. */
  755. foreach ($queryParams as $name => $queryParamInfo) {
  756. $parameters[] = $this->createQueryParam(
  757. $name,
  758. $queryParamInfo['type'],
  759. $queryParamInfo['description']
  760. );
  761. }
  762. }
  763. return $parameters;
  764. }
  765. /**
  766. * Generate body schema
  767. *
  768. * @param string $parameterName
  769. * @param array $parameterInfo
  770. * @param string $description
  771. * @param array $bodySchema
  772. * @return array
  773. */
  774. private function generateBodySchema($parameterName, $parameterInfo, $description, $bodySchema)
  775. {
  776. $required = isset($parameterInfo['required']) ? $parameterInfo['required'] : null;
  777. /*
  778. * There can only be one body parameter, multiple PHP parameters are represented as different
  779. * properties of the body.
  780. */
  781. if ($required) {
  782. $bodySchema['required'][] = $parameterName;
  783. }
  784. $bodySchema['properties'][$parameterName] = $this->getObjectSchema(
  785. $parameterInfo['type'],
  786. $description
  787. );
  788. $bodySchema['type'] = 'object';
  789. return $bodySchema;
  790. }
  791. /**
  792. * Generate method 200 response
  793. *
  794. * @param array $parameters
  795. * @param array $responses
  796. * @return array
  797. */
  798. private function generateMethodSuccessResponse($parameters, $responses)
  799. {
  800. if (isset($parameters['result']) && is_array($parameters['result'])) {
  801. $description = '';
  802. if (isset($parameters['result']['documentation'])) {
  803. $description = $parameters['result']['documentation'];
  804. }
  805. $schema = [];
  806. if (isset($parameters['result']['type'])) {
  807. $schema = $this->getObjectSchema($parameters['result']['type'], $description);
  808. }
  809. // Some methods may have a non-standard HTTP success code.
  810. $specificResponseData = $parameters['result']['response_codes']['success'] ?? [];
  811. // Default HTTP success code to 200 if nothing has been supplied.
  812. $responseCode = $specificResponseData['code'] ?? '200';
  813. // Default HTTP response status to 200 Success if nothing has been supplied.
  814. $responseDescription = $specificResponseData['description'] ?? '200 Success.';
  815. $responses[$responseCode]['description'] = $responseDescription;
  816. if (!empty($schema)) {
  817. $responses[$responseCode]['schema'] = $schema;
  818. }
  819. }
  820. return $responses;
  821. }
  822. /**
  823. * Generate method exception error responses
  824. *
  825. * @param array $exceptionClass
  826. * @param array $responses
  827. * @return array
  828. */
  829. private function generateMethodExceptionErrorResponses($exceptionClass, $responses)
  830. {
  831. $httpCode = '500';
  832. $description = 'Internal Server error';
  833. if (is_subclass_of($exceptionClass, \Magento\Framework\Exception\LocalizedException::class)) {
  834. // Map HTTP codes for LocalizedExceptions according to exception type
  835. if (is_subclass_of($exceptionClass, \Magento\Framework\Exception\NoSuchEntityException::class)) {
  836. $httpCode = WebapiException::HTTP_NOT_FOUND;
  837. $description = '404 Not Found';
  838. } elseif (is_subclass_of($exceptionClass, \Magento\Framework\Exception\AuthorizationException::class)
  839. || is_subclass_of($exceptionClass, \Magento\Framework\Exception\AuthenticationException::class)
  840. ) {
  841. $httpCode = WebapiException::HTTP_UNAUTHORIZED;
  842. $description = self::UNAUTHORIZED_DESCRIPTION;
  843. } else {
  844. // Input, Expired, InvalidState exceptions will fall to here
  845. $httpCode = WebapiException::HTTP_BAD_REQUEST;
  846. $description = '400 Bad Request';
  847. }
  848. }
  849. $responses[$httpCode]['description'] = $description;
  850. $responses[$httpCode]['schema']['$ref'] = self::ERROR_SCHEMA;
  851. return $responses;
  852. }
  853. /**
  854. * Retrieve a list of services visible to current user.
  855. *
  856. * @return string[]
  857. */
  858. public function getListOfServices()
  859. {
  860. $listOfAllowedServices = [];
  861. foreach ($this->serviceMetadata->getServicesConfig() as $serviceName => $service) {
  862. foreach ($service[ServiceMetadata::KEY_SERVICE_METHODS] as $method) {
  863. if ($this->authorization->isAllowed($method[ServiceMetadata::KEY_ACL_RESOURCES])) {
  864. $listOfAllowedServices[] = $serviceName;
  865. break;
  866. }
  867. }
  868. }
  869. return $listOfAllowedServices;
  870. }
  871. }