-
Notifications
You must be signed in to change notification settings - Fork 16
/
hal.js
300 lines (256 loc) · 9.1 KB
/
hal.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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
(function (exports) {
/**
* Link to another hypermedia
* @param String rel → the relation identifier
* @param String|Object value → the href, or the hash of all attributes (including href)
*/
function Link (rel, value) {
if (!(this instanceof Link)) {
return new Link(rel, value);
}
if (!rel) throw new Error('Required <link> attribute "rel"');
this.rel = rel;
if (typeof value === 'object') {
// If value is a hashmap, just copy properties
if (!value.href) throw new Error('Required <link> attribute "href"');
var expectedAttributes = ['rel', 'href', 'name', 'hreflang', 'title', 'templated', 'icon', 'align', 'method'];
for (var attr in value) {
if (value.hasOwnProperty(attr)) {
if (!~expectedAttributes.indexOf(attr)) {
// Unexpected attribute: ignore it
continue;
}
this[attr] = value[attr];
}
}
} else {
// value is a scalar: use its value as href
if (!value) throw new Error('Required <link> attribute "href"');
this.href = String(value);
}
}
/**
* XML representation of a link
*/
Link.prototype.toXML = function () {
var xml = '<link';
for (var attr in this) {
if (this.hasOwnProperty(attr)) {
xml += ' ' + attr + '="' + escapeXml(this[attr]) + '"';
}
}
xml += ' />';
return xml;
}
/**
* JSON representation of a link
*/
Link.prototype.toJSON = function () {
// Note: calling "JSON.stringify(this)" will fail as JSON.stringify itself calls toJSON()
// We need to copy properties to a new object
return Object.keys(this).reduce((function (object, key) {
object[key] = this[key];
return object;
}).bind(this), {});
};
/**
* A hypertext resource
* @param Object object → the base properties
* Define "href" if you choose not to pass parameter "uri"
* Do not define "_links" and "_embedded" unless you know what you're doing
* @param String uri → href for the <link rel="self"> (can use reserved "href" property instead)
*/
function Resource (object, uri) {
// new Resource(resource) === resource
if (object instanceof Resource) {
return object;
}
// Still work if "new" is omitted
if (!(this instanceof Resource)) {
return new Resource(object, uri);
}
// Initialize _links and _embedded properties
this._links = {};
this._embedded = {};
// Copy properties from object
// we copy AFTER initializing _links and _embedded so that user
// **CAN** (but should not) overwrite them
for (var property in object) {
if (object.hasOwnProperty(property)) {
this[property] = object[property];
}
}
// Use uri or object.href to initialize the only required <link>: rel = self
uri = uri || this.href;
if (uri === this.href) {
delete this.href;
}
// If we have a URI, add this link
// If not, we won't have a valid object (this may lead to a fatal error later)
if (uri) this.link(new Link('self', uri));
};
/**
* Add a link to a resource
* @param Link link
*
* Alternative usage: function (rel, value)
* @see Link
*/
Resource.prototype.link = function (link) {
if (arguments.length > 1) {
link = Link(arguments[0], arguments[1]);
}
if (typeof this._links[link.rel] === "undefined") {
this._links[link.rel] = link;
} else if (Array.isArray(this._links[link.rel])) {
this._links[link.rel].push(link)
} else {
this._links[link.rel] = [this._links[link.rel], link]
}
return this;
};
/**
* Add an embedded resource
* @param String rel → the relation identifier (should be plural)
* @param Resource|Resource[] → resource(s) to embed
*/
Resource.prototype.embed = function (rel, resource, pluralize) {
if (typeof pluralize === 'undefined') pluralize = true;
// [Naive pluralize](https://github.com/naholyr/js-hal#why-this-crappy-singularplural-management%E2%80%AF)
if (pluralize && rel.substring(rel.length - 1) !== 's') {
rel += 's';
}
// Initialize embedded container
if (this._embedded[rel] && !Array.isArray(this._embedded[rel])) {
this._embedded[rel] = [this._embedded[rel]];
} else if (!this._embedded[rel]) {
this._embedded[rel] = [];
}
// Append resource(s)
if (Array.isArray(resource)) {
this._embedded[rel] = this._embedded[rel].concat(resource.map(function (object) {
return new Resource(object);
}));
} else {
this._embedded[rel] = Resource(resource);
}
return this;
};
/**
* Convert a resource to a stringifiable anonymous object
* @private
* @param Resource resource
*/
function resourceToJsonObject (resource) {
var result = {};
for (var prop in resource) {
if (prop === '_links') {
if (Object.keys(resource._links).length > 0) {
// Note: we need to copy data to remove "rel" property without corrupting original Link object
result._links = Object.keys(resource._links).reduce(function (links, rel) {
if (Array.isArray(resource._links[rel])) {
links[rel] = new Array()
for (var i=0; i < resource._links[rel].length; i++)
links[rel].push(resource._links[rel][i].toJSON())
} else {
var link = resource._links[rel].toJSON();
links[rel] = link;
delete link.rel;
}
return links;
}, {});
}
} else if (prop === '_embedded') {
if (Object.keys(resource._embedded).length > 0) {
// Note that we do not reformat _embedded
// which means we voluntarily DO NOT RESPECT the following constraint:
// > Relations with one corresponding Resource/Link have a single object
// > value, relations with multiple corresponding HAL elements have an
// > array of objects as their value.
// Come on, resource one is *really* dumb.
result._embedded = {};
for (var rel in resource._embedded) {
result._embedded[rel] = resource._embedded[rel].map(resourceToJsonObject);
}
}
} else if (resource.hasOwnProperty(prop)) {
result[prop] = resource[prop];
}
}
return result;
}
/**
* JSON representation of the resource
* Requires "JSON.stringify()"
* @param String indent → how you want your JSON to be indented
*/
Resource.prototype.toJSON = function (indent) {
return resourceToJsonObject(this);
};
/**
* Escape an XML string: encodes double quotes and tag enclosures
* @private
*/
function escapeXml (string) {
return String(string).replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
};
/**
* Convert a resource to its XML representation
* @private
* @param Resource resource
* @param String rel → relation identifier for embedded object
* @param String currentIdent → current indentation
* @param String nextIndent → next indentation
*/
function resourceToXml (resource, rel, currentIndent, nextIndent) {
// Do not add line feeds if no indentation is asked
var LF = (currentIndent || nextIndent) ? '\n' : '';
// Resource tag
var xml = currentIndent + '<resource';
// Resource attributes: rel, href, name
if (rel) xml += ' rel="' + escapeXml(rel) + '"';
if (resource.href || resource._links.self) xml += ' href="' + escapeXml(resource.href || resource._links.self.href) + '"';
if (resource.name) xml += ' name="' + escapeXml(resource.name) + '"';
xml += '>' + LF;
// Add <link> tags
for (var rel in resource._links) {
if (!resource.href && rel === 'self') continue;
xml += currentIndent + nextIndent + resource._links[rel].toXML() + LF;
}
// Add embedded
for (var embed in resource._embedded) {
// [Naive singularize](https://github.com/naholyr/js-hal#why-this-crappy-singularplural-management%E2%80%AF)
var rel = embed.replace(/s$/, '');
resource._embedded[embed].forEach(function (res) {
xml += resourceToXml(res, rel, currentIndent + nextIndent, currentIndent + nextIndent + nextIndent) + LF;
});
}
// Add properties as tags
for (var prop in resource) {
if (resource.hasOwnProperty(prop) && prop !== '_links' && prop !== '_embedded') {
xml += currentIndent + nextIndent + '<' + prop + '>' + String(resource[prop]) + '</' + prop + '>' + LF;
}
}
// Close tag and return the shit
xml += currentIndent + '</resource>';
return xml;
}
/**
* XML representation of the resource
* @param String indent → how you want your XML to be indented
*/
Resource.prototype.toXML = function (indent) {
return resourceToXml(this, null, '', indent || '');
};
/**
* Returns the JSON representation indented using tabs
*/
Resource.prototype.toString = function () {
return this.toJSON('\t');
};
/**
* Public API
*/
exports.Resource = Resource;
exports.Link = Link;
})(typeof exports === 'undefined' ? this['hal']={} : exports);