Skip to content
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
103 changes: 64 additions & 39 deletions src/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@
class Resolver
{
/**
* Schema resolution stack. Each item on the stack is an array
* containing an uri and a schema.
* @var stdClass
*/
private $rootSchema;

/**
* Stack of URIs used for resolving relative URIs.
*
* @var array
* @var Uri[]
*/
private $stack = [];
private $uriStack = [];

/**
* Schema cache. Each schema visited at a given URI is stored
Expand All @@ -59,40 +63,54 @@ class Resolver
*/
public function initialize(stdClass $schema, Uri $uri)
{
$this->registerSchema($schema, $uri);
$this->stack = [[$uri, $schema]];
if ($uri->isAbsolute() && !$uri->hasPointer()) {
$this->registerSchema($schema, $uri);
}

$this->rootSchema = $schema;
$this->uriStack = [$uri];
}

/**
* Returns the URI of the current schema.
* Returns URI of root schema.
*
* @return Uri
*
* @throws EmptyStackException
*/
public function getCurrentUri()
public function getRootUri()
{
if (count($this->stack) === 0) {
if (count($this->uriStack) === 0) {
throw new EmptyStackException();
}

return end($this->stack)[0];
return reset($this->uriStack);
}

/**
* Returns the current schema.
* Returns root schema.
*
* @return stdClass
* @return stdClass|null
*/
public function getRootSchema()
{
return $this->rootSchema;
}

/**
* Returns the URI of the current schema.
*
* @return Uri
*
* @throws EmptyStackException
*/
public function getCurrentSchema()
public function getCurrentUri()
{
if (count($this->stack) === 0) {
if (count($this->uriStack) === 0) {
throw new EmptyStackException();
}

return end($this->stack)[1];
return end($this->uriStack);
}

/**
Expand All @@ -109,24 +127,22 @@ public function setPreFetchHook(Closure $preFetchHook)

/**
* Pushes an URI and its associated schema onto the resolution stack,
* making them the current URI/schema pair. If no schema is passed, the
* current schema is reused (useful when entering a resolution scope
* within the current schema).
* making them the current URI/schema pair.
*
* @param Uri $uri
* @param stdClass $schema
*
* @throws EmptyStackException
*/
public function enter(Uri $uri, stdClass $schema = null)
public function enter(Uri $uri, stdClass $schema)
{
$currentUri = $this->getCurrentUri();
$resolvedUri = $uri->resolveAgainst($currentUri);
$this->uriStack[] = $resolvedUri;

if (!$uri->isAbsolute()) {
$uri->resolveAgainst($currentUri);
if ($resolvedUri->isAbsolute() && !$resolvedUri->hasPointer()) {
$this->registerSchema($schema, $resolvedUri);
}

$this->stack[] = [$uri, $schema ?: $this->getCurrentSchema()];
}

/**
Expand All @@ -137,11 +153,11 @@ public function enter(Uri $uri, stdClass $schema = null)
*/
public function leave()
{
if (count($this->stack) === 0) {
if (count($this->uriStack) === 0) {
throw new EmptyStackException();
}

array_pop($this->stack);
array_pop($this->uriStack);
}

/**
Expand All @@ -161,17 +177,18 @@ public function resolve(stdClass $reference)
$baseUri = $this->getCurrentUri();
$uri = new Uri($reference->{'$ref'});

if (!$uri->isAbsolute()) {
$uri->resolveAgainst($baseUri);
}

$identifier = $uri->getPrimaryResourceIdentifier();

if (!isset($this->schemas[$identifier])) {
$schema = $this->fetchSchemaAt($identifier);
$this->registerSchema($schema, $uri);
if ($baseUri->getPrimaryResourceIdentifier() === '' && $uri->getPrimaryResourceIdentifier() === '') {
$schema = $this->getRootSchema();
} else {
$schema = $this->schemas[$identifier];
$uri = $uri->resolveAgainst($baseUri);
$identifier = $uri->getPrimaryResourceIdentifier();

if (isset($this->schemas[$identifier])) {
$schema = $this->schemas[$identifier];
} else {
$schema = $this->fetchSchemaAt($identifier);
$this->registerSchema($schema, $uri);
}
}

$resolved = $this->resolvePointer($schema, $uri);
Expand All @@ -188,15 +205,23 @@ public function resolve(stdClass $reference)
}

/**
* Caches a schema reference for future use.
* Registers a schema reference for future use.
*
* @param stdClass $schema
* @param Uri $uri
*/
private function registerSchema(stdClass $schema, Uri $uri)
public function registerSchema(stdClass $schema, Uri $uri)
{
if (!isset($this->schemas[$uri->getPrimaryResourceIdentifier()])) {
$this->schemas[$uri->getPrimaryResourceIdentifier()] = $schema;
if (!$uri->isAbsolute()) {
throw new \LogicException('Unable to register schema without absolute URI');
}

$identifier = $uri->getPrimaryResourceIdentifier();

if (!isset($this->schemas[$identifier])) {
$this->schemas[$identifier] = $schema;
} elseif (!Utils::areEqual($this->schemas[$identifier], $schema)) {
throw new \LogicException('Different schema is already registered with given URI');
}
}

Expand Down
50 changes: 26 additions & 24 deletions src/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function getRawUri()
*/
public function getRawPointer()
{
return isset($this->parts['fragment']) ? $this->parts['fragment'] : '';
return $this->parts['fragment'];
}

/**
Expand Down Expand Up @@ -125,6 +125,14 @@ public function getPointerSegments()
return $this->segments;
}

/**
* @return bool
*/
public function hasPointer()
{
return !empty($this->segments);
}

/**
* Returns the primary resource identifier part of the URI, i.e. everything
* excluding its fragment part.
Expand All @@ -146,26 +154,20 @@ public function getPrimaryResourceIdentifier()
*
* @param Uri $uri
*
* @return string
* @return Uri
*/
public function resolveAgainst(Uri $uri)
{
if ($this->isAbsolute()) {
throw new \LogicException(
'Cannot resolve against another URI: URI is already absolute'
);
}

if (!$uri->isAbsolute()) {
return $this;
} elseif (!$uri->isAbsolute()) {
throw new \LogicException(
'Cannot resolve against another URI: reference URI is not absolute'
);
} else {
$resolvedUri = $this->buildResolvedUriAgainst($uri);
return new self($resolvedUri);
}

$resolved = $this->buildResolvedUriAgainst($uri);
$this->buildFromRawUri($resolved);

return $resolved;
}

/**
Expand Down Expand Up @@ -233,17 +235,13 @@ private function buildSegments()
{
$segments = [];

if (isset($this->parts['fragment'])) {
$rawSegments = explode('/', $this->parts['fragment']);
if (substr($this->parts['fragment'], 0, 1) === '/') {
$rawSegments = explode('/', substr($this->parts['fragment'], 1));

foreach ($rawSegments as $segment) {
$segment = trim($segment);

if ($segment !== '') {
$segment = str_replace('~1', '/', $segment);
$segment = str_replace('~0', '~', $segment);
$segments[] = $segment;
}
$segment = str_replace('~1', '/', $segment);
$segment = str_replace('~0', '~', $segment);
$segments[] = $segment;
}
}

Expand All @@ -254,16 +252,20 @@ private function buildPrimaryIdentifier()
{
$identifier = '';

if ($this->parts['scheme']) {
if ($this->parts['scheme'] !== '') {
$identifier .= $this->parts['scheme'].'://';
}

$identifier .= $this->authority.$this->parts['path'];

if ($this->parts['query']) {
if ($this->parts['query'] !== '') {
$identifier .= '?'.$this->parts['query'];
}

if ($this->parts['fragment'] !== '' && $this->parts['fragment'][0] !== '/') {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't right: as per RFC 3986, a fragment denotes a secondary resource; it can't be part of the primary resource identifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I was unaware of the RFC 3986 terminology. The purpose of this is that we need non-JSON-pointer fragments to be used as part of schema identifiers.

$identifier .= '#'.$this->parts['fragment'];
}

return $identifier;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ public static function lastJsonErrorMessage()

private static function doAreEqual($a, $b, array $stack)
{
if ($a === $b) {
return true;
}

// keep track of object references to avoid infinite recursion
if (is_object($a)) {
if (in_array($a, $stack)) {
Expand Down
2 changes: 1 addition & 1 deletion src/Walker.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private function doResolveReferences(stdClass $schema, Uri $uri)

$inScope = false;
if (property_exists($schema, 'id') && is_string($schema->id)) {
$this->resolver->enter(new Uri($schema->id));
$this->resolver->enter(new Uri($schema->id), $schema);
$inScope = true;
}

Expand Down
7 changes: 0 additions & 7 deletions tests/Data/schemas/valid/root-reference-2.json

This file was deleted.

7 changes: 0 additions & 7 deletions tests/Data/schemas/valid/root-reference-3.json

This file was deleted.

8 changes: 0 additions & 8 deletions tests/Data/schemas/valid/scoped-references.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@
},
"bar": {
"$ref": "/valid/exclusiveMinimum-not-present.json"
},
"baz": {
"id": "exclusiveMaximum-not-present.json",
"oneOf": [
{
"$ref": "#"
}
]
}
}
}
Loading