-
-
Notifications
You must be signed in to change notification settings - Fork 233
/
Copy pathpointer.js
254 lines (223 loc) · 7.21 KB
/
pointer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
'use strict';
module.exports = Pointer;
var $Ref = require('./ref'),
url = require('./util/url'),
ono = require('ono'),
slashes = /\//g,
tildes = /~/g,
escapedSlash = /~1/g,
escapedTilde = /~0/g;
/**
* This class represents a single JSON pointer and its resolved value.
*
* @param {$Ref} $ref
* @param {string} path
* @constructor
*/
function Pointer($ref, path) {
/**
* The {@link $Ref} object that contains this {@link Pointer} object.
* @type {$Ref}
*/
this.$ref = $ref;
/**
* The file path or URL, containing the JSON pointer in the hash.
* This path is relative to the path of the main JSON schema file.
* @type {string}
*/
this.path = path;
/**
* The value of the JSON pointer.
* Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
* @type {?*}
*/
this.value = undefined;
/**
* Indicates whether the pointer references itself.
* @type {boolean}
*/
this.circular = false;
}
/**
* Resolves the value of a nested property within the given object.
*
* @param {*} obj - The object that will be crawled
* @param {$RefParserOptions} options
*
* @returns {Pointer}
* Returns a JSON pointer whose {@link Pointer#value} is the resolved value.
* If resolving this value required resolving other JSON references, then
* the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path
* of the resolved value.
*/
Pointer.prototype.resolve = function(obj, options) {
var tokens = Pointer.parse(this.path);
// Crawl the object, one token at a time
this.value = obj;
for (var i = 0; i < tokens.length; i++) {
if (resolveIf$Ref(this, options)) {
// The $ref path has changed, so append the remaining tokens to the path
this.path = Pointer.join(this.path, tokens.slice(i));
}
var token = tokens[i];
if (this.value[token] === undefined) {
throw ono.syntax('Error resolving $ref pointer "%s". \nToken "%s" does not exist.', this.path, token);
}
else {
this.value = this.value[token];
}
}
// Resolve the final value
resolveIf$Ref(this, options);
return this;
};
/**
* Sets the value of a nested property within the given object.
*
* @param {*} obj - The object that will be crawled
* @param {*} value - the value to assign
* @param {$RefParserOptions} options
*
* @returns {*}
* Returns the modified object, or an entirely new object if the entire object is overwritten.
*/
Pointer.prototype.set = function(obj, value, options) {
var tokens = Pointer.parse(this.path);
var token;
if (tokens.length === 0) {
// There are no tokens, replace the entire object with the new value
this.value = value;
return value;
}
// Crawl the object, one token at a time
this.value = obj;
for (var i = 0; i < tokens.length - 1; i++) {
resolveIf$Ref(this, options);
token = tokens[i];
if (this.value && this.value[token] !== undefined) {
// The token exists
this.value = this.value[token];
}
else {
// The token doesn't exist, so create it
this.value = setValue(this, token, {});
}
}
// Set the value of the final token
resolveIf$Ref(this, options);
token = tokens[tokens.length - 1];
setValue(this, token, value);
// Return the updated object
return obj;
};
/**
* Parses a JSON pointer (or a path containing a JSON pointer in the hash)
* and returns an array of the pointer's tokens.
* (e.g. "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
*
* The pointer is parsed according to RFC 6901
* {@link https://tools.ietf.org/html/rfc6901#section-3}
*
* @param {string} path
* @returns {string[]}
*/
Pointer.parse = function(path) {
// Get the JSON pointer from the path's hash
var pointer = url.getHash(path).substr(1);
// If there's no pointer, then there are no tokens,
// so return an empty array
if (!pointer) {
return [];
}
// Split into an array
pointer = pointer.split('/');
// Decode each part, according to RFC 6901
for (var i = 0; i < pointer.length; i++) {
pointer[i] = decodeURI(pointer[i].replace(escapedSlash, '/').replace(escapedTilde, '~'));
}
if (pointer[0] !== '') {
throw ono.syntax('Invalid $ref pointer "%s". Pointers must begin with "#/"', pointer);
}
return pointer.slice(1);
};
/**
* Creates a JSON pointer path, by joining one or more tokens to a base path.
*
* @param {string} base - The base path (e.g. "schema.json#/definitions/person")
* @param {string|string[]} tokens - The token(s) to append (e.g. ["name", "first"])
* @returns {string}
*/
Pointer.join = function(base, tokens) {
// Ensure that the base path contains a hash
if (base.indexOf('#') === -1) {
base += '#';
}
// Append each token to the base path
tokens = Array.isArray(tokens) ? tokens : [tokens];
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
// Encode the token, according to RFC 6901
base += '/' + encodeURI(token.replace(tildes, '~0').replace(slashes, '~1'));
}
return base;
};
/**
* If the given pointer's {@link Pointer#value} is a JSON reference,
* then the reference is resolved and {@link Pointer#value} is replaced with the resolved value.
* In addition, {@link Pointer#path} and {@link Pointer#$ref} are updated to reflect the
* resolution path of the new value.
*
* @param {Pointer} pointer
* @param {$RefParserOptions} options
* @returns {boolean} - Returns `true` if the resolution path changed
*/
function resolveIf$Ref(pointer, options) {
// Is the value a JSON reference? (and allowed?)
if ($Ref.isAllowed$Ref(pointer.value, options)) {
var $refPath = url.resolve(pointer.path, pointer.value.$ref);
if ($refPath === pointer.path) {
// The value is a reference to itself, so there's nothing to do.
pointer.circular = true;
}
else {
var resolved = pointer.$ref.$refs._resolve($refPath, options);
if ($Ref.isExtended$Ref(pointer.value)) {
// This JSON reference "extends" the resolved value, rather than simply pointing to it.
// So the resolved path does NOT change. Just the value does.
pointer.value = $Ref.dereference(pointer.value, resolved.value);
}
else {
// Resolve the reference
pointer.$ref = resolved.$ref;
pointer.path = resolved.path;
pointer.value = resolved.value;
}
return true;
}
}
}
/**
* Sets the specified token value of the {@link Pointer#value}.
*
* The token is evaluated according to RFC 6901.
* {@link https://tools.ietf.org/html/rfc6901#section-4}
*
* @param {Pointer} pointer - The JSON Pointer whose value will be modified
* @param {string} token - A JSON Pointer token that indicates how to modify `obj`
* @param {*} value - The value to assign
* @returns {*} - Returns the assigned value
*/
function setValue(pointer, token, value) {
if (pointer.value && typeof pointer.value === 'object') {
if (token === '-' && Array.isArray(pointer.value)) {
pointer.value.push(value);
}
else {
pointer.value[token] = value;
}
}
else {
throw ono.syntax('Error assigning $ref pointer "%s". \nCannot set "%s" of a non-object.', pointer.path, token);
}
return value;
}