Last active
November 10, 2015 10:56
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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