Skip to content

Commit

Permalink
Merge branch 'master' of github.com:thephpleague/openapi-psr7-validator
Browse files Browse the repository at this point in the history
* 'master' of github.com:thephpleague/openapi-psr7-validator: (47 commits)
  Drop test for valid epsilon
  Add use statement for round
  Use epsilon to compare floating points
  Fix type errors
  Unignore phpstan errors, fixed now. Add polyfill 8.0 for `str_starts_with`
  Fix tests for boolean validation
  Fix string-to-boolean casting
  Handle path parameters for arrays correctly
  Add getVerboseMessage to AddressValidationFailed exception
  Fix test notice
  Fix content type parameter comparison
  (thephpleague#134) Add support for comma-separated list of encoding content types
  Fix cs errors. Use PHP 7.2 as phpstan reference language version to avoid false positive errors
  PathFinder: Adding filtering of placeholder routes scored by matching parts count
  Also remove check from multi-part server request validation
  Use devizzent/cebe-php-openapi to be compatible with OpenApi 3.1
  Allow specifying encoding for optional parts of multipart data without validation errors
  Temporary silence phpstan for checking type of magic props
  Fix codestyle
  Access operation security element directly, avoiding getSerializableData()
  ...

# Conflicts:
#	src/PSR7/Validators/BodyValidator/MultipartValidator.php
  • Loading branch information
janmaennig committed Jul 31, 2023
2 parents 12e5307 + bccdd3f commit dd4925b
Show file tree
Hide file tree
Showing 45 changed files with 859 additions and 195 deletions.
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
"require": {
"php": ">=7.2",
"ext-json": "*",
"cebe/php-openapi": "^1.6",
"devizzent/cebe-php-openapi": "^1.0",
"league/uri": "^6.3",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"psr/http-message": "^1.0",
"psr/http-server-middleware": "^1.0",
"respect/validation": "^1.1.3 || ^2.0",
"riverline/multipart-parser": "^2.0.3",
"symfony/polyfill-php80": "^1.27",
"webmozart/assert": "^1.4"
},
"require-dev": {
Expand All @@ -44,7 +45,8 @@
"symfony/cache": "^5.1"
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": true
},
"prefer-stable": true
}
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
parameters:
phpVersion: 70200
level: 6
paths:
- src
- tests
treatPhpDocTypesAsCertain: false
ignoreErrors:
- '#Caught class Respect\\Validation\\Exceptions\\ExceptionInterface not found.#'
- '#Call to an undefined static method Respect\\Validation\\Validator::numeric\(\).#'
18 changes: 18 additions & 0 deletions src/PSR7/Exception/Validation/AddressValidationFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\Schema\Exception\SchemaMismatch;
use Throwable;

use function implode;
use function rtrim;
use function sprintf;

abstract class AddressValidationFailed extends ValidationFailed
Expand Down Expand Up @@ -42,6 +45,21 @@ public static function fromAddr(OperationAddress $address): self
return $ex;
}

public function getVerboseMessage(): string
{
$previous = $this->getPrevious();
if (! $previous instanceof SchemaMismatch) {
return $this->getMessage();
}

return sprintf(
'%s. [%s in %s]',
$this->getMessage(),
rtrim($previous->getMessage(), '.'),
implode('->', $previous->dataBreadCrumb()->buildChain())
);
}

public function getAddress(): OperationAddress
{
return $this->address;
Expand Down
26 changes: 25 additions & 1 deletion src/PSR7/OperationAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
use League\OpenAPIValidation\PSR7\Exception\Validation\InvalidPath;
use League\OpenAPIValidation\Schema\Exception\InvalidSchema;

use function explode;
use function implode;
use function preg_match;
use function preg_match_all;
use function preg_quote;
use function preg_replace;
use function preg_split;
use function sprintf;
use function strtok;
use function trim;

use const PREG_SPLIT_DELIM_CAPTURE;

Expand Down Expand Up @@ -62,7 +65,28 @@ public function path(): string

public function hasPlaceholders(): bool
{
return preg_match(self::PATH_PLACEHOLDER, $this->path()) === 1;
return (bool) $this->countPlaceholders();
}

public function countPlaceholders(): int
{
return preg_match_all(self::PATH_PLACEHOLDER, $this->path()) ?? 0;
}

public function countExactMatchParts(string $comparisonPath): int
{
$comparisonPathParts = explode('/', trim($comparisonPath, '/'));
$pathParts = explode('/', trim($this->path(), '/'));
$exactMatchCount = 0;
foreach ($comparisonPathParts as $key => $comparisonPathPart) {
if ($comparisonPathPart !== $pathParts[$key]) {
continue;
}

$exactMatchCount++;
}

return $exactMatchCount;
}

/**
Expand Down
59 changes: 59 additions & 0 deletions src/PSR7/PathFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
use cebe\openapi\spec\PathItem;
use cebe\openapi\spec\Server;

use function array_keys;
use function array_unique;
use function count;
use function ltrim;
use function max;
use function parse_url;
use function preg_match;
use function preg_match_all;
use function preg_replace;
use function rtrim;
use function sprintf;
use function strtolower;
use function trim;
use function usort;

use const PHP_URL_PATH;
Expand Down Expand Up @@ -206,6 +211,60 @@ private function prioritizeStaticPaths(array $paths): array
return 0;
});

return $this->attemptNarrowDown($paths);
}

/**
* Some paths are more static than others.
*
* @param OperationAddress[] $paths
*
* @return OperationAddress[]
*/
private function attemptNarrowDown(array $paths): array
{
if (count($paths) === 1) {
return $paths;
}

$partCounts = [];
$placeholderCounts = [];
foreach ($paths as $path) {
$partCounts[] = $this->countParts($path->path());
$placeholderCounts[] = $path->countPlaceholders();
}

$partCounts[] = $this->countParts($this->path);
if (count(array_unique($partCounts)) === 1 && count(array_unique($placeholderCounts)) > 1) {
// All paths have the same number of parts but there are differing placeholder counts. We can narrow down!
return $this->filterToHighestExactMatchingParts($paths);
}

return $paths;
}

/**
* Scores all paths by how many parts match exactly with $this->path and returns only the highest scoring group
*
* @param OperationAddress[] $paths
*
* @return OperationAddress[]
*/
private function filterToHighestExactMatchingParts(array $paths): array
{
$scoredCandidates = [];
foreach ($paths as $candidate) {
$score = $candidate->countExactMatchParts($this->path);
$scoredCandidates[$score][] = $candidate;
}

$highestScoreKey = max(array_keys($scoredCandidates));

return $scoredCandidates[$highestScoreKey];
}

private function countParts(string $path): int
{
return preg_match_all('#/#', trim($path, '/')) + 1;
}
}
15 changes: 7 additions & 8 deletions src/PSR7/SpecFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
use League\OpenAPIValidation\Schema\Exception\InvalidSchema;
use Webmozart\Assert\Assert;

use function is_array;
use function json_decode;
use function json_encode;
use function property_exists;
use function substr;

final class SpecFinder
Expand Down Expand Up @@ -152,15 +152,14 @@ public function findSecuritySpecs(OperationAddress $addr): array
$opSpec = $this->findOperationSpec($addr);

// 1. Collect security params
if (property_exists($opSpec->getSerializableData(), 'security')) {
// security is set on operation level
$securitySpecs = $opSpec->security;
} else {
// security is set on root level (fallback option)
$securitySpecs = $this->openApi->security;
$securitySpecs = $opSpec->security;

// security is set on operation level
if (is_array($securitySpecs)) {
return $securitySpecs;
}

return $securitySpecs;
return $this->openApi->security;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function validate(OperationAddress $addr, MessageInterface $message): voi

// 0. Multipart body message MUST be described with a set of object properties
if ($schema->type !== CebeType::OBJECT) {
throw TypeMismatch::becauseTypeDoesNotMatch('object', $schema->type);
throw TypeMismatch::becauseTypeDoesNotMatch(['object'], $schema->type);
}

// 1. Parse message body
Expand Down
Loading

0 comments on commit dd4925b

Please sign in to comment.