RSpec 1.1.4 and Helpers
July 1st, 2008
Last week, RSpec 1.1.4 was released; and given my optimistic disposition I immediately upgraded. Happily, the upgrade was almost seamless. Two interdependent issues kept this from being a flawless upgrade:
- A new deprecation warning on our helper specs (no one really likes to see deprecation warnings)
- A bug in the new way to write helper specs (a show stopper)
Lets see what happens when we run our spec1.
Wonderland$ spec spec/helpers/dog_helper_spec.rb
Modules will no longer be automatically included in RSpec version 1.1.4. Called from ./spec/helpers/dog_helper_spec.rb:8
.Modules will no longer be automatically included in RSpec version 1.1.4. Called from ./spec/helpers/dog_helper_spec.rb:16
.
Finished in 0.393129 seconds
2 examples, 0 failures
Lets examine the helper spec that generated these warnings:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe DogHelper do
describe "name_or_description()" do
it "should return a description for a dog without a name" do
dog = mock_model(Dog, :name => nil, :sex => 'male', :breed => 'Labrador', :color => 'black')
name_or_description(dog).should == "(male Labrador, black)"
end
end
describe "owner_link()" do
it "should return a link to the owner" do
owner = mock_model(User, :login => 'sally_smith', :to_param => 'sally_smith')
dog = mock_model(Dog, :owner => owner)
owner_link(dog).should =~ /users\/sally_smith/
end
end
end
So, what does this deprecation warning tell us? We are being warned that calls to name_or_description and owner_link, which are called on the implicit self of the RSpec it blocks, are going to fail in the future when the helper module (DogHelper in this example) is no longer automatically included by RSpec.
Or put another way, calling helper methods like name_or_description directly, exactly as one might in a view, will no longer work.
The new way to call helper methods is through the helper attribute of RSpec, giving access to an ActionView::Base instance with the helper module included. Lets go ahead and make this change to our two helper calls:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe DogHelper do
describe "name_or_description()" do
it "should return a description for a dog without a name" do
dog = mock_model(Dog, :name => nil, :sex => 'male', :breed => 'Labrador', :color => 'black')
helper.name_or_description(dog).should == "(male Labrador, black)"
end
end
describe "owner_link()" do
it "should return a link to the owner" do
owner = mock_model(User, :login => 'sally_smith', :to_param => 'sally_smith')
dog = mock_model(Dog, :owner => owner)
helper.owner_link(dog).should =~ /users\/sally_smith/
end
end
end
Lets try this out!
Wonderland$ spec spec/helpers/dog_helper_spec.rb
.F
1)
NoMethodError in 'DogHelper owner_link() should return a link to the owner'
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.url_for
(eval):17:in `user_path'
/Users/leshill/src/rails/rspec114/app/helpers/dog_helper.rb:9:in `owner_link'
./spec/helpers/dog_helper_spec.rb:16:
Finished in 0.398686 seconds
2 examples, 1 failure
Hmmm, we have gotten rid of the deprecation warnings, but we now have a new (and truly unexpected!) failure.
This is a bona-fide bug in 1.1.4, and might lead to our having to rollback to 1.1.3 if there was no easy workaround. Thankfully, there is a fix already available for 1.1.5 , and that fix can be easily adapted within our own specs.
Lets add this before block to our spec:
before(:each) do
# Patch until 1.1.5
helper_controller = @controller
helper.instance_eval { @controller = helper_controller }
end
And then try it out:
Wonderland$ spec spec/helpers/dog_helper_spec.rb
..
Finished in 0.40663 seconds
2 examples, 0 failures
Much better!
1 I am using a mock spec to demonstrate the issue, the minimal Rails app which fits around this spec is left as an exercise for the reader.
A participation deficit
May 11th, 2008
Clay Shirky is brilliant, and I have been reading his essays for a very long time1. Recently, he has been discussing his notion of a Cognitive Surplus . Good stuff; if you have not read it, go read it now.
The headline is that if we just switch 1% of yearly TV consumption hours to ‘participation’ we get the equivalent of 100 Wikipedia projects a year (that is development, comments, authors, editors, talk; the whole corpus as it stands today).
This the Cognitive Surplus; previously the surplus was masked by mind-numbing television until we switched from just being able to consume media to consuming, producing, and sharing media.
We are in a new media landscape, transformed irrevocably from mass consumption of media due to the communication advances of the last few decades.
Those advances enabled us to erase a participation deficit. The way the deficit manifested for me was an apparent inability to be alone (so says my wife) in spite of being a person who values solitude. Before the nascent intertubes, I had no way to participate. I could (and did) watch television. But I also did many other things, including hack on computers. Only there was no sharing, no participation. Once the nascent intertubes arrived, I2 and many others did participate. Our participation accelerated on the wave of Moore’s and Metcalfe’s laws to transform society in ways we could not anticipate, but that we were already exploring. We still cannot anticipate how we are changing society, and we are still exploring.
If you have read this, take a moment and write your own blog entry or tweet or upload a photo/video or post a message or whatever-social-sharing-thing-you-might-do and revel in your participation.
1 A decade?
2 My first email (lost forever) was in 1982.
A little bit of history
May 8th, 2008
I am a bit late on the history meme. Here I am:
Wonderland$ history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head
149 git
60 spec
46 ls
45 gst
42 cd
28 rake
14 ssh
8 ss
7 houston
7 gco
Things to note1:
- 3 are git
- 3 are Rails development related
- 4 are common unix commands
1 4 are aliases
Stone knives and bearskins
May 6th, 2008
Recently while discussing jQuery, we got onto the topic of AJAX. This prompted me to declare:
Before AJAX, we used stone knives and bearskins to make our web apps
Actually, we were using JavaScript and XML to drive our web apps, we just did not call it AJAX, and it was a very challenging task. In spite of the difficulty of building it, we stuck with it and we were pretty darn happy with our pre-web 2.0 web2.0 app; so much so that we actually had people coming by just to see our UI in action.
I went back and looked for some of our old code, hoping to find the CVS repository1, but only finding a tarball of version 1.4. The following code is from client/script/services.js and dated August 24, 2001—although some version of this code would have existed in the spring of 2000:
BrServices.prototype.issueRequest = function (action) {
...
var request = brUtils.newXmlDOM(action);
brUtils.dumpError(request.parseError);
var tryCount = 0;
var condition = brServices.isOkay;
var s = "";
while (condition == brServices.isOkay) {
try {
s += "\n";
var httpOb = new ActiveXObject("MSXML2.XMLHTTP"); // Was Microsoft.XMLHTTP
s += "new: " + httpOb.readyState;
httpOb.open("POST", brServices.brxmlURL, false);
s += ", open: " + httpOb.readyState;
httpOb.setRequestHeader("Content-Type", "text/xml; charset='UTF-8'");
s += ", setRequestHeader: " + httpOb.readyState;
httpOb.send(request);
...
Just looking at this snippet from our internal library makes me all the happier that we made the thing work at all. Kudos to Mark Judd and Brian Levine, two of our very talented small team, who were responsible for our UI.
1 You already know this, but just in case, CVS was a source code repository that pretty much everyone used before Subversion (unless you bought one from a vendor).
Control your layout
April 22nd, 2008
Recently, I added jQuery ajax tabs to an application. To reuse the existing views, I moved the relevant view code to partials for each required action:
render :partial => "entry", :object => journal_entry
and using something like the following in all the relevant controller methods:
format.html { render :partial => 'index' if request.xhr? }
As I added more features to the ajax tabs, I was conditionally rendering partials in every relevant action, as well as creating a partial. Not DRY.
layout provides a much DRY-er solution without the need for the partials. In application.rb:
layout proc { |controller| controller.request.xhr? ? false : 'application' }
When a controller action is invoked, if it is an XHR, no layout will be used. Otherwise the default Rails layout, ‘application’, will be used1.
1 layout expects the name of a layout to be returned from the proc; in this case, I am returning the name of the default. Returning false indicates that no layout should be used.
include and extend
April 6th, 2008
include and extend are commonly used to add methods to Ruby classes, and sometimes, why we use one or the other is not clear. Both do similar operations, but they are not equivalent. extend is more parsimonious adding only methods, includealso includes constants and module variables.
A simple guideline is to include instance methods and to extend class methods.
Here is an example of a class with an instance method and a class method. We have reopened the class so that we can show the differences from the original empty class:
def list_diff(label, list1, list2)
puts "#{label}: #{(list1 - list2).sort.join ' '}"
end
class One
end
methods_from_new = One.new.methods
methods_from_class = One.methods
class One
def instance_method
end
def One.class_method
end
end
list_diff "Instance One.new diff", One.new.methods, methods_from_new
list_diff "Class One diff", One.methods, methods_from_class
when run shows the expected differences:
Instance One.new diff: instance_method
Class One diff: class_method
Now lets create and include a module:
module ModuleTwo
CONSTANT_TWO = "two"
def module_method
end
end
class One
include ModuleTwo
end
list_diff "Instance One.new diff", One.new.methods, methods_from_new
list_diff "Class One diff", One.methods, methods_from_class
puts "has constant? #{One.constants.sort.join ' '}"
when run shows that module_method and CONSTANT_TWO are now available:
Instance One.new diff: instance_method module_method
Class One diff: class_method
has constant? CONSTANT_TWO
And if we extend another module:
module ModuleThree
CONSTANT_THREE = 'three'
def another_module_method
end
end
class One
extend ModuleThree
end
list_diff "Instance One.new diff", One.new.methods, methods_from_new
list_diff "Class One diff", One.methods, methods_from_class
puts "has constant? #{One.constants.sort.join ' '}"
When run, shows that another_module_method is now available as a class method, but CONSTANT_THREE is not1:
Instance One.new diff: instance_method module_method
Class One diff: another_module_method class_method
has constant? CONSTANT_TWO
Finally, we can use include instead of extend to include class methods:
module ModuleFour
CONSTANT_FOUR = 'four'
def yet_another_module_method
end
end
class One
class << self
include ModuleFour
end
end
list_diff "Instance One.new diff", One.new.methods, methods_from_new
list_diff "Class One diff", One.methods, methods_from_class
puts "has constant? #{One.constants.sort.join ' '}"
When run, shows that yet_another_module_method is available as a class method, but CONSTANT_FOUR is not2
Instance One.new diff: instance_method module_method
Class One diff: another_module_method class_method yet_another_module_method
has constant? CONSTANT_TWO
1 Remember, include does more3 than extend.
2 For more details, check Programming Ruby: The Pragmatic Programmer’s Guide .
3 Bonus!
module ModuleFive
CONSTANT_FIVE = 'five'
def module_five
end
end
one = One.new
one.extend ModuleFive
list_diff "Instance extends module", one.methods, methods_from_new
puts "has constant? #{one.class.constants.sort.join ' '}"
Instance extends module: instance_method module_five module_method
has constant? CONSTANT_TWO
Patching with git
March 27th, 2008
I patched the Ruby on Rails bundle for TextMate to allow footnotes to catch Haml views, which might be in files named something.haml or something.html.haml.
The change itself is trivial; making a patch using git was something new and, as it turns out, five steps:
1. Grab the source with git
Wonderland$ git clone git://github.com/drnic/ruby-on-rails-tmbundle.git
2. Make your changes
3. Commit locally, from the root of the source treee
Wonderland$ git commit -a -m 'add support for html.haml templates' Wonderland$ git show
4. Generate a patch file
Wonderland$ git format-patch origin
5. Email the patch file
Syntax Highlighting with the Code Highlighter Macro for Mephisto
March 24th, 2008
Dan Webb has an alternative to the built-in syntax highlighting provided by Mephisto that uses JavaScript to markup your code by using regular expressions to tokenize the code. Nifty, and based on this DHTML behavior.
Here is a summary of the install:
- Use svn to pull from http://svn.danwebb.net/external/rails/plugins/filtered_column_code_highlighter/trunk/ into the vendor/plugins directory
- Do either of:
- Copy the assets/*.js to your public/javascripts directory
- rake install (copies to themes/site-1/javascripts)
- Create a new CSS stylesheet (codehighlight.css) with the syntax coloring you want; I used this as a start.
- Add the following to your layout.liquid template
{{ 'codehighlight' | stylesheet }}
{{ 'code_highlighter' | javascript }}
{{ 'html' | javascript }}
{{ 'ruby' | javascript }}
{{ 'css' | javascript }}
{{ 'javascript' | javascript }}
Now the following markup:
<filter:jscode lang=”javascript”>document.someScriptThing = “BOO”</filter:jscode>
Generates:
document.someScriptThing = "BOO"
define_method
March 23rd, 2008
While we were waiting for March 20th RubyJax meeting to get started, Steve Bristol entertained us with a Ruby quiz. One of the questions was particularly interesting as it had many possible answers. I will paraphrase the question as:
“How many ways are there to add a method to an existing class in Ruby?”
I have cataloged the answers that were tossed out as well as a few I just added.
1 . Open the class and add a new method
class String
def method_one
puts 'method 1'
end
end
"abcde".method_one
2 . Create a singleton method on an instance
s = "12345"
def s.method_two
puts 'method 2'
end
s.method_two
3 . Use Kernel#method_missing
class String
def method_missing(name, *args)
super unless name == :method_three
puts 'method 3'
end
end
"abcde".method_three
4 . Use Module#define_method
class String
def create_method(name, &block)
self.class.send(:define_method, name, block)
end
end
"abcde".create_method(:method_four) { puts 'method 4' }
"12345".method_four
5 . A variation of #1 : include a module in the class
module ExtraMethods
def method_five
puts 'method 5'
end
end
class String
include ExtraMethods
end
"abcde".method_five
6 . A variation of #4 : use Object#instance_eval
String.instance_eval("define_method(:method_six) { puts 'method 6' }")
"abcde".method_six
7 . We could use Kernel#eval to execute any of the above as well; here we will emulate instance_eval with a binding
class String
def self.get_binding
binding
end
end
eval("define_method(:method_seven) { puts 'method 7' }", String.get_binding)
"abcde".method_seven
Any others?
Monkey Patch!
February 22nd, 2008
So after missing the wisdom[1] of this:
And responding to this:
http://b.lesseverything.com/2008/2/19/haml-doesn-t-like-javascript
I went to bed.
This morning I wrote my first Rails (really Haml) Monkey Patch[2]. This adds some of what Steve wanted: undisturbed inline javascript with variable interpolation. I am not sure what else he wanted as I did not read his blog carefully :)
Throw this into lib/inline_javascript.rb
module Haml
module Filters
class InlineJavascript
HEAD =<<EOH
<script type="text/javascript">
//<![CDATA[
EOH
FOOT =<<EOF
//]]>
</script>
EOF
def initialize(text)
@text = HEAD + text + FOOT
end
def render
@text
end
end
end
module Precompiler
def close_filtered(filter)
@flat_spaces = -1
filtered = filter.new(@filter_buffer).render
if filter == Haml::Filters::Preserve
push_silent("_hamlout.buffer << #{filtered.dump} << \"\\n\";")
elsif filter == Haml::Filters::InlineJavascript
# suppress eval option does not apply to us
flush_merged_text
js = unescape_interpolation(filtered)
@precompiled << "_hamlout.buffer << #{js};"
else
push_text(filtered.rstrip.gsub("\n", "\n#{' ' * @output_tabs}"))
end
@filter_buffer = nil
@template_tabs -= 1
end
end
end
Throw this at the bottom of your environment.rb, taken from here http://groups.google.com/group/haml/msg/2d890cf1ede761ea
require 'inline_javascript'
Haml::Template.options = {
:filters => {
'javascript' => Haml::Filters::InlineJavascript
}
}
And then do this in your Haml:
:javascript
function oh_yea() {
alert('Hello' + '#{@message}')
}
%a{ :href =>"javascript:oh_yea()" } Oh Yea!
Update
I decided to make a patch and submit it to the Haml folks directly. This gave me the ‘opportunity’ to get git going.
Nathan Weizenbaum took the time to put in a more comprehensive fix to expose the interpolation functionality to all filters – look for it in an upcoming release.
[1] It’s turtles all the way down.
[2] You do not want to know what my subconscious originally thought up as the way to do this – that was twisted.