Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

171 reference to incorrect component should be invalid #197

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/node_modules

/.php_cs.cache
/.php-cs-fixer.cache
/.phpunit.result.cache

php-cs-fixer.phar
2 changes: 1 addition & 1 deletion .php_cs.dist → .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

return PhpCsFixer\Config::create()
return (new PhpCsFixer\Config())
->setRules([
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ check-style: php-cs-fixer.phar

fix-style: php-cs-fixer.phar
$(DOCKER_PHP) vendor/bin/indent --tabs composer.json
$(DOCKER_PHP) vendor/bin/indent --spaces .php_cs.dist
$(DOCKER_PHP) vendor/bin/indent --spaces .php-cs-fixer.dist.php
$(DOCKER_PHP) ./php-cs-fixer.phar fix src/ --diff

install: composer.lock yarn.lock
Expand Down
61 changes: 57 additions & 4 deletions src/Reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Reader
{
/**
* Populate OpenAPI spec object from JSON data.
* Saves reference context for resolving internal references
* @phpstan-template T of SpecObjectInterface
* @phpstan-param class-string<T> $baseType
* @phpstan-return T
Expand All @@ -34,7 +35,33 @@ class Reader
* The type of the returned object depends on the `$baseType` argument.
* @throws TypeErrorException in case invalid spec data is supplied.
*/
public static function readFromJson(string $json, string $baseType = OpenApi::class): SpecObjectInterface
public static function readFromJson(string $json, string $baseType = OpenApi::class, bool $resolveReferences = true): SpecObjectInterface
{
$spec = static::fromJson($json, $baseType);
$context = ReferenceContext::readFromString($spec, $json);
$context->setDefaultCacheKey($baseType);
$context->mode = ReferenceContext::RESOLVE_MODE_INLINE;
$spec->setReferenceContext($context);
if($resolveReferences && $context->hasComponentsRef()) {
$spec->resolveReferences();
}
return $spec;
}

/**
* Populate OpenAPI spec object from JSON data.
* @phpstan-template T of SpecObjectInterface
* @phpstan-param class-string<T> $baseType
* @phpstan-return T
* @param string $json the JSON string to decode.
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
* You may choose a different type if you instantiate objects from sub sections of a specification.
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
* The type of the returned object depends on the `$baseType` argument.
* @throws TypeErrorException in case invalid spec data is supplied.
*/
protected static function fromJson(string $json, string $baseType = OpenApi::class): SpecObjectInterface
{
return new $baseType(json_decode($json, true));
}
Expand All @@ -52,7 +79,33 @@ public static function readFromJson(string $json, string $baseType = OpenApi::cl
* The type of the returned object depends on the `$baseType` argument.
* @throws TypeErrorException in case invalid spec data is supplied.
*/
public static function readFromYaml(string $yaml, string $baseType = OpenApi::class): SpecObjectInterface
public static function readFromYaml(string $yaml, string $baseType = OpenApi::class, bool $resolveReferences = true): SpecObjectInterface
{
$spec = static::fromYaml($yaml, $baseType);
$context = ReferenceContext::readFromString($spec, $yaml);
$context->setDefaultCacheKey($baseType);
$context->mode = ReferenceContext::RESOLVE_MODE_INLINE;
$spec->setReferenceContext($context);
if($resolveReferences && $context->hasComponentsRef()) {
$spec->resolveReferences();
}
return $spec;
}

/**
* Populate OpenAPI spec object from YAML data.
* @phpstan-template T of SpecObjectInterface
* @phpstan-param class-string<T> $baseType
* @phpstan-return T
* @param string $yaml the YAML string to decode.
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
* You may choose a different type if you instantiate objects from sub sections of a specification.
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
* The type of the returned object depends on the `$baseType` argument.
* @throws TypeErrorException in case invalid spec data is supplied.
*/
public static function fromYaml(string $yaml, string $baseType = OpenApi::class): SpecObjectInterface
{
return new $baseType(Yaml::parse($yaml));
}
Expand Down Expand Up @@ -89,7 +142,7 @@ public static function readFromJsonFile(string $fileName, string $baseType = Ope
$e->fileName = $fileName;
throw $e;
}
$spec = static::readFromJson($fileContent, $baseType);
$spec = static::fromJson($fileContent, $baseType);
$context = new ReferenceContext($spec, $fileName);
$spec->setReferenceContext($context);
if ($resolveReferences !== false) {
Expand Down Expand Up @@ -135,7 +188,7 @@ public static function readFromYamlFile(string $fileName, string $baseType = Ope
$e->fileName = $fileName;
throw $e;
}
$spec = static::readFromYaml($fileContent, $baseType);
$spec = static::fromYaml($fileContent, $baseType);
$context = new ReferenceContext($spec, $fileName);
$spec->setReferenceContext($context);
if ($resolveReferences !== false) {
Expand Down
80 changes: 79 additions & 1 deletion src/ReferenceContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use cebe\openapi\exceptions\IOException;
use cebe\openapi\exceptions\UnresolvableReferenceException;
use cebe\openapi\json\JsonPointer;
use cebe\openapi\spec\OpenApi;
use cebe\openapi\spec\Reference;
use Symfony\Component\Yaml\Yaml;

Expand Down Expand Up @@ -51,6 +52,21 @@ class ReferenceContext
*/
private $_cache;

/**
* @var bool checks if content is read from string or file
*/
private $_read_from_string = false;

/**
* @var string Default cache key for data read from string
*/
private $_string_cache_key = OpenApi::class;

/**
* @var bool checks if read string contains components
*/
private $_content_has_components = false;


/**
* ReferenceContext constructor.
Expand All @@ -62,7 +78,7 @@ class ReferenceContext
public function __construct(?SpecObjectInterface $base, string $uri, $cache = null)
{
$this->_baseSpec = $base;
$this->_uri = $this->normalizeUri($uri);
$this->_uri = empty($uri) ? static::class : $this->normalizeUri($uri);
$this->_cache = $cache ?? new ReferenceContextCache();
if ($cache === null && $base !== null) {
$this->_cache->set($this->_uri, null, $base);
Expand Down Expand Up @@ -210,6 +226,7 @@ public function resolveRelativeUri(string $uri): string
*/
public function fetchReferencedFile($uri)
{
$uri = $this->resolveCacheUri($uri);
if ($this->_cache->has('FILE_CONTENT://' . $uri, 'FILE_CONTENT')) {
return $this->_cache->get('FILE_CONTENT://' . $uri, 'FILE_CONTENT');
}
Expand All @@ -221,6 +238,15 @@ public function fetchReferencedFile($uri)
throw $e;
}
// TODO lazy content detection, should be improved
$parsedContent = $this->parseAndCacheContent($content, $uri);
return $parsedContent;
}

/**
* Parse content from string to either Yaml or Json
*/
protected function parseAndCacheContent($content, $uri): array
{
if (strpos(ltrim($content), '{') === 0) {
$parsedContent = json_decode($content, true);
} else {
Expand All @@ -229,6 +255,43 @@ public function fetchReferencedFile($uri)
$this->_cache->set('FILE_CONTENT://' . $uri, 'FILE_CONTENT', $parsedContent);
return $parsedContent;
}

/**
* Prepare content read and cache from JSON or YAML string
*/
public function prepareDataFromString($content)
{
$this->_read_from_string = true;
$parsedContent = $this->parseAndCacheContent($content, $this->resolveCacheUri(''));

if(array_key_exists('components', $parsedContent) && str_contains($content, '#/components')) {
$this->_content_has_components = true;
}
}

/**
* Is content from string or file
*/
public function isFromFile(): bool
{
return !$this->_read_from_string;
}

/**
* Uses default base classname to cache what ever is read
*/
public function setDefaultCacheKey($key = OpenApi::class)
{
$this->_string_cache_key = $key;
}

/**
* Return result indicating if components and ref is present in read string
*/
public function hasComponentsRef(): bool
{
return $this->_content_has_components;
}

/**
* Retrieve the referenced data via JSON pointer.
Expand Down Expand Up @@ -267,4 +330,19 @@ public function resolveReferenceData($uri, JsonPointer $pointer, $data, $toType)

return $referencedObject;
}

protected function resolveCacheUri($uri)
{
return empty($uri) ? $this->_string_cache_key : $uri;
}

/**
* Static function that reads from string and initialises base class
*/
public static function readFromString($base, $content): static
{
$context = new static($base, '');
$context->prepareDataFromString($content);
return $context;
}
}
10 changes: 5 additions & 5 deletions src/json/JsonPointer.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ public function evaluate($jsonDocument)

foreach ($this->getPath() as $part) {
if (is_array($currentReference)) {
// if (!preg_match('~^([1-9]*[0-9]|-)$~', $part)) {
// throw new NonexistentJsonPointerReferenceException(
// "Failed to evaluate pointer '$this->_pointer'. Invalid pointer path '$part' for Array at path '$currentPath'."
// );
// }
// if (!preg_match('~^([1-9]*[0-9]|-)$~', $part)) {
// throw new NonexistentJsonPointerReferenceException(
// "Failed to evaluate pointer '$this->_pointer'. Invalid pointer path '$part' for Array at path '$currentPath'."
// );
// }
if ($part === '-' || !array_key_exists($part, $currentReference)) {
throw new NonexistentJsonPointerReferenceException(
"Failed to evaluate pointer '$this->_pointer'. Array has no member $part at path '$currentPath'."
Expand Down
87 changes: 46 additions & 41 deletions src/spec/Reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public function setContext(ReferenceContext $context)
/**
* @return ReferenceContext
*/
public function getContext() : ?ReferenceContext
public function getContext(): ?ReferenceContext
{
return $this->_context;
}
Expand Down Expand Up @@ -186,53 +186,58 @@ public function resolve(ReferenceContext $context = null)
return $this;
}
try {
if ($jsonReference->getDocumentUri() === '') {
if ($context->mode === ReferenceContext::RESOLVE_MODE_INLINE) {
return $this;
}

// resolve in current document
$baseSpec = $context->getBaseSpec();
if ($baseSpec !== null) {
// TODO type error if resolved object does not match $this->_to ?
/** @var SpecObjectInterface $referencedObject */
$referencedObject = $jsonReference->getJsonPointer()->evaluate($baseSpec);
// transitive reference
if ($referencedObject instanceof Reference) {
$referencedObject = $this->resolveTransitiveReference($referencedObject, $context);
if ($context->isFromFile()) {
if ($jsonReference->getDocumentUri() === '') {
if ($context->mode === ReferenceContext::RESOLVE_MODE_INLINE) {
return $this;
}
if ($referencedObject instanceof SpecObjectInterface) {
$referencedObject->setReferenceContext($context);

// resolve in current document
$baseSpec = $context->getBaseSpec();
if ($baseSpec !== null) {
// TODO type error if resolved object does not match $this->_to ?
/** @var SpecObjectInterface $referencedObject */
$referencedObject = $jsonReference->getJsonPointer()->evaluate($baseSpec);
// transitive reference
if ($referencedObject instanceof Reference) {
$referencedObject = $this->resolveTransitiveReference($referencedObject, $context);
}
if ($referencedObject instanceof SpecObjectInterface) {
$referencedObject->setReferenceContext($context);
}
return $referencedObject;
} else {
// if current document was loaded via reference, it may be null,
// so we load current document by URI instead.
$jsonReference = JsonReference::createFromUri($context->getUri(), $jsonReference->getJsonPointer());
}
return $referencedObject;
} else {
// if current document was loaded via reference, it may be null,
// so we load current document by URI instead.
$jsonReference = JsonReference::createFromUri($context->getUri(), $jsonReference->getJsonPointer());
}
}

// resolve in external document
$file = $context->resolveRelativeUri($jsonReference->getDocumentUri());
try {
$referencedDocument = $context->fetchReferencedFile($file);
} catch (\Throwable $e) {
$exception = new UnresolvableReferenceException(
"Failed to resolve Reference '$this->_ref' to $this->_to Object: " . $e->getMessage(),
$e->getCode(),
$e
);
$exception->context = $this->getDocumentPosition();
throw $exception;
}
// resolve in external document
$file = $context->resolveRelativeUri($jsonReference->getDocumentUri());
try {
$referencedDocument = $context->fetchReferencedFile($file);
} catch (\Throwable $e) {
$exception = new UnresolvableReferenceException(
"Failed to resolve Reference '$this->_ref' to $this->_to Object: " . $e->getMessage(),
$e->getCode(),
$e
);
$exception->context = $this->getDocumentPosition();
throw $exception;
}

$referencedDocument = $this->adjustRelativeReferences($referencedDocument, $file, null, $context);
$referencedObject = $context->resolveReferenceData($file, $jsonReference->getJsonPointer(), $referencedDocument, $this->_to);
$referencedDocument = $this->adjustRelativeReferences($referencedDocument, $file, null, $context);
$referencedObject = $context->resolveReferenceData($file, $jsonReference->getJsonPointer(), $referencedDocument, $this->_to);

if ($referencedObject instanceof DocumentContextInterface) {
if ($referencedObject->getDocumentPosition() === null && $this->getDocumentPosition() !== null) {
$referencedObject->setDocumentContext($context->getBaseSpec(), $this->getDocumentPosition());
if ($referencedObject instanceof DocumentContextInterface) {
if ($referencedObject->getDocumentPosition() === null && $this->getDocumentPosition() !== null) {
$referencedObject->setDocumentContext($context->getBaseSpec(), $this->getDocumentPosition());
}
}
} else {
$referencedDocument = $context->fetchReferencedFile('');
$referencedObject = $context->resolveReferenceData('', $jsonReference->getJsonPointer(), $referencedDocument, $this->_to);
}

// transitive reference
Expand Down
Loading