ParsesValidationRules.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. <?php
  2. namespace Knuckles\Scribe\Extracting;
  3. use Illuminate\Contracts\Validation\Rule;
  4. use Illuminate\Support\Arr;
  5. use Illuminate\Support\Facades\Validator;
  6. use Illuminate\Support\Str;
  7. use Knuckles\Scribe\Exceptions\CouldntProcessValidationRule;
  8. use Knuckles\Scribe\Exceptions\ProblemParsingValidationRules;
  9. use Knuckles\Scribe\Exceptions\ScribeException;
  10. use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
  11. use Knuckles\Scribe\Tools\WritingUtils as w;
  12. use Throwable;
  13. trait ParsesValidationRules
  14. {
  15. use ParamHelpers;
  16. public static \stdClass $MISSING_VALUE;
  17. public function getParametersFromValidationRules(array $validationRules, array $customParameterData = []): array
  18. {
  19. self::$MISSING_VALUE = new \stdClass();
  20. $validationRules = $this->normaliseRules($validationRules);
  21. $parameters = [];
  22. $dependentRules = [];
  23. foreach ($validationRules as $parameter => $ruleset) {
  24. try {
  25. if (count($customParameterData) && !isset($customParameterData[$parameter])) {
  26. c::debug($this->getMissingCustomDataMessage($parameter));
  27. }
  28. $userSpecifiedParameterInfo = $customParameterData[$parameter] ?? [];
  29. // Make sure the user-specified description comes first (and add full stops where needed).
  30. $description = $userSpecifiedParameterInfo['description'] ?? '';
  31. if (!empty($description) && !Str::endsWith($description, '.')) {
  32. $description .= '.';
  33. }
  34. $parameterData = [
  35. 'name' => $parameter,
  36. 'required' => false,
  37. 'type' => null,
  38. 'example' => self::$MISSING_VALUE,
  39. 'description' => $description,
  40. ];
  41. $dependentRules[$parameter] = [];
  42. // First, parse only "independent" rules
  43. foreach ($ruleset as $rule) {
  44. $parsed = $this->parseRule($rule, $parameterData, true);
  45. if (!$parsed) {
  46. $dependentRules[$parameter][] = $rule;
  47. }
  48. }
  49. $parameterData['description'] = trim($parameterData['description']);
  50. // Set a default type
  51. if (is_null($parameterData['type'])) {
  52. $parameterData['type'] = 'string';
  53. }
  54. $parameterData['name'] = $parameter;
  55. $parameters[$parameter] = $parameterData;
  56. } catch (Throwable $e) {
  57. if ($e instanceof ScribeException) {
  58. // This is a lower-level error that we've encountered and wrapped;
  59. // Pass it on to the user.
  60. throw $e;
  61. }
  62. throw ProblemParsingValidationRules::forParam($parameter, $e);
  63. }
  64. }
  65. // Now parse any "dependent" rules and set examples. At this point, we should know all field's types.
  66. foreach ($dependentRules as $parameter => $ruleset) {
  67. try {
  68. $parameterData = $parameters[$parameter];
  69. $userSpecifiedParameterInfo = $customParameterData[$parameter] ?? [];
  70. foreach ($ruleset as $rule) {
  71. $this->parseRule($rule, $parameterData, false, $parameters);
  72. }
  73. // Make sure the user-specified example overwrites others.
  74. if (isset($userSpecifiedParameterInfo['example'])) {
  75. if ($this->shouldCastUserExample()) {
  76. // Examples in comments are strings, we need to cast them properly
  77. $parameterData['example'] = $this->castToType($userSpecifiedParameterInfo['example'], $parameterData['type'] ?? 'string');
  78. } else {
  79. $parameterData['example'] = $userSpecifiedParameterInfo['example'];
  80. }
  81. }
  82. // End descriptions with a full stop
  83. $parameterData['description'] = trim($parameterData['description']);
  84. if (!empty($parameterData['description']) && !Str::endsWith($parameterData['description'], '.')) {
  85. $parameterData['description'] .= '.';
  86. }
  87. $parameters[$parameter] = $parameterData;
  88. } catch (Throwable $e) {
  89. if ($e instanceof ScribeException) {
  90. // This is a lower-level error that we've encountered and wrapped;
  91. // Pass it on to the user.
  92. throw $e;
  93. }
  94. throw ProblemParsingValidationRules::forParam($parameter, $e);
  95. }
  96. }
  97. return $parameters;
  98. }
  99. /**
  100. * Transform validation rules from:
  101. * 'param1' => 'int|required' TO 'param1' => ['int', 'required']
  102. *
  103. * @param array<string,string|string[]> $rules
  104. *
  105. * @return array
  106. */
  107. protected function normaliseRules(array $rules): array
  108. {
  109. // We can simply call Validator::make($data, $rules)->getRules() to get the normalised rules,
  110. // but Laravel will ignore any nested array rules (`ids.*')
  111. // unless the key referenced (`ids`) exists in the dataset and is a non-empty array
  112. // So we'll create a single-item array for each array parameter
  113. $testData = [];
  114. foreach ($rules as $key => $ruleset) {
  115. if (!Str::contains($key, '.*')) continue;
  116. // All we need is for Laravel to see this key exists
  117. Arr::set($testData, str_replace('.*', '.0', $key), Str::random());
  118. }
  119. // Now this will return the complete ruleset.
  120. // Nested array parameters will be present, with '*' replaced by '0'
  121. $newRules = Validator::make($testData, $rules)->getRules();
  122. // Transform the key names back from 'ids.0' to 'ids.*'
  123. return collect($newRules)->mapWithKeys(function ($val, $paramName) use ($rules) {
  124. if (Str::contains($paramName, '.0')) {
  125. $genericArrayKeyName = str_replace('.0', '.*', $paramName);
  126. // But only if that was the original value
  127. if (isset($rules[$genericArrayKeyName])) {
  128. $paramName = $genericArrayKeyName;
  129. }
  130. }
  131. return [$paramName => $val];
  132. })->toArray();
  133. }
  134. /**
  135. * Parse a validation rule and extract a parameter type, description and setter (used to generate an example).
  136. *
  137. * If $independentOnly is true, only independent rules will be parsed.
  138. * If a rule depends on another parameter (eg gt:field) or needs to know the type of the parameter first (eg:
  139. * size:34), it will return false.
  140. */
  141. protected function parseRule($rule, array &$parameterData, bool $independentOnly, array $allParameters = []): bool
  142. {
  143. try {
  144. if (!(is_string($rule) || $rule instanceof Rule)) {
  145. return true;
  146. }
  147. // Convert string rules into rule + arguments (eg "in:1,2" becomes ["in", ["1", "2"]])
  148. $parsedRule = $this->parseStringRuleIntoRuleAndArguments($rule);
  149. [$rule, $arguments] = $parsedRule;
  150. $dependentRules = ['between', 'max', 'min', 'size', 'gt', 'gte', 'lt', 'lte', 'before', 'after', 'before_or_equal', 'after_or_equal'];
  151. if ($independentOnly && in_array($rule, $dependentRules)) {
  152. return false;
  153. }
  154. // Reminders:
  155. // 1. Append to the description (with a leading space); don't overwrite.
  156. // 2. Avoid testing on the value of $parameterData['type'],
  157. // as that may not have been set yet, since the rules can be in any order.
  158. // For this reason, only deterministic rules are supported
  159. // 3. All rules supported must be rules that we can generate a valid dummy value for.
  160. switch ($rule) {
  161. case 'required':
  162. $parameterData['required'] = true;
  163. break;
  164. /*
  165. * Primitive types. No description should be added
  166. */
  167. case 'bool':
  168. case 'boolean':
  169. $parameterData['setter'] = function () {
  170. return Arr::random([true, false]);
  171. };
  172. $parameterData['type'] = 'boolean';
  173. break;
  174. case 'string':
  175. $parameterData['setter'] = function () {
  176. return $this->generateDummyValue('string');
  177. };
  178. $parameterData['type'] = 'string';
  179. break;
  180. case 'int':
  181. case 'integer':
  182. $parameterData['setter'] = function () {
  183. return $this->generateDummyValue('integer');
  184. };
  185. $parameterData['type'] = 'integer';
  186. break;
  187. case 'numeric':
  188. $parameterData['setter'] = function () {
  189. return $this->generateDummyValue('number');
  190. };
  191. $parameterData['type'] = 'number';
  192. break;
  193. case 'array':
  194. $parameterData['setter'] = function () {
  195. return [$this->generateDummyValue('string')];
  196. };
  197. $parameterData['type'] = 'array'; // The cleanup code in normaliseArrayAndObjectParameters() will set this to a valid type (x[] or object)
  198. break;
  199. case 'file':
  200. $parameterData['type'] = 'file';
  201. $parameterData['description'] .= ' Must be a file.';
  202. $parameterData['setter'] = function () {
  203. return $this->generateDummyValue('file');
  204. };
  205. break;
  206. /**
  207. * Special string types
  208. */
  209. case 'alpha':
  210. $parameterData['description'] .= " Must contain only letters.";
  211. $parameterData['setter'] = function () {
  212. return $this->getFaker()->lexify('??????');
  213. };
  214. break;
  215. case 'alpha_dash':
  216. $parameterData['description'] .= " Must contain only letters, numbers, dashes and underscores.";
  217. $parameterData['setter'] = function () {
  218. return $this->getFaker()->lexify('???-???_?');
  219. };
  220. break;
  221. case 'alpha_num':
  222. $parameterData['description'] .= " Must contain only letters and numbers.";
  223. $parameterData['setter'] = function () {
  224. return $this->getFaker()->bothify('#?#???#');
  225. };
  226. break;
  227. case 'timezone':
  228. // Laravel's message merely says "The value must be a valid zone"
  229. $parameterData['description'] .= " Must be a valid time zone, such as <code>Africa/Accra</code>.";
  230. $parameterData['setter'] = function () {
  231. return $this->getFaker()->timezone;
  232. };
  233. break;
  234. case 'email':
  235. $parameterData['description'] .= ' ' . $this->getDescription($rule);
  236. $parameterData['setter'] = function () {
  237. return $this->getFaker()->safeEmail;
  238. };
  239. $parameterData['type'] = 'string';
  240. break;
  241. case 'url':
  242. $parameterData['setter'] = function () {
  243. return $this->getFaker()->url;
  244. };
  245. $parameterData['type'] = 'string';
  246. // Laravel's message is "The value format is invalid". Ugh.🤮
  247. $parameterData['description'] .= " Must be a valid URL.";
  248. break;
  249. case 'ip':
  250. $parameterData['description'] .= ' ' . $this->getDescription($rule);
  251. $parameterData['type'] = 'string';
  252. $parameterData['setter'] = function () {
  253. return $this->getFaker()->ipv4;
  254. };
  255. break;
  256. case 'json':
  257. $parameterData['type'] = 'string';
  258. $parameterData['description'] .= ' ' . $this->getDescription($rule);
  259. $parameterData['setter'] = function () {
  260. return json_encode([$this->getFaker()->word, $this->getFaker()->word,]);
  261. };
  262. break;
  263. case 'date':
  264. $parameterData['type'] = 'string';
  265. $parameterData['description'] .= ' ' . $this->getDescription($rule);
  266. $parameterData['setter'] = fn() => date('Y-m-d\TH:i:s', time());
  267. break;
  268. case 'date_format':
  269. $parameterData['type'] = 'string';
  270. // Laravel description here is "The value must match the format Y-m-d". Not descriptive enough.
  271. $parameterData['description'] .= " Must be a valid date in the format <code>{$arguments[0]}</code>.";
  272. $parameterData['setter'] = function () use ($arguments) {
  273. return date($arguments[0], time());
  274. };
  275. break;
  276. case 'after':
  277. case 'after_or_equal':
  278. $parameterData['type'] = 'string';
  279. $parameterData['description'] .= ' ' . $this->getDescription($rule, [':date' => "<code>{$arguments[0]}</code>"]);
  280. // TODO introduce the concept of "modifiers", like date_format
  281. // The startDate may refer to another field, in which case, we just ignore it for now.
  282. $startDate = isset($allParameters[$arguments[0]]) ? 'today' : $arguments[0];
  283. $parameterData['setter'] = fn() => $this->getFaker()->dateTimeBetween($startDate, '+100 years')->format('Y-m-d');
  284. break;
  285. case 'before':
  286. case 'before_or_equal':
  287. $parameterData['type'] = 'string';
  288. // The argument can be either another field or a date
  289. // The endDate may refer to another field, in which case, we just ignore it for now.
  290. $endDate = isset($allParameters[$arguments[0]]) ? 'today' : $arguments[0];
  291. $parameterData['description'] .= ' ' . $this->getDescription($rule, [':date' => "<code>{$arguments[0]}</code>"]);
  292. $parameterData['setter'] = fn() => $this->getFaker()->dateTimeBetween('-30 years', $endDate)->format('Y-m-d');
  293. break;
  294. case 'starts_with':
  295. $parameterData['description'] .= ' Must start with one of ' . w::getListOfValuesAsFriendlyHtmlString($arguments);
  296. $parameterData['setter'] = fn() => $this->getFaker()->lexify("{$arguments[0]}????");;
  297. break;
  298. case 'ends_with':
  299. $parameterData['description'] .= ' Must end with one of ' . w::getListOfValuesAsFriendlyHtmlString($arguments);
  300. $parameterData['setter'] = fn() => $this->getFaker()->lexify("????{$arguments[0]}");;
  301. break;
  302. case 'uuid':
  303. $parameterData['description'] .= ' ' . $this->getDescription($rule) . ' ';
  304. $parameterData['setter'] = fn() => $this->getFaker()->uuid();;
  305. break;
  306. case 'regex':
  307. $parameterData['description'] .= ' ' . $this->getDescription($rule, [':regex' => $arguments[0]]);
  308. $parameterData['setter'] = fn() => $this->getFaker()->regexify($arguments[0]);;
  309. break;
  310. /**
  311. * Special number types.
  312. */
  313. case 'digits':
  314. $parameterData['description'] .= ' ' . $this->getDescription($rule, [':digits' => $arguments[0]]);
  315. $parameterData['setter'] = fn() => $this->getFaker()->numerify(str_repeat("#", $arguments[0]));
  316. $parameterData['type'] = 'string';
  317. break;
  318. case 'digits_between':
  319. $parameterData['description'] .= ' ' . $this->getDescription($rule, [':min' => $arguments[0], ':max' => $arguments[1]]);
  320. $parameterData['setter'] = fn() => $this->getFaker()->numerify(str_repeat("#", rand($arguments[0], $arguments[1])));
  321. $parameterData['type'] = 'string';
  322. break;
  323. /**
  324. * These rules can apply to numbers, strings, arrays or files
  325. */
  326. case 'size':
  327. $parameterData['description'] .= ' ' . $this->getDescription(
  328. $rule, [':size' => $arguments[0]], $this->getLaravelValidationBaseTypeMapping($parameterData['type'])
  329. );
  330. $parameterData['setter'] = $this->getDummyValueGenerator($parameterData['type'], $arguments[0]);
  331. break;
  332. case 'min':
  333. $parameterData['description'] .= ' ' . $this->getDescription(
  334. $rule, [':min' => $arguments[0]], $this->getLaravelValidationBaseTypeMapping($parameterData['type'])
  335. );
  336. $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], $arguments[0]);
  337. break;
  338. case 'max':
  339. $parameterData['description'] .= ' ' . $this->getDescription(
  340. $rule, [':max' => $arguments[0]], $this->getLaravelValidationBaseTypeMapping($parameterData['type'])
  341. );
  342. $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], 0, $arguments[0]);
  343. break;
  344. case 'between':
  345. $parameterData['description'] .= ' ' . $this->getDescription(
  346. $rule, [':min' => $arguments[0], ':max' => $arguments[1]], $this->getLaravelValidationBaseTypeMapping($parameterData['type'])
  347. );
  348. $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], $arguments[0], $arguments[1]);
  349. break;
  350. /**
  351. * Special file types.
  352. */
  353. case 'image':
  354. $parameterData['type'] = 'file';
  355. $parameterData['description'] .= ' ' . $this->getDescription($rule) . ' ';
  356. $parameterData['setter'] = function () {
  357. // This is fine because the file example generator generates an image
  358. return $this->generateDummyValue('file');
  359. };
  360. break;
  361. /**
  362. * Other rules.
  363. */
  364. case 'in':
  365. // Not using the rule description here because it only says "The attribute is invalid"
  366. $parameterData['description'] .= ' Must be one of ' . w::getListOfValuesAsFriendlyHtmlString($arguments) . ' ';
  367. $parameterData['setter'] = function () use ($arguments) {
  368. return Arr::random($arguments);
  369. };
  370. break;
  371. /**
  372. * These rules only add a description. Generating valid examples is too complex.
  373. */
  374. case 'not_in':
  375. $parameterData['description'] .= ' Must not be one of ' . w::getListOfValuesAsFriendlyHtmlString($arguments) . ' ';
  376. break;
  377. case 'required_if':
  378. $parameterData['description'] .= ' ' . $this->getDescription(
  379. $rule, [':other' => "<code>{$arguments[0]}</code>", ':value' => "<code>{$arguments[1]}</code>"]
  380. ) . ' ';
  381. break;
  382. case 'required_unless':
  383. $parameterData['description'] .= ' ' . $this->getDescription(
  384. $rule, [':other' => "<code>" . array_shift($arguments) . "</code>", ':values' => w::getListOfValuesAsFriendlyHtmlString($arguments)]
  385. ) . ' ';
  386. break;
  387. case 'required_with':
  388. $parameterData['description'] .= ' ' . $this->getDescription(
  389. $rule, [':values' => w::getListOfValuesAsFriendlyHtmlString($arguments)]
  390. ) . ' ';
  391. break;
  392. case 'required_without':
  393. $description = $this->getDescription(
  394. $rule, [':values' => w::getListOfValuesAsFriendlyHtmlString($arguments)]
  395. ) . ' ';
  396. $parameterData['description'] .= str_replace('must be present', 'is not present', $description);
  397. break;
  398. case 'required_with_all':
  399. case 'required_without_all':
  400. $parameterData['description'] .= ' ' . $this->getDescription(
  401. $rule, [':values' => w::getListOfValuesAsFriendlyHtmlString($arguments, "and")]
  402. ) . ' ';
  403. break;
  404. case 'same':
  405. case 'different':
  406. $parameterData['description'] .= ' ' . $this->getDescription(
  407. $rule, [':other' => "<code>{$arguments[0]}</code>"]
  408. ) . ' ';
  409. break;
  410. default:
  411. // Other rules not supported
  412. break;
  413. }
  414. return true;
  415. } catch (Throwable $e) {
  416. throw CouldntProcessValidationRule::forParam($parameterData['name'], $rule, $e);
  417. }
  418. }
  419. /**
  420. * Parse a string rule into the base rule and arguments.
  421. * Laravel validation rules are specified in the format {rule}:{arguments}
  422. * Arguments are separated by commas.
  423. * For instance the rule "max:3" states that the value may only be three letters.
  424. *
  425. * @param string|Rule $rule
  426. *
  427. * @return array
  428. */
  429. protected function parseStringRuleIntoRuleAndArguments($rule): array
  430. {
  431. $ruleArguments = [];
  432. // Convert any Rule objects to strings
  433. if ($rule instanceof Rule) {
  434. $className = substr(strrchr(get_class($rule), "\\"), 1);
  435. return [$className, []];
  436. }
  437. if (strpos($rule, ':') !== false) {
  438. [$rule, $argumentsString] = explode(':', $rule, 2);
  439. // These rules can have commas in their arguments, so we don't split on commas
  440. if (in_array(strtolower($rule), ['regex', 'date', 'date_format'])) {
  441. $ruleArguments = [$argumentsString];
  442. } else {
  443. $ruleArguments = str_getcsv($argumentsString);
  444. }
  445. }
  446. return [strtolower(trim($rule)), $ruleArguments];
  447. }
  448. protected function getParameterExample(array $parameterData)
  449. {
  450. // If no example was given by the user, set an autogenerated example.
  451. // Each parsed rule returns a 'setter' function. We'll evaluate the last one.
  452. if ($parameterData['example'] === self::$MISSING_VALUE) {
  453. if (isset($parameterData['setter'])) {
  454. return $parameterData['setter']();
  455. } else {
  456. return $parameterData['required']
  457. ? $this->generateDummyValue($parameterData['type'])
  458. : null;
  459. }
  460. } else if (!is_null($parameterData['example']) && $parameterData['example'] !== self::$MISSING_VALUE) {
  461. // Casting again is important since values may have been cast to string in the validator
  462. return $this->castToType($parameterData['example'], $parameterData['type']);
  463. }
  464. return $parameterData['example'] === self::$MISSING_VALUE ? null : $parameterData['example'];
  465. }
  466. /**
  467. * Laravel uses .* notation for arrays. This PR aims to normalise that into our "new syntax".
  468. *
  469. * 'years.*' with type 'integer' becomes 'years' with type 'integer[]'
  470. * 'cars.*.age' with type 'string' becomes 'cars[].age' with type 'string' and 'cars' with type 'object[]'
  471. * 'cars.*.things.*.*' with type 'string' becomes 'cars[].things' with type 'string[][]' and 'cars' with type
  472. * 'object[]'
  473. *
  474. * @param array[] $parametersFromValidationRules
  475. *
  476. * @return array
  477. */
  478. public function normaliseArrayAndObjectParameters(array $parametersFromValidationRules): array
  479. {
  480. $results = [];
  481. $originalParams = $parametersFromValidationRules;
  482. foreach ($parametersFromValidationRules as $name => $details) {
  483. if (isset($results[$name])) {
  484. continue;
  485. }
  486. if ($details['type'] === 'array') {
  487. // This is the parent field, a generic array type. If a child item exists,
  488. // this will be overwritten with the correct type (such as object or object[]) by the code below
  489. $details['type'] = 'string[]';
  490. }
  491. if (Str::endsWith($name, '.*')) {
  492. // Wrap array example properly
  493. $details['example'] = $this->exampleOrDefault($details, $this->getParameterExample($details));
  494. $needsWrapping = !is_array($details['example']);
  495. $nestingLevel = 0;
  496. // Change cars.*.dogs.things.*.* with type X to cars.*.dogs.things with type X[][]
  497. while (Str::endsWith($name, '.*')) {
  498. $details['type'] .= '[]';
  499. if ($needsWrapping) {
  500. $details['example'] = [$details['example']];
  501. }
  502. $name = substr($name, 0, -2);
  503. $nestingLevel++;
  504. }
  505. }
  506. // Now make sure the field cars.*.dogs exists
  507. $parentPath = $name;
  508. while (Str::contains($parentPath, '.')) {
  509. $parentPath = preg_replace('/\.[^.]+$/', '', $parentPath);
  510. if (empty($parametersFromValidationRules[$parentPath])) {
  511. if (Str::endsWith($parentPath, '.*')) {
  512. $parentPath = substr($parentPath, 0, -2);
  513. $type = 'object[]';
  514. $example = [[]];
  515. } else {
  516. $type = 'object';
  517. $example = [];
  518. }
  519. $normalisedPath = str_replace('.*.', '[].', $parentPath);
  520. $results[$normalisedPath] = [
  521. 'name' => $normalisedPath,
  522. 'type' => $type,
  523. 'required' => false,
  524. 'description' => '',
  525. 'example' => $example,
  526. ];
  527. } else {
  528. // if the parent field already exists with a type 'array'
  529. $parentDetails = $parametersFromValidationRules[$parentPath];
  530. unset($parametersFromValidationRules[$parentPath]);
  531. if (Str::endsWith($parentPath, '.*')) {
  532. $parentPath = substr($parentPath, 0, -2);
  533. $parentDetails['type'] = 'object[]';
  534. // Set the example too. Very likely the example array was an array of strings or an empty array
  535. if (!$this->examplePresent($parentDetails) || is_string($parentDetails['example'][0]) || is_string($parentDetails['example'][0][0])) {
  536. $parentDetails['example'] = [[]];
  537. }
  538. } else {
  539. $parentDetails['type'] = 'object';
  540. if (!$this->examplePresent($parentDetails) || is_string($parentDetails['example'][0])) {
  541. $parentDetails['example'] = [];
  542. }
  543. }
  544. $normalisedPath = str_replace('.*.', '[].', $parentPath);
  545. $parentDetails['name'] = $normalisedPath;
  546. $results[$normalisedPath] = $parentDetails;
  547. }
  548. }
  549. $details['name'] = $name = str_replace('.*.', '[].', $name);
  550. // If an example was specified on the parent, use that instead.
  551. if (isset($originalParams[$details['name']]) && $this->examplePresent($originalParams[$details['name']])) {
  552. $details['example'] = $originalParams[$details['name']]['example'];
  553. }
  554. // Change type 'array' to 'object' if there are subfields
  555. if (
  556. $details['type'] === 'array'
  557. && Arr::first(array_keys($parametersFromValidationRules), function ($key) use ($name) {
  558. return preg_match("/{$name}\\.[^*]/", $key);
  559. })
  560. ) {
  561. $details['type'] = 'object';
  562. }
  563. $details['example'] = $this->getParameterExample($details);
  564. unset($details['setter']);
  565. $results[$name] = $details;
  566. }
  567. return $results;
  568. }
  569. private function exampleOrDefault(array $parameterData, $default)
  570. {
  571. if (!isset($parameterData['example']) || $parameterData['example'] === self::$MISSING_VALUE) {
  572. return $default;
  573. }
  574. return $parameterData['example'];
  575. }
  576. private function examplePresent(array $parameterData)
  577. {
  578. return isset($parameterData['example']) && $parameterData['example'] !== self::$MISSING_VALUE;
  579. }
  580. protected function getDescription(string $rule, array $arguments = [], $baseType = 'string'): string
  581. {
  582. $description = trans("validation.{$rule}");
  583. // For rules that can apply to multiple types (eg 'max' rule), Laravel returns an array of possible messages
  584. // 'numeric' => 'The :attribute must not be greater than :max'
  585. // 'file' => 'The :attribute must have a size less than :max kilobytes'
  586. if (is_array($description)) {
  587. $description = $description[$baseType];
  588. }
  589. // Convert messages from failure type ("The :attribute is not a valid date.") to info ("The :attribute must be a valid date.")
  590. $description = str_replace(['is not', 'does not'], ['must be', 'must'], $description);
  591. $description = str_replace('may not', 'must not', $description);
  592. foreach ($arguments as $placeholder => $argument) {
  593. $description = str_replace($placeholder, $argument, $description);
  594. }
  595. // FOr rules that validate subfields
  596. $description = str_replace("The :attribute field", "This field", $description);
  597. return str_replace("The value must", "Must", str_replace(":attribute", "value", $description));
  598. }
  599. private function getLaravelValidationBaseTypeMapping(string $parameterType): string
  600. {
  601. $mapping = [
  602. 'number' => 'numeric',
  603. 'integer' => 'numeric',
  604. 'file' => 'file',
  605. 'string' => 'string',
  606. 'array' => 'array',
  607. ];
  608. if (Str::endsWith($parameterType, '[]')) {
  609. return 'array';
  610. }
  611. return $mapping[$parameterType] ?? 'string';
  612. }
  613. protected function getMissingCustomDataMessage($parameterName)
  614. {
  615. return "";
  616. }
  617. protected function shouldCastUserExample()
  618. {
  619. return false;
  620. }
  621. }