Skip to content

Commit

Permalink
Smarter example generation
Browse files Browse the repository at this point in the history
  • Loading branch information
shalvah committed Nov 16, 2022
1 parent 46da8d6 commit 46e3bbc
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 64 deletions.
97 changes: 61 additions & 36 deletions src/Extracting/ParamHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -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
Expand Down
27 changes: 11 additions & 16 deletions src/Extracting/ParsesValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <code>Africa/Accra</code>.";
$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.";
Expand Down Expand Up @@ -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]]);
Expand Down Expand Up @@ -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;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
8 changes: 4 additions & 4 deletions src/Extracting/Strategies/GetFieldsFromTagStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
5 changes: 3 additions & 2 deletions tests/Unit/ValidationRuleParsingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down Expand Up @@ -458,4 +459,4 @@ public function message()
{
return '.';
}
}
}

0 comments on commit 46e3bbc

Please sign in to comment.