277 lines
8 KiB
PHP
277 lines
8 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the JsonSchema package.
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace JsonSchema;
|
|
|
|
use JsonSchema\Exception\JsonDecodingException;
|
|
use JsonSchema\Uri\Retrievers\UriRetrieverInterface;
|
|
use JsonSchema\Uri\UriRetriever;
|
|
|
|
/**
|
|
* Take in an object that's a JSON schema and take care of all $ref references
|
|
*
|
|
* @author Tyler Akins <fidian@rumkin.com>
|
|
* @see README.md
|
|
*/
|
|
class RefResolver
|
|
{
|
|
/**
|
|
* HACK to prevent too many recursive expansions.
|
|
* Happens e.g. when you want to validate a schema against the schema
|
|
* definition.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected static $depth = 0;
|
|
|
|
/**
|
|
* maximum references depth
|
|
* @var integer
|
|
*/
|
|
public static $maxDepth = 7;
|
|
|
|
/**
|
|
* @var UriRetrieverInterface
|
|
*/
|
|
protected $uriRetriever = null;
|
|
|
|
/**
|
|
* @var object
|
|
*/
|
|
protected $rootSchema = null;
|
|
|
|
/**
|
|
* @param UriRetriever $retriever
|
|
*/
|
|
public function __construct($retriever = null)
|
|
{
|
|
$this->uriRetriever = $retriever;
|
|
}
|
|
|
|
/**
|
|
* Retrieves a given schema given a ref and a source URI
|
|
*
|
|
* @param string $ref Reference from schema
|
|
* @param string $sourceUri URI where original schema was located
|
|
* @return object Schema
|
|
*/
|
|
public function fetchRef($ref, $sourceUri)
|
|
{
|
|
$retriever = $this->getUriRetriever();
|
|
$jsonSchema = $retriever->retrieve($ref, $sourceUri);
|
|
$this->resolve($jsonSchema);
|
|
|
|
return $jsonSchema;
|
|
}
|
|
|
|
/**
|
|
* Return the URI Retriever, defaulting to making a new one if one
|
|
* was not yet set.
|
|
*
|
|
* @return UriRetriever
|
|
*/
|
|
public function getUriRetriever()
|
|
{
|
|
if (is_null($this->uriRetriever)) {
|
|
$this->setUriRetriever(new UriRetriever);
|
|
}
|
|
|
|
return $this->uriRetriever;
|
|
}
|
|
|
|
/**
|
|
* Resolves all $ref references for a given schema. Recurses through
|
|
* the object to resolve references of any child schemas.
|
|
*
|
|
* The 'format' property is omitted because it isn't required for
|
|
* validation. Theoretically, this class could be extended to look
|
|
* for URIs in formats: "These custom formats MAY be expressed as
|
|
* an URI, and this URI MAY reference a schema of that format."
|
|
*
|
|
* The 'id' property is not filled in, but that could be made to happen.
|
|
*
|
|
* @param object $schema JSON Schema to flesh out
|
|
* @param string $sourceUri URI where this schema was located
|
|
*/
|
|
public function resolve($schema, $sourceUri = null)
|
|
{
|
|
if (self::$depth > self::$maxDepth) {
|
|
self::$depth = 0;
|
|
throw new JsonDecodingException(JSON_ERROR_DEPTH);
|
|
}
|
|
++self::$depth;
|
|
|
|
if (! is_object($schema)) {
|
|
--self::$depth;
|
|
return;
|
|
}
|
|
|
|
if (null === $sourceUri && ! empty($schema->id)) {
|
|
$sourceUri = $schema->id;
|
|
}
|
|
|
|
if (null === $this->rootSchema) {
|
|
$this->rootSchema = $schema;
|
|
}
|
|
|
|
// Resolve $ref first
|
|
$this->resolveRef($schema, $sourceUri);
|
|
|
|
// These properties are just schemas
|
|
// eg. items can be a schema or an array of schemas
|
|
foreach (array('additionalItems', 'additionalProperties', 'extends', 'items') as $propertyName) {
|
|
$this->resolveProperty($schema, $propertyName, $sourceUri);
|
|
}
|
|
|
|
// These are all potentially arrays that contain schema objects
|
|
// eg. type can be a value or an array of values/schemas
|
|
// eg. items can be a schema or an array of schemas
|
|
foreach (array('disallow', 'extends', 'items', 'type', 'allOf', 'anyOf', 'oneOf') as $propertyName) {
|
|
$this->resolveArrayOfSchemas($schema, $propertyName, $sourceUri);
|
|
}
|
|
|
|
// These are all objects containing properties whose values are schemas
|
|
foreach (array('dependencies', 'patternProperties', 'properties') as $propertyName) {
|
|
$this->resolveObjectOfSchemas($schema, $propertyName, $sourceUri);
|
|
}
|
|
|
|
--self::$depth;
|
|
}
|
|
|
|
/**
|
|
* Given an object and a property name, that property should be an
|
|
* array whose values can be schemas.
|
|
*
|
|
* @param object $schema JSON Schema to flesh out
|
|
* @param string $propertyName Property to work on
|
|
* @param string $sourceUri URI where this schema was located
|
|
*/
|
|
public function resolveArrayOfSchemas($schema, $propertyName, $sourceUri)
|
|
{
|
|
if (! isset($schema->$propertyName) || ! is_array($schema->$propertyName)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($schema->$propertyName as $possiblySchema) {
|
|
$this->resolve($possiblySchema, $sourceUri);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given an object and a property name, that property should be an
|
|
* object whose properties are schema objects.
|
|
*
|
|
* @param object $schema JSON Schema to flesh out
|
|
* @param string $propertyName Property to work on
|
|
* @param string $sourceUri URI where this schema was located
|
|
*/
|
|
public function resolveObjectOfSchemas($schema, $propertyName, $sourceUri)
|
|
{
|
|
if (! isset($schema->$propertyName) || ! is_object($schema->$propertyName)) {
|
|
return;
|
|
}
|
|
|
|
foreach (get_object_vars($schema->$propertyName) as $possiblySchema) {
|
|
$this->resolve($possiblySchema, $sourceUri);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given an object and a property name, that property should be a
|
|
* schema object.
|
|
*
|
|
* @param object $schema JSON Schema to flesh out
|
|
* @param string $propertyName Property to work on
|
|
* @param string $sourceUri URI where this schema was located
|
|
*/
|
|
public function resolveProperty($schema, $propertyName, $sourceUri)
|
|
{
|
|
if (! isset($schema->$propertyName)) {
|
|
return;
|
|
}
|
|
|
|
$this->resolve($schema->$propertyName, $sourceUri);
|
|
}
|
|
|
|
/**
|
|
* Look for the $ref property in the object. If found, remove the
|
|
* reference and augment this object with the contents of another
|
|
* schema.
|
|
*
|
|
* @param object $schema JSON Schema to flesh out
|
|
* @param string $sourceUri URI where this schema was located
|
|
*/
|
|
public function resolveRef($schema, $sourceUri)
|
|
{
|
|
$ref = '$ref';
|
|
|
|
if (empty($schema->$ref)) {
|
|
return;
|
|
}
|
|
|
|
$splitRef = explode('#', $schema->$ref, 2);
|
|
|
|
$refDoc = $splitRef[0];
|
|
$refPath = null;
|
|
if (count($splitRef) === 2) {
|
|
$refPath = explode('/', $splitRef[1]);
|
|
array_shift($refPath);
|
|
}
|
|
|
|
if (empty($refDoc) && empty($refPath)) {
|
|
// TODO: Not yet implemented - root pointer ref, causes recursion issues
|
|
return;
|
|
}
|
|
|
|
if (!empty($refDoc)) {
|
|
$refSchema = $this->fetchRef($refDoc, $sourceUri);
|
|
} else {
|
|
$refSchema = $this->rootSchema;
|
|
}
|
|
|
|
if (null !== $refPath) {
|
|
$refSchema = $this->resolveRefSegment($refSchema, $refPath);
|
|
}
|
|
|
|
unset($schema->$ref);
|
|
|
|
// Augment the current $schema object with properties fetched
|
|
foreach (get_object_vars($refSchema) as $prop => $value) {
|
|
$schema->$prop = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set URI Retriever for use with the Ref Resolver
|
|
*
|
|
* @param UriRetriever $retriever
|
|
* @return $this for chaining
|
|
*/
|
|
public function setUriRetriever(UriRetriever $retriever)
|
|
{
|
|
$this->uriRetriever = $retriever;
|
|
|
|
return $this;
|
|
}
|
|
|
|
protected function resolveRefSegment($data, $pathParts)
|
|
{
|
|
foreach ($pathParts as $path) {
|
|
$path = strtr($path, array('~1' => '/', '~0' => '~', '%25' => '%'));
|
|
|
|
if (is_array($data)) {
|
|
$data = $data[$path];
|
|
} else {
|
|
$data = $data->{$path};
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|