ParamHelpers.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <?php
  2. namespace Knuckles\Scribe\Extracting;
  3. use Faker\Factory;
  4. use Illuminate\Http\UploadedFile;
  5. use Illuminate\Support\Arr;
  6. use Illuminate\Support\Str;
  7. trait ParamHelpers
  8. {
  9. protected function getFakeFactoryByName(string $name): ?\Closure
  10. {
  11. $faker = $this->getFaker();
  12. $name = strtolower(array_reverse(explode('.', $name))[0]);
  13. $normalizedName = match (true) {
  14. Str::endsWith($name, ['email', 'email_address']) => 'email',
  15. Str::endsWith($name, ['uuid']) => 'uuid',
  16. Str::endsWith($name, ['url']) => 'url',
  17. Str::endsWith($name, ['locale']) => 'locale',
  18. Str::endsWith($name, ['timezone']) => 'timezone',
  19. default => $name,
  20. };
  21. return match ($normalizedName) {
  22. 'email' => fn() => $faker->safeEmail(),
  23. 'password', 'pwd' => fn() => $faker->password(),
  24. 'url' => fn() => $faker->url(),
  25. 'description' => fn() => $faker->sentence(),
  26. 'uuid' => fn() => $faker->uuid(),
  27. 'locale' => fn() => $faker->locale(),
  28. 'timezone' => fn() => $faker->timezone(),
  29. default => null,
  30. };
  31. }
  32. protected function getFaker(): \Faker\Generator
  33. {
  34. $faker = Factory::create();
  35. if ($seed = $this->config->get('examples.faker_seed')) {
  36. $faker->seed($seed);
  37. }
  38. return $faker;
  39. }
  40. protected function generateDummyValue(string $type, array $hints = [])
  41. {
  42. if(!empty($hints['enumValues'])) {
  43. return Arr::random($hints['enumValues']);
  44. }
  45. $fakeFactory = $this->getDummyValueGenerator($type, $hints);
  46. return $fakeFactory();
  47. }
  48. protected function getDummyValueGenerator(string $type, array $hints = []): \Closure
  49. {
  50. $baseType = $type;
  51. $isListType = false;
  52. if (Str::endsWith($type, '[]')) {
  53. $baseType = strtolower(substr($type, 0, strlen($type) - 2));
  54. $isListType = true;
  55. }
  56. $size = $hints['size'] ?? null;
  57. if ($isListType) {
  58. // Return a one-array item for a list by default.
  59. return $size
  60. ? fn() => [$this->generateDummyValue($baseType, range(0, min($size - 1, 5)))]
  61. : fn() => [$this->generateDummyValue($baseType, $hints)];
  62. }
  63. if (($hints['name'] ?? false) && $baseType != 'file') {
  64. $fakeFactoryByName = $this->getFakeFactoryByName($hints['name']);
  65. if ($fakeFactoryByName) return $fakeFactoryByName;
  66. }
  67. $faker = $this->getFaker();
  68. $min = $hints['min'] ?? null;
  69. $max = $hints['max'] ?? null;
  70. // If max and min were provided, the override size.
  71. $isExactSize = is_null($min) && is_null($max) && !is_null($size);
  72. $fakeFactoriesByType = [
  73. 'integer' => function () use ($size, $isExactSize, $max, $faker, $min) {
  74. if ($isExactSize) return $size;
  75. return $max ? $faker->numberBetween((int)$min, (int)$max) : $faker->numberBetween(1, 20);
  76. },
  77. 'number' => function () use ($size, $isExactSize, $max, $faker, $min) {
  78. if ($isExactSize) return $size;
  79. return $max ? $faker->numberBetween((int)$min, (int)$max) : $faker->randomFloat();
  80. },
  81. 'boolean' => fn() => $faker->boolean(),
  82. 'string' => fn() => $size ? $faker->lexify(str_repeat("?", $size)) : $faker->word(),
  83. 'object' => fn() => [],
  84. 'file' => fn() => UploadedFile::fake()->create('test.jpg')->size($size ?: 10),
  85. ];
  86. return $fakeFactoriesByType[$baseType] ?? $fakeFactoriesByType['string'];
  87. }
  88. private function getDummyDataGeneratorBetween(string $type, $min, $max = 90, string $fieldName = null): \Closure
  89. {
  90. $hints = [
  91. 'name' => $fieldName,
  92. 'size' => $this->getFaker()->numberBetween($min, $max),
  93. 'min' => $min,
  94. 'max' => $max,
  95. ];
  96. return $this->getDummyValueGenerator($type, $hints);
  97. }
  98. protected function isSupportedTypeInDocBlocks(string $type): bool
  99. {
  100. $types = [
  101. 'integer',
  102. 'int',
  103. 'number',
  104. 'float',
  105. 'double',
  106. 'boolean',
  107. 'bool',
  108. 'string',
  109. 'object',
  110. ];
  111. return in_array(str_replace('[]', '', $type), $types);
  112. }
  113. /**
  114. * Cast a value to a specified type.
  115. *
  116. * @param mixed $value
  117. * @param string $type
  118. *
  119. * @return mixed
  120. */
  121. protected function castToType($value, string $type)
  122. {
  123. if ($value === null) {
  124. return null;
  125. }
  126. if ($type === "array") {
  127. $type = "string[]";
  128. }
  129. if (Str::endsWith($type, '[]')) {
  130. $baseType = strtolower(substr($type, 0, strlen($type) - 2));
  131. return is_array($value) ? array_map(function ($v) use ($baseType) {
  132. return $this->castToType($v, $baseType);
  133. }, $value) : json_decode($value);
  134. }
  135. if ($type === 'object') {
  136. return is_array($value) ? $value : json_decode($value, true);
  137. }
  138. $casts = [
  139. 'integer' => 'intval',
  140. 'int' => 'intval',
  141. 'float' => 'floatval',
  142. 'number' => 'floatval',
  143. 'double' => 'floatval',
  144. 'boolean' => 'boolval',
  145. 'bool' => 'boolval',
  146. ];
  147. // First, we handle booleans. We can't use a regular cast,
  148. // because PHP considers string 'false' as true.
  149. if ($value == 'false' && ($type == 'boolean' || $type == 'bool')) {
  150. return false;
  151. }
  152. if (isset($casts[$type])) {
  153. return $casts[$type]($value);
  154. }
  155. // Return the value unchanged if there's no applicable cast
  156. return $value;
  157. }
  158. /**
  159. * Normalizes the stated "type" of a parameter (eg "int", "integer", "double", "array"...)
  160. * to a number of standard JSON types (integer, boolean, number, object...).
  161. * Will return the input if no match.
  162. *
  163. * @param string|null $typeName
  164. * @param mixed $value
  165. *
  166. * @return string
  167. */
  168. public static function normalizeTypeName(?string $typeName, $value = null): string
  169. {
  170. if (!$typeName) {
  171. return 'string';
  172. }
  173. $base = str_replace('[]', '', strtolower($typeName));
  174. return match ($base) {
  175. 'bool' => str_replace($base, 'boolean', $typeName),
  176. 'int' => str_replace($base, 'integer', $typeName),
  177. 'float', 'double' => str_replace($base, 'number', $typeName),
  178. 'array' => (empty($value) || array_keys($value)[0] === 0)
  179. ? static::normalizeTypeName(gettype($value[0] ?? '')) . '[]'
  180. : 'object',
  181. default => $typeName
  182. };
  183. }
  184. /**
  185. * Allows users to specify that we shouldn't generate an example for the parameter
  186. * by writing 'No-example'.
  187. *
  188. * @param string $description
  189. *
  190. * @return bool If true, don't generate an example for this.
  191. */
  192. protected function shouldExcludeExample(string $description): bool
  193. {
  194. return strpos($description, ' No-example') !== false;
  195. }
  196. /**
  197. * Allows users to specify an example for the parameter by writing 'Example: the-example',
  198. * to be used in example requests and response calls.
  199. *
  200. * @param string $description
  201. * @param string $type The type of the parameter. Used to cast the example provided, if any.
  202. *
  203. * @return array The description and included example.
  204. */
  205. protected function parseExampleFromParamDescription(string $description, string $type): array
  206. {
  207. $exampleWasSpecified = false;
  208. $example = null;
  209. $enumValues = [];
  210. if (preg_match('/(.*)\bExample:\s*([\s\S]+)\s*/s', $description, $content)) {
  211. $exampleWasSpecified = true;
  212. $description = trim($content[1]);
  213. if ($content[2] == 'null') {
  214. // If we intentionally put null as example we return null as example
  215. $example = null;
  216. } else {
  217. // Examples are parsed as strings by default, we need to cast them properly
  218. $example = $this->castToType($content[2], $type);
  219. }
  220. }
  221. if (preg_match('/(.*)\bEnum:\s*([\s\S]+)\s*/s', $description, $content)) {
  222. $description = trim($content[1]);
  223. $enumValues = array_map(
  224. fn ($value) => $this->castToType(trim($value), $type),
  225. explode(',', rtrim(trim($content[2]), '.'))
  226. );
  227. }
  228. return [$description, $example, $enumValues, $exampleWasSpecified];
  229. }
  230. }