Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
shalvah committed Oct 18, 2024
2 parents b600c0c + a6aee1d commit 41e4a56
Show file tree
Hide file tree
Showing 19 changed files with 475 additions and 21 deletions.
1 change: 1 addition & 0 deletions camel/Extraction/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Parameter extends BaseDTO
public string $type = 'string';
public array $enumValues = [];
public bool $exampleWasSpecified = false;
public bool $nullable = false;

public function __construct(array $parameters = [])
{
Expand Down
3 changes: 3 additions & 0 deletions camel/Extraction/ResponseField.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class ResponseField extends BaseDTO
/** @var string */
public $type;

/** @var boolean */
public $required;

/** @var mixed */
public $example;

Expand Down
2 changes: 2 additions & 0 deletions src/Attributes/GenericParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function __construct(
public ?bool $required = true,
public mixed $example = null, /* Pass 'No-example' to omit the example */
public mixed $enum = null, // Can pass a list of values, or a native PHP enum
public ?bool $nullable = false,
) {
}

Expand All @@ -26,6 +27,7 @@ public function toArray()
"required" => $this->required,
"example" => $this->example,
"enumValues" => $this->getEnumValues(),
'nullable' => $this->nullable,
];
}

Expand Down
3 changes: 2 additions & 1 deletion src/Attributes/ResponseField.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public function __construct(
public ?string $description = '',
public ?bool $required = true,
public mixed $example = null, /* Pass 'No-example' to omit the example */
public mixed $enum = null, // Can pass a list of values, or a native PHP enum
public mixed $enum = null, // Can pass a list of values, or a native PHP enum,
public ?bool $nullable = false,
) {
}
}
2 changes: 1 addition & 1 deletion src/Extracting/InstantiatesExampleModels.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ protected function instantiateExampleModel(
protected function getExampleModelFromFactoryCreate(string $type, array $factoryStates = [], array $relations = [])
{
$factory = Utils::getModelFactory($type, $factoryStates, $relations);
return $factory->create()->load($relations)->refresh();
return $factory->create()->refresh()->load($relations);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/Extracting/ParsesValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function getParametersFromValidationRules(array $validationRules, array $
'type' => null,
'example' => self::$MISSING_VALUE,
'description' => $description,
'nullable' => false,
];
$dependentRules[$parameter] = [];

Expand All @@ -69,6 +70,11 @@ public function getParametersFromValidationRules(array $validationRules, array $
}

$parameterData['name'] = $parameter;

if ($parameterData['required'] === true){
$parameterData['nullable'] = false;
}

$parameters[$parameter] = $parameterData;
} catch (Throwable $e) {
if ($e instanceof ScribeException) {
Expand Down Expand Up @@ -531,6 +537,9 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly
case 'different':
$parameterData['description'] .= " The value and <code>{$arguments[0]}</code> must be different.";
break;
case 'nullable':
$parameterData['nullable'] = true;
break;
case 'exists':
$parameterData['description'] .= " The <code>{$arguments[1]}</code> of an existing record in the {$arguments[0]} table.";
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders;

use PhpParser\Node;

/**
* This class looks for
* $anyVariable = Request::validate(...);
* or just
* Request::validate(...);
*
* Also supports `->validateWithBag('', ...)`
*/
class RequestValidateFacade
{
public static function find(Node $node)
{
if (!($node instanceof Node\Stmt\Expression)) return;

$expr = $node->expr;
if ($expr instanceof Node\Expr\Assign) {
$expr = $expr->expr; // If it's an assignment, get the expression on the RHS
}

if (
$expr instanceof Node\Expr\StaticCall
&& in_array((string) $expr->class, ['Request', \Illuminate\Support\Facades\Request::class])
) {
if ($expr->name->name == "validate") {
return $expr->args[0]->value;
}

if ($expr->name->name == "validateWithBag") {
return $expr->args[1]->value;
}
}
}
}
2 changes: 2 additions & 0 deletions src/Extracting/Strategies/GetFromInlineValidatorBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Knuckles\Scribe\Extracting\MethodAstParser;
use Knuckles\Scribe\Extracting\ParsesValidationRules;
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\RequestValidate;
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\RequestValidateFacade;
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ThisValidate;
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ValidatorMake;
use PhpParser\Node;
Expand Down Expand Up @@ -175,6 +176,7 @@ protected function findValidationExpression($statements): ?array
{
$strategies = [
RequestValidate::class, // $request->validate(...);
RequestValidateFacade::class, // Request::validate(...);
ValidatorMake::class, // Validator::make($request, ...)
ThisValidate::class, // $this->validate(...);
];
Expand Down
4 changes: 4 additions & 0 deletions src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ protected function normalizeParameterData(array $data): array
$data['example'] = null;
}

if ($data['required']){
$data['nullable'] = false;
}

$data['description'] = trim($data['description'] ?? '');
return $data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,28 @@ class GetFromResponseFieldTag extends GetFieldsFromTagStrategy
protected function parseTag(string $tagContent): array
{
// Format:
// @responseField <name> <type> <description>
// @responseField <name> <type> <"required" (optional)> <description>
// Examples:
// @responseField text string The text.
// @responseField text string required The text.
// @responseField user_id integer The ID of the user.
preg_match('/(.+?)\s+(.+?)\s+([\s\S]*)/', $tagContent, $content);
preg_match('/(.+?)\s+(.+?)\s+(.+?)\s+([\s\S]*)/', $tagContent, $content);
if (empty($content)) {
// This means only name and type were supplied
[$name, $type] = preg_split('/\s+/', $tagContent);
$description = '';
$required = false;
} else {
[$_, $name, $type, $description] = $content;
[$_, $name, $type, $required, $description] = $content;
if($required !== "required"){
$description = $required . " " . $description;
}

$required = $required === "required";
$description = trim($description);
}

$type = static::normalizeTypeName($type);
$data = compact('name', 'type', 'description');
$data = compact('name', 'type', 'required', 'description');

// Support optional type in annotation
// The type can also be a union or nullable type (eg ?string or string|null)
Expand Down
60 changes: 55 additions & 5 deletions src/Writing/OpenAPISpecWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,16 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE
],
'example' => $decoded,
],
],
],
];

case 'object':
$properties = collect($decoded)->mapWithKeys(function ($value, $key) use ($endpoint) {
return [$key => $this->generateSchemaForValue($value, $endpoint, $key)];
})->toArray();
$required = $this->filterRequiredFields($endpoint, array_keys($properties));

return [
$data = [
'application/json' => [
'schema' => [
'type' => 'object',
Expand All @@ -417,6 +418,11 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE
],
],
];
if ($required) {
$data['application/json']['schema']['required'] = $required;
}

return $data;
}
}

Expand Down Expand Up @@ -488,6 +494,7 @@ public function generateFieldData($field): array
'type' => 'string',
'format' => 'binary',
'description' => $field->description ?: '',
'nullable' => $field->nullable,
];
} else if (Utils::isArrayType($field->type)) {
$baseType = Utils::getBaseTypeFromArrayType($field->type);
Expand All @@ -500,6 +507,10 @@ public function generateFieldData($field): array
$baseItem['enum'] = $field->enumValues;
}

if ($field->nullable) {
$baseItem['nullable'] = true;
}

$fieldData = [
'type' => 'array',
'description' => $field->description ?: '',
Expand All @@ -509,6 +520,7 @@ public function generateFieldData($field): array
'name' => '',
'type' => $baseType,
'example' => ($field->example ?: [null])[0],
'nullable' => $field->nullable,
])
: $baseItem,
];
Expand All @@ -535,6 +547,7 @@ public function generateFieldData($field): array
'type' => 'object',
'description' => $field->description ?: '',
'example' => $field->example,
'nullable'=> $field->nullable,
'properties' => $this->objectIfEmpty(collect($field->__fields)->mapWithKeys(function ($subfield, $subfieldName) {
return [$subfieldName => $this->generateFieldData($subfield)];
})->all()),
Expand All @@ -544,6 +557,7 @@ public function generateFieldData($field): array
'type' => static::normalizeTypeName($field->type),
'description' => $field->description ?: '',
'example' => $field->example,
'nullable' => $field->nullable,
];
if (!empty($field->enumValues)) {
$schema['enum'] = $field->enumValues;
Expand Down Expand Up @@ -585,19 +599,29 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin
$subFieldPath = sprintf('%s.%s', $path, $subField);
$properties[$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath);
}
$required = $this->filterRequiredFields($endpoint, array_keys($properties), $path);

return [
$schema = [
'type' => 'object',
'properties' => $this->objectIfEmpty($properties),
];
if ($required) {
$schema['required'] = $required;
}
$this->setDescription($schema, $endpoint, $path);

return $schema;
}

$schema = [
'type' => $this->convertScribeOrPHPTypeToOpenAPIType(gettype($value)),
'example' => $value,
];
if (isset($endpoint->responseFields[$path]->description)) {
$schema['description'] = $endpoint->responseFields[$path]->description;
$this->setDescription($schema, $endpoint, $path);

// Set enum values for the property if they exist
if (isset($endpoint->responseFields[$path]->enumValues)) {
$schema['enum'] = $endpoint->responseFields[$path]->enumValues;
}

if ($schema['type'] === 'array' && !empty($value)) {
Expand All @@ -616,4 +640,30 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin

return $schema;
}

/**
* Given an enpoint and a set of object keys at a path, return the properties that are specified as required.
*/
public function filterRequiredFields(OutputEndpointData $endpoint, array $properties, string $path = ''): array
{
$required = [];
foreach ($properties as $property) {
$responseField = $endpoint->responseFields["$path.$property"] ?? $endpoint->responseFields[$property] ?? null;
if ($responseField && $responseField->required) {
$required[] = $property;
}
}

return $required;
}

/*
* Set the description for the schema. If the field has a description, it is set in the schema.
*/
private function setDescription(array &$schema, OutputEndpointData $endpoint, string $path): void
{
if (isset($endpoint->responseFields[$path]->description)) {
$schema['description'] = $endpoint->responseFields[$path]->description;
}
}
}
Loading

0 comments on commit 41e4a56

Please sign in to comment.