Use instance_eval to clean up your API
I recently wrote a small application to parse Apache log files. The output is this context is not really important, but rather the API of the application that I wrote.
Two things were required to parse a log file:
- the log file format
- the location of the log file
I wanted the initialize the object with block notation. So starting off, this is what we wrote, which is faily typical in terms of most internal DSLs:
LogParser::Application.new do |p| p.format = "%V %h %l %u %t \"%r\" %s" p.log_file = '/apache_path/access_log' end
This would then result in a constructor that looks something like this…
module LogParser
class Application
def initialize(&block)
block.call(self) if block_given?
end
end
end
That’s perfectly acceptable and it will work just fine. But it’s not a winner in terms of elegance. Enter instance_eval.
Just like call, instance_eval will execute a block, but it will change the value of self as it executes the block.
So, to get an API that looks like this…
LogParser::Application.new do format "%V %h %l %u %t \"%r\" %s" log_file '/apache_path/access_log' end
…we must change the constructor to look like this…
module LogParser
class Application
def initialize(&block)
instance_eval(block) if block_given?
end
end
end
The example above has a few caveats though. In the first example, we are assigning values to format and log_file using setter methods: format= and log_file=. These can be created manually or by using attr_accessor.
In the second example, we are calling methods format and log_file. It’s a subtle difference but something that should be considered when developing your own API.
Here’s the final code for the Application class used above…
module LogParser
class Application
def initialize(&block)
instance_eval(&block) if block_given?
end
def format regex_pattern
@format = regex_pattern
end
def log_file filepath
@log_file = filepath
end
# rest of code omitted
end
end
Hope this helps.
Git flow steps for a new project
I use in all my projects, and it makes my life worth living.
However, whenever I start a new project I can never seem to remember the initial steps that needs to be taken to create the remote develop branch, which include tracking the branch locally.
So for my own sanity, here is what you need to do when starting a new git-flow repo…
Step 1:
Create your repo on github. This will create the default master branch only.
Step 2:
In you shiny new repo, create the develop branch locally.
git flow init
Step 3:
Create the remote develop branch.
git push origin develop
Step 4:
Track the remote develop branch.
git branch --set-upstream develop origin/develop
Done.
Now write some code.
When checking out an existing repo, you only need Step 4 in order to track the remote develop branch.
Now write some code.
ActiveResource: How to avoid “undefined class/module” exception
I use webmock to stub external endpoints in test and development mode. It’s a fantastic gem and I cannot recommend it highly enough.
I ran into some problems this week using it together with ActiveResource (AR). We were allowing our test-driven approach to define the response of a yet-to-be-built API method. We were defining the API response in YAML, and then when setting the webmock stub, converting the YAML to json. Nothing too fancy there.
The problems started happening when we added a nested level to our YAML file. When AR loads a response, any nested levels are created as nested AR classes, and our app was bombing out when we introduced this nested level. We kept getting “undefined class/module …” exceptions.
Here’s what our YAML looked like:
:quote_id: 102 :detail: :premium: 100.00 :total: 145.00
See the nested detail level? When AR receives the json version of this response, the AR object will have quote_id as an instance method. But, it will have premium and total as instance methods on a nested Detail object.
Why was this an issue for us?
We receive the API response and need to persist it in our application, but we want to do this persistence using the session (and ActiveRecord session mind). It’s not a good idea to put complex objects into the session store, so before doing that we use Marshal to serialize the object. Similarly, when retrieving the object from the session, we then need to deserialize the object before we can use it. It’s at this point that things go wrong because AR will not know about the nested class within the object. It will fail.
The solution is quite simple, although not entirely flexible in that you do need to know about which nested classes you will be handling.
I created two class methods to deal with the serialization. Serializing the object is easy. Deserializing requires us to use the find_or_create_resource_for method, which can be found in the ActiveResource::Base class. It’s used internally by AR to overcome this exact issue.
Here’s my AR class (simplified for brevity). Note that the single-item list of nested classes contains the Detail class, as defined by the nested level in my YAML file above.
class PaymentDetail < ActiveResource::Base
self.site = Rails.application.config.special_api
self.format = :json
NESTED_CLASSES = %w(Detail)
def self.stringify_quote obj
Marshal.dump obj
end
def self.load_quote str
# preload nested classes to avoid an ActiveResource undefined class exception
NESTED_CLASSES.each { |klass| PaymentDetail.new.send(:find_or_create_resource_for, klass) }
Marshal.load str
end
end
How does this help me?
This will allow you to easily persist any AR object in a session (or cache) for later use, which is especially useful for API’s that are read-only. It means you can persist the state of an AR object even when the source of that object – the external API – does not allow the changing of that state.
Hope this saves someone some time!
