Skip to content

Instantly share code, notes, and snippets.

@jdmichaud
Last active December 22, 2017 10:57
Show Gist options
  • Save jdmichaud/1ff8f230359e333344279db6fd99ecf5 to your computer and use it in GitHub Desktop.
Save jdmichaud/1ff8f230359e333344279db6fd99ecf5 to your computer and use it in GitHub Desktop.
Observable model
// 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';
}
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