AL MA TE RI Building Resources TE D Ruby on Rails is opinionated software. This doesn’t mean that it’s going to make fun of your haircut, or tell you what kind of car to drive. It does mean that Rails has definite ideas about how your web project should be structured, how it should interact with a database, how you should test, and even what kinds of tools you should use.
Chapter 1: Building Resources fancy graphics. For the moment, though, all it has is the standard Rails application structure, which you should see in your command window after you execute the following command: rails -d mysql soupsonline If you leave off the -d mysql, then your application will be created to use SQLite3, which is the new Rails default. The database can be changed later in developemnt.
Chapter 1: Building Resources From the data perspective, the place to start is the recipe — that’s the main unit of data that the users will be looking at. What’s the data for a recipe? Pulling out my handy-dandy Joy of Cooking (Simon & Schuster), I see that a recipe consists of a title (“Cream of Cauliflower Soup”), a resulting amount (“About 6 cups”), a description (“This recipe is the blueprint for a multitude of vegetable soups . . .
Chapter 1: Building Resources The REST of the Stor y I pledge right now that will be the only REST-related pun in the whole book (unless I think of a really good one later on). REST is another one of those tortured software acronyms — it stands for REpresentational State Transfer.
Chapter 1: Building Resources information into the server. In addition, most web applications used separate URLs for their GET and POST operations, even where it was technically feasible to share URLs. For example, the Java Servlet specification allows the same servlet to respond differently to a GET or POST, but all of the servlets I’ve written either defined one of the methods as a clone of the other, or only respond to one method, ignoring or failing if the other is invoked.
Chapter 1: Building Resources A RESTful view can also use some logically named methods to generate the URL that you might use inside a link_to call in your view. Rather than fussing around with action parameters, or passing the object or ID you want to control, Rails will automatically respond to methods such as recipe_path or edit_recipe_path — assuming, of course, that you’ve defined a resource for recipes.
Chapter 1: Building Resources create app/views/recipes/show.html.erb create app/views/recipes/new.html.erb create app/views/recipes/edit.html.erb create app/views/layouts/recipes.html.erb create public/stylesheets/scaffold.css create app/models/recipe.rb create test/unit/recipe_test.rb create test/fixtures/recipes.yml create db/migrate create db/migrate/001_create_recipes.rb create app/controllers/recipes_controller.rb create test/functional/recipes_controller_test.rb create app/helpers/recipes_helper.
Chapter 1: Building Resources (continued) def self.down drop_table :recipes end end The t.string syntax is a Rails 2.0 method for spelling what would previously have been written t.column :string. The timestamps method adds the special Rails columns created_at and updated_at. The creation of the ingredient resource generates a similar migration at db/migrate/002_ create_ingredients.rb. Routes The most important additions are the new routes added to the routes.
Chapter 1: Building Resources When you call one of these path or URL methods with a PUT or DELETE HTTP method, you must make sure that the link_to or redirect call also contains the option:method => :delete or :method => :put to ensure that the URL is properly sent by Rails (link_to assumes GET; the form methods and link_to_remote assume POST).
Chapter 1: Building Resources are well named. But if you get into things like user_address_street_house_room_url(x, y, z, a, b), it could get a little hairy.
Chapter 1: Building Resources traditional rails route — parts of the prefix specified as a Ruby symbol are converted to variables when the path is dereferenced. For example, if you wanted all recipes to be attached to a chef, you could add the option :path_prefix => “/chef/:chef_name”, and the show recipe URL, for example, would change to /chef/juliachild/recipe/1. Within the controller, the variable params[:chef_name] would be set to juliachild.
Chapter 1: Building Resources When the respond_to method is called, the outer block is invoked. Each format method is called, and does nothing unless the format method name matches the type of the request. (Metaprogramming fans should note that this is elegantly implemented using method_missing.) If the types match, then behavior associated with that type is invoked — either the block if one is explicitly passed or the default behavior if not.
Chapter 1: Building Resources New The default new method is similar to show, except a new recipe object is created: # GET /recipes/new # GET /recipes/new.xml def new @recipe = Recipe.new respond_to do |format| format.html # new.html.erb format.xml { render :xml => @recipe } end end Edit The default edit method is extremely simple because it does not have an XML representation defined, so the traditional Rails default behavior happens automatically, and a respond_to method is not needed.
Chapter 1: Building Resources I mentioned earlier that you could have code other than the format methods inside the respond_to block, and this example shows one reason why you might want to do that. The actual saving of the recipe takes place inside that block. If the save is successful, then the HTML response simply redirects to the show method.
Chapter 1: Building Resources respond_to do |format| format.html { redirect_to(recipes_url) } format.xml { head :ok } end end Views The views that are created by the generated script are largely similar to their non-REST counterparts, but I would like show the differences that come from using the RESTful URL features. In the edit.html.
Chapter 1: Building Resources Route Display If you find yourself becoming confused by all the RESTful routing magic, as of Version 2.0, Rails provides a rake command, routes, that gives you a complete list of the routes that have been defined in your application (output has been truncated). For example: $ rake routes recipes GET /recipes {:controller=>”recipes”, :action=>”index”} formatted_recipes GET /recipes.
Chapter 1: Building Resources ❑ You changed the default routing after the scaffolds were generated, and therefore the ingredient forms, as generated, use invalid methods to create URLs. ❑ The basic index listing of recipes is useful from an administrative point of view, but it is not what you want to present to a user. In addition to the functional changes, you’ll need it to be much nicer looking. That list will take you through the end of this chapter.
Chapter 1: Building Resources do is get a sense of the damage with the following (this output has been modified slightly for readability): $ rake (in /Users/noel/Documents/Programming/ruby/soupsonline) /usr/local/bin/ruby -Ilib:test “/usr/local/lib/ruby/gems/1.8/gems/rake0.7.3/lib/rake/rake_test_loader.rb” “test/unit/ingredient_test.rb” “test/unit/recipe_test.rb” Started .. Finished in 0.327569 seconds. 2 tests, 2 assertions, 0 failures, 0 errors /usr/local/bin/ruby -Ilib:test “/usr/local/lib/ruby/gems/1.
Chapter 1: Building Resources A couple of the tests also confirm that Rails redirects to the ingredient index listing, with a line like this: assert_redirected_to ingredient_path(assigns(:ingredient)) This line no longer works because, now that ingredients are a nested resource, the pathnames are all defined in terms of a parent recipe.
Chapter 1: Building Resources The redirection method automatically handles the list of nested resource objects. The destroy action redirects to the list action, so you need to change that redirection as follows: redirect_to(recipe_ingredients_url) In this case, the controller automatically infers that it should use the @recipe attribute to generate the correct index path. The Views All you need to do to the view objects at this point is change the URLs for the forms and links.
Chapter 1: Building Resources Rails Testing Tip The default test runner text is fine as far as it goes, but sometimes it’s not very easy to tell which methods have failed. If you include diagnostic print statements in your tests while debugging, it can be difficult to tell which output goes with which tests. There are a few options for more useful test output.
Chapter 1: Building Resources Building a Recipe Editor If you fire up the Rails server and look at the recipe input form, you’ll see that at this point, it looks something like what is shown in Figure 1-1. Figure 1-1 While maintaining the proper amount of reverence to the tool that provided this form for free, it’s easy to see that it won’t do. Ingredients aren’t listed, all the boxes are the wrong size, and basically the thing looks totally generic.
Chapter 1: Building Resources In this book, I’m going to try where possible to present working tests for the code samples as they are presented. The idea is to give you a sense of strategies for testing various parts of a Rails application, and to reinforce the idea that writing tests for all your Rails code is an achievable and desirable goal. I’d like to start by reinforcing the previously created tests for the Recipe new form and the create method.
Chapter 1: Building Resources If the second argument is a string or regular expression, then the selector tests to see if there is a tag of that type whose contents either equal the string or match the regular expression. The type tag can be augmented in several different ways. Putting a dot after it, as in “form.title”, checks to see if there’s a form tag that is of the CSS class title. Putting a hash mark after the type “form#form_1” performs a similar test on the DOM ID of the tag.
Chapter 1: Building Resources <%= f.text_area :description, :rows => 5, :cols => 55, :class => “input” %>
Ingredients:
<%= f.text_area :ingredient_string, :rows => 5, :cols => 55, :class => “input” %>
Directions:
<%= f.text_area :directions, :rows => 15, :cols => 55, :class => “input” %>
<%= f.submit “Create”, :class => “title” %>
<% end %> <%= link_to ‘Back’, recipes_path %> There are a couple of changes.Chapter 1: Building Resources The test cases I started with are described in the following table. Case Description 2 cups carrots, diced The basic input structure 2 cups carrots Basic input, minus the instructions 1 carrots, diced Basic input, minus the unit 1 cup carrots Singular unit 2.
Chapter 1: Building Resources attr_accessor :result, :tokens, :state, :ingredient_words, :instruction_words def initialize(str, ingredient) @result = ingredient @tokens = str.split() @state = :amount @ingredient_words = [] @instruction_words = [] end def parse tokens.each do |token| consumed = self.send(state, token) redo unless consumed end result.ingredient = ingredient_words.join(“ “) result.instruction = instruction_words.join(“ “) result end end The parse method is of the most interest.
Chapter 1: Building Resources The unit method actually has provisions not to consume the token. If the token is numerical, the parser assumes it’s a continuation of the amount, resets the state, and returns false so that the amount method will take a crack at the same token. For example: def unit(token) if token.to_i > 0 self.state = :amount return false end if UNITS.index(token) or UNITS.index(token.pluralize) result.unit = token.pluralize self.state = :ingredient return true else self.
Chapter 1: Building Resources :instruction => “”, :unit => “”, :amount => 0) parser = IngredientParser.new(str, result) parser.parse end Finally, the display_string method of Ingredient makes sure everything is in a standard format as follows: def display_string str = [amount_as_fraction, unit_inflected, ingredient_inflected].compact.join(“ “) str += “, #{instruction}” unless instruction.blank? str end The compact.
Chapter 1: Building Resources Figure 1-2 Asserting Creation Let’s tighten up the remaining recipe controller tests while adding ingredient functionality. The test for creating a recipe asserts that the number of recipes changes, but it doesn’t assert anything about the entered data.
Chapter 1: Building Resources “2 cups carrots, diced\n\n1/2 tablespoon salt\n\n1 1/3 cups stock”, :directions => “Ask Grandma”} assert_difference(‘Recipe.count’) do post :create, :recipe => recipe_hash end expected_recipe = Recipe.new(recipe_hash) new_recipe = Recipe.find(:all, :order => “id DESC”, :limit => 1)[0] assert_equal(expected_recipe, new_recipe) assert_equal(3, new_recipe.ingredients.
Chapter 1: Building Resources This might seem like overkill, to have a unit test for equality, but it took very little time to put together, and it makes me less concerned about the bane of the unit tester — the test that really is failing but incorrectly reports that it passed. The data for the new ingredients comes in as a raw string via the ingredient text area. It’s the responsibility of the recipe object to convert that string into the actual ingredient objects.
Chapter 1: Building Resources To make this work, the Recipe class is augmented with a getter and setter method for the attribute ingredient_string — this is the slightly unusual case where you want a getter and setter to do something genuinely different. The setter takes the string and converts it to ingredient objects, and the getter returns the recreated string: def ingredient_string=(str) ingredient_strings = str.split(“\n”) order_of = 1 ingredient_strings.each do |istr| next if istr.
Chapter 1: Building Resources The edit test is changed to be almost identical to the new test, the only difference being the form action itself. The easiest way to make this test pass is to take the form block from the new.html.erb file and put it in a partial file called _form.html.erb, and change the new and edit views to refer to it.
Chapter 1: Building Resources Figure 1-4 To allow Ajax to work in your Rails application, you must load the relevant JavaScript files by including the following line in the app/views.layouts/recipes.html.erb file. Place the line in the HTML header. <%= javascript_include_tag :defaults %> I find the best way to build in-place action like this is to build the action as a standalone first, and then incorporate it into the view where needed.
Chapter 1: Building Resources The tests are very similar to what you’d use for the normal edit and update, just with different URLs. The response for the update method is the ingredient display string, not a redirect to the show ingredient page, which enables the updated ingredient to be inserted back into place on the recipe page.
Chapter 1: Building Resources Notice the pathname in the result. This is app/views/ingredients/remote_edit.html.erb. The following remote_update method in the ingredient controller is a simplification of update (for one thing, I’m not concerned here with responding in formats other than HTML): def remote_update @ingredient = Ingredient.find(params[:id]) if @ingredient.
Chapter 1: Building Resources Resources The primary source for the REST details in this chapter was the RESTful Rails tutorial, written by Ralf Wirdemann and Thomas Baustert and translated from the original German by Florian Görsdorf and Adam Groves. It’s available at www.b-simple.de/documents. It’s an excellent reference for the details of the Rails version of REST. For more details on recipes in general, I reference The Joy of Cooking by Imra S. Rombauer, Marion Rombauer Becker, and Ethan Becker.