Last active
December 22, 2017 10:57
-
-
Save jdmichaud/1ff8f230359e333344279db6fd99ecf5 to your computer and use it in GitHub Desktop.
Observable model
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
// npm install --save rxjs | |
// npm install --save-dev mocha | |
// # We need babel for decorator (ES7) | |
// npm install --save-dev babel-cli babel-preset-env | |
// cat > .babelrc << EOF | |
// { | |
// "presets": ["env"] | |
// } | |
// EOF | |
// node_modules/.bin/mocha observable-model.js | |
// TMPFILE=$(mktemp --tmpdir=.) && echo $TMPFILE && ./node_modules/.bin/babel observable-model.js > $TMPFILE && node_modules/.bin/mocha $TMPFILE && rm $TMPFILE | |
// TMPFILE=$(mktemp --tmpdir=.) && echo $TMPFILE && ./node_modules/.bin/babel observable-model.js > $TMPFILE && node $TMPFILE && rm $TMPFILE | |
'use strict'; | |
var Rx = require('rxjs'); | |
var Mocha = require('mocha'); | |
var assert = require('assert'); | |
/* | |
* Decorator function which adds a proxy to a model which provide API for | |
* change notifications | |
*/ | |
function ObservableProxy(target) { | |
const originalProperties = Object.entries(target).map(entry => entry[0]); | |
const derivedProperties = {}; | |
let sealed = false; | |
const proxy = new Proxy(target, { | |
get: function(target, name) { | |
if (originalProperties.includes(name)) { | |
return target[name]; | |
} else { | |
return derivedProperties[name]; | |
} | |
}, | |
set: function(target, name, value) { | |
if (originalProperties.includes(name)) { | |
target[name] = value; | |
derivedProperties.observer[name] && derivedProperties.observer[name].next(value); | |
return true; | |
} else if (!sealed) { | |
derivedProperties[name] = value; | |
return true; | |
} | |
return false; | |
}, | |
}); | |
proxy.observable = {}; | |
proxy.observer = {}; | |
proxy.observe = {}; | |
originalProperties.forEach(entry => { | |
proxy.observable[entry] = Rx.Observable.create(function (observer) { | |
proxy.observer[entry] = observer; | |
}); | |
proxy.observe[entry] = function (onNext) { | |
proxy.observable[entry].subscribe(onNext); | |
}; | |
}); | |
sealed = true; | |
return proxy; | |
} | |
/* | |
* The factory wraps a model with an ObservableProxy | |
*/ | |
function ModelFactory(clazz, config) { | |
return ObservableProxy(new clazz(config)); | |
} | |
/* | |
* A model is a simple ES6 class | |
*/ | |
class Model { | |
constructor({ | |
mandatoryProperty, | |
defaultProperty = 42, | |
property, | |
}) { | |
if (mandatoryProperty === undefined || mandatoryProperty === null) | |
throw Error('missing mandatoryProperty'); | |
this.mandatoryProperty = mandatoryProperty; | |
this.defaultProperty = defaultProperty; | |
this.property = property; | |
} | |
} | |
// TODO: Decorator for Mandatory and DefaultValue | |
// TODO: Recursivity tests | |
// TODO: load functions | |
if (Mocha.describe) { | |
// Specifications | |
Mocha.describe('ObservableProxy', function () { | |
Mocha.it('shall define an observer function for every property of the model', function () { | |
const model = ModelFactory(Model, { mandatoryProperty: 'someProperty'}); | |
assert.notEqual(model.observe.property, undefined); | |
assert.notEqual(model.observe.defaultProperty, undefined); | |
assert.notEqual(model.observe.mandatoryProperty, undefined); | |
assert.equal(model.observe.toto, undefined); | |
}); | |
Mocha.it('shall let the user access properties directly', function () { | |
const model = ModelFactory(Model, { mandatoryProperty: 'someProperty'}); | |
assert.equal(model.defaultProperty, 42); | |
model.property = 666; | |
assert.equal(model.property, 666); | |
}); | |
Mocha.it('shall notify the user upon changes', function () { | |
let variable; | |
const model = ModelFactory(Model, { mandatoryProperty: 'someProperty'}); | |
model.observe.property((newValue) => variable = newValue); | |
model.property = 'toto'; | |
assert.equal(variable, 'toto'); | |
}); | |
Mocha.it('shall prevent the user to add properties', function () { | |
const model = ModelFactory(Model, { mandatoryProperty: 'someProperty'}); | |
// Check if we are in strict mode | |
var isStrict = (function() { return !this; })(); | |
if (isStrict) { | |
// Will throw in strict mode | |
assert.throws(function () { model.newProperty = 'fsociety' }); | |
} else { | |
model.newProperty = 'fsociety' | |
assert.equal(model.newProperty, undefined); | |
} | |
}); | |
}); | |
} else { | |
// Demo | |
const model = ModelFactory(Model, { mandatoryProperty: 'someProperty' }); | |
model.observe.property((newValue) => console.log(`new value: ${newValue}`)); | |
model.property = 'toto'; | |
} |
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
import { Observable } from 'rxjs/Observable'; | |
import { Observer } from 'rxjs/Observer'; | |
type Proxify<T> = { [P in keyof T]: T[P] }; | |
type Observablify<T> = { [P in keyof T]: Observable<T[P]> }; | |
type Observerify<T> = { [P in keyof T]: Observer<T[P]> }; | |
class Observified<T> { | |
observables: Observablify<T>; | |
observers: Observerify<T>; | |
} | |
type Proxybler<T> = Proxify<T> & Observified<T>; | |
class Proxyfier { | |
static proxify<T>(o: T): Proxybler<T> { | |
const proxy = {} as Proxybler<T>; | |
for (const k in o) { | |
Object.defineProperty(proxy, k, { | |
get: () => { | |
return o[k]; | |
}, | |
set: (value: any): void => { | |
if (k in o) { | |
o[k] = value; | |
// proxy.observers[k]. | |
return; | |
} | |
// This should never happen if TypeScript is compiled with the type | |
// checker | |
throw new Error(`${k} unknown property of object o.constructor.name`); | |
}, | |
}); | |
} | |
return proxy; | |
} | |
} | |
class Model { | |
public static create( | |
config: Partial<Model> & { mandatoryProperty: number } | |
): Proxybler<Model> { | |
return Proxyfier.proxify(new Model(config)); | |
} | |
/** Object cannot be created manually */ | |
private constructor(config: Partial<Model> & { mandatoryProperty: number }) { | |
// TODO: use a polyfill here | |
(Object as any).assign(this, config); | |
} | |
public mandatoryProperty: number; | |
public defaultProperty: number = 0; | |
public property?: number; | |
} | |
const model = Model.create({ mandatoryProperty: 5 }); | |
model.defaultProperty = 42; | |
model.observables.defaultProperty.subscribe(value => console.log(`observed: ${value}`)); | |
console.log(model.defaultProperty); | |
// This will raise a compilation error | |
// ERROR in ./src/scripts/app.ts | |
// (XX,7): error TS2339: Property 'toto' does not exist on type 'Proxify<Model>'. | |
// model.toto = 6; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment