Skip to content

Instantly share code, notes, and snippets.

@lyschoening
Last active November 10, 2015 10:56
Show Gist options
  • Save lyschoening/a9e94a3fc77ae571df63 to your computer and use it in GitHub Desktop.
Save lyschoening/a9e94a3fc77ae571df63 to your computer and use it in GitHub Desktop.
A helper class for dealing with JSON from Potion-based APIs in JavaScript
class Potion {
/**
*
* @param {function} fetch for GET requests to look up references
* @param {object} constructors a map of resource URIs to constructors, e.g. {'/type': Type}
* @param {function} defaultConstructor an optional fallback for other types
* @param {string} prefix an optional API prefix
* @param {object} cache an optional cache object with get(string), put(string, object) and remove(string) methods
*/
constructor({
fetch = fetch,
constructors = {},
defaultConstructor = null,
prefix = '',
cache = {}},
} = {}) {
this._cache = cache;
this._prefix = prefix;
this._constructors = constructors;
this._defaultConstructor = defaultConstructor;
this._fetch = fetch;
this._promises = {};
}
register(resourceURI, constructor) {
this._constructors[resourceURI] = constructor;
}
parseURI(uri) {
uri = decodeURIComponent(uri);
if (uri.indexOf(this._prefix) === 0) {
uri = uri.substring(this._prefix.length);
}
for (var resourceURI in this._constructors) {
if (uri.indexOf(`${resourceURI}/`) === 0) {
let remainder = uri.substring(resourceURI.length + 1);
return {
constructor: this._constructors[resourceURI],
path: remainder.split('/'),
uri
}
}
}
if (this._defaultConstructor) {
let [name, ...path] = uri.split('/').slice(1);
return {
constructor: this._defaultConstructor,
name,
path,
uri
}
} else {
throw new Error(`Unknown Resource URI: ${uri}`);
}
}
async fromJSON(value) {
if (typeof value == 'object' && value !== null) {
if (value instanceof Array) {
return await Promise.all(value.map((item) => this.fromJSON(item)));
} else if (typeof value.$uri == 'string') {
let {constructor, uri} = this.parseURI(value.$uri);
let converted = {};
for (let key of Object.key(value)) {
if (key == '$uri') {
converted[key] = uri;
} else if (constructor.deferredProperties && constructor.deferredProperties.includes(key)) {
converted[toCamelCase(key)] = () => this.fromJSON(value[key]);
} else {
converted[toCamelCase(key)] = await this.fromJSON(value[key]);
}
}
let instance;
if (this._cache.get && !(instance = this._cache.get(uri))) {
instance = new constructor(converted);
if (this._cache.put) {
this._cache.put(uri, instance);
}
} else {
Object.assign(instance, converted);
}
return value;
} else if (Object.keys(value).length == 1) {
if (typeof value.$ref == 'string') {
let {uri} = this.parseURI(value.$ref);
return this.get(uri);
} else if (typeof value.$date != 'undefined') {
return new Date(value.$date);
}
}
let converted = {};
for (let key of Object.keys(value)) {
converted[toCamelCase(key)] = await this.fromJSON(value[key]);
}
return converted;
} else {
return value;
}
}
toJSON(value) {
if (typeof value == 'object' && value !== null) {
if (typeof value.$uri == 'string') {
return {$ref: `${this._prefix}${value.$uri}`};
} else if (value instanceof Date) {
return {$date: value.getTime()}
} else if (value instanceof Array) {
return value.map(this.toJSON);
} else {
let serialized = {};
for (let key of Object.keys(value)) {
serialized[fromCamelCase(key)] = this.toJSON(value)
}
return serialized;
}
} else {
return value;
}
}
stringify(value) {
return JSON.stringify(this.toJSON(value));
}
async get(uri) {
let instance;
if (this._cache.get && (instance = this._cache.get(uri))) {
return instance;
}
let promise = this._promises[uri];
if (!promise) {
promise = this._promises[uri] = this.fromJSON(await this._fetch(`${this._prefix}${uri}`));
}
instance = await promise;
delete this._promises[uri];
return instance;
}
clear(item) {
delete this._promises[item.$uri];
if (this._cache.remove) {
this._cache.remove(item.$uri)
} else if (this._cache.set) {
this._cache.set(item.$uri, undefined)
}
}
}
function fromCamelCase(string, separator = '_') {
return string.replace(/([a-z][A-Z])/g, (g) => `${g[0]}${separator}${g[1].toLowerCase()}`);
}
function toCamelCase(string) {
return string.replace(/_([a-z0-9])/g, (g) => g[1].toUpperCase());
}
class Type {
static deferredProperties = [];
static readOnlyProperties = [];
constructor(contents = {}) {
super();
for (let key of this.constructor.deferredProperties) {
let value = contents[key];
Object.defineProperty(this, key, {
enumerable: false,
get: () => {
if (typeof value == 'function') {
return Promise.resolve(value());
} else {
return Promise.resolve(value);
}
},
set: (newValue) => {
value = newValue;
}
});
}
Object.assign(this, contents);
}
get $id() {
return parseInt(this.$uri.split('/').pop())
}
toJSON() {
let instance = {};
for (let key of Object.keys(this)) {
if (!this.constructor.readOnlyProperties.includes(key) && key != '$uri') {
instance[fromCamelCase(key)] = this[key]
}
}
return instance;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment