Skip to content

Commit 6ebb3f7

Browse files
committed
Added separate classes to implement JSON Reference and JSON pointer
1 parent 77c7758 commit 6ebb3f7

6 files changed

Lines changed: 446 additions & 0 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace cebe\openapi\json;
4+
5+
6+
use Exception;
7+
8+
/**
9+
* InvalidJsonPointerSyntaxException represents the error condition "Invalid pointer syntax" of the JSON pointer specification.
10+
*
11+
* @link https://tools.ietf.org/html/rfc6901 (7. Error Handling)
12+
*/
13+
class InvalidJsonPointerSyntaxException extends Exception
14+
{
15+
}

src/json/JsonPointer.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace cebe\openapi\json;
4+
5+
/**
6+
* Represents a JSON Pointer (RFC 6901)
7+
*
8+
* A JSON Pointer only works in the context of a single JSON document,
9+
* if you need to reference values in external documents, use [[JsonReference]] instead.
10+
*
11+
* @link https://tools.ietf.org/html/rfc6901
12+
* @see JsonReference
13+
*/
14+
final class JsonPointer
15+
{
16+
/**
17+
* @var string
18+
*/
19+
private $_pointer;
20+
21+
/**
22+
* JSON Pointer constructor.
23+
* @param string $pointer The JSON Pointer.
24+
* Must be either an empty string (for referencing the whole document), or a string starting with `/`.
25+
* @throws InvalidJsonPointerSyntaxException in case an invalid JSON pointer string is passed
26+
*/
27+
public function __construct(string $pointer)
28+
{
29+
if (!preg_match('~^(/[^/]*)*$~', $pointer)) {
30+
throw new InvalidJsonPointerSyntaxException("Invalid JSON Pointer syntax: $pointer");
31+
}
32+
$this->_pointer = $pointer;
33+
}
34+
35+
/**
36+
* @return string returns the JSON Pointer.
37+
*/
38+
public function getPointer(): string
39+
{
40+
return $this->_pointer;
41+
}
42+
43+
/**
44+
* @return array the JSON pointer path as array.
45+
*/
46+
public function getPath(): array
47+
{
48+
if ($this->_pointer === '') {
49+
return [];
50+
}
51+
$pointer = substr($this->_pointer, 1);
52+
return array_map([get_class($this), 'decode'], explode('/', $pointer));
53+
}
54+
55+
/**
56+
* Evaluate the JSON Pointer on the provided document.
57+
*
58+
* Note that this does only resolve the JSON Pointer, it will not load external
59+
* documents by URI. Loading the Document from the URI is supposed to be done outside of this class.
60+
*
61+
* @param mixed $jsonDocument
62+
* @return mixed
63+
* @throws NonexistentJsonPointerReferenceException
64+
*/
65+
public function evaluate($jsonDocument)
66+
{
67+
$currentReference = $jsonDocument;
68+
$currentPath = '';
69+
70+
foreach ($this->getPath() as $part) {
71+
72+
if (is_array($currentReference) || $currentReference instanceof \ArrayAccess) {
73+
if (!array_key_exists($part, $currentReference)) {
74+
throw new NonexistentJsonPointerReferenceException(
75+
"Failed to evaluate pointer '$this->_pointer'. Array has no member $part at path '$currentPath'."
76+
);
77+
}
78+
$currentReference = $currentReference[$part];
79+
} elseif (is_object($currentReference)) {
80+
if (!isset($currentReference->$part) && !property_exists($currentReference, $part)) {
81+
throw new NonexistentJsonPointerReferenceException(
82+
"Failed to evaluate pointer '$this->_pointer'. Object has no member $part at path '$currentPath'."
83+
);
84+
}
85+
$currentReference = $currentReference->$part;
86+
} else {
87+
throw new NonexistentJsonPointerReferenceException(
88+
"Failed to evaluate pointer '$this->_pointer'. Value at path '$currentPath' is neither an array nor an object."
89+
);
90+
}
91+
92+
$currentPath = "$currentPath/$part";
93+
}
94+
95+
return $currentReference;
96+
}
97+
98+
/**
99+
* Encodes a string for use inside of a JSON pointer.
100+
*/
101+
public static function encode(string $string): string
102+
{
103+
return strtr($string, [
104+
'~' => '~0',
105+
'/' => '~1',
106+
]);
107+
}
108+
109+
/**
110+
* Decodes a string used inside of a JSON pointer.
111+
*/
112+
public static function decode(string $string): string
113+
{
114+
return strtr($string, [
115+
'~1' => '/',
116+
'~0' => '~',
117+
]);
118+
}
119+
}

src/json/JsonReference.php

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace cebe\openapi\json;
4+
5+
use JsonSerializable;
6+
7+
/**
8+
* Represents a JSON Reference (IETF draft-pbryan-zyp-json-ref-03)
9+
*
10+
* Includes the URI to another JSON document and the JSON Pointer as
11+
* the fragment section of the URI.
12+
*
13+
* @link https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03
14+
* @see JsonPointer
15+
*/
16+
final class JsonReference implements JsonSerializable
17+
{
18+
/**
19+
* @var string
20+
*/
21+
private $_uri = '';
22+
/**
23+
* @var JsonPointer
24+
*/
25+
private $_pointer;
26+
27+
/**
28+
* Create a JSON Reference instance from a JSON document.
29+
* @param string $json the JSON object, e.g. `{ "$ref": "http://example.com/example.json#/foo/bar" }`.
30+
* @return JsonReference
31+
* @throws MalformedJsonReferenceObjectException
32+
* @throws InvalidJsonPointerSyntaxException if an invalid JSON pointer string is passed as part of the fragment section.
33+
*/
34+
public static function createFromJson(string $json): JsonReference
35+
{
36+
$refObject = json_decode($json, true);
37+
if (!isset($refObject['$ref'])) {
38+
throw new MalformedJsonReferenceObjectException('JSON Reference Object must contain the "$ref" member.');
39+
}
40+
return static::createFromReference($refObject['$ref']);
41+
}
42+
43+
/**
44+
* Create a JSON Reference instance from an URI and a JSON Pointer.
45+
* If no JSON Pointer is given this will be interpreted as an empty string JSON pointer, which
46+
* references the whole document.
47+
* @param string $uri the URI to the document without a fragment part.
48+
* @param JsonPointer $jsonPointer
49+
* @return JsonReference
50+
*/
51+
public static function createFromUri(string $uri, ?JsonPointer $jsonPointer = null): JsonReference
52+
{
53+
$jsonReference = static::createFromReference($uri);
54+
$jsonReference->_pointer = $jsonPointer ?: new JsonPointer('');
55+
return $jsonReference;
56+
}
57+
58+
/**
59+
* Create a JSON Reference instance from a reference URI.
60+
* @param string $referenceURI the JSON Reference URI, e.g. `"http://example.com/example.json#/foo/bar"`.
61+
* @return JsonReference
62+
* @throws InvalidJsonPointerSyntaxException if an invalid JSON pointer string is passed as part of the fragment section.
63+
*/
64+
public static function createFromReference(string $referenceURI): JsonReference
65+
{
66+
$jsonReference = new JsonReference();
67+
if (strpos($referenceURI, '#') !== false) {
68+
list($uri, $fragment) = explode('#', $referenceURI, 2);
69+
$jsonReference->_uri = $uri;
70+
$jsonReference->_pointer = new JsonPointer(rawurldecode($fragment));
71+
} else {
72+
$jsonReference->_uri = $referenceURI;
73+
$jsonReference->_pointer = new JsonPointer('');
74+
}
75+
return $jsonReference;
76+
}
77+
78+
private function __construct() {}
79+
80+
public function __clone()
81+
{
82+
$this->_pointer = clone $this->_pointer;
83+
}
84+
85+
86+
/**
87+
* @return string returns the JSON Pointer.
88+
*/
89+
public function getJsonPointer(): JsonPointer
90+
{
91+
return $this->_pointer;
92+
}
93+
94+
/**
95+
* @return string returns the URI of the referenced JSON document without the fragment (JSON Pointer) part.
96+
*/
97+
public function getDocumentUri(): string
98+
{
99+
return $this->_uri;
100+
}
101+
102+
/**
103+
* @return string returns the JSON Pointer in URI format.
104+
*/
105+
public function getReference(): string
106+
{
107+
// https://tools.ietf.org/html/rfc6901#section-6
108+
// A JSON Pointer can be represented in a URI fragment identifier by
109+
// encoding it into octets using UTF-8 [RFC3629], while percent-encoding
110+
// those characters not allowed by the fragment rule in [RFC3986].
111+
// https://tools.ietf.org/html/rfc3986#page-25
112+
// The characters slash ("/") and question mark ("?") are allowed to
113+
// represent data within the fragment identifier.
114+
// https://tools.ietf.org/html/rfc3986#section-2.4
115+
// the "%7E" can be replaced by "~" without changing its interpretation.
116+
return $this->_uri . '#' . strtr(rawurlencode($this->_pointer->getPointer()), ['%2F' => '/', '%3F' => '?', '%7E' => '~']);
117+
}
118+
119+
/**
120+
* Specify data which should be serialized to JSON
121+
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
122+
* @return mixed data which can be serialized by <b>json_encode</b>,
123+
* which is a value of any type other than a resource.
124+
*/
125+
public function jsonSerialize()
126+
{
127+
return (object)['$ref' => $this->getReference()];
128+
}
129+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace cebe\openapi\json;
4+
5+
6+
use Exception;
7+
8+
/**
9+
* MalformedJsonReferenceObjectException is thrown if a JSON Reference Object does not contain the "$ref" member.
10+
*
11+
* @link https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 (3. Syntax)
12+
*/
13+
class MalformedJsonReferenceObjectException extends Exception
14+
{
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace cebe\openapi\json;
4+
5+
use Exception;
6+
7+
/**
8+
* NonexistentJsonPointerReferenceException represents the error condition
9+
* "A pointer that references a nonexistent value" of the JSON pointer specification.
10+
*
11+
* @link https://tools.ietf.org/html/rfc6901 (7. Error Handling)
12+
*/
13+
class NonexistentJsonPointerReferenceException extends Exception
14+
{
15+
16+
}

0 commit comments

Comments
 (0)