Splits Adapter loading from AdaptableController

- Adds dynamic prototype conformance check upon setting adapter
- Throws when adapter is undefined, invalid in controller
This commit is contained in:
Florent Vilmart
2016-02-21 23:47:07 -05:00
parent 33fa5a7b2a
commit 23e55e941e
10 changed files with 210 additions and 116 deletions

View File

@@ -1,70 +1,44 @@
var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController; var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default; var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
var FilesController = require("../src/Controllers/FilesController").FilesController;
var MockController = function(options) {
AdaptableController.call(this, options);
}
MockController.prototype = Object.create(AdaptableController.prototype);
MockController.prototype.constructor = AdaptableController;
describe("AdaptableController", ()=>{ describe("AdaptableController", ()=>{
it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var controller = new AdaptableController({
adapter: adapterPath,
key: "value",
foo: "bar"
});
expect(controller.adapter instanceof Object).toBe(true);
expect(controller.options.key).toBe("value");
expect(controller.options.foo).toBe("bar");
expect(controller.adapter.options.key).toBe("value");
expect(controller.adapter.options.foo).toBe("bar");
done();
});
it("should instantiate an adapter from string", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var controller = new AdaptableController(adapterPath);
expect(controller.adapter instanceof Object).toBe(true);
done();
});
it("should instantiate an adapter from string that is module", (done) => {
var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
var controller = new AdaptableController({
adapter: adapterPath
});
expect(controller.adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate an adapter from function/Class", (done) => {
var controller = new AdaptableController({
adapter: FilesAdapter
});
expect(controller.adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate the default adapter from Class", (done) => {
AdaptableController.setDefaultAdapter(FilesAdapter);
var controller = new AdaptableController();
expect(controller.adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the default adapter", (done) => {
var adapter = new FilesAdapter();
AdaptableController.setDefaultAdapter(adapter);
var controller = new AdaptableController();
expect(controller.adapter).toBe(adapter);
done();
});
it("should use the provided adapter", (done) => { it("should use the provided adapter", (done) => {
var adapter = new FilesAdapter(); var adapter = new FilesAdapter();
var controller = new AdaptableController(adapter); var controller = new FilesController(adapter);
expect(controller.adapter).toBe(adapter); expect(controller.adapter).toBe(adapter);
done(); done();
}); });
it("should throw when creating a new mock controller", (done) => {
var adapter = new FilesAdapter();
expect(() => {
new MockController(adapter);
}).toThrow();
done();
});
it("should fail to instantiate a controller with wrong adapter", (done) => {
function WrongAdapter() {};
var adapter = new WrongAdapter();
expect(() => {
new FilesController(adapter);
}).toThrow();
done();
});
it("should fail to instantiate a controller without an adapter", (done) => {
expect(() => {
new FilesController();
}).toThrow();
done();
});
}); });

View File

@@ -0,0 +1,68 @@
var AdapterLoader = require("../src/Adapters/AdapterLoader").AdapterLoader;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
describe("AdaptableController", ()=>{
it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = AdapterLoader.load({
adapter: adapterPath,
key: "value",
foo: "bar"
});
expect(adapter instanceof Object).toBe(true);
expect(adapter.options.key).toBe("value");
expect(adapter.options.foo).toBe("bar");
done();
});
it("should instantiate an adapter from string", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = AdapterLoader.load(adapterPath);
expect(adapter instanceof Object).toBe(true);
expect(adapter.options).toBe(adapterPath);
done();
});
it("should instantiate an adapter from string that is module", (done) => {
var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
var adapter = AdapterLoader.load({
adapter: adapterPath
});
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate an adapter from function/Class", (done) => {
var adapter = AdapterLoader.load({
adapter: FilesAdapter
});
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate the default adapter from Class", (done) => {
var adapter = AdapterLoader.load(null, FilesAdapter);
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the default adapter", (done) => {
var defaultAdapter = new FilesAdapter();
var adapter = AdapterLoader.load(null, defaultAdapter);
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the provided adapter", (done) => {
var originalAdapter = new FilesAdapter();
var adapter = AdapterLoader.load(originalAdapter);
expect(adapter).toBe(originalAdapter);
done();
});
});

View File

@@ -1,4 +1,5 @@
var FilesController = require('../src/Controllers/FilesController').FilesController; var FilesController = require('../src/Controllers/FilesController').FilesController;
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
var Config = require("../src/Config"); var Config = require("../src/Config");
// Small additional tests to improve overall coverage // Small additional tests to improve overall coverage
@@ -6,7 +7,8 @@ describe("FilesController",()=>{
it("should properly expand objects", (done) => { it("should properly expand objects", (done) => {
var config = new Config(Parse.applicationId); var config = new Config(Parse.applicationId);
var filesController = new FilesController(); var adapter = new GridStoreAdapter();
var filesController = new FilesController(adapter);
var result = filesController.expandFilesInObject(config, function(){}); var result = filesController.expandFilesInObject(config, function(){});
expect(result).toBeUndefined(); expect(result).toBeUndefined();

View File

@@ -76,13 +76,11 @@ describe('LoggerController', () => {
}); });
it('should throw without an adapter', (done) => { it('should throw without an adapter', (done) => {
LoggerController.setDefaultAdapter(undefined);
var loggerController = new LoggerController();
expect(() => { expect(() => {
loggerController.getLogs(); var loggerController = new LoggerController();
}).toThrow(); }).toThrow();
LoggerController.setDefaultAdapter(FileLoggerAdapter);
done(); done();
}); });
}); });

View File

@@ -0,0 +1,39 @@
export class AdapterLoader {
static load(options, defaultAdapter) {
let adapter;
// We have options and options have adapter key
if (options) {
// Pass an adapter as a module name, a function or an instance
if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
adapter = options;
}
if (options.adapter) {
adapter = options.adapter;
}
}
if (!adapter) {
adapter = defaultAdapter;
}
// This is a string, require the module
if (typeof adapter === "string") {
adapter = require(adapter);
// If it's define as a module, get the default
if (adapter.default) {
adapter = adapter.default;
}
}
// From there it's either a function or an object
// if it's an function, instanciate and pass the options
if (typeof adapter === "function") {
var Adapter = adapter;
adapter = new Adapter(options);
}
return adapter;
}
}
export default AdapterLoader;

View File

@@ -8,8 +8,6 @@ based on the parameters passed
*/ */
const DefaultAdapters = {};
export class AdaptableController { export class AdaptableController {
/** /**
* Check whether the api call has master key or not. * Check whether the api call has master key or not.
@@ -22,51 +20,49 @@ export class AdaptableController {
* - object: a plain javascript object (options.constructor === Object), if options.adapter is set, we'll try to load it with the same mechanics. * - object: a plain javascript object (options.constructor === Object), if options.adapter is set, we'll try to load it with the same mechanics.
* - function: we'll create a new instance from that function, and pass the options object * - function: we'll create a new instance from that function, and pass the options object
*/ */
constructor(options) { constructor(adapter, options) {
this.setAdapter(adapter, options);
let adapter; }
// We have options and options have adapter key setAdapter(adapter, options) {
if (options) { this.validateAdapter(adapter);
// Pass an adapter as a module name, a function or an instance
if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
adapter = options;
}
if (options.adapter) {
adapter = options.adapter;
}
}
if (!adapter) {
adapter = this.defaultAdapter();
}
// This is a string, require the module
if (typeof adapter === "string") {
adapter = require(adapter);
// If it's define as a module, get the default
if (adapter.default) {
adapter = adapter.default;
}
}
// From there it's either a function or an object
// if it's an function, instanciate and pass the options
if (typeof adapter === "function") {
var Adapter = adapter;
adapter = new Adapter(options);
}
this.adapter = adapter; this.adapter = adapter;
this.options = options; this.options = options;
} }
defaultAdapter() { expectedAdapterType() {
return DefaultAdapters[this.constructor.name]; throw new Error("Subclasses should implement expectedAdapterType()");
} }
// Sets the default adapter for that Class validateAdapter(adapter) {
static setDefaultAdapter(defaultAdapter) {
DefaultAdapters[this.name] = defaultAdapter; if (!adapter) {
throw new Error(this.constructor.name+" requires an adapter");
}
let Type = this.expectedAdapterType();
// Allow skipping for testing
if (!Type) {
return;
}
// Makes sure the prototype matches
let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => {
const adapterType = typeof adapter[key];
const expectedType = typeof Type.prototype[key];
if (adapterType !== expectedType) {
obj[key] = {
expected: expectedType,
actual: adapterType
}
}
return obj;
}, {});
if (Object.keys(mismatches).length > 0) {
console.error(adapter, mismatches);
throw new Error("Adapter prototype don't match expected prototype");
}
} }
} }

View File

@@ -2,6 +2,7 @@
import { Parse } from 'parse/node'; import { Parse } from 'parse/node';
import { randomHexString } from '../cryptoUtils'; import { randomHexString } from '../cryptoUtils';
import AdaptableController from './AdaptableController'; import AdaptableController from './AdaptableController';
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
export class FilesController extends AdaptableController { export class FilesController extends AdaptableController {
@@ -29,7 +30,7 @@ export class FilesController extends AdaptableController {
* with the current mount point and app id. * with the current mount point and app id.
* Object may be a single object or list of REST-format objects. * Object may be a single object or list of REST-format objects.
*/ */
expandFilesInObject(config, object) { expandFilesInObject(config, object) {
if (object instanceof Array) { if (object instanceof Array) {
object.map((obj) => this.expandFilesInObject(config, obj)); object.map((obj) => this.expandFilesInObject(config, obj));
return; return;
@@ -52,6 +53,10 @@ export class FilesController extends AdaptableController {
} }
} }
} }
expectedAdapterType() {
return FilesAdapter;
}
} }
export default FilesController; export default FilesController;

View File

@@ -1,6 +1,7 @@
import { Parse } from 'parse/node'; import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import AdaptableController from './AdaptableController'; import AdaptableController from './AdaptableController';
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
const Promise = Parse.Promise; const Promise = Parse.Promise;
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
@@ -70,6 +71,10 @@ export class LoggerController extends AdaptableController {
}); });
return promise; return promise;
} }
expectedAdapterType() {
return LoggerAdapter;
}
} }
export default LoggerController; export default LoggerController;

View File

@@ -2,6 +2,7 @@ import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import rest from '../rest'; import rest from '../rest';
import AdaptableController from './AdaptableController'; import AdaptableController from './AdaptableController';
import { PushAdapter } from '../Adapters/Push/PushAdapter';
export class PushController extends AdaptableController { export class PushController extends AdaptableController {
@@ -25,7 +26,7 @@ export class PushController extends AdaptableController {
deviceType + ' is not supported push type.'); deviceType + ' is not supported push type.');
} }
} }
}; }
/** /**
* Check whether the api call has master key or not. * Check whether the api call has master key or not.
@@ -53,7 +54,8 @@ export class PushController extends AdaptableController {
rest.find(config, auth, '_Installation', where).then(function(response) { rest.find(config, auth, '_Installation', where).then(function(response) {
return pushAdapter.send(body, response.results); return pushAdapter.send(body, response.results);
}); });
}; }
/** /**
* Get expiration time from the request body. * Get expiration time from the request body.
* @param {Object} request A request object * @param {Object} request A request object
@@ -80,7 +82,11 @@ export class PushController extends AdaptableController {
body['expiration_time'] + ' is not valid time.'); body['expiration_time'] + ' is not valid time.');
} }
return expirationTime.valueOf(); return expirationTime.valueOf();
}; }
expectedAdapterType() {
return PushAdapter;
}
}; };
export default PushController; export default PushController;

View File

@@ -33,14 +33,10 @@ import { PushRouter } from './Routers/PushRouter';
import { FilesRouter } from './Routers/FilesRouter'; import { FilesRouter } from './Routers/FilesRouter';
import { LogsRouter } from './Routers/LogsRouter'; import { LogsRouter } from './Routers/LogsRouter';
import { AdapterLoader } from './Adapters/AdapterLoader';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { LoggerController } from './Controllers/LoggerController'; import { LoggerController } from './Controllers/LoggerController';
FilesController.setDefaultAdapter(GridStoreAdapter);
PushController.setDefaultAdapter(ParsePushAdapter);
LoggerController.setDefaultAdapter(FileLoggerAdapter);
// Mutate the Parse object to add the Cloud Code handlers // Mutate the Parse object to add the Cloud Code handlers
addParseCloud(); addParseCloud();
@@ -109,12 +105,17 @@ function ParseServer({
throw "argument 'cloud' must either be a string or a function"; throw "argument 'cloud' must either be a string or a function";
} }
} }
const filesControllerAdapter = AdapterLoader.load(filesAdapter, GridStoreAdapter);
const pushControllerAdapter = AdapterLoader.load(push, ParsePushAdapter);
const loggerControllerAdapter = AdapterLoader.load(loggerAdapter, FileLoggerAdapter);
// We pass the options and the base class for the adatper, // We pass the options and the base class for the adatper,
// Note that passing an instance would work too // Note that passing an instance would work too
const filesController = new FilesController(filesAdapter); const filesController = new FilesController(filesControllerAdapter);
const pushController = new PushController(push); const pushController = new PushController(pushControllerAdapter);
const loggerController = new LoggerController(loggerAdapter); const loggerController = new LoggerController(loggerControllerAdapter);
cache.apps[appId] = { cache.apps[appId] = {
masterKey: masterKey, masterKey: masterKey,