diff --git a/src/Extracting/ParamHelpers.php b/src/Extracting/ParamHelpers.php index f3f69f9d..8afd230b 100644 --- a/src/Extracting/ParamHelpers.php +++ b/src/Extracting/ParamHelpers.php @@ -9,6 +9,32 @@ trait ParamHelpers { + protected function getFakeFactoryByName(string $name): ?\Closure + { + $faker = $this->getFaker(); + + $name = strtolower(array_reverse(explode('.', $name))[0]); + $normalizedName = match (true) { + Str::endsWith($name, ['email', 'email_address']) => 'email', + Str::endsWith($name, ['uuid']) => 'uuid', + Str::endsWith($name, ['url']) => 'url', + Str::endsWith($name, ['locale']) => 'locale', + Str::endsWith($name, ['timezone']) => 'timezone', + default => $name, + }; + + return match ($normalizedName) { + 'email' => fn() => $faker->safeEmail(), + 'password', 'pwd' => fn() => $faker->password(), + 'url' => fn() => $faker->url(), + 'description' => fn() => $faker->sentence(), + 'uuid' => fn() => $faker->uuid(), + 'locale' => fn() => $faker->locale(), + 'timezone' => fn() => $faker->timezone(), + default => null, + }; + } + protected function getFaker(): \Faker\Generator { $faker = Factory::create(); @@ -18,14 +44,14 @@ protected function getFaker(): \Faker\Generator return $faker; } - protected function generateDummyValue(string $type, int $size = null) + protected function generateDummyValue(string $type, array $hints = []) { - $fakeFactory = $this->getDummyValueGenerator($type, $size); + $fakeFactory = $this->getDummyValueGenerator($type, $hints); return $fakeFactory(); } - protected function getDummyValueGenerator(string $type, int $size = null): \Closure + protected function getDummyValueGenerator(string $type, array $hints = []): \Closure { $baseType = $type; $isListType = false; @@ -35,54 +61,53 @@ protected function getDummyValueGenerator(string $type, int $size = null): \Clos $isListType = true; } + $size = $hints['size'] ?? null; if ($isListType) { - // Return a one-array item for a list. - return fn() => [$this->generateDummyValue($baseType)]; + // Return a one-array item for a list by default. + return $size + ? fn() => [$this->generateDummyValue($baseType, range(0, min($size - 1, 5)))] + : fn() => [$this->generateDummyValue($baseType, $hints)]; } - $faker = $this->getFaker(); + if (($hints['name'] ?? false) && $baseType != 'file') { + $fakeFactoryByName = $this->getFakeFactoryByName($hints['name']); + if ($fakeFactoryByName) return $fakeFactoryByName; + } - $fakeFactories = [ - 'integer' => fn() => $size ?: $faker->numberBetween(1, 20), - 'number' => fn() => $size ?: $faker->randomFloat(), + $faker = $this->getFaker(); + $min = $hints['min'] ?? null; + $max = $hints['max'] ?? null; + // If max and min were provided, the override size. + $isExactSize = is_null($min) && is_null($max) && !is_null($size); + + $fakeFactoriesByType = [ + 'integer' => function () use ($size, $isExactSize, $max, $faker, $min) { + if ($isExactSize) return $size; + return $max ? $faker->numberBetween((int)$min, (int)$max) : $faker->numberBetween(1, 20); + }, + 'number' => function () use ($size, $isExactSize, $max, $faker, $min) { + if ($isExactSize) return $size; + return $max ? $faker->numberBetween((int)$min, (int)$max) : $faker->randomFloat(); + }, 'boolean' => fn() => $faker->boolean(), 'string' => fn() => $size ? $faker->lexify(str_repeat("?", $size)) : $faker->word(), 'object' => fn() => [], 'file' => fn() => UploadedFile::fake()->create('test.jpg')->size($size ?: 10), ]; - return $fakeFactories[$baseType] ?? $fakeFactories['string']; + return $fakeFactoriesByType[$baseType] ?? $fakeFactoriesByType['string']; } - private function getDummyDataGeneratorBetween(string $type, $min, $max = 300): \Closure + private function getDummyDataGeneratorBetween(string $type, $min, $max = 90, string $fieldName = null): \Closure { - $baseType = $type; - $isListType = false; - - if (Str::endsWith($type, '[]')) { - $baseType = strtolower(substr($type, 0, strlen($type) - 2)); - $isListType = true; - } - - $randomSize = $this->getFaker()->numberBetween($min, $max); - - if ($isListType) { - return fn() => array_map( - fn() => $this->generateDummyValue($baseType), - range(0, $randomSize - 1) - ); - } - - $faker = $this->getFaker(); - - $fakeFactories = [ - 'integer' => fn() => $faker->numberBetween((int)$min, (int)$max), - 'number' => fn() => $faker->numberBetween((int)$min, (int)$max), - 'string' => fn() => $faker->lexify(str_repeat("?", $randomSize)), - 'file' => fn() => UploadedFile::fake()->create('test.jpg')->size($randomSize), + $hints = [ + 'name' => $fieldName, + 'size' => $this->getFaker()->numberBetween($min, $max), + 'min' => $min, + 'max' => $max, ]; - return $fakeFactories[$baseType] ?? $fakeFactories['string']; + return $this->getDummyValueGenerator($type, $hints); } protected function isSupportedTypeInDocBlocks(string $type): bool diff --git a/src/Extracting/ParsesValidationRules.php b/src/Extracting/ParsesValidationRules.php index 7fefbb39..cd121a4e 100644 --- a/src/Extracting/ParsesValidationRules.php +++ b/src/Extracting/ParsesValidationRules.php @@ -206,8 +206,8 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly $parameterData['type'] = 'boolean'; break; case 'string': - $parameterData['setter'] = function () { - return $this->generateDummyValue('string'); + $parameterData['setter'] = function () use ($parameterData) { + return $this->generateDummyValue('string', ['name' => $parameterData['name']]); }; $parameterData['type'] = 'string'; break; @@ -262,21 +262,15 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly case 'timezone': // Laravel's message merely says "The value must be a valid zone" $parameterData['description'] .= " Must be a valid time zone, such as Africa/Accra."; - $parameterData['setter'] = function () { - return $this->getFaker()->timezone(); - }; + $parameterData['setter'] = $this->getFakeFactoryByName('timezone'); break; case 'email': $parameterData['description'] .= ' ' . $this->getDescription($rule); - $parameterData['setter'] = function () { - return $this->getFaker()->safeEmail(); - }; + $parameterData['setter'] = $this->getFakeFactoryByName('email'); $parameterData['type'] = 'string'; break; case 'url': - $parameterData['setter'] = function () { - return $this->getFaker()->url(); - }; + $parameterData['setter'] = $this->getFakeFactoryByName('url'); $parameterData['type'] = 'string'; // Laravel's message is "The value format is invalid". Ugh.🤮 $parameterData['description'] .= " Must be a valid URL."; @@ -336,7 +330,7 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly break; case 'uuid': $parameterData['description'] .= ' ' . $this->getDescription($rule) . ' '; - $parameterData['setter'] = fn() => $this->getFaker()->uuid();; + $parameterData['setter'] = $this->getFakeFactoryByName('uuid'); break; case 'regex': $parameterData['description'] .= ' ' . $this->getDescription($rule, [':regex' => $arguments[0]]); @@ -364,26 +358,27 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly $parameterData['description'] .= ' ' . $this->getDescription( $rule, [':size' => $arguments[0]], $this->getLaravelValidationBaseTypeMapping($parameterData['type']) ); - $parameterData['setter'] = $this->getDummyValueGenerator($parameterData['type'], $arguments[0]); + $parameterData['setter'] = $this->getDummyValueGenerator($parameterData['type'], ['size' => $arguments[0]]); break; case 'min': $parameterData['description'] .= ' ' . $this->getDescription( $rule, [':min' => $arguments[0]], $this->getLaravelValidationBaseTypeMapping($parameterData['type']) ); - $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], floatval($arguments[0])); + $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], floatval($arguments[0]), fieldName: $parameterData['name']); break; case 'max': $parameterData['description'] .= ' ' . $this->getDescription( $rule, [':max' => $arguments[0]], $this->getLaravelValidationBaseTypeMapping($parameterData['type']) ); - $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], 0, floatval($arguments[0])/3); + $max = min($arguments[0], 20); + $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], 0, $max, $parameterData['name']); break; case 'between': $parameterData['description'] .= ' ' . $this->getDescription( $rule, [':min' => $arguments[0], ':max' => $arguments[1]], $this->getLaravelValidationBaseTypeMapping($parameterData['type']) ); // Avoid exponentially complex operations by using the minimum length - $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], floatval($arguments[0]), floatval($arguments[0]) + 1); + $parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], floatval($arguments[0]), floatval($arguments[0]) + 1, $parameterData['name']); break; /** diff --git a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php index 3e82b1a3..33992997 100644 --- a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php +++ b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php @@ -33,7 +33,7 @@ public function parseTag(string $tagContent): array } $type = static::normalizeTypeName($type); - [$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent); + [$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent, $name); return compact('name', 'type', 'description', 'required', 'example'); } diff --git a/src/Extracting/Strategies/GetFieldsFromTagStrategy.php b/src/Extracting/Strategies/GetFieldsFromTagStrategy.php index c126a4cc..60b4c0f8 100644 --- a/src/Extracting/Strategies/GetFieldsFromTagStrategy.php +++ b/src/Extracting/Strategies/GetFieldsFromTagStrategy.php @@ -33,17 +33,17 @@ public function getFromTags(array $tagsOnMethod, array $tagsOnClass = []): array abstract protected function parseTag(string $tagContent): array; - protected function getDescriptionAndExample(string $description, string $type, string $tagContent): array + protected function getDescriptionAndExample(string $description, string $type, string $tagContent, string $fieldName): array { [$description, $example] = $this->parseExampleFromParamDescription($description, $type); - $example = $this->setExampleIfNeeded($example, $type, $tagContent); + $example = $this->setExampleIfNeeded($example, $type, $tagContent, $fieldName); return [$description, $example]; } - protected function setExampleIfNeeded(mixed $currentExample, string $type, string $tagContent): mixed + protected function setExampleIfNeeded(mixed $currentExample, string $type, string $tagContent, string $fieldName): mixed { return (is_null($currentExample) && !$this->shouldExcludeExample($tagContent)) - ? $this->generateDummyValue($type) + ? $this->generateDummyValue($type, hints: ['name' => $fieldName]) : $currentExample; } } diff --git a/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php b/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php index 10d5c7af..eafc534a 100644 --- a/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php +++ b/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php @@ -33,7 +33,7 @@ protected function normalizeParameterData(array $data): array { $data['type'] = static::normalizeTypeName($data['type']); if (is_null($data['example'])) { - $data['example'] = $this->generateDummyValue($data['type']); + $data['example'] = $this->generateDummyValue($data['type'], ['name' => $data['name']]); } else if ($data['example'] == 'No-example' || $data['example'] == 'No-example.') { $data['example'] = null; } diff --git a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php index f2b13b7f..3f1cbe83 100644 --- a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php +++ b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php @@ -63,7 +63,7 @@ public function parseTag(string $tagContent): array } - [$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent); + [$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent, $name); return compact('name', 'description', 'required', 'example', 'type'); } diff --git a/src/Extracting/Strategies/UrlParameters/GetFromLaravelAPI.php b/src/Extracting/Strategies/UrlParameters/GetFromLaravelAPI.php index c8b8d6cf..b21caaed 100644 --- a/src/Extracting/Strategies/UrlParameters/GetFromLaravelAPI.php +++ b/src/Extracting/Strategies/UrlParameters/GetFromLaravelAPI.php @@ -166,7 +166,7 @@ protected function setTypesAndExamplesForOthers(array $parameters, ExtractedEndp $parameterRegex = $endpointData->route->wheres[$name] ?? null; $parameters[$name]['example'] = $parameterRegex ? $this->castToType($this->getFaker()->regexify($parameterRegex), $parameters[$name]['type']) - : $this->generateDummyValue($parameters[$name]['type']); + : $this->generateDummyValue($parameters[$name]['type'], hints: ['name' => $name]); } } return $parameters; diff --git a/src/Extracting/Strategies/UrlParameters/GetFromLumenAPI.php b/src/Extracting/Strategies/UrlParameters/GetFromLumenAPI.php index 2b07234d..5e9b620b 100644 --- a/src/Extracting/Strategies/UrlParameters/GetFromLumenAPI.php +++ b/src/Extracting/Strategies/UrlParameters/GetFromLumenAPI.php @@ -49,7 +49,7 @@ public function __invoke(ExtractedEndpointData $endpointData, array $routeRules 'name' => $name, 'description' => '', 'required' => !boolval($isThisParameterOptional), - 'example' => $this->generateDummyValue($type), + 'example' => $this->generateDummyValue($type, hints: ['name' => $name]), 'type' => $type, ]; } diff --git a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php index 664653b4..b6194971 100644 --- a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php +++ b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php @@ -46,7 +46,7 @@ protected function parseTag(string $tagContent): array : static::normalizeTypeName($type); } - [$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent); + [$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent, $name); return compact('name', 'description', 'required', 'example', 'type'); } diff --git a/tests/Unit/ValidationRuleParsingTest.php b/tests/Unit/ValidationRuleParsingTest.php index 35f9cba5..9df7ae4d 100644 --- a/tests/Unit/ValidationRuleParsingTest.php +++ b/tests/Unit/ValidationRuleParsingTest.php @@ -50,7 +50,8 @@ public function can_parse_supported_rules(array $ruleset, array $customInfo, arr try { $validator->validate(); } catch (ValidationException $e) { - dump('Value: ', $exampleData[$parameterName]); + dump('Rules: ', $ruleset); + dump('Generated value: ', $exampleData[$parameterName]); dump($e->errors()); $this->fail("Generated example data from validation rule failed to match actual."); } @@ -458,4 +459,4 @@ public function message() { return '.'; } -} \ No newline at end of file +}