swish-http
Version:
A Swish implementation that tunnels over HTTP
354 lines (342 loc) • 13.3 kB
JavaScript
(function (global, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
global.SwishHttp = factory();
}
})(this, function () {
// Hopefully good enough
var upDirRegex = /\/[^/]+\/\.\.\//g;
function basicResolveUrl(base, other) {
if (/\:\/\//.test(other)) return other;
if (other.substring(0, 2) === '//') {
base = base.replace(/[#?].*/, '');
if (!/\:\/\//.test(base)) return other;
return base.replace(/:\/\/.*/, ':') + other;
} else if (other[0] === '/') {
base = base.replace(/[#?].*/, '');
if (/(\:|^)\/\//.test(base)) {
return base.replace(/[^/]\/([^/]|$).*/, function (r) {
return r[0];
}) + other;
} else {
return other;
}
} else if (other[0] === '?') {
return base.replace(/[#?].*/, '') + other;
} else if (other[1] === '#') {
return base.replace(/#.*/, '') + other;
} else {
base = base.replace(/[#?].*/, '');
base = base.replace(/[^/]+$/, ''); // Strip out final component
var result = base + other;
while (upDirRegex.test(result)) {
result = result.replace(upDirRegex, '/');
}
return result;
}
}
var queryKeywords = {
'_schema_json': function (result, value) {
result.schema = JSON.parse(value);
},
'_options_json': function (result, value) {
result.options = JSON.parse(value);
}
};
function parseQuery(query) {
var result = {
schema: {},
options: {}
};
for (var key in query) {
if (queryKeywords[key]) {
try {
queryKeywords[key](result, query[key]);
} catch (e) {
return {error: 'Error decoding query', key: key, message: e.message};
}
} else if (key[0] === '/') { // JSON Pointer
var parts = key.substring(1).split('/');
for (var j = 0; j < parts.length; j++) {
parts[j] = stringToBasicValue(parts[j].replace(/~1/g, '/').replace(/~0/g, '~'));
}
result = setValueWithPath(result, parts, stringToBasicValue(query[key]));
}
}
return result;
}
function createQuery(schema, options) {
var query = {};
var combined = {};
for (var key in schema) {
combined.schema = schema;
break;
}
for (var key in options) {
combined.options = options;
break;
}
var paths = objectToPaths(combined);
for (var i = 0; i < paths.length; i++) {
var pair = paths[i], path = pair.path;
// basic value and JSON Pointer
for (var j = 0; j < path.length; j++) {
path[j] = '/' + basicValueToString(path[j]).replace(/~/g, '~0').replace(/\//g, '~1');
}
path = path.join('');
query[path] = basicValueToString(pair.value);
}
return query;
}
function setValueWithPath(root, path, value) {
if (!path.length) return value;
var key = path[0], remainder = path.slice(1);
if (typeof key === 'number') {
if (!Array.isArray(root)) root = [];
} else if (!root || typeof root !== 'object') {
root = {};
}
root[key] = setValueWithPath(root[key], remainder, value);
return root;
}
function basicValueToString(value) {
if (typeof value === 'string') {
// Prefix '_' to any strings that would otherwise decode incorrectly
if (stringToBasicValue(value) !== value) {
return '_' + value;
}
}
if (Array.isArray(value)) return '[]';
if (value && typeof value === 'object') return '{}';
return value + "";
}
function stringToBasicValue(string) {
if (string[0] === '_') return string.substring(1);
if (string === 'true') return true;
if (string === 'false') return false;
if (string === 'null') return null;
if (string === '{}') return {};
if (string === '[]') return [];
if (/\-?^[0-9]+(\.[0-9]+)?$/.test(string) && !isNaN(parseFloat(string))) return parseFloat(string);
return string;
}
function objectToPaths(value, paths, prefix) {
paths = paths || [];
prefix = prefix || [];
if (Array.isArray(value)) {
if (!value.length) paths.push({path: prefix, value: []});
for (var i = 0; i < value.length; i++) {
objectToPaths(value[i], paths, prefix.concat(i));
}
} else if (value && typeof value === 'object') {
var set = false;
for (var key in value) {
objectToPaths(value[key], paths, prefix.concat(key));
set = true;
}
if (!set) paths.push({path: prefix, value: {}});
} else if (typeof value !== 'undefined') {
paths.push({path: prefix, value: value});
}
return paths;
}
// copied from core swish module
function exampleToSchema(obj) {
var schema = {};
if (Array.isArray(obj)) {
var optionEnums = [];
var optionSchemas = [];
for (var i = 0; i < obj.length; i++) {
var subSchema = exampleToSchema(obj[i]);
optionSchemas.push(subSchema);
if (optionEnums && subSchema['enum']) {
optionEnums = optionEnums.concat(subSchema['enum']);
for (var key in subSchema) {
if (key !== 'enum') {
optionEnums = null;
break;
}
}
} else {
optionEnums = null;
}
}
if (optionEnums) return {enum: optionEnums};
return {anyOf: optionSchemas};
} else if (obj && typeof obj === 'object') {
schema.type = 'object';
schema.properties = {};
schema.required = [];
for (var key in obj) {
schema.properties[key] = exampleToSchema(obj[key]);
schema.required.push(key);
}
} else {
schema['enum'] = [obj];
}
return schema;
}
// copied from core swish module
function patchIsRemove(patch) {
for (var i = patch.length - 1; i >= 0; i--) {
var change = patch[i];
if (change.path !== "") continue;
if (change.op === "remove") {
return true;
}
if (change.op === "replace" || change.op === 'add') {
return typeof change.value === 'undefined';
}
}
return false;
}
function SwishClient(url) {
if (!(this instanceof SwishClient)) return new SwishClient(url);
this._url = url;
}
SwishClient.prototype = {
_req: function(method, schema, options, data, contentType, callback) {
var url = this._url;
if (options._overrideUrl) {
url = options._overrideUrl;
} else {
var query = createQuery(schema, options);
for (var key in query) {
url += (url.indexOf('?') >= 0) ? '&' : '?';
url += encodeURIComponent(key) + '=' + encodeURIComponent(query[key]);
}
}
var r = new XMLHttpRequest();
r.open(method, url, true);
r.onreadystatechange = function () {
if (r.readyState != 4) return;
var error = null, data = r.responseText;
try {
data = JSON.parse(data);
} catch (e) {
error = e;
}
if (r.status < 200 || r.status >= 300) {
error = new Error(r.status + ' ' + r.statusText + ' (' + url + ')');
}
callback(error, data, r, url);
}
if (typeof data !== 'undefined') {
r.setRequestHeader('Content-Type', contentType || 'application/json');
r.send(JSON.stringify(data));
} else {
r.send();
}
},
create: function (object, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
this.createMultiple([object], options, function (error, results) {
callback(error, results && results[0]);
});
},
createMultiple: function (entries, options, callback) {
var thisStore = this;
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
if (entries.length === 1 && !Array.isArray(entries)) {
this._req('POST', null, options, entries[0], null, callback);
} else {
this._req('POST', null, options, entries, null, callback);
}
},
searchByExample: function (example, options, callback) {
this.search(exampleToSchema(example), options, callback);
},
search: function (schema, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
this._req('GET', schema, options, undefined, null, function (error, result, xhr, requestUrl) {
if (error) return callback(error);
var continueOptions = null;
var linkHeader = xhr.getResponseHeader('Link');
if (linkHeader) {
var links = linkHeader.match(/([^,"']|"([^"\\]|\\.)*")+/g);
for (var i = 0; i < links.length; i++) {
var parts = links[i].match(/([^;"']|"([^"\\]|\\.)*")+/g);
var url = parts.shift().replace(/^\s*</, '').replace(/>\s*$/, '');
var props = {};
for (var j = 0; j < parts.length; j++) {
var part = parts[j].replace(/^\s+/, '').replace(/\s+$/, '');
var key = part.split('=', 1)[0];
var value = part.substring(key.length + 1);
if (value[0] === '"' && value[value.length - 1] === '"') {
// TODO: do we need to de-escape the value?
value = value.substring(1, value.length - 1);
}
props[key] = value;
}
if (props.rel === 'next') {
continueOptions = {_overrideUrl: basicResolveUrl(requestUrl, url)};
}
}
}
callback(null, result, continueOptions);
});
},
replaceByExample: function (example, replacement, options, callback) {
this.replace(exampleToSchema(example), replacement, options, callback);
},
replace: function (schema, replacement, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
this._req('PUT', schema, options, replacement, null, callback);
},
updateByExample: function (example, merge, options, callback) {
this.update(exampleToSchema(example), merge, options, callback);
},
update: function (schema, merge, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
this._req('PATCH', schema, options, merge, 'application/merge-patch+json', callback);
},
patchByExample: function (example, patch, options, callback) {
this.patch(exampleToSchema(example), patch, options, callback);
},
patch: function (schema, patch, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
if (patchIsRemove(patch)) {
this._req('DELETE', schema, options, undefined, null, callback);
} else {
this._req('PATCH', schema, options, patch, 'application/json-patch+json', callback);
}
},
removeByExample: function (example, options, callback) {
this.remove(exampleToSchema(example), options, callback);
},
remove: function (schema, options, callback) {
// Removal patch
this.patch(schema, [{op: 'remove', path: ''}], options, callback);
},
};
SwishClient.parseQuery = parseQuery;
SwishClient.createQuery = createQuery;
return SwishClient;
});