Decent Exposure gem, Mongoid and find_by method
Decent Exposure is ‘a Ruby gem for cleaner controllers’ and I love it.
As for now, hovewer, it lacks a strategy for use with Mongoid. In most basic cases a strategy for ActiveRecord works fine, but it breaks when one needs to use a custom finder. That is because Mongoid doesn’t provide dynamic finders – instead of ActiveRecord style:
Article.find_by_name('foo')
(which will be deprecated in Rails 4 AFAIK) we use:
Article.find_by(name: 'foo')
I wrote a simple initializer which monkey-patches decent_exposure allowing us to use lambda as a ‘finder’ option, like this:
expose :article, finder: lambda { |c| find_by(name: c.params[:id]) }
Just drop these lines of code into config/initializers/decent_exposure_mongoid.rb
module DecentExposure
class ActiveRecordStrategy < Strategy
def singular_resource
if id
case finder
when Symbol
scope.send(finder, id)
when Proc
scope.instance_exec(controller, &finder)
else
raise "Symbol or Proc needed"
end
else
scope.new
end
end
end
end
ActiveRecord dynamic attributes as serializable Struct
ActiveRecord gives us a nice feature to store dynamic attributes in a single database column:
class Product < ActiveRecord::Base serialize :props, Hash end
If you use it often it may be a sign that you should think about a different storage than a database.
But sometimes it is convenient to denormalize your data.
If you don’t know your attributes until runtime, it is the way to go.
But sometimes you know them, but still want to denormalize for some reasons.
Asynchronous singleton API
After an awesome tech talk with @petermichaux, @racino, @varjs, @qvist, we came to an interesting point, as how to handle a certain situation:
- you need a singleton
- you need it to instantiate lazily
- its instantiation involves querying server-side, thus you only want to do it when it’s absolutely necessary (hence the lazy approach)
- obviously, many parts of the system may be wanting to use what that singleton offers, in any given order, at any given time
What problems does this pose? It is somewhat similar to what is encountered in Java, where threads are involved. You would probably want the getInstance method to be synchronized. This is to some extent also true in case of the lazy, asynchronous instantiation in JavaScript. Because what happens, when getInstance is called multiple times, before instantiation is finished? Bad, unpredictable things.
This could seem as an obvious call for the deferred approach. But what got cooked rather resembled the state machine. So, enough talking, let’s code!
Passing the singleton object instance
We cannot get the singleton and assign it immediately, like so:
var mySingletonInstance = MySingleton.getInstance();
since it might not return anything at that point. We need to do it using callbacks passed to the instantiation logic, which get called when the job gets done. So we would rather go like this when using the singleton:
MySingleton.getInstance({
success : function(singletonInstance) {
// here we get access to our desired instance
},
failure : function(error) {
// bugger, something went wrong
}
});
Concurrent singleton instance access
All cool, but we still need to handle the case where multiple other parts of the system request access to the instance before it gets created. Like this:
// in module foo
MySingleton.getInstance({
success : function(singletonInstance) {
// do something useful with what the server provided
},
failure : function(error) {
// bugger, something went wrong
}
});
// in module bar
MySingleton.getInstance({
success : function(singletonInstance) {
// do something just as useful
},
failure : function(error) {
// bugger, something went wrong
}
});
Callback queue
What we need be doing, is collect all the callbacks, and execute them once the object is ready to be used. So let’s build up a skeleton of the solution:
var MySingleton = {};
(function() {
var callbackQueue = [];
var instance = null;
var doRequests = function(callbacks) {
//code for interaction with the server
};
MySingleton.getInstance = function(callbacks) {
}
}());
States
Let’s think about what state our singleton can be in. One is before instantiation, which you could call before. When it’s called for its instance, it could be working. Once all is done it could be success, and if something went wrong, failure. Extending our skeleton with the states:
var MySingleton = {};
(function() {
var state = "before";
var callbackQueue = [];
var instance = null;
var doRequests = function(callbacks) {
//code for interaction with the server
};
MySingleton.getInstance = function(callbacks) {
switch (state) {
case "before":
//this would be the case of the first call for the instance; here we start instantiating and queue up the first callback
state = "working";
callbackQueue.push(callbacks);
break;
case "working":
//here we just queue up further callbacks
case "success":
//in this case we execute the success callback immediately
case "failure":
//like in success, but we pass some custom error object to the failure callback
default:
throw new Error("Invalid state: " + state)
}
};
}());
Asynchronous instantiation
One extra thing we’ve got to remember is that the instantiation itself, due to its asynchronous nature, also happens with the help of callbacks, hence the extra handling of them in the before branch:
var MySingleton = {};
(function() {
var state = "before";
var callbackQueue = [];
var myInstance = null;
var error = null;
var doRequests = function(callbacks) {
//code for interaction with the server
};
MySingleton.getInstance = function(callbacks) {
switch (state) {
case "before":
state = "working";
callbackQueue.push(callbacks);
doRequests({
success : function(singletonInstance) {
state = "success";
myInstance = singletonInstance;
},
failure : function(errorObj) {
state = "failure";
error = errorObj;
}
});
break;
case "working":
//here we just queue up further callbacks
case "success":
//in this case we execute the callback immediately
case "failure":
//like in success, but we pass some custom error object
default:
throw new Error("Invalid state: " + state)
}
};
}());
Resolving callback queue
Now let’s take care of resolving the callback queue. Throughout this article, I’ve been assuming the callbacks were passed in form of an object like:
MySingleton.getInstance({
success : function(singletonInstance) {
},
failure : function(error) {
}
});
So here goes:
var MySingleton = {};
(function() {
var state = "before";
var callbackQueue = [];
var myInstance = null;
var error = null;
var doRequests = function(callbacks) {
//code for interaction with the server
};
MySingleton.getInstance = function(callbacks) {
switch (state) {
case "before":
state = "working";
callbackQueue.push(callbacks);
doRequests({
success : function(singletonInstance) {
state = "success";
myInstance = singletonInstance;
for (var i = 0, ilen = callbackQueue.length; i < ilen; i++) {
callbackQueue[i].success(myInstance);
}
callbackQueue = [];
},
failure : function(errorObj) {
state = "failure";
error = errorObj;
for (var i = 0, ilen = callbackQueue.length; i < ilen; i++) {
callbackQueue[i].failure(error);
}
callbackQueue = [];
}
});
break;
case "working":
//here we just queue up further callbacks
case "success":
//in this case we execute the callback immediately
case "failure":
//like in success, but we pass some custom error object
default:
throw new Error("Invalid state: " + state)
}
};
}());
Final solution
Let’s build the rest of the state branches:
var MySingleton = {};
(function() {
var state = "before";
var callbackQueue = [];
var myInstance = null;
var error = null;
var doRequests = function(callbacks) {
//code for interaction with the server
};
MySingleton.getInstance = function(callbacks) {
switch (state) {
case "before":
state = "working";
callbackQueue.push(callbacks);
doRequests({
success : function(singletonInstance) {
state = "success";
myInstance = singletonInstance;
for (var i = 0, ilen = callbackQueue.length; i < ilen; i++) {
callbackQueue[i].success(myInstance);
}
callbackQueue = [];
},
failure : function(errorObj) {
state = "failure";
error = errorObj;
for (var i = 0, ilen = callbackQueue.length; i < ilen; i++) {
callbackQueue[i].failure(error);
}
callbackQueue = [];
}
});
break;
case "working":
callbackQueue.push(callbacks);
break;
case "success":
callbacks.success(myInstance);
break;
case "failure":
callbacks.failure(error);
break;
default:
throw new Error("Invalid state: " + state)
}
};
}());
We’re done!
And that’s about it! Further improvements may include making MySingleton and its getInstance protected against overwriting. To achieve this, we could use ES5 object and property descriptors, some of which I’ve already covered.
