-
Notifications
You must be signed in to change notification settings - Fork 363
Fully implement $ref compliance with draft 4 #210
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
Changes from all commits
0a0e8c7
23b9e9c
079d2ba
4708e0c
6df6a8f
b3ac9a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <?php | ||
|
|
||
| namespace JsonSchema\Exception; | ||
|
|
||
| class InvalidPointerException extends \RuntimeException | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| <?php | ||
|
|
||
| namespace JsonSchema; | ||
|
|
||
| use JsonSchema\Exception\InvalidPointerException; | ||
| use JsonSchema\Exception\ResourceNotFoundException; | ||
|
|
||
| /** | ||
| * Resolve JSON Pointers (RFC 6901) | ||
| */ | ||
| class PointerResolver | ||
| { | ||
| const EMPTY_ELEMENT = '_empty_'; | ||
| const LAST_ELEMENT = '-'; | ||
| const SEPARATOR = '/'; | ||
|
|
||
| /** | ||
| * Get the part of the document which the pointer points to. | ||
| * | ||
| * @param object $json The json document to resolve within. | ||
| * @param string $pointer The Json Pointer. | ||
| * | ||
| * @throws InvalidPointerException | ||
| * @throws ResourceNotFoundException | ||
| * | ||
| * @return mixed | ||
| */ | ||
| public function resolvePointer($json, $pointer) | ||
| { | ||
| if ($pointer === '') { | ||
| return $json; | ||
| } | ||
|
|
||
| $this->validatePointer($pointer); | ||
|
|
||
| $parts = array_slice(array_map('urldecode', explode('/', $pointer)), 1); | ||
|
|
||
| return $this->resolve($json, $pointer, $this->decodeParts($parts)); | ||
| } | ||
|
|
||
| /** | ||
| * Decode any escaped sequences. | ||
| * | ||
| * @param array $parts The json pointer parts. | ||
| * | ||
| * @return array | ||
| */ | ||
| private function decodeParts(array $parts) | ||
| { | ||
| $mappings = array( | ||
| '~1' => '/', | ||
| '~0' => '~', | ||
| ); | ||
|
|
||
| foreach ($parts as &$part) { | ||
| $part = strtr($part, $mappings); | ||
| } | ||
|
|
||
| return $parts; | ||
| } | ||
|
|
||
| /** | ||
| * Recurse through $json until location described by $parts is found. | ||
| * | ||
| * @param mixed $json The json document. | ||
| * @param string $json The original json pointer. | ||
| * @param array $parts The (remaining) parts of the pointer. | ||
| * | ||
| * @throws ResourceNotFoundException | ||
| * | ||
| * @return mixed | ||
| */ | ||
| private function resolve($json, $pointer, array $parts) | ||
| { | ||
| // Check for completion | ||
| if (count($parts) === 0) { | ||
| return $json; | ||
| } | ||
|
|
||
| $part = array_shift($parts); | ||
|
|
||
| // Ensure we deal with empty keys the same way as json_decode does | ||
| if ($part === '') { | ||
| $part = self::EMPTY_ELEMENT; | ||
| } | ||
|
|
||
| if (is_object($json) && property_exists($json, $part)) { | ||
| return $this->resolve($json->$part, $pointer, $parts); | ||
| } elseif (is_array($json)) { | ||
| if ($part === self::LAST_ELEMENT) { | ||
| return $this->resolve(end($json), $pointer, $parts); | ||
| } | ||
| if (filter_var($part, FILTER_VALIDATE_INT) !== false && | ||
| array_key_exists($part, $json) | ||
| ) { | ||
| return $this->resolve($json[$part], $pointer, $parts); | ||
| } | ||
| } | ||
|
|
||
| $message = "Failed to resolve pointer $pointer from document id" | ||
| . (isset($json->id) ? $json->id : ''); | ||
| throw new ResourceNotFoundException($message); | ||
| } | ||
|
|
||
| /** | ||
| * Validate a pointer string. | ||
| * | ||
| * @param string $pointer The pointer to validate. | ||
| * | ||
| * @throws InvalidPointerException | ||
| */ | ||
| private function validatePointer($pointer) | ||
| { | ||
| if ($pointer !== '' && !is_string($pointer)) { | ||
| throw new InvalidPointerException('Pointer is not a string'); | ||
| } | ||
|
|
||
| $firstCharacter = substr($pointer, 0, 1); | ||
|
|
||
| if ($firstCharacter !== self::SEPARATOR) { | ||
| throw new InvalidPointerException('Pointer starts with invalid character'); | ||
| } | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| use JsonSchema\Exception\JsonDecodingException; | ||
| use JsonSchema\Uri\Retrievers\UriRetrieverInterface; | ||
| use JsonSchema\Uri\UriRetriever; | ||
| use JsonSchema\Uri\UriResolver; | ||
|
|
||
| /** | ||
| * Take in an object that's a JSON schema and take care of all $ref references | ||
|
|
@@ -21,30 +22,22 @@ | |
| */ | ||
| 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; | ||
| const SELF_REF_LOCATION = '#'; | ||
|
|
||
| /** | ||
| * maximum references depth | ||
| * @var integer | ||
| * @var UriRetrieverInterface | ||
| */ | ||
| public static $maxDepth = 7; | ||
| protected $uriRetriever = null; | ||
|
|
||
| /** | ||
| * @var UriRetrieverInterface | ||
| * @var array | ||
| */ | ||
| protected $uriRetriever = null; | ||
| protected $schemas = array(); | ||
|
|
||
| /** | ||
| * @var object | ||
| * @var array | ||
| */ | ||
| protected $rootSchema = null; | ||
| protected $scopes = array(); | ||
|
|
||
| /** | ||
| * @param UriRetriever $retriever | ||
|
|
@@ -63,11 +56,36 @@ public function __construct($retriever = null) | |
| */ | ||
| public function fetchRef($ref, $sourceUri) | ||
| { | ||
| $retriever = $this->getUriRetriever(); | ||
| $jsonSchema = $retriever->retrieve($ref, $sourceUri); | ||
| $this->resolve($jsonSchema); | ||
| // Get absolute uri | ||
| $resolver = new UriResolver(); | ||
| $uri = $resolver->resolve($ref, $sourceUri); | ||
|
|
||
| // Split in to location and fragment | ||
| $location = $resolver->extractLocation($uri); | ||
| $fragment = $resolver->extractFragment($uri); | ||
|
|
||
| // Retrieve dereferenced schema | ||
| if ($location == null) { | ||
| $schema = $this->schemas[self::SELF_REF_LOCATION]; | ||
| } elseif (array_key_exists($location, $this->schemas)) { | ||
| $schema = $this->schemas[$location]; | ||
| } else { | ||
| $retriever = $this->getUriRetriever(); | ||
| $schema = $retriever->retrieve($location); | ||
|
|
||
| $this->schemas[$location] = $schema; | ||
| $this->resolve($schema, $location); | ||
| } | ||
|
|
||
| // Resolve JSON pointer | ||
| $retriever = $this->getUriRetriever(); | ||
| $object = $retriever->resolvePointer($schema, $fragment); | ||
|
|
||
| if ($object instanceof \stdClass) { | ||
| $object->id = $uri; | ||
| } | ||
|
|
||
| return $jsonSchema; | ||
| return $object; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -101,47 +119,75 @@ public function getUriRetriever() | |
| */ | ||
| 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; | ||
| if (!is_object($schema)) { | ||
| return; | ||
| } | ||
|
|
||
| if (null === $sourceUri && ! empty($schema->id)) { | ||
| $sourceUri = $schema->id; | ||
| // Fill in id property | ||
| if ($sourceUri) { | ||
| $schema->id = $sourceUri; | ||
| } | ||
|
|
||
| if (null === $this->rootSchema) { | ||
| $this->rootSchema = $schema; | ||
| } | ||
|
|
||
| // Resolve $ref first | ||
| $this->resolveRef($schema, $sourceUri); | ||
| // First determine our resolution scope | ||
| $scope = $this->enterResolutionScope($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); | ||
| $this->resolveProperty($schema, $propertyName, $scope); | ||
| } | ||
|
|
||
| // 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); | ||
| $this->resolveArrayOfSchemas($schema, $propertyName, $scope); | ||
| } | ||
|
|
||
| // These are all objects containing properties whose values are schemas | ||
| foreach (array('dependencies', 'patternProperties', 'properties') as $propertyName) { | ||
| $this->resolveObjectOfSchemas($schema, $propertyName, $sourceUri); | ||
| foreach (array('definitions', 'dependencies', 'patternProperties', 'properties') as $propertyName) { | ||
| $this->resolveObjectOfSchemas($schema, $propertyName, $scope); | ||
| } | ||
|
|
||
| --self::$depth; | ||
| // Resolve $ref | ||
| $this->resolveRef($schema, $scope); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you should resolveRef before other resolutions. Before 134 line.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for doing it afterward is to ensure we don't get stuck in an infinite loop whilst attempting to dereference everything. |
||
|
|
||
| // Pop back out of our scope | ||
| $this->leaveResolutionScope(); | ||
| } | ||
|
|
||
| /** | ||
| * Enters a new resolution scope for the given schema. Inspects the | ||
| * partial for the presence of 'id' and then returns that as a absolute | ||
| * uri. Returns the new scope. | ||
| * | ||
| * @param object $schemaPartial JSON Schema to get the resolution scope for | ||
| * @param string $sourceUri URI where this schema was located | ||
| * @return string | ||
| */ | ||
| private function enterResolutionScope($schemaPartial, $sourceUri) | ||
| { | ||
| if (count($this->scopes) === 0) { | ||
| $this->scopes[] = self::SELF_REF_LOCATION; | ||
| $this->schemas[self::SELF_REF_LOCATION] = $schemaPartial; | ||
| } | ||
|
|
||
| if (!empty($schemaPartial->id)) { | ||
| $resolver = new UriResolver(); | ||
| $this->scopes[] = $resolver->resolve($schemaPartial->id, $sourceUri); | ||
| } else { | ||
| $this->scopes[] = end($this->scopes); | ||
| } | ||
|
|
||
| return end($this->scopes); | ||
| } | ||
|
|
||
| /** | ||
| * Leaves the current resolution scope. | ||
| */ | ||
| private function leaveResolutionScope() | ||
| { | ||
| array_pop($this->scopes); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -215,36 +261,20 @@ public function resolveRef($schema, $sourceUri) | |
| 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); | ||
| } | ||
| // Retrieve the referenced schema | ||
| $uri = $schema->$ref; | ||
| $refSchema = $this->fetchRef($schema->$ref, $sourceUri, $schema); | ||
|
|
||
| // Remove the reference node | ||
| unset($schema->$ref); | ||
|
|
||
| // Augment the current $schema object with properties fetched | ||
| // Augment the properties (FIXME this is a bit naive and might need fixing) | ||
| foreach (get_object_vars($refSchema) as $prop => $value) { | ||
| $schema->$prop = $value; | ||
| } | ||
|
|
||
| // Check for nested references | ||
| $this->resolveRef($schema, $sourceUri); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 :)